From 42b76294db48e604b48ade266444190a29bf1424 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 18 Apr 2024 09:48:57 +0800 Subject: [PATCH 001/290] 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/290] 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/290] 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/290] 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/290] Remove redundant constructor --- osu.Game/Online/Multiplayer/InvalidPasswordException.cs | 3 --- 1 file changed, 3 deletions(-) diff --git a/osu.Game/Online/Multiplayer/InvalidPasswordException.cs b/osu.Game/Online/Multiplayer/InvalidPasswordException.cs index 8f2543ee1e..b76a1cc05d 100644 --- a/osu.Game/Online/Multiplayer/InvalidPasswordException.cs +++ b/osu.Game/Online/Multiplayer/InvalidPasswordException.cs @@ -9,8 +9,5 @@ namespace osu.Game.Online.Multiplayer [Serializable] public class InvalidPasswordException : HubException { - public InvalidPasswordException() - { - } } } From 16e69b08a161506d191ddf89e782928da79146d0 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 30 Sep 2024 19:52:51 +0900 Subject: [PATCH 006/290] Avoid unnecessarily handling two skin changed events when making mutable skin --- osu.Game/Overlays/SkinEditor/SkinEditor.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/osu.Game/Overlays/SkinEditor/SkinEditor.cs b/osu.Game/Overlays/SkinEditor/SkinEditor.cs index 6f7781ee9c..eca8b7f1d2 100644 --- a/osu.Game/Overlays/SkinEditor/SkinEditor.cs +++ b/osu.Game/Overlays/SkinEditor/SkinEditor.cs @@ -401,6 +401,10 @@ namespace osu.Game.Overlays.SkinEditor private void skinChanged() { + if (skins.EnsureMutableSkin()) + // Another skin changed event will arrive which will complete the process. + return; + headerText.Clear(); headerText.AddParagraph(SkinEditorStrings.SkinEditor, cp => cp.Font = OsuFont.Default.With(size: 16)); From 1f2f4a533f8159b986f90538388845820a2c50b9 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 30 Sep 2024 19:53:06 +0900 Subject: [PATCH 007/290] Fix initial skin state being stored wrong to undo history --- osu.Game/Overlays/SkinEditor/SkinEditor.cs | 21 ++++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/osu.Game/Overlays/SkinEditor/SkinEditor.cs b/osu.Game/Overlays/SkinEditor/SkinEditor.cs index eca8b7f1d2..ec9931c673 100644 --- a/osu.Game/Overlays/SkinEditor/SkinEditor.cs +++ b/osu.Game/Overlays/SkinEditor/SkinEditor.cs @@ -422,17 +422,24 @@ namespace osu.Game.Overlays.SkinEditor }); changeHandler?.Dispose(); + changeHandler = null; - skins.EnsureMutableSkin(); + // Schedule is required to ensure that all layout in `LoadComplete` methods has been completed + // before storing an undo state. + // + // See https://github.com/ppy/osu/blob/8e6a4559e3ae8c9892866cf9cf8d4e8d1b72afd0/osu.Game/Skinning/SkinReloadableDrawable.cs#L76. + Schedule(() => + { + var targetContainer = getTarget(selectedTarget.Value); - var targetContainer = getTarget(selectedTarget.Value); + if (targetContainer != null) + changeHandler = new SkinEditorChangeHandler(targetContainer); - if (targetContainer != null) - changeHandler = new SkinEditorChangeHandler(targetContainer); - hasBegunMutating = true; + hasBegunMutating = true; - // Reload sidebar components. - selectedTarget.TriggerChange(); + // Reload sidebar components. + selectedTarget.TriggerChange(); + }); } /// From f84f6b78d9fdd4a1fda1a36c97cb4915981a3a20 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 16 Oct 2024 13:48:29 +0200 Subject: [PATCH 008/290] Add failing test coverage of skin editor still not undoing correctly to initial state --- .../TestSceneSkinEditorNavigation.cs | 73 +++++++++++++++++++ 1 file changed, 73 insertions(+) diff --git a/osu.Game.Tests/Visual/Navigation/TestSceneSkinEditorNavigation.cs b/osu.Game.Tests/Visual/Navigation/TestSceneSkinEditorNavigation.cs index 5267a57a05..8323aaeaf4 100644 --- a/osu.Game.Tests/Visual/Navigation/TestSceneSkinEditorNavigation.cs +++ b/osu.Game.Tests/Visual/Navigation/TestSceneSkinEditorNavigation.cs @@ -5,6 +5,7 @@ using System; using System.Linq; +using Newtonsoft.Json; using NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Extensions; @@ -23,6 +24,7 @@ using osu.Game.Rulesets.Osu; using osu.Game.Rulesets.Osu.Mods; using osu.Game.Screens.Edit.Components; using osu.Game.Screens.Play; +using osu.Game.Screens.Play.HUD; using osu.Game.Screens.Play.HUD.HitErrorMeters; using osu.Game.Skinning; using osu.Game.Tests.Beatmaps.IO; @@ -101,6 +103,77 @@ namespace osu.Game.Tests.Visual.Navigation AddUntilStep("current skin is mutable", () => !Game.Dependencies.Get().CurrentSkin.Value.SkinInfo.Value.Protected); } + [Test] + public void TestMutateProtectedSkinFromMainMenu_UndoToInitialStateIsCorrect() + { + AddStep("set default skin", () => Game.Dependencies.Get().CurrentSkinInfo.SetDefault()); + AddStep("import beatmap", () => BeatmapImportHelper.LoadQuickOszIntoOsu(Game).WaitSafely()); + + openSkinEditor(); + AddUntilStep("current skin is mutable", () => !Game.Dependencies.Get().CurrentSkin.Value.SkinInfo.Value.Protected); + + AddUntilStep("wait for player", () => + { + DismissAnyNotifications(); + return Game.ScreenStack.CurrentScreen is Player; + }); + + string state = string.Empty; + + AddUntilStep("wait for accuracy counter", () => Game.ChildrenOfType().Any(counter => counter.IsLoaded)); + AddStep("dump state of accuracy meter", () => state = JsonConvert.SerializeObject(Game.ChildrenOfType().First().CreateSerialisedInfo())); + AddStep("add any component", () => Game.ChildrenOfType().First().TriggerClick()); + AddStep("undo", () => + { + InputManager.PressKey(Key.ControlLeft); + InputManager.Key(Key.Z); + InputManager.ReleaseKey(Key.ControlLeft); + }); + AddUntilStep("only one accuracy meter left", + () => Game.ChildrenOfType().Single().ChildrenOfType().Count(), + () => Is.EqualTo(1)); + AddAssert("accuracy meter state unchanged", + () => JsonConvert.SerializeObject(Game.ChildrenOfType().First().CreateSerialisedInfo()), + () => Is.EqualTo(state)); + } + + [Test] + public void TestMutateProtectedSkinFromPlayer_UndoToInitialStateIsCorrect() + { + AddStep("set default skin", () => Game.Dependencies.Get().CurrentSkinInfo.SetDefault()); + AddStep("import beatmap", () => BeatmapImportHelper.LoadQuickOszIntoOsu(Game).WaitSafely()); + advanceToSongSelect(); + AddUntilStep("wait for selected", () => !Game.Beatmap.IsDefault); + + AddStep("enable NF", () => Game.SelectedMods.Value = new[] { new OsuModNoFail() }); + AddStep("enter gameplay", () => InputManager.Key(Key.Enter)); + + AddUntilStep("wait for player", () => + { + DismissAnyNotifications(); + return Game.ScreenStack.CurrentScreen is Player; + }); + openSkinEditor(); + + string state = string.Empty; + + AddUntilStep("wait for accuracy counter", () => Game.ChildrenOfType().Any(counter => counter.IsLoaded)); + AddStep("dump state of accuracy meter", () => state = JsonConvert.SerializeObject(Game.ChildrenOfType().First().CreateSerialisedInfo())); + AddStep("add any component", () => Game.ChildrenOfType().First().TriggerClick()); + AddStep("undo", () => + { + InputManager.PressKey(Key.ControlLeft); + InputManager.Key(Key.Z); + InputManager.ReleaseKey(Key.ControlLeft); + }); + AddUntilStep("only one accuracy meter left", + () => Game.ChildrenOfType().Single().ChildrenOfType().Count(), + () => Is.EqualTo(1)); + AddAssert("accuracy meter state unchanged", + () => JsonConvert.SerializeObject(Game.ChildrenOfType().First().CreateSerialisedInfo()), + () => Is.EqualTo(state)); + } + [Test] public void TestComponentsDeselectedOnSkinEditorHide() { From 66ca7448436e7d66072343a1c4af950da3e0d385 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 16 Oct 2024 14:23:16 +0200 Subject: [PATCH 009/290] Fix `SkinEditorChangeHandler` not actually storing initial state --- osu.Game/Overlays/SkinEditor/SkinEditorChangeHandler.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Overlays/SkinEditor/SkinEditorChangeHandler.cs b/osu.Game/Overlays/SkinEditor/SkinEditorChangeHandler.cs index 673ba873c4..b805e50df6 100644 --- a/osu.Game/Overlays/SkinEditor/SkinEditorChangeHandler.cs +++ b/osu.Game/Overlays/SkinEditor/SkinEditorChangeHandler.cs @@ -34,7 +34,7 @@ namespace osu.Game.Overlays.SkinEditor return; components = new BindableList { BindTarget = firstTarget.Components }; - components.BindCollectionChanged((_, _) => SaveState()); + components.BindCollectionChanged((_, _) => SaveState(), true); } protected override void WriteCurrentStateToStream(MemoryStream stream) From 936677f56abd22328fc9450d3b529b87a672f440 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 16 Oct 2024 14:47:29 +0200 Subject: [PATCH 010/290] Fix `SkinEditor` potentially initialising change handler while components are not loaded yet --- osu.Game/Overlays/SkinEditor/SkinEditor.cs | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/osu.Game/Overlays/SkinEditor/SkinEditor.cs b/osu.Game/Overlays/SkinEditor/SkinEditor.cs index ec9931c673..130684e289 100644 --- a/osu.Game/Overlays/SkinEditor/SkinEditor.cs +++ b/osu.Game/Overlays/SkinEditor/SkinEditor.cs @@ -353,9 +353,10 @@ namespace osu.Game.Overlays.SkinEditor return; } - changeHandler = new SkinEditorChangeHandler(skinComponentsContainer); - changeHandler.CanUndo.BindValueChanged(v => undoMenuItem.Action.Disabled = !v.NewValue, true); - changeHandler.CanRedo.BindValueChanged(v => redoMenuItem.Action.Disabled = !v.NewValue, true); + if (skinComponentsContainer.IsLoaded) + bindChangeHandler(skinComponentsContainer); + else + skinComponentsContainer.OnLoadComplete += d => Schedule(() => bindChangeHandler((SkinnableContainer)d)); content.Child = new SkinBlueprintContainer(skinComponentsContainer); @@ -397,6 +398,13 @@ namespace osu.Game.Overlays.SkinEditor SelectedComponents.Clear(); placeComponent(component); } + + void bindChangeHandler(SkinnableContainer skinnableContainer) + { + changeHandler = new SkinEditorChangeHandler(skinnableContainer); + changeHandler.CanUndo.BindValueChanged(v => undoMenuItem.Action.Disabled = !v.NewValue, true); + changeHandler.CanRedo.BindValueChanged(v => redoMenuItem.Action.Disabled = !v.NewValue, true); + } } private void skinChanged() From 99518f4a564ed2e14895c5744a25f3af4138db64 Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Mon, 4 Nov 2024 04:28:16 -0500 Subject: [PATCH 011/290] Specify type of text input in most `TextBox` usages --- osu.Game/Graphics/UserInterface/OsuNumberBox.cs | 7 +++---- osu.Game/Graphics/UserInterface/OsuPasswordTextBox.cs | 10 +++------- osu.Game/Graphics/UserInterfaceV2/FormNumberBox.cs | 6 ++++++ osu.Game/Overlays/Login/LoginForm.cs | 2 ++ osu.Game/Overlays/Settings/SettingsNumberBox.cs | 6 +++++- osu.Game/Screens/Edit/Setup/MetadataSection.cs | 6 +++++- 6 files changed, 24 insertions(+), 13 deletions(-) diff --git a/osu.Game/Graphics/UserInterface/OsuNumberBox.cs b/osu.Game/Graphics/UserInterface/OsuNumberBox.cs index db4b7b2ab3..86753f6aa9 100644 --- a/osu.Game/Graphics/UserInterface/OsuNumberBox.cs +++ b/osu.Game/Graphics/UserInterface/OsuNumberBox.cs @@ -1,17 +1,16 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using osu.Framework.Input; + namespace osu.Game.Graphics.UserInterface { public partial class OsuNumberBox : OsuTextBox { - protected override bool AllowIme => false; - public OsuNumberBox() { + InputProperties = new TextInputProperties(TextInputType.Number, false); SelectAllOnFocus = true; } - - protected override bool CanAddCharacter(char character) => char.IsAsciiDigit(character); } } diff --git a/osu.Game/Graphics/UserInterface/OsuPasswordTextBox.cs b/osu.Game/Graphics/UserInterface/OsuPasswordTextBox.cs index 0be7b4dc48..143962542d 100644 --- a/osu.Game/Graphics/UserInterface/OsuPasswordTextBox.cs +++ b/osu.Game/Graphics/UserInterface/OsuPasswordTextBox.cs @@ -18,7 +18,7 @@ using osu.Game.Localisation; namespace osu.Game.Graphics.UserInterface { - public partial class OsuPasswordTextBox : OsuTextBox, ISuppressKeyEventLogging + public partial class OsuPasswordTextBox : OsuTextBox { protected override Drawable GetDrawableCharacter(char c) => new FallingDownContainer { @@ -28,12 +28,6 @@ namespace osu.Game.Graphics.UserInterface protected override bool AllowUniqueCharacterSamples => false; - protected override bool AllowClipboardExport => false; - - protected override bool AllowWordNavigation => false; - - protected override bool AllowIme => false; - private readonly CapsWarning warning; [Resolved] @@ -41,6 +35,8 @@ namespace osu.Game.Graphics.UserInterface public OsuPasswordTextBox() { + InputProperties = new TextInputProperties(TextInputType.Password, false); + Add(warning = new CapsWarning { Size = new Vector2(20), diff --git a/osu.Game/Graphics/UserInterfaceV2/FormNumberBox.cs b/osu.Game/Graphics/UserInterfaceV2/FormNumberBox.cs index c3256e0038..61d3b3fc31 100644 --- a/osu.Game/Graphics/UserInterfaceV2/FormNumberBox.cs +++ b/osu.Game/Graphics/UserInterfaceV2/FormNumberBox.cs @@ -2,6 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using System.Globalization; +using osu.Framework.Input; namespace osu.Game.Graphics.UserInterfaceV2 { @@ -19,6 +20,11 @@ namespace osu.Game.Graphics.UserInterfaceV2 { public bool AllowDecimals { get; init; } + public InnerNumberBox() + { + InputProperties = new TextInputProperties(TextInputType.Number, false); + } + protected override bool CanAddCharacter(char character) => char.IsAsciiDigit(character) || (AllowDecimals && CultureInfo.CurrentCulture.NumberFormat.NumberDecimalSeparator.Contains(character)); } diff --git a/osu.Game/Overlays/Login/LoginForm.cs b/osu.Game/Overlays/Login/LoginForm.cs index 13e528ff8f..0ff30da2a1 100644 --- a/osu.Game/Overlays/Login/LoginForm.cs +++ b/osu.Game/Overlays/Login/LoginForm.cs @@ -7,6 +7,7 @@ using osu.Framework.Extensions.LocalisationExtensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.UserInterface; +using osu.Framework.Input; using osu.Framework.Input.Events; using osu.Game.Configuration; using osu.Game.Graphics; @@ -63,6 +64,7 @@ namespace osu.Game.Overlays.Login }, username = new OsuTextBox { + InputProperties = new TextInputProperties(TextInputType.Username, false), PlaceholderText = UsersStrings.LoginUsername.ToLower(), RelativeSizeAxes = Axes.X, Text = api.ProvidedUsername, diff --git a/osu.Game/Overlays/Settings/SettingsNumberBox.cs b/osu.Game/Overlays/Settings/SettingsNumberBox.cs index fbcdb4a968..2548f3c87b 100644 --- a/osu.Game/Overlays/Settings/SettingsNumberBox.cs +++ b/osu.Game/Overlays/Settings/SettingsNumberBox.cs @@ -5,6 +5,7 @@ using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.UserInterface; +using osu.Framework.Input; namespace osu.Game.Overlays.Settings { @@ -66,7 +67,10 @@ namespace osu.Game.Overlays.Settings private partial class OutlinedNumberBox : OutlinedTextBox { - protected override bool AllowIme => false; + public OutlinedNumberBox() + { + InputProperties = new TextInputProperties(TextInputType.Number, false); + } protected override bool CanAddCharacter(char character) => char.IsAsciiDigit(character); diff --git a/osu.Game/Screens/Edit/Setup/MetadataSection.cs b/osu.Game/Screens/Edit/Setup/MetadataSection.cs index 20c0a74d84..3acaefe91e 100644 --- a/osu.Game/Screens/Edit/Setup/MetadataSection.cs +++ b/osu.Game/Screens/Edit/Setup/MetadataSection.cs @@ -4,6 +4,7 @@ using System.Linq; using osu.Framework.Allocation; using osu.Framework.Graphics.UserInterface; +using osu.Framework.Input; using osu.Framework.Localisation; using osu.Game.Beatmaps; using osu.Game.Graphics.UserInterfaceV2; @@ -119,7 +120,10 @@ namespace osu.Game.Screens.Edit.Setup private partial class RomanisedTextBox : InnerTextBox { - protected override bool AllowIme => false; + public RomanisedTextBox() + { + InputProperties = new TextInputProperties(TextInputType.Text, false); + } protected override bool CanAddCharacter(char character) => MetadataUtils.IsRomanised(character); From 62ea4e09709eb51906e732e30c449103ff5ac2e1 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Fri, 6 Dec 2024 16:37:00 +0900 Subject: [PATCH 012/290] Add failing test --- .../ManiaBeatmapConversionTest.cs | 1 + ...-specific-spinner-expected-conversion.json | 60 +++++++++++++++++++ .../Beatmaps/mania-specific-spinner.osu | 27 +++++++++ 3 files changed, 88 insertions(+) create mode 100644 osu.Game.Rulesets.Mania.Tests/Resources/Testing/Beatmaps/mania-specific-spinner-expected-conversion.json create mode 100644 osu.Game.Rulesets.Mania.Tests/Resources/Testing/Beatmaps/mania-specific-spinner.osu diff --git a/osu.Game.Rulesets.Mania.Tests/ManiaBeatmapConversionTest.cs b/osu.Game.Rulesets.Mania.Tests/ManiaBeatmapConversionTest.cs index 609c2e8953..b167ea3ab1 100644 --- a/osu.Game.Rulesets.Mania.Tests/ManiaBeatmapConversionTest.cs +++ b/osu.Game.Rulesets.Mania.Tests/ManiaBeatmapConversionTest.cs @@ -22,6 +22,7 @@ namespace osu.Game.Rulesets.Mania.Tests [TestCase("basic")] [TestCase("zero-length-slider")] + [TestCase("mania-specific-spinner")] [TestCase("20544")] [TestCase("100374")] [TestCase("1450162")] diff --git a/osu.Game.Rulesets.Mania.Tests/Resources/Testing/Beatmaps/mania-specific-spinner-expected-conversion.json b/osu.Game.Rulesets.Mania.Tests/Resources/Testing/Beatmaps/mania-specific-spinner-expected-conversion.json new file mode 100644 index 0000000000..aa1fa7f16d --- /dev/null +++ b/osu.Game.Rulesets.Mania.Tests/Resources/Testing/Beatmaps/mania-specific-spinner-expected-conversion.json @@ -0,0 +1,60 @@ +{ + "Mappings": [ + { + "RandomW": 273071671, + "RandomX": 842502087, + "RandomY": 3579807591, + "RandomZ": 273326509, + "StartTime": 11783.0, + "Objects": [ + { + "StartTime": 11783.0, + "EndTime": 15116.0, + "Column": 0 + } + ] + }, + { + "RandomW": 2659271247, + "RandomX": 3579807591, + "RandomY": 273326509, + "RandomZ": 273071671, + "StartTime": 91545.0, + "Objects": [ + { + "StartTime": 91545.0, + "EndTime": 92735.0, + "Column": 0 + } + ] + }, + { + "RandomW": 3083635271, + "RandomX": 273326509, + "RandomY": 273071671, + "RandomZ": 2659271247, + "StartTime": 152497.0, + "Objects": [ + { + "StartTime": 152497.0, + "EndTime": 153687.0, + "Column": 1 + } + ] + }, + { + "RandomW": 4073591514, + "RandomX": 273071671, + "RandomY": 2659271247, + "RandomZ": 3083635271, + "StartTime": 231545.0, + "Objects": [ + { + "StartTime": 231545.0, + "EndTime": 232974.0, + "Column": 3 + } + ] + } + ] +} \ No newline at end of file diff --git a/osu.Game.Rulesets.Mania.Tests/Resources/Testing/Beatmaps/mania-specific-spinner.osu b/osu.Game.Rulesets.Mania.Tests/Resources/Testing/Beatmaps/mania-specific-spinner.osu new file mode 100644 index 0000000000..fb709744d7 --- /dev/null +++ b/osu.Game.Rulesets.Mania.Tests/Resources/Testing/Beatmaps/mania-specific-spinner.osu @@ -0,0 +1,27 @@ +osu file format v14 + +[General] +Mode: 3 + +[Difficulty] +HPDrainRate:5 +CircleSize:4 +OverallDifficulty:5 +ApproachRate:0 +SliderMultiplier:2.6 +SliderTickRate:1 + +[TimingPoints] +355,476.190476190476,4,2,1,60,1,0 +60652,-100,4,2,1,60,0,1 +92735,-100,4,2,1,60,0,0 +121485,-100,4,2,1,60,0,1 +153688,-100,4,2,1,60,0,0 +182497,-100,4,2,1,60,0,1 +213688,-100,4,2,1,60,0,0 + +[HitObjects] +256,192,11783,12,0,15116,0:0:0:0: +256,192,91545,12,0,92735,0:0:0:0: +256,192,152497,12,0,153687,0:0:0:0: +256,192,231545,12,0,232974,0:0:0:0: From 8b456e13794adaf471791aa70b14a83bdeedf96a Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Fri, 6 Dec 2024 16:01:21 +0900 Subject: [PATCH 013/290] Always convert mania spinners A big part of these changes is refactoring, which is somewhat necessary because it was previously implemented as two separate pathways which in-fact need to be joined at the hip when handling spinners. I've chosen to use `IHasLegacyHitObjectType` here because there's no other flag that allows us to tell `ConvertHold` apart from `ConvertSpinner`. --- .../Beatmaps/ManiaBeatmapConverter.cs | 163 ++++++++---------- .../Beatmaps/Legacy/LegacyHitObjectType.cs | 4 +- 2 files changed, 79 insertions(+), 88 deletions(-) diff --git a/osu.Game.Rulesets.Mania/Beatmaps/ManiaBeatmapConverter.cs b/osu.Game.Rulesets.Mania/Beatmaps/ManiaBeatmapConverter.cs index 970d68759f..79e4c6020d 100644 --- a/osu.Game.Rulesets.Mania/Beatmaps/ManiaBeatmapConverter.cs +++ b/osu.Game.Rulesets.Mania/Beatmaps/ManiaBeatmapConverter.cs @@ -7,11 +7,13 @@ using System.Linq; using System.Collections.Generic; using System.Threading; using osu.Game.Beatmaps; +using osu.Game.Beatmaps.Legacy; using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects.Types; using osu.Game.Rulesets.Mania.Beatmaps.Patterns; using osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy; using osu.Game.Rulesets.Mods; +using osu.Game.Rulesets.Objects.Legacy; using osu.Game.Rulesets.Scoring.Legacy; using osu.Game.Utils; using osuTK; @@ -124,16 +126,85 @@ namespace osu.Game.Rulesets.Mania.Beatmaps protected override IEnumerable ConvertHitObject(HitObject original, IBeatmap beatmap, CancellationToken cancellationToken) { - if (original is ManiaHitObject maniaOriginal) + if (original is ManiaHitObject maniaObj) { - yield return maniaOriginal; + yield return maniaObj; yield break; } - var objects = IsForCurrentRuleset ? generateSpecific(original, beatmap) : generateConverted(original, beatmap); - foreach (ManiaHitObject obj in objects) - yield return obj; + if (original is not IHasLegacyHitObjectType legacy) + yield break; + + double startTime = original.StartTime; + double endTime = (original as IHasDuration)?.EndTime ?? startTime; + Vector2 position = (original as IHasPosition)?.Position ?? Vector2.Zero; + + Patterns.PatternGenerator conversion; + + switch (legacy.LegacyType & LegacyHitObjectType.ObjectTypes) + { + case LegacyHitObjectType.Circle: + if (IsForCurrentRuleset) + { + conversion = new SpecificBeatmapPatternGenerator(Random, original, beatmap, TotalColumns, lastPattern); + recordNote(startTime, position); + } + else + { + computeDensity(startTime); + conversion = new HitObjectPatternGenerator(Random, original, beatmap, TotalColumns, lastPattern, lastTime, lastPosition, density, lastStair); + recordNote(startTime, position); + } + + break; + + case LegacyHitObjectType.Slider: + if (IsForCurrentRuleset) + { + conversion = new SpecificBeatmapPatternGenerator(Random, original, beatmap, TotalColumns, lastPattern); + recordNote(original.StartTime, position); + } + else + { + var generator = new PathObjectPatternGenerator(Random, original, beatmap, TotalColumns, lastPattern); + conversion = generator; + + for (int i = 0; i <= generator.SpanCount; i++) + { + double time = original.StartTime + generator.SegmentDuration * i; + + recordNote(time, position); + computeDensity(time); + } + } + + break; + + case LegacyHitObjectType.Spinner: + conversion = new EndTimeObjectPatternGenerator(Random, original, beatmap, TotalColumns, lastPattern); + recordNote(endTime, new Vector2(256, 192)); + computeDensity(endTime); + break; + + case LegacyHitObjectType.Hold: + conversion = new SpecificBeatmapPatternGenerator(Random, original, beatmap, TotalColumns, lastPattern); + recordNote(endTime, position); + computeDensity(endTime); + break; + + default: + throw new ArgumentException($"Invalid legacy object type: {legacy.LegacyType}", nameof(original)); + } + + foreach (var newPattern in conversion.Generate()) + { + lastPattern = conversion is EndTimeObjectPatternGenerator ? lastPattern : newPattern; + lastStair = (conversion as HitObjectPatternGenerator)?.StairType ?? lastStair; + + foreach (var obj in newPattern.HitObjects) + yield return obj; + } } private readonly LimitedCapacityQueue prevNoteTimes = new LimitedCapacityQueue(max_notes_for_density); @@ -157,88 +228,6 @@ namespace osu.Game.Rulesets.Mania.Beatmaps lastPosition = position; } - /// - /// Method that generates hit objects for osu!mania specific beatmaps. - /// - /// The original hit object. - /// The original beatmap. This is used to look-up any values dependent on a fully-loaded beatmap. - /// The hit objects generated. - private IEnumerable generateSpecific(HitObject original, IBeatmap originalBeatmap) - { - var generator = new SpecificBeatmapPatternGenerator(Random, original, originalBeatmap, TotalColumns, lastPattern); - - foreach (var newPattern in generator.Generate()) - { - lastPattern = newPattern; - - foreach (var obj in newPattern.HitObjects) - yield return obj; - } - } - - /// - /// Method that generates hit objects for non-osu!mania beatmaps. - /// - /// The original hit object. - /// The original beatmap. This is used to look-up any values dependent on a fully-loaded beatmap. - /// The hit objects generated. - private IEnumerable generateConverted(HitObject original, IBeatmap originalBeatmap) - { - Patterns.PatternGenerator? conversion = null; - - switch (original) - { - case IHasPath: - { - var generator = new PathObjectPatternGenerator(Random, original, originalBeatmap, TotalColumns, lastPattern); - conversion = generator; - - var positionData = original as IHasPosition; - - for (int i = 0; i <= generator.SpanCount; i++) - { - double time = original.StartTime + generator.SegmentDuration * i; - - recordNote(time, positionData?.Position ?? Vector2.Zero); - computeDensity(time); - } - - break; - } - - case IHasDuration endTimeData: - { - conversion = new EndTimeObjectPatternGenerator(Random, original, originalBeatmap, TotalColumns, lastPattern); - - recordNote(endTimeData.EndTime, new Vector2(256, 192)); - computeDensity(endTimeData.EndTime); - break; - } - - case IHasPosition positionData: - { - computeDensity(original.StartTime); - - conversion = new HitObjectPatternGenerator(Random, original, originalBeatmap, TotalColumns, lastPattern, lastTime, lastPosition, density, lastStair); - - recordNote(original.StartTime, positionData.Position); - break; - } - } - - if (conversion == null) - yield break; - - foreach (var newPattern in conversion.Generate()) - { - lastPattern = conversion is EndTimeObjectPatternGenerator ? lastPattern : newPattern; - lastStair = (conversion as HitObjectPatternGenerator)?.StairType ?? lastStair; - - foreach (var obj in newPattern.HitObjects) - yield return obj; - } - } - /// /// A pattern generator for osu!mania-specific beatmaps. /// diff --git a/osu.Game/Beatmaps/Legacy/LegacyHitObjectType.cs b/osu.Game/Beatmaps/Legacy/LegacyHitObjectType.cs index 6fab66bf70..ca3f7cc354 100644 --- a/osu.Game/Beatmaps/Legacy/LegacyHitObjectType.cs +++ b/osu.Game/Beatmaps/Legacy/LegacyHitObjectType.cs @@ -13,6 +13,8 @@ namespace osu.Game.Beatmaps.Legacy NewCombo = 1 << 2, Spinner = 1 << 3, ComboOffset = (1 << 4) | (1 << 5) | (1 << 6), - Hold = 1 << 7 + Hold = 1 << 7, + + ObjectTypes = Circle | Slider | Spinner | Hold } } From 8e1bd98386647a440ca3a6f9303accdb9c813565 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Fri, 6 Dec 2024 16:05:51 +0900 Subject: [PATCH 014/290] Split out + rename `PassThroughPatternGenerator` Better symbolises the intent of this generator which is to convert hitobjects in their most simple forms - anything with an end time converts to a hold or otherwise converts to a normal note. --- .../Beatmaps/ManiaBeatmapConverter.cs | 54 +--------------- .../Legacy/PassThroughPatternGenerator.cs | 61 +++++++++++++++++++ 2 files changed, 64 insertions(+), 51 deletions(-) create mode 100644 osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/PassThroughPatternGenerator.cs diff --git a/osu.Game.Rulesets.Mania/Beatmaps/ManiaBeatmapConverter.cs b/osu.Game.Rulesets.Mania/Beatmaps/ManiaBeatmapConverter.cs index 79e4c6020d..c469f4e4e9 100644 --- a/osu.Game.Rulesets.Mania/Beatmaps/ManiaBeatmapConverter.cs +++ b/osu.Game.Rulesets.Mania/Beatmaps/ManiaBeatmapConverter.cs @@ -147,7 +147,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps case LegacyHitObjectType.Circle: if (IsForCurrentRuleset) { - conversion = new SpecificBeatmapPatternGenerator(Random, original, beatmap, TotalColumns, lastPattern); + conversion = new PassThroughPatternGenerator(Random, original, beatmap, TotalColumns, lastPattern); recordNote(startTime, position); } else @@ -162,7 +162,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps case LegacyHitObjectType.Slider: if (IsForCurrentRuleset) { - conversion = new SpecificBeatmapPatternGenerator(Random, original, beatmap, TotalColumns, lastPattern); + conversion = new PassThroughPatternGenerator(Random, original, beatmap, TotalColumns, lastPattern); recordNote(original.StartTime, position); } else @@ -188,7 +188,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps break; case LegacyHitObjectType.Hold: - conversion = new SpecificBeatmapPatternGenerator(Random, original, beatmap, TotalColumns, lastPattern); + conversion = new PassThroughPatternGenerator(Random, original, beatmap, TotalColumns, lastPattern); recordNote(endTime, position); computeDensity(endTime); break; @@ -227,53 +227,5 @@ namespace osu.Game.Rulesets.Mania.Beatmaps lastTime = time; lastPosition = position; } - - /// - /// A pattern generator for osu!mania-specific beatmaps. - /// - private class SpecificBeatmapPatternGenerator : Patterns.Legacy.PatternGenerator - { - public SpecificBeatmapPatternGenerator(LegacyRandom random, HitObject hitObject, IBeatmap beatmap, int totalColumns, Pattern previousPattern) - : base(random, hitObject, beatmap, previousPattern, totalColumns) - { - } - - public override IEnumerable Generate() - { - yield return generate(); - } - - private Pattern generate() - { - var positionData = HitObject as IHasXPosition; - - int column = GetColumn(positionData?.X ?? 0); - - var pattern = new Pattern(); - - if (HitObject is IHasDuration endTimeData) - { - pattern.Add(new HoldNote - { - StartTime = HitObject.StartTime, - Duration = endTimeData.Duration, - Column = column, - Samples = HitObject.Samples, - NodeSamples = (HitObject as IHasRepeats)?.NodeSamples ?? HoldNote.CreateDefaultNodeSamples(HitObject) - }); - } - else if (HitObject is IHasXPosition) - { - pattern.Add(new Note - { - StartTime = HitObject.StartTime, - Samples = HitObject.Samples, - Column = column - }); - } - - return pattern; - } - } } } diff --git a/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/PassThroughPatternGenerator.cs b/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/PassThroughPatternGenerator.cs new file mode 100644 index 0000000000..a8d2dc5ae6 --- /dev/null +++ b/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/PassThroughPatternGenerator.cs @@ -0,0 +1,61 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Collections.Generic; +using osu.Game.Beatmaps; +using osu.Game.Rulesets.Mania.Objects; +using osu.Game.Rulesets.Objects; +using osu.Game.Rulesets.Objects.Types; +using osu.Game.Utils; + +namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy +{ + /// + /// A simple generator which, for any object, if the hitobject has an end time + /// it becomes a or otherwise a . + /// + internal class PassThroughPatternGenerator : PatternGenerator + { + public PassThroughPatternGenerator(LegacyRandom random, HitObject hitObject, IBeatmap beatmap, int totalColumns, Pattern previousPattern) + : base(random, hitObject, beatmap, previousPattern, totalColumns) + { + } + + public override IEnumerable Generate() + { + yield return generate(); + } + + private Pattern generate() + { + var positionData = HitObject as IHasXPosition; + + int column = GetColumn(positionData?.X ?? 0); + + var pattern = new Pattern(); + + if (HitObject is IHasDuration endTimeData) + { + pattern.Add(new HoldNote + { + StartTime = HitObject.StartTime, + Duration = endTimeData.Duration, + Column = column, + Samples = HitObject.Samples, + NodeSamples = (HitObject as IHasRepeats)?.NodeSamples ?? HoldNote.CreateDefaultNodeSamples(HitObject) + }); + } + else if (HitObject is IHasXPosition) + { + pattern.Add(new Note + { + StartTime = HitObject.StartTime, + Samples = HitObject.Samples, + Column = column + }); + } + + return pattern; + } + } +} From e65f8ba7a079e0ee55f3b2c1504b3653e5a8d9ed Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Fri, 6 Dec 2024 16:11:57 +0900 Subject: [PATCH 015/290] Simplify implementation --- .../Patterns/Legacy/PassThroughPatternGenerator.cs | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/PassThroughPatternGenerator.cs b/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/PassThroughPatternGenerator.cs index a8d2dc5ae6..6c22854d68 100644 --- a/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/PassThroughPatternGenerator.cs +++ b/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/PassThroughPatternGenerator.cs @@ -22,14 +22,8 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy } public override IEnumerable Generate() - { - yield return generate(); - } - - private Pattern generate() { var positionData = HitObject as IHasXPosition; - int column = GetColumn(positionData?.X ?? 0); var pattern = new Pattern(); @@ -45,7 +39,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy NodeSamples = (HitObject as IHasRepeats)?.NodeSamples ?? HoldNote.CreateDefaultNodeSamples(HitObject) }); } - else if (HitObject is IHasXPosition) + else { pattern.Add(new Note { @@ -55,7 +49,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy }); } - return pattern; + yield return pattern; } } } From e8728abc00a84f1b93eb2522049b54376e0d455f Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Fri, 6 Dec 2024 16:21:59 +0900 Subject: [PATCH 016/290] Rename `LegacyPatternGenerator` to stop naming conflicts --- osu.Game.Rulesets.Mania/Beatmaps/ManiaBeatmapConverter.cs | 2 +- .../Patterns/Legacy/EndTimeObjectPatternGenerator.cs | 2 +- .../Beatmaps/Patterns/Legacy/HitObjectPatternGenerator.cs | 2 +- .../{PatternGenerator.cs => LegacyPatternGenerator.cs} | 8 ++++---- .../Patterns/Legacy/PassThroughPatternGenerator.cs | 2 +- .../Patterns/Legacy/PathObjectPatternGenerator.cs | 2 +- 6 files changed, 9 insertions(+), 9 deletions(-) rename osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/{PatternGenerator.cs => LegacyPatternGenerator.cs} (96%) diff --git a/osu.Game.Rulesets.Mania/Beatmaps/ManiaBeatmapConverter.cs b/osu.Game.Rulesets.Mania/Beatmaps/ManiaBeatmapConverter.cs index c469f4e4e9..aefe60a3c9 100644 --- a/osu.Game.Rulesets.Mania/Beatmaps/ManiaBeatmapConverter.cs +++ b/osu.Game.Rulesets.Mania/Beatmaps/ManiaBeatmapConverter.cs @@ -140,7 +140,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps double endTime = (original as IHasDuration)?.EndTime ?? startTime; Vector2 position = (original as IHasPosition)?.Position ?? Vector2.Zero; - Patterns.PatternGenerator conversion; + PatternGenerator conversion; switch (legacy.LegacyType & LegacyHitObjectType.ObjectTypes) { diff --git a/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/EndTimeObjectPatternGenerator.cs b/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/EndTimeObjectPatternGenerator.cs index 52bb87ae19..12aba3a483 100644 --- a/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/EndTimeObjectPatternGenerator.cs +++ b/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/EndTimeObjectPatternGenerator.cs @@ -12,7 +12,7 @@ using osu.Game.Utils; namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy { - internal class EndTimeObjectPatternGenerator : PatternGenerator + internal class EndTimeObjectPatternGenerator : LegacyPatternGenerator { private readonly int endTime; private readonly PatternType convertType; diff --git a/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/HitObjectPatternGenerator.cs b/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/HitObjectPatternGenerator.cs index 9880369dfb..5af26d61f4 100644 --- a/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/HitObjectPatternGenerator.cs +++ b/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/HitObjectPatternGenerator.cs @@ -16,7 +16,7 @@ using osu.Game.Utils; namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy { - internal class HitObjectPatternGenerator : PatternGenerator + internal class HitObjectPatternGenerator : LegacyPatternGenerator { public PatternType StairType { get; private set; } diff --git a/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/PatternGenerator.cs b/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/LegacyPatternGenerator.cs similarity index 96% rename from osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/PatternGenerator.cs rename to osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/LegacyPatternGenerator.cs index 48b8778501..7a3033e68b 100644 --- a/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/PatternGenerator.cs +++ b/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/LegacyPatternGenerator.cs @@ -15,7 +15,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy /// /// A pattern generator for legacy hit objects. /// - internal abstract class PatternGenerator : Patterns.PatternGenerator + internal abstract class LegacyPatternGenerator : PatternGenerator { /// /// The column index at which to start generating random notes. @@ -27,7 +27,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy /// protected readonly LegacyRandom Random; - protected PatternGenerator(LegacyRandom random, HitObject hitObject, IBeatmap beatmap, Pattern previousPattern, int totalColumns) + protected LegacyPatternGenerator(LegacyRandom random, HitObject hitObject, IBeatmap beatmap, Pattern previousPattern, int totalColumns) : base(hitObject, beatmap, totalColumns, previousPattern) { ArgumentNullException.ThrowIfNull(random); @@ -132,7 +132,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy /// A function to retrieve the next column. If null, a randomisation scheme will be used. /// A function to perform additional validation checks to determine if a column is a valid candidate for a . /// The minimum column index. If null, is used. - /// The maximum column index. If null, TotalColumns is used. + /// The maximum column index. If null, TotalColumns is used. /// A list of patterns for which the validity of a column should be checked against. /// A column is not a valid candidate if a occupies the same column in any of the patterns. /// A column which has passed the check and for which there are no @@ -189,7 +189,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy /// Returns a random column index in the range [, ). /// /// The minimum column index. If null, is used. - /// The maximum column index. If null, is used. + /// The maximum column index. If null, is used. protected int GetRandomColumn(int? lowerBound = null, int? upperBound = null) => Random.Next(lowerBound ?? RandomStart, upperBound ?? TotalColumns); /// diff --git a/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/PassThroughPatternGenerator.cs b/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/PassThroughPatternGenerator.cs index 6c22854d68..efeb99e8b4 100644 --- a/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/PassThroughPatternGenerator.cs +++ b/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/PassThroughPatternGenerator.cs @@ -14,7 +14,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy /// A simple generator which, for any object, if the hitobject has an end time /// it becomes a or otherwise a . /// - internal class PassThroughPatternGenerator : PatternGenerator + internal class PassThroughPatternGenerator : LegacyPatternGenerator { public PassThroughPatternGenerator(LegacyRandom random, HitObject hitObject, IBeatmap beatmap, int totalColumns, Pattern previousPattern) : base(random, hitObject, beatmap, previousPattern, totalColumns) diff --git a/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/PathObjectPatternGenerator.cs b/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/PathObjectPatternGenerator.cs index c54da74424..cd608161ee 100644 --- a/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/PathObjectPatternGenerator.cs +++ b/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/PathObjectPatternGenerator.cs @@ -21,7 +21,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy /// /// A pattern generator for IHasDistance hit objects. /// - internal class PathObjectPatternGenerator : PatternGenerator + internal class PathObjectPatternGenerator : LegacyPatternGenerator { public readonly int StartTime; public readonly int EndTime; From 1bbf32d56768cffba66fbbc3a7776647d5956fe3 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Fri, 6 Dec 2024 16:27:31 +0900 Subject: [PATCH 017/290] Add some explanatory comments In particular, the spinner one is the most relevant to this batch of changes. --- osu.Game.Rulesets.Mania/Beatmaps/ManiaBeatmapConverter.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/osu.Game.Rulesets.Mania/Beatmaps/ManiaBeatmapConverter.cs b/osu.Game.Rulesets.Mania/Beatmaps/ManiaBeatmapConverter.cs index aefe60a3c9..b91aa5f6e1 100644 --- a/osu.Game.Rulesets.Mania/Beatmaps/ManiaBeatmapConverter.cs +++ b/osu.Game.Rulesets.Mania/Beatmaps/ManiaBeatmapConverter.cs @@ -152,6 +152,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps } else { + // Note: The density is used during the pattern generator constructor, and intentionally computed first. computeDensity(startTime); conversion = new HitObjectPatternGenerator(Random, original, beatmap, TotalColumns, lastPattern, lastTime, lastPosition, density, lastStair); recordNote(startTime, position); @@ -182,6 +183,8 @@ namespace osu.Game.Rulesets.Mania.Beatmaps break; case LegacyHitObjectType.Spinner: + // Note: Some older mania-specific beatmaps can have spinners that are converted rather than passed through. + // Newer beatmaps will usually use the "hold" hitobject type below. conversion = new EndTimeObjectPatternGenerator(Random, original, beatmap, TotalColumns, lastPattern); recordNote(endTime, new Vector2(256, 192)); computeDensity(endTime); From e703d9e814df82b30c76ffb44b0afa42f1228f6d Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Fri, 6 Dec 2024 17:16:04 +0900 Subject: [PATCH 018/290] NRT refactorings + rename generators to match usage In particular, "EndTimeObject" is no longer correct - it's strictly used for spinners and not holds. --- .../Beatmaps/ManiaBeatmapConverter.cs | 10 +++++----- ...nGenerator.cs => HitCirclePatternGenerator.cs} | 15 +++++++++------ .../Patterns/Legacy/LegacyPatternGenerator.cs | 8 +++----- ...ternGenerator.cs => SliderPatternGenerator.cs} | 12 +++++------- ...ernGenerator.cs => SpinnerPatternGenerator.cs} | 7 +++++-- .../Beatmaps/Patterns/Pattern.cs | 8 ++++---- 6 files changed, 31 insertions(+), 29 deletions(-) rename osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/{HitObjectPatternGenerator.cs => HitCirclePatternGenerator.cs} (96%) rename osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/{PathObjectPatternGenerator.cs => SliderPatternGenerator.cs} (97%) rename osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/{EndTimeObjectPatternGenerator.cs => SpinnerPatternGenerator.cs} (91%) diff --git a/osu.Game.Rulesets.Mania/Beatmaps/ManiaBeatmapConverter.cs b/osu.Game.Rulesets.Mania/Beatmaps/ManiaBeatmapConverter.cs index b91aa5f6e1..0792c75e54 100644 --- a/osu.Game.Rulesets.Mania/Beatmaps/ManiaBeatmapConverter.cs +++ b/osu.Game.Rulesets.Mania/Beatmaps/ManiaBeatmapConverter.cs @@ -154,7 +154,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps { // Note: The density is used during the pattern generator constructor, and intentionally computed first. computeDensity(startTime); - conversion = new HitObjectPatternGenerator(Random, original, beatmap, TotalColumns, lastPattern, lastTime, lastPosition, density, lastStair); + conversion = new HitCirclePatternGenerator(Random, original, beatmap, TotalColumns, lastPattern, lastTime, lastPosition, density, lastStair); recordNote(startTime, position); } @@ -168,7 +168,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps } else { - var generator = new PathObjectPatternGenerator(Random, original, beatmap, TotalColumns, lastPattern); + var generator = new SliderPatternGenerator(Random, original, beatmap, TotalColumns, lastPattern); conversion = generator; for (int i = 0; i <= generator.SpanCount; i++) @@ -185,7 +185,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps case LegacyHitObjectType.Spinner: // Note: Some older mania-specific beatmaps can have spinners that are converted rather than passed through. // Newer beatmaps will usually use the "hold" hitobject type below. - conversion = new EndTimeObjectPatternGenerator(Random, original, beatmap, TotalColumns, lastPattern); + conversion = new SpinnerPatternGenerator(Random, original, beatmap, TotalColumns, lastPattern); recordNote(endTime, new Vector2(256, 192)); computeDensity(endTime); break; @@ -202,8 +202,8 @@ namespace osu.Game.Rulesets.Mania.Beatmaps foreach (var newPattern in conversion.Generate()) { - lastPattern = conversion is EndTimeObjectPatternGenerator ? lastPattern : newPattern; - lastStair = (conversion as HitObjectPatternGenerator)?.StairType ?? lastStair; + lastPattern = conversion is SpinnerPatternGenerator ? lastPattern : newPattern; + lastStair = (conversion as HitCirclePatternGenerator)?.StairType ?? lastStair; foreach (var obj in newPattern.HitObjects) yield return obj; diff --git a/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/HitObjectPatternGenerator.cs b/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/HitCirclePatternGenerator.cs similarity index 96% rename from osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/HitObjectPatternGenerator.cs rename to osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/HitCirclePatternGenerator.cs index 5af26d61f4..28499f3edc 100644 --- a/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/HitObjectPatternGenerator.cs +++ b/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/HitCirclePatternGenerator.cs @@ -16,13 +16,16 @@ using osu.Game.Utils; namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy { - internal class HitObjectPatternGenerator : LegacyPatternGenerator + /// + /// Converter for legacy "HitCircle" hit objects. + /// + internal class HitCirclePatternGenerator : LegacyPatternGenerator { public PatternType StairType { get; private set; } private readonly PatternType convertType; - public HitObjectPatternGenerator(LegacyRandom random, HitObject hitObject, IBeatmap beatmap, int totalColumns, Pattern previousPattern, double previousTime, Vector2 previousPosition, + public HitCirclePatternGenerator(LegacyRandom random, HitObject hitObject, IBeatmap beatmap, int totalColumns, Pattern previousPattern, double previousTime, Vector2 previousPosition, double density, PatternType lastStair) : base(random, hitObject, beatmap, previousPattern, totalColumns) { @@ -114,10 +117,10 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy } if (convertType.HasFlag(PatternType.Cycle) && PreviousPattern.HitObjects.Count() == 1 - // If we convert to 7K + 1, let's not overload the special key - && (TotalColumns != 8 || lastColumn != 0) - // Make sure the last column was not the centre column - && (TotalColumns % 2 == 0 || lastColumn != TotalColumns / 2)) + // If we convert to 7K + 1, let's not overload the special key + && (TotalColumns != 8 || lastColumn != 0) + // Make sure the last column was not the centre column + && (TotalColumns % 2 == 0 || lastColumn != TotalColumns / 2)) { // Generate a new pattern by cycling backwards (similar to Reverse but for only one hit object) int column = RandomStart + TotalColumns - lastColumn - 1; diff --git a/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/LegacyPatternGenerator.cs b/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/LegacyPatternGenerator.cs index 7a3033e68b..a7ced095b3 100644 --- a/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/LegacyPatternGenerator.cs +++ b/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/LegacyPatternGenerator.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System; using System.Linq; using JetBrains.Annotations; @@ -96,8 +94,8 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy if (conversionDifficulty != null) return conversionDifficulty.Value; - HitObject lastObject = Beatmap.HitObjects.LastOrDefault(); - HitObject firstObject = Beatmap.HitObjects.FirstOrDefault(); + HitObject? lastObject = Beatmap.HitObjects.LastOrDefault(); + HitObject? firstObject = Beatmap.HitObjects.FirstOrDefault(); // Drain time in seconds int drainTime = (int)(((lastObject?.StartTime ?? 0) - (firstObject?.StartTime ?? 0) - Beatmap.TotalBreakTime) / 1000); @@ -138,7 +136,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy /// A column which has passed the check and for which there are no /// s in any of occupying the same column. /// If there are no valid candidate columns. - protected int FindAvailableColumn(int initialColumn, int? lowerBound = null, int? upperBound = null, Func nextColumn = null, [InstantHandle] Func validation = null, + protected int FindAvailableColumn(int initialColumn, int? lowerBound = null, int? upperBound = null, Func? nextColumn = null, [InstantHandle] Func? validation = null, params Pattern[] patterns) { lowerBound ??= RandomStart; diff --git a/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/PathObjectPatternGenerator.cs b/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/SliderPatternGenerator.cs similarity index 97% rename from osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/PathObjectPatternGenerator.cs rename to osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/SliderPatternGenerator.cs index cd608161ee..e539baa94a 100644 --- a/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/PathObjectPatternGenerator.cs +++ b/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/SliderPatternGenerator.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System; using System.Collections.Generic; using System.Diagnostics; @@ -19,9 +17,9 @@ using osu.Game.Utils; namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy { /// - /// A pattern generator for IHasDistance hit objects. + /// Converter for legacy "Slider" hit objects. /// - internal class PathObjectPatternGenerator : LegacyPatternGenerator + internal class SliderPatternGenerator : LegacyPatternGenerator { public readonly int StartTime; public readonly int EndTime; @@ -30,7 +28,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy private PatternType convertType; - public PathObjectPatternGenerator(LegacyRandom random, HitObject hitObject, IBeatmap beatmap, int totalColumns, Pattern previousPattern) + public SliderPatternGenerator(LegacyRandom random, HitObject hitObject, IBeatmap beatmap, int totalColumns, Pattern previousPattern) : base(random, hitObject, beatmap, previousPattern, totalColumns) { convertType = PatternType.None; @@ -484,9 +482,9 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy /// Retrieves the list of node samples that occur at time greater than or equal to . /// /// The time to retrieve node samples at. - private IList> nodeSamplesAt(int time) + private IList>? nodeSamplesAt(int time) { - if (!(HitObject is IHasPathWithRepeats curveData)) + if (HitObject is not IHasPathWithRepeats curveData) return null; int index = SegmentDuration == 0 ? 0 : (time - StartTime) / SegmentDuration; diff --git a/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/EndTimeObjectPatternGenerator.cs b/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/SpinnerPatternGenerator.cs similarity index 91% rename from osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/EndTimeObjectPatternGenerator.cs rename to osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/SpinnerPatternGenerator.cs index 12aba3a483..39896d3e13 100644 --- a/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/EndTimeObjectPatternGenerator.cs +++ b/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/SpinnerPatternGenerator.cs @@ -12,12 +12,15 @@ using osu.Game.Utils; namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy { - internal class EndTimeObjectPatternGenerator : LegacyPatternGenerator + /// + /// Converter for legacy "Spinner" hit objects. + /// + internal class SpinnerPatternGenerator : LegacyPatternGenerator { private readonly int endTime; private readonly PatternType convertType; - public EndTimeObjectPatternGenerator(LegacyRandom random, HitObject hitObject, IBeatmap beatmap, int totalColumns, Pattern previousPattern) + public SpinnerPatternGenerator(LegacyRandom random, HitObject hitObject, IBeatmap beatmap, int totalColumns, Pattern previousPattern) : base(random, hitObject, beatmap, previousPattern, totalColumns) { endTime = (int)((HitObject as IHasDuration)?.EndTime ?? 0); diff --git a/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Pattern.cs b/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Pattern.cs index 4b3902657f..9e4d8b599e 100644 --- a/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Pattern.cs +++ b/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Pattern.cs @@ -1,9 +1,8 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.Linq; using osu.Game.Rulesets.Mania.Objects; @@ -14,8 +13,8 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns /// internal class Pattern { - private List hitObjects; - private HashSet containedColumns; + private List? hitObjects; + private HashSet? containedColumns; /// /// All the hit objects contained in this pattern. @@ -72,6 +71,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns containedColumns?.Clear(); } + [MemberNotNull(nameof(hitObjects), nameof(containedColumns))] private void prepareStorage() { hitObjects ??= new List(); From 8dda5aada88523eda32272a4095dac5a085b577a Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Fri, 6 Dec 2024 17:29:17 +0900 Subject: [PATCH 019/290] Populate default `LegacyType` value on convert hitobjects Normally not an issue, but some tests create their own hitobjects deriving from `ConvertHitObject`. --- osu.Game/Rulesets/Objects/Legacy/ConvertHitObject.cs | 2 +- osu.Game/Rulesets/Objects/Legacy/ConvertHold.cs | 6 ++++++ osu.Game/Rulesets/Objects/Legacy/ConvertSlider.cs | 6 ++++++ osu.Game/Rulesets/Objects/Legacy/ConvertSpinner.cs | 6 ++++++ 4 files changed, 19 insertions(+), 1 deletion(-) diff --git a/osu.Game/Rulesets/Objects/Legacy/ConvertHitObject.cs b/osu.Game/Rulesets/Objects/Legacy/ConvertHitObject.cs index 28683583ee..ced9b24ebf 100644 --- a/osu.Game/Rulesets/Objects/Legacy/ConvertHitObject.cs +++ b/osu.Game/Rulesets/Objects/Legacy/ConvertHitObject.cs @@ -27,7 +27,7 @@ namespace osu.Game.Rulesets.Objects.Legacy public Vector2 Position { get; set; } - public LegacyHitObjectType LegacyType { get; set; } + public LegacyHitObjectType LegacyType { get; set; } = LegacyHitObjectType.Circle; public override Judgement CreateJudgement() => new IgnoreJudgement(); diff --git a/osu.Game/Rulesets/Objects/Legacy/ConvertHold.cs b/osu.Game/Rulesets/Objects/Legacy/ConvertHold.cs index d74224892b..939e4a495f 100644 --- a/osu.Game/Rulesets/Objects/Legacy/ConvertHold.cs +++ b/osu.Game/Rulesets/Objects/Legacy/ConvertHold.cs @@ -1,6 +1,7 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using osu.Game.Beatmaps.Legacy; using osu.Game.Rulesets.Objects.Types; namespace osu.Game.Rulesets.Objects.Legacy @@ -16,5 +17,10 @@ namespace osu.Game.Rulesets.Objects.Legacy public double Duration { get; set; } public double EndTime => StartTime + Duration; + + public ConvertHold() + { + LegacyType = LegacyHitObjectType.Hold; + } } } diff --git a/osu.Game/Rulesets/Objects/Legacy/ConvertSlider.cs b/osu.Game/Rulesets/Objects/Legacy/ConvertSlider.cs index fee68f2f11..dbbe142944 100644 --- a/osu.Game/Rulesets/Objects/Legacy/ConvertSlider.cs +++ b/osu.Game/Rulesets/Objects/Legacy/ConvertSlider.cs @@ -8,6 +8,7 @@ using osu.Framework.Bindables; using osu.Game.Audio; using osu.Game.Beatmaps; using osu.Game.Beatmaps.ControlPoints; +using osu.Game.Beatmaps.Legacy; namespace osu.Game.Rulesets.Objects.Legacy { @@ -56,6 +57,11 @@ namespace osu.Game.Rulesets.Objects.Legacy public bool GenerateTicks { get; set; } = true; + public ConvertSlider() + { + LegacyType = LegacyHitObjectType.Slider; + } + protected override void ApplyDefaultsToSelf(ControlPointInfo controlPointInfo, IBeatmapDifficultyInfo difficulty) { base.ApplyDefaultsToSelf(controlPointInfo, difficulty); diff --git a/osu.Game/Rulesets/Objects/Legacy/ConvertSpinner.cs b/osu.Game/Rulesets/Objects/Legacy/ConvertSpinner.cs index 59551cd37a..c2b4a9e16b 100644 --- a/osu.Game/Rulesets/Objects/Legacy/ConvertSpinner.cs +++ b/osu.Game/Rulesets/Objects/Legacy/ConvertSpinner.cs @@ -1,6 +1,7 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using osu.Game.Beatmaps.Legacy; using osu.Game.Rulesets.Objects.Types; namespace osu.Game.Rulesets.Objects.Legacy @@ -16,5 +17,10 @@ namespace osu.Game.Rulesets.Objects.Legacy public double Duration { get; set; } public double EndTime => StartTime + Duration; + + public ConvertSpinner() + { + LegacyType = LegacyHitObjectType.Spinner; + } } } From ec8b320e21ddb366c5527ba80d00c0414ca8a38d Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Fri, 6 Dec 2024 17:45:19 +0900 Subject: [PATCH 020/290] Handle non-legacy types Also used in some tests (e.g. beatmaps containing `HitCircle`s). --- .../Beatmaps/ManiaBeatmapConverter.cs | 34 ++++++++++++++----- 1 file changed, 26 insertions(+), 8 deletions(-) diff --git a/osu.Game.Rulesets.Mania/Beatmaps/ManiaBeatmapConverter.cs b/osu.Game.Rulesets.Mania/Beatmaps/ManiaBeatmapConverter.cs index 0792c75e54..79234a3ba2 100644 --- a/osu.Game.Rulesets.Mania/Beatmaps/ManiaBeatmapConverter.cs +++ b/osu.Game.Rulesets.Mania/Beatmaps/ManiaBeatmapConverter.cs @@ -126,23 +126,41 @@ namespace osu.Game.Rulesets.Mania.Beatmaps protected override IEnumerable ConvertHitObject(HitObject original, IBeatmap beatmap, CancellationToken cancellationToken) { - if (original is ManiaHitObject maniaObj) + LegacyHitObjectType legacyType; + + switch (original) { - yield return maniaObj; + case ManiaHitObject maniaObj: + { + yield return maniaObj; - yield break; + yield break; + } + + case IHasLegacyHitObjectType legacy: + legacyType = legacy.LegacyType & LegacyHitObjectType.ObjectTypes; + break; + + case IHasPath: + legacyType = LegacyHitObjectType.Slider; + break; + + case IHasDuration: + legacyType = LegacyHitObjectType.Hold; + break; + + default: + legacyType = LegacyHitObjectType.Circle; + break; } - if (original is not IHasLegacyHitObjectType legacy) - yield break; - double startTime = original.StartTime; double endTime = (original as IHasDuration)?.EndTime ?? startTime; Vector2 position = (original as IHasPosition)?.Position ?? Vector2.Zero; PatternGenerator conversion; - switch (legacy.LegacyType & LegacyHitObjectType.ObjectTypes) + switch (legacyType) { case LegacyHitObjectType.Circle: if (IsForCurrentRuleset) @@ -197,7 +215,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps break; default: - throw new ArgumentException($"Invalid legacy object type: {legacy.LegacyType}", nameof(original)); + throw new ArgumentException($"Invalid legacy object type: {legacyType}", nameof(original)); } foreach (var newPattern in conversion.Generate()) From 0a00f7a7c21b8db0d0d5ea73814a64767d7ea59e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20M=C3=BCller?= Date: Sat, 7 Dec 2024 11:11:43 +0900 Subject: [PATCH 021/290] Implement skinnable mod display MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Also makes the mod display initialization sequence (start expanded, then unexpand) controlled by HUDOverlay rather than mod display itself. This enabled different treatment depending on whether the mod display is viewed in the skin editor or in the player. Co-authored-by: Bartłomiej Dach --- .../Visual/Gameplay/TestSceneSkinEditor.cs | 6 ++ osu.Game/Rulesets/UI/ModIcon.cs | 13 +++- osu.Game/Screens/Play/HUD/ModDisplay.cs | 75 +++++++++++++------ .../Screens/Play/HUD/SkinnableModDisplay.cs | 51 +++++++++++++ osu.Game/Screens/Play/HUDOverlay.cs | 11 ++- 5 files changed, 133 insertions(+), 23 deletions(-) create mode 100644 osu.Game/Screens/Play/HUD/SkinnableModDisplay.cs diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSkinEditor.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSkinEditor.cs index 91188f5bac..49a8a65cd0 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneSkinEditor.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSkinEditor.cs @@ -20,6 +20,7 @@ using osu.Game.Overlays.Settings; using osu.Game.Overlays.SkinEditor; using osu.Game.Rulesets; using osu.Game.Rulesets.Osu; +using osu.Game.Rulesets.Osu.Mods; using osu.Game.Screens.Edit; using osu.Game.Screens.Play.HUD; using osu.Game.Screens.Play.HUD.HitErrorMeters; @@ -53,6 +54,11 @@ namespace osu.Game.Tests.Visual.Gameplay { base.SetUpSteps(); + AddStep("Add DT and HD", () => + { + LoadPlayer([new OsuModDoubleTime { SpeedChange = { Value = 1.337 } }, new OsuModHidden()]); + }); + AddStep("reset skin", () => skins.CurrentSkinInfo.SetDefault()); AddUntilStep("wait for hud load", () => targetContainer.ComponentsLoaded); diff --git a/osu.Game/Rulesets/UI/ModIcon.cs b/osu.Game/Rulesets/UI/ModIcon.cs index 5237425075..6abc7355d5 100644 --- a/osu.Game/Rulesets/UI/ModIcon.cs +++ b/osu.Game/Rulesets/UI/ModIcon.cs @@ -39,7 +39,18 @@ namespace osu.Game.Rulesets.UI private IMod mod; private readonly bool showTooltip; - private readonly bool showExtendedInformation; + + private bool showExtendedInformation; + + public bool ShowExtendedInformation + { + get => showExtendedInformation; + set + { + showExtendedInformation = value; + updateExtendedInformation(); + } + } public IMod Mod { diff --git a/osu.Game/Screens/Play/HUD/ModDisplay.cs b/osu.Game/Screens/Play/HUD/ModDisplay.cs index b37d41e7a2..9f42175a70 100644 --- a/osu.Game/Screens/Play/HUD/ModDisplay.cs +++ b/osu.Game/Screens/Play/HUD/ModDisplay.cs @@ -20,9 +20,27 @@ namespace osu.Game.Screens.Play.HUD /// public partial class ModDisplay : CompositeDrawable, IHasCurrentValue> { - private const int fade_duration = 1000; + private ExpansionMode expansionMode = ExpansionMode.ExpandOnHover; - public ExpansionMode ExpansionMode = ExpansionMode.ExpandOnHover; + public ExpansionMode ExpansionMode + { + get => expansionMode; + set + { + if (expansionMode == value) + return; + + expansionMode = value; + + if (IsLoaded) + { + if (expansionMode == ExpansionMode.AlwaysExpanded || (expansionMode == ExpansionMode.ExpandOnHover && IsHovered)) + expand(); + else if (expansionMode == ExpansionMode.AlwaysContracted || (expansionMode == ExpansionMode.ExpandOnHover && !IsHovered)) + contract(); + } + } + } private readonly BindableWithCurrent> current = new BindableWithCurrent>(Array.Empty()); @@ -37,7 +55,19 @@ namespace osu.Game.Screens.Play.HUD } } - private readonly bool showExtendedInformation; + private bool showExtendedInformation; + + public bool ShowExtendedInformation + { + get => showExtendedInformation; + set + { + showExtendedInformation = value; + foreach (var icon in iconsContainer) + icon.ShowExtendedInformation = value; + } + } + private readonly FillFlowContainer iconsContainer; public ModDisplay(bool showExtendedInformation = true) @@ -59,10 +89,23 @@ namespace osu.Game.Screens.Play.HUD Current.BindValueChanged(updateDisplay, true); - iconsContainer.FadeInFromZero(fade_duration, Easing.OutQuint); + switch (expansionMode) + { + case ExpansionMode.AlwaysExpanded: + expand(0); + break; - if (ExpansionMode == ExpansionMode.AlwaysExpanded || ExpansionMode == ExpansionMode.AlwaysContracted) - FinishTransforms(true); + case ExpansionMode.AlwaysContracted: + contract(0); + break; + + case ExpansionMode.ExpandOnHover: + if (IsHovered) + expand(0); + else + contract(0); + break; + } } private void updateDisplay(ValueChangedEvent> mods) @@ -71,28 +114,18 @@ namespace osu.Game.Screens.Play.HUD foreach (Mod mod in mods.NewValue.AsOrdered()) iconsContainer.Add(new ModIcon(mod, showExtendedInformation: showExtendedInformation) { Scale = new Vector2(0.6f) }); - - appearTransform(); } - private void appearTransform() - { - expand(); - - using (iconsContainer.BeginDelayedSequence(1200)) - contract(); - } - - private void expand() + private void expand(double duration = 500) { if (ExpansionMode != ExpansionMode.AlwaysContracted) - iconsContainer.TransformSpacingTo(new Vector2(5, 0), 500, Easing.OutQuint); + iconsContainer.TransformSpacingTo(new Vector2(5, 0), duration, Easing.OutQuint); } - private void contract() + private void contract(double duration = 500) { if (ExpansionMode != ExpansionMode.AlwaysExpanded) - iconsContainer.TransformSpacingTo(new Vector2(-25, 0), 500, Easing.OutQuint); + iconsContainer.TransformSpacingTo(new Vector2(-25, 0), duration, Easing.OutQuint); } protected override bool OnHover(HoverEvent e) @@ -123,6 +156,6 @@ namespace osu.Game.Screens.Play.HUD /// /// The will always be contracted. /// - AlwaysContracted + AlwaysContracted, } } diff --git a/osu.Game/Screens/Play/HUD/SkinnableModDisplay.cs b/osu.Game/Screens/Play/HUD/SkinnableModDisplay.cs new file mode 100644 index 0000000000..ce4a4e978e --- /dev/null +++ b/osu.Game/Screens/Play/HUD/SkinnableModDisplay.cs @@ -0,0 +1,51 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Collections.Generic; +using osu.Framework.Allocation; +using osu.Framework.Bindables; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Game.Configuration; +using osu.Game.Rulesets.Mods; +using osu.Game.Skinning; + +namespace osu.Game.Screens.Play.HUD +{ + /// + /// Displays a single-line horizontal auto-sized flow of mods. For cases where wrapping is required, use instead. + /// + public partial class SkinnableModDisplay : CompositeDrawable, ISerialisableDrawable + { + private ModDisplay modDisplay = null!; + + [Resolved] + private Bindable> mods { get; set; } = null!; + + [SettingSource("Show extended info", "Whether to show extended information for each mod.")] + public Bindable ShowExtendedInformation { get; } = new Bindable(true); + + [SettingSource("Expansion mode", "How the mod display expands when interacted with.")] + public Bindable ExpansionModeSetting { get; } = new Bindable(ExpansionMode.ExpandOnHover); + + [BackgroundDependencyLoader] + private void load() + { + InternalChild = modDisplay = new ModDisplay(); + modDisplay.Current = mods; + AutoSizeAxes = Axes.Both; + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + ShowExtendedInformation.BindValueChanged(_ => modDisplay.ShowExtendedInformation = ShowExtendedInformation.Value, true); + ExpansionModeSetting.BindValueChanged(_ => modDisplay.ExpansionMode = ExpansionModeSetting.Value, true); + + FinishTransforms(true); + } + + public bool UsesFixedAnchor { get; set; } + } +} diff --git a/osu.Game/Screens/Play/HUDOverlay.cs b/osu.Game/Screens/Play/HUDOverlay.cs index fca871e42f..5d92fee841 100644 --- a/osu.Game/Screens/Play/HUDOverlay.cs +++ b/osu.Game/Screens/Play/HUDOverlay.cs @@ -35,6 +35,8 @@ namespace osu.Game.Screens.Play { public const float FADE_DURATION = 300; + private const float mods_fade_duration = 1000; + public const Easing FADE_EASING = Easing.OutQuint; /// @@ -85,7 +87,6 @@ namespace osu.Game.Screens.Play private readonly BindableBool replayLoaded = new BindableBool(); private static bool hasShownNotificationOnce; - private readonly FillFlowContainer bottomRightElements; private readonly FillFlowContainer topRightElements; @@ -248,6 +249,14 @@ namespace osu.Game.Screens.Play updateVisibility(); }, true); + + ModDisplay.ExpansionMode = ExpansionMode.AlwaysExpanded; + Scheduler.AddDelayed(() => + { + ModDisplay.ExpansionMode = ExpansionMode.ExpandOnHover; + }, 1200); + + ModDisplay.FadeInFromZero(mods_fade_duration, FADE_EASING); } protected override void Update() From db18492fbc36064ca11ab4d5c485111201906e97 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20M=C3=BCller?= Date: Sat, 7 Dec 2024 13:12:09 +0900 Subject: [PATCH 022/290] Update default osk for skinnable mod display --- .../Archives/modified-default-20241207.osk | Bin 0 -> 1661 bytes osu.Game.Tests/Skins/SkinDeserialisationTest.cs | 4 +++- 2 files changed, 3 insertions(+), 1 deletion(-) create mode 100644 osu.Game.Tests/Resources/Archives/modified-default-20241207.osk diff --git a/osu.Game.Tests/Resources/Archives/modified-default-20241207.osk b/osu.Game.Tests/Resources/Archives/modified-default-20241207.osk new file mode 100644 index 0000000000000000000000000000000000000000..8ed25fa8f43000764ac2949525b600ba6dd7d051 GIT binary patch literal 1661 zcmWIWW@Zs#U|`^2*p|^Ad3F9}u|+`ML9hq|LveOyo?d2NrfQEN-ys75mg@aU3%P{6 z0;HCdgp`ChG;8R6WYu|N{p3bhg*2=GBW{&Nw=ecR`~5FOw6EN4{k_*Op0xR?ZdB

s$CWvpaJhzM6zc;gYTHDJI$M-hetZt_6y6!A~DVl+qdevfm zPu4#(kUVR@Wl2SFX6)L5e`J3;LEqAN!x{ZX2*?!ulod;WZ~>DZATSLm4( z=b4t|nN~M1Jub@bN{ns!0@&a8Y{)u*8CU?a;qV~J3>E(5CbmgKxTS;@@6 zATGziAO>`vZ(?SiN2rT)er`d2UTR)RG1#4NX9niqG7va^|F>qqH?BYrg>3b&3m2#Q zTsGOV?X`2LtD2<{f3ouJg&S7guids#m$6(Zm?QV<2ZvAFi_hJ(zx#92?&heoKUrfE z+~)dkO^c16UsTe~#ib z1J`!{3wPee&fBGKllp~glE^QKq!riYc3sOp{jlHm`kz-@{rpnSJ)GgBXQ}ix%3fYT zroQKHRe0vgZx1$P>|wV!yCmz!3-K?)Qt3U~e=}VF94zd5Ijcg`q2wda zq~prfh`zQkRG-aUuWcoIDg37A+wyg<;`W~G z;8-SblIOTGm(%y`wLiCc=tgLlue(%~k%NbHjg8I~MZa&~?vn?rYwRP_8L#Md;oM$ge-6_20SFW3K#%}jt z9Ofsv4~I{^)_kEY&haka-n%X0pR=6mm;5|8X;t`d-Zyvoa=T;B&DIunNKk83o8U5I zR#2`btMo>uG+~#oUMpp$ub8lD!_HX6%zyb}d*>*g-+1Nxm;doEt#WJ>{{kgPY>+7PT#*2-1yZ>{eX4X@jNoUP~nbZb|`G7beC$TauGc_j# zQJ8Rf>uQ}lb3S-ekiiwkkFi}sr-dgM1=)VS^r^Aws)ONzjHtlOG*80`^0M3_E1s@m z1r`U4Od<@p%Uz%~5YPysV5Km+F7#3ks)vE0@dQ*Cyv#$_ie3UCw5BuSDv!|3Ko1y% a86vFc!4%-l$_A2W0m6?!x(uj-fdK$KrKltT literal 0 HcmV?d00001 diff --git a/osu.Game.Tests/Skins/SkinDeserialisationTest.cs b/osu.Game.Tests/Skins/SkinDeserialisationTest.cs index 7372557161..962a9b2a7a 100644 --- a/osu.Game.Tests/Skins/SkinDeserialisationTest.cs +++ b/osu.Game.Tests/Skins/SkinDeserialisationTest.cs @@ -68,7 +68,9 @@ namespace osu.Game.Tests.Skins // Covers legacy rank display "Archives/modified-classic-20230809.osk", // Covers legacy key counter - "Archives/modified-classic-20240724.osk" + "Archives/modified-classic-20240724.osk", + // Covers skinnable mod display + "Archives/modified-default-20241207.osk", }; ///

From 13759f5aa034c70c2df34805b74c4b1da5dc839a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sat, 7 Dec 2024 13:47:09 +0900 Subject: [PATCH 023/290] Back out test change It was mostly a demonstrative thing to use in the heat in the moment for the skinnable mod display and it breaks all other tests. So let's just not. --- osu.Game.Tests/Visual/Gameplay/TestSceneSkinEditor.cs | 5 ----- 1 file changed, 5 deletions(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSkinEditor.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSkinEditor.cs index 49a8a65cd0..61ccc8b82c 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneSkinEditor.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSkinEditor.cs @@ -54,11 +54,6 @@ namespace osu.Game.Tests.Visual.Gameplay { base.SetUpSteps(); - AddStep("Add DT and HD", () => - { - LoadPlayer([new OsuModDoubleTime { SpeedChange = { Value = 1.337 } }, new OsuModHidden()]); - }); - AddStep("reset skin", () => skins.CurrentSkinInfo.SetDefault()); AddUntilStep("wait for hud load", () => targetContainer.ComponentsLoaded); From 2713ae601a2bbbf4b7c389968c23e76c392b9a42 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sat, 7 Dec 2024 14:41:30 +0900 Subject: [PATCH 024/290] Remove unused using --- osu.Game.Tests/Visual/Gameplay/TestSceneSkinEditor.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSkinEditor.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSkinEditor.cs index 61ccc8b82c..91188f5bac 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneSkinEditor.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSkinEditor.cs @@ -20,7 +20,6 @@ using osu.Game.Overlays.Settings; using osu.Game.Overlays.SkinEditor; using osu.Game.Rulesets; using osu.Game.Rulesets.Osu; -using osu.Game.Rulesets.Osu.Mods; using osu.Game.Screens.Edit; using osu.Game.Screens.Play.HUD; using osu.Game.Screens.Play.HUD.HitErrorMeters; From 12e5999700bf1a6082b3fbc64a372bf2164e158a Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Fri, 13 Dec 2024 15:53:27 +0900 Subject: [PATCH 025/290] Add another failing test --- .../ManiaBeatmapConversionTest.cs | 1 + .../Beatmaps/4869637-expected-conversion.json | 1 + .../Resources/Testing/Beatmaps/4869637.osu | 1442 +++++++++++++++++ 3 files changed, 1444 insertions(+) create mode 100644 osu.Game.Rulesets.Mania.Tests/Resources/Testing/Beatmaps/4869637-expected-conversion.json create mode 100644 osu.Game.Rulesets.Mania.Tests/Resources/Testing/Beatmaps/4869637.osu diff --git a/osu.Game.Rulesets.Mania.Tests/ManiaBeatmapConversionTest.cs b/osu.Game.Rulesets.Mania.Tests/ManiaBeatmapConversionTest.cs index b167ea3ab1..92a01f8627 100644 --- a/osu.Game.Rulesets.Mania.Tests/ManiaBeatmapConversionTest.cs +++ b/osu.Game.Rulesets.Mania.Tests/ManiaBeatmapConversionTest.cs @@ -26,6 +26,7 @@ namespace osu.Game.Rulesets.Mania.Tests [TestCase("20544")] [TestCase("100374")] [TestCase("1450162")] + [TestCase("4869637")] public void Test(string name) => base.Test(name); protected override IEnumerable CreateConvertValue(HitObject hitObject) diff --git a/osu.Game.Rulesets.Mania.Tests/Resources/Testing/Beatmaps/4869637-expected-conversion.json b/osu.Game.Rulesets.Mania.Tests/Resources/Testing/Beatmaps/4869637-expected-conversion.json new file mode 100644 index 0000000000..05429cae7e --- /dev/null +++ b/osu.Game.Rulesets.Mania.Tests/Resources/Testing/Beatmaps/4869637-expected-conversion.json @@ -0,0 +1 @@ +{"Mappings":[{"RandomW":273326509,"RandomX":386,"RandomY":842502087,"RandomZ":3579807591,"StartTime":355.0,"Objects":[{"StartTime":355.0,"EndTime":355.0,"Column":1}]},{"RandomW":273326509,"RandomX":386,"RandomY":842502087,"RandomZ":3579807591,"StartTime":712.0,"Objects":[{"StartTime":712.0,"EndTime":712.0,"Column":1}]},{"RandomW":273326509,"RandomX":386,"RandomY":842502087,"RandomZ":3579807591,"StartTime":1307.0,"Objects":[{"StartTime":1307.0,"EndTime":1307.0,"Column":2}]},{"RandomW":273326509,"RandomX":386,"RandomY":842502087,"RandomZ":3579807591,"StartTime":1664.0,"Objects":[{"StartTime":1664.0,"EndTime":1664.0,"Column":2}]},{"RandomW":273326509,"RandomX":386,"RandomY":842502087,"RandomZ":3579807591,"StartTime":2259.0,"Objects":[{"StartTime":2259.0,"EndTime":2259.0,"Column":3}]},{"RandomW":273326509,"RandomX":386,"RandomY":842502087,"RandomZ":3579807591,"StartTime":2616.0,"Objects":[{"StartTime":2616.0,"EndTime":2616.0,"Column":2}]},{"RandomW":273326509,"RandomX":386,"RandomY":842502087,"RandomZ":3579807591,"StartTime":3212.0,"Objects":[{"StartTime":3212.0,"EndTime":3212.0,"Column":2}]},{"RandomW":273326509,"RandomX":386,"RandomY":842502087,"RandomZ":3579807591,"StartTime":3569.0,"Objects":[{"StartTime":3569.0,"EndTime":3569.0,"Column":3}]},{"RandomW":273326509,"RandomX":386,"RandomY":842502087,"RandomZ":3579807591,"StartTime":4164.0,"Objects":[{"StartTime":4164.0,"EndTime":4164.0,"Column":1}]},{"RandomW":273326509,"RandomX":386,"RandomY":842502087,"RandomZ":3579807591,"StartTime":4521.0,"Objects":[{"StartTime":4521.0,"EndTime":4521.0,"Column":2}]},{"RandomW":273326509,"RandomX":386,"RandomY":842502087,"RandomZ":3579807591,"StartTime":5117.0,"Objects":[{"StartTime":5117.0,"EndTime":5117.0,"Column":2}]},{"RandomW":273326509,"RandomX":386,"RandomY":842502087,"RandomZ":3579807591,"StartTime":5474.0,"Objects":[{"StartTime":5474.0,"EndTime":5474.0,"Column":1}]},{"RandomW":273326509,"RandomX":386,"RandomY":842502087,"RandomZ":3579807591,"StartTime":6069.0,"Objects":[{"StartTime":6069.0,"EndTime":6069.0,"Column":3}]},{"RandomW":273326509,"RandomX":386,"RandomY":842502087,"RandomZ":3579807591,"StartTime":6426.0,"Objects":[{"StartTime":6426.0,"EndTime":6426.0,"Column":2}]},{"RandomW":273326509,"RandomX":386,"RandomY":842502087,"RandomZ":3579807591,"StartTime":7022.0,"Objects":[{"StartTime":7022.0,"EndTime":7022.0,"Column":2}]},{"RandomW":273326509,"RandomX":386,"RandomY":842502087,"RandomZ":3579807591,"StartTime":7378.0,"Objects":[{"StartTime":7378.0,"EndTime":7378.0,"Column":3}]},{"RandomW":273326509,"RandomX":386,"RandomY":842502087,"RandomZ":3579807591,"StartTime":7974.0,"Objects":[{"StartTime":7974.0,"EndTime":7974.0,"Column":1}]},{"RandomW":273326509,"RandomX":386,"RandomY":842502087,"RandomZ":3579807591,"StartTime":7974.0,"Objects":[{"StartTime":7974.0,"EndTime":7974.0,"Column":0}]},{"RandomW":273326509,"RandomX":386,"RandomY":842502087,"RandomZ":3579807591,"StartTime":8450.0,"Objects":[{"StartTime":8450.0,"EndTime":8450.0,"Column":0}]},{"RandomW":273326509,"RandomX":386,"RandomY":842502087,"RandomZ":3579807591,"StartTime":8450.0,"Objects":[{"StartTime":8450.0,"EndTime":8450.0,"Column":2}]},{"RandomW":273326509,"RandomX":386,"RandomY":842502087,"RandomZ":3579807591,"StartTime":8926.0,"Objects":[{"StartTime":8926.0,"EndTime":8926.0,"Column":0}]},{"RandomW":273326509,"RandomX":386,"RandomY":842502087,"RandomZ":3579807591,"StartTime":8927.0,"Objects":[{"StartTime":8927.0,"EndTime":8927.0,"Column":2}]},{"RandomW":273326509,"RandomX":386,"RandomY":842502087,"RandomZ":3579807591,"StartTime":9402.0,"Objects":[{"StartTime":9402.0,"EndTime":9402.0,"Column":1}]},{"RandomW":273326509,"RandomX":386,"RandomY":842502087,"RandomZ":3579807591,"StartTime":9402.0,"Objects":[{"StartTime":9402.0,"EndTime":9402.0,"Column":0}]},{"RandomW":273326509,"RandomX":386,"RandomY":842502087,"RandomZ":3579807591,"StartTime":9878.0,"Objects":[{"StartTime":9878.0,"EndTime":9878.0,"Column":0}]},{"RandomW":273326509,"RandomX":386,"RandomY":842502087,"RandomZ":3579807591,"StartTime":9879.0,"Objects":[{"StartTime":9879.0,"EndTime":9879.0,"Column":3}]},{"RandomW":273326509,"RandomX":386,"RandomY":842502087,"RandomZ":3579807591,"StartTime":10354.0,"Objects":[{"StartTime":10354.0,"EndTime":10354.0,"Column":2}]},{"RandomW":273326509,"RandomX":386,"RandomY":842502087,"RandomZ":3579807591,"StartTime":10354.0,"Objects":[{"StartTime":10354.0,"EndTime":10354.0,"Column":0}]},{"RandomW":273326509,"RandomX":386,"RandomY":842502087,"RandomZ":3579807591,"StartTime":10831.0,"Objects":[{"StartTime":10831.0,"EndTime":10831.0,"Column":0}]},{"RandomW":273326509,"RandomX":386,"RandomY":842502087,"RandomZ":3579807591,"StartTime":10832.0,"Objects":[{"StartTime":10832.0,"EndTime":10832.0,"Column":2}]},{"RandomW":273326509,"RandomX":386,"RandomY":842502087,"RandomZ":3579807591,"StartTime":11307.0,"Objects":[{"StartTime":11307.0,"EndTime":11307.0,"Column":3}]},{"RandomW":273326509,"RandomX":386,"RandomY":842502087,"RandomZ":3579807591,"StartTime":11307.0,"Objects":[{"StartTime":11307.0,"EndTime":11307.0,"Column":0}]},{"RandomW":273071671,"RandomX":842502087,"RandomY":3579807591,"RandomZ":273326509,"StartTime":11783.0,"Objects":[{"StartTime":11783.0,"EndTime":15116.0,"Column":0}]},{"RandomW":273071671,"RandomX":842502087,"RandomY":3579807591,"RandomZ":273326509,"StartTime":11783.0,"Objects":[{"StartTime":11783.0,"EndTime":11783.0,"Column":3}]},{"RandomW":273071671,"RandomX":842502087,"RandomY":3579807591,"RandomZ":273326509,"StartTime":12259.0,"Objects":[{"StartTime":12259.0,"EndTime":12259.0,"Column":3}]},{"RandomW":273071671,"RandomX":842502087,"RandomY":3579807591,"RandomZ":273326509,"StartTime":12735.0,"Objects":[{"StartTime":12735.0,"EndTime":12735.0,"Column":3}]},{"RandomW":273071671,"RandomX":842502087,"RandomY":3579807591,"RandomZ":273326509,"StartTime":13212.0,"Objects":[{"StartTime":13212.0,"EndTime":13212.0,"Column":3}]},{"RandomW":273071671,"RandomX":842502087,"RandomY":3579807591,"RandomZ":273326509,"StartTime":13688.0,"Objects":[{"StartTime":13688.0,"EndTime":13688.0,"Column":3}]},{"RandomW":273071671,"RandomX":842502087,"RandomY":3579807591,"RandomZ":273326509,"StartTime":14164.0,"Objects":[{"StartTime":14164.0,"EndTime":14164.0,"Column":3}]},{"RandomW":273071671,"RandomX":842502087,"RandomY":3579807591,"RandomZ":273326509,"StartTime":14640.0,"Objects":[{"StartTime":14640.0,"EndTime":14640.0,"Column":3}]},{"RandomW":273071671,"RandomX":842502087,"RandomY":3579807591,"RandomZ":273326509,"StartTime":15116.0,"Objects":[{"StartTime":15116.0,"EndTime":15235.0,"Column":1}]},{"RandomW":273071671,"RandomX":842502087,"RandomY":3579807591,"RandomZ":273326509,"StartTime":15593.0,"Objects":[{"StartTime":15593.0,"EndTime":15593.0,"Column":0}]},{"RandomW":273071671,"RandomX":842502087,"RandomY":3579807591,"RandomZ":273326509,"StartTime":15831.0,"Objects":[{"StartTime":15831.0,"EndTime":15831.0,"Column":0}]},{"RandomW":273071671,"RandomX":842502087,"RandomY":3579807591,"RandomZ":273326509,"StartTime":16069.0,"Objects":[{"StartTime":16069.0,"EndTime":16069.0,"Column":1}]},{"RandomW":273071671,"RandomX":842502087,"RandomY":3579807591,"RandomZ":273326509,"StartTime":16307.0,"Objects":[{"StartTime":16307.0,"EndTime":16307.0,"Column":0}]},{"RandomW":273071671,"RandomX":842502087,"RandomY":3579807591,"RandomZ":273326509,"StartTime":16545.0,"Objects":[{"StartTime":16545.0,"EndTime":16783.0,"Column":2}]},{"RandomW":273071671,"RandomX":842502087,"RandomY":3579807591,"RandomZ":273326509,"StartTime":17021.0,"Objects":[{"StartTime":17021.0,"EndTime":17259.0,"Column":3}]},{"RandomW":273071671,"RandomX":842502087,"RandomY":3579807591,"RandomZ":273326509,"StartTime":17259.0,"Objects":[{"StartTime":17259.0,"EndTime":17259.0,"Column":1}]},{"RandomW":273071671,"RandomX":842502087,"RandomY":3579807591,"RandomZ":273326509,"StartTime":17497.0,"Objects":[{"StartTime":17497.0,"EndTime":17735.0,"Column":2}]},{"RandomW":273071671,"RandomX":842502087,"RandomY":3579807591,"RandomZ":273326509,"StartTime":17974.0,"Objects":[{"StartTime":17974.0,"EndTime":18212.0,"Column":3}]},{"RandomW":273071671,"RandomX":842502087,"RandomY":3579807591,"RandomZ":273326509,"StartTime":18212.0,"Objects":[{"StartTime":18212.0,"EndTime":18212.0,"Column":1}]},{"RandomW":273071671,"RandomX":842502087,"RandomY":3579807591,"RandomZ":273326509,"StartTime":18450.0,"Objects":[{"StartTime":18450.0,"EndTime":18688.0,"Column":2}]},{"RandomW":273071671,"RandomX":842502087,"RandomY":3579807591,"RandomZ":273326509,"StartTime":18926.0,"Objects":[{"StartTime":18926.0,"EndTime":19164.0,"Column":3}]},{"RandomW":273071671,"RandomX":842502087,"RandomY":3579807591,"RandomZ":273326509,"StartTime":19402.0,"Objects":[{"StartTime":19402.0,"EndTime":19402.0,"Column":0}]},{"RandomW":273071671,"RandomX":842502087,"RandomY":3579807591,"RandomZ":273326509,"StartTime":19640.0,"Objects":[{"StartTime":19640.0,"EndTime":19640.0,"Column":1}]},{"RandomW":273071671,"RandomX":842502087,"RandomY":3579807591,"RandomZ":273326509,"StartTime":19878.0,"Objects":[{"StartTime":19878.0,"EndTime":19878.0,"Column":1}]},{"RandomW":273071671,"RandomX":842502087,"RandomY":3579807591,"RandomZ":273326509,"StartTime":20116.0,"Objects":[{"StartTime":20116.0,"EndTime":20116.0,"Column":0}]},{"RandomW":273071671,"RandomX":842502087,"RandomY":3579807591,"RandomZ":273326509,"StartTime":20354.0,"Objects":[{"StartTime":20354.0,"EndTime":20592.0,"Column":2}]},{"RandomW":273071671,"RandomX":842502087,"RandomY":3579807591,"RandomZ":273326509,"StartTime":20831.0,"Objects":[{"StartTime":20831.0,"EndTime":21069.0,"Column":3}]},{"RandomW":273071671,"RandomX":842502087,"RandomY":3579807591,"RandomZ":273326509,"StartTime":21069.0,"Objects":[{"StartTime":21069.0,"EndTime":21069.0,"Column":1}]},{"RandomW":273071671,"RandomX":842502087,"RandomY":3579807591,"RandomZ":273326509,"StartTime":21307.0,"Objects":[{"StartTime":21307.0,"EndTime":21545.0,"Column":2}]},{"RandomW":273071671,"RandomX":842502087,"RandomY":3579807591,"RandomZ":273326509,"StartTime":21783.0,"Objects":[{"StartTime":21783.0,"EndTime":22021.0,"Column":3}]},{"RandomW":273071671,"RandomX":842502087,"RandomY":3579807591,"RandomZ":273326509,"StartTime":22021.0,"Objects":[{"StartTime":22021.0,"EndTime":22021.0,"Column":1}]},{"RandomW":273071671,"RandomX":842502087,"RandomY":3579807591,"RandomZ":273326509,"StartTime":22259.0,"Objects":[{"StartTime":22259.0,"EndTime":22497.0,"Column":2}]},{"RandomW":273071671,"RandomX":842502087,"RandomY":3579807591,"RandomZ":273326509,"StartTime":22735.0,"Objects":[{"StartTime":22735.0,"EndTime":22973.0,"Column":3}]},{"RandomW":273071671,"RandomX":842502087,"RandomY":3579807591,"RandomZ":273326509,"StartTime":23212.0,"Objects":[{"StartTime":23212.0,"EndTime":23212.0,"Column":0}]},{"RandomW":273071671,"RandomX":842502087,"RandomY":3579807591,"RandomZ":273326509,"StartTime":23450.0,"Objects":[{"StartTime":23450.0,"EndTime":23450.0,"Column":0}]},{"RandomW":273071671,"RandomX":842502087,"RandomY":3579807591,"RandomZ":273326509,"StartTime":23688.0,"Objects":[{"StartTime":23688.0,"EndTime":23688.0,"Column":1}]},{"RandomW":273071671,"RandomX":842502087,"RandomY":3579807591,"RandomZ":273326509,"StartTime":23926.0,"Objects":[{"StartTime":23926.0,"EndTime":23926.0,"Column":1}]},{"RandomW":273071671,"RandomX":842502087,"RandomY":3579807591,"RandomZ":273326509,"StartTime":24164.0,"Objects":[{"StartTime":24164.0,"EndTime":24402.0,"Column":2}]},{"RandomW":273071671,"RandomX":842502087,"RandomY":3579807591,"RandomZ":273326509,"StartTime":24641.0,"Objects":[{"StartTime":24641.0,"EndTime":24879.0,"Column":3}]},{"RandomW":273071671,"RandomX":842502087,"RandomY":3579807591,"RandomZ":273326509,"StartTime":24878.0,"Objects":[{"StartTime":24878.0,"EndTime":24878.0,"Column":1}]},{"RandomW":273071671,"RandomX":842502087,"RandomY":3579807591,"RandomZ":273326509,"StartTime":25117.0,"Objects":[{"StartTime":25117.0,"EndTime":25355.0,"Column":2}]},{"RandomW":273071671,"RandomX":842502087,"RandomY":3579807591,"RandomZ":273326509,"StartTime":25593.0,"Objects":[{"StartTime":25593.0,"EndTime":25831.0,"Column":3}]},{"RandomW":273071671,"RandomX":842502087,"RandomY":3579807591,"RandomZ":273326509,"StartTime":25831.0,"Objects":[{"StartTime":25831.0,"EndTime":25831.0,"Column":1}]},{"RandomW":273071671,"RandomX":842502087,"RandomY":3579807591,"RandomZ":273326509,"StartTime":26069.0,"Objects":[{"StartTime":26069.0,"EndTime":26307.0,"Column":2}]},{"RandomW":273071671,"RandomX":842502087,"RandomY":3579807591,"RandomZ":273326509,"StartTime":26545.0,"Objects":[{"StartTime":26545.0,"EndTime":26783.0,"Column":3}]},{"RandomW":273071671,"RandomX":842502087,"RandomY":3579807591,"RandomZ":273326509,"StartTime":27021.0,"Objects":[{"StartTime":27021.0,"EndTime":27021.0,"Column":1}]},{"RandomW":273071671,"RandomX":842502087,"RandomY":3579807591,"RandomZ":273326509,"StartTime":27259.0,"Objects":[{"StartTime":27259.0,"EndTime":27259.0,"Column":1}]},{"RandomW":273071671,"RandomX":842502087,"RandomY":3579807591,"RandomZ":273326509,"StartTime":27497.0,"Objects":[{"StartTime":27497.0,"EndTime":27497.0,"Column":0}]},{"RandomW":273071671,"RandomX":842502087,"RandomY":3579807591,"RandomZ":273326509,"StartTime":27735.0,"Objects":[{"StartTime":27735.0,"EndTime":27735.0,"Column":0}]},{"RandomW":273071671,"RandomX":842502087,"RandomY":3579807591,"RandomZ":273326509,"StartTime":27974.0,"Objects":[{"StartTime":27974.0,"EndTime":28212.0,"Column":2}]},{"RandomW":273071671,"RandomX":842502087,"RandomY":3579807591,"RandomZ":273326509,"StartTime":28450.0,"Objects":[{"StartTime":28450.0,"EndTime":28688.0,"Column":3}]},{"RandomW":273071671,"RandomX":842502087,"RandomY":3579807591,"RandomZ":273326509,"StartTime":28688.0,"Objects":[{"StartTime":28688.0,"EndTime":28688.0,"Column":1}]},{"RandomW":273071671,"RandomX":842502087,"RandomY":3579807591,"RandomZ":273326509,"StartTime":28926.0,"Objects":[{"StartTime":28926.0,"EndTime":29164.0,"Column":2}]},{"RandomW":273071671,"RandomX":842502087,"RandomY":3579807591,"RandomZ":273326509,"StartTime":29402.0,"Objects":[{"StartTime":29402.0,"EndTime":29640.0,"Column":3}]},{"RandomW":273071671,"RandomX":842502087,"RandomY":3579807591,"RandomZ":273326509,"StartTime":29640.0,"Objects":[{"StartTime":29640.0,"EndTime":29640.0,"Column":1}]},{"RandomW":273071671,"RandomX":842502087,"RandomY":3579807591,"RandomZ":273326509,"StartTime":29878.0,"Objects":[{"StartTime":29878.0,"EndTime":30116.0,"Column":2}]},{"RandomW":273071671,"RandomX":842502087,"RandomY":3579807591,"RandomZ":273326509,"StartTime":30354.0,"Objects":[{"StartTime":30354.0,"EndTime":30592.0,"Column":3}]},{"RandomW":273071671,"RandomX":842502087,"RandomY":3579807591,"RandomZ":273326509,"StartTime":30831.0,"Objects":[{"StartTime":30831.0,"EndTime":30831.0,"Column":0}]},{"RandomW":273071671,"RandomX":842502087,"RandomY":3579807591,"RandomZ":273326509,"StartTime":30831.0,"Objects":[{"StartTime":30831.0,"EndTime":30831.0,"Column":2}]},{"RandomW":273071671,"RandomX":842502087,"RandomY":3579807591,"RandomZ":273326509,"StartTime":31069.0,"Objects":[{"StartTime":31069.0,"EndTime":31069.0,"Column":1}]},{"RandomW":273071671,"RandomX":842502087,"RandomY":3579807591,"RandomZ":273326509,"StartTime":31307.0,"Objects":[{"StartTime":31307.0,"EndTime":31307.0,"Column":1}]},{"RandomW":273071671,"RandomX":842502087,"RandomY":3579807591,"RandomZ":273326509,"StartTime":31307.0,"Objects":[{"StartTime":31307.0,"EndTime":31307.0,"Column":0}]},{"RandomW":273071671,"RandomX":842502087,"RandomY":3579807591,"RandomZ":273326509,"StartTime":31545.0,"Objects":[{"StartTime":31545.0,"EndTime":31545.0,"Column":2}]},{"RandomW":273071671,"RandomX":842502087,"RandomY":3579807591,"RandomZ":273326509,"StartTime":31783.0,"Objects":[{"StartTime":31783.0,"EndTime":31783.0,"Column":0}]},{"RandomW":273071671,"RandomX":842502087,"RandomY":3579807591,"RandomZ":273326509,"StartTime":31783.0,"Objects":[{"StartTime":31783.0,"EndTime":32021.0,"Column":3}]},{"RandomW":273071671,"RandomX":842502087,"RandomY":3579807591,"RandomZ":273326509,"StartTime":32021.0,"Objects":[{"StartTime":32021.0,"EndTime":32021.0,"Column":1}]},{"RandomW":273071671,"RandomX":842502087,"RandomY":3579807591,"RandomZ":273326509,"StartTime":32259.0,"Objects":[{"StartTime":32259.0,"EndTime":32497.0,"Column":2}]},{"RandomW":273071671,"RandomX":842502087,"RandomY":3579807591,"RandomZ":273326509,"StartTime":32259.0,"Objects":[{"StartTime":32259.0,"EndTime":32259.0,"Column":0}]},{"RandomW":273071671,"RandomX":842502087,"RandomY":3579807591,"RandomZ":273326509,"StartTime":32735.0,"Objects":[{"StartTime":32735.0,"EndTime":32735.0,"Column":0}]},{"RandomW":273071671,"RandomX":842502087,"RandomY":3579807591,"RandomZ":273326509,"StartTime":32735.0,"Objects":[{"StartTime":32735.0,"EndTime":32973.0,"Column":3}]},{"RandomW":273071671,"RandomX":842502087,"RandomY":3579807591,"RandomZ":273326509,"StartTime":32974.0,"Objects":[{"StartTime":32974.0,"EndTime":32974.0,"Column":1}]},{"RandomW":273071671,"RandomX":842502087,"RandomY":3579807591,"RandomZ":273326509,"StartTime":33212.0,"Objects":[{"StartTime":33212.0,"EndTime":33450.0,"Column":2}]},{"RandomW":273071671,"RandomX":842502087,"RandomY":3579807591,"RandomZ":273326509,"StartTime":33212.0,"Objects":[{"StartTime":33212.0,"EndTime":33212.0,"Column":0}]},{"RandomW":273071671,"RandomX":842502087,"RandomY":3579807591,"RandomZ":273326509,"StartTime":33688.0,"Objects":[{"StartTime":33688.0,"EndTime":33688.0,"Column":0}]},{"RandomW":273071671,"RandomX":842502087,"RandomY":3579807591,"RandomZ":273326509,"StartTime":33688.0,"Objects":[{"StartTime":33688.0,"EndTime":33926.0,"Column":3}]},{"RandomW":273071671,"RandomX":842502087,"RandomY":3579807591,"RandomZ":273326509,"StartTime":34164.0,"Objects":[{"StartTime":34164.0,"EndTime":34402.0,"Column":2}]},{"RandomW":273071671,"RandomX":842502087,"RandomY":3579807591,"RandomZ":273326509,"StartTime":34164.0,"Objects":[{"StartTime":34164.0,"EndTime":34164.0,"Column":0}]},{"RandomW":273071671,"RandomX":842502087,"RandomY":3579807591,"RandomZ":273326509,"StartTime":34640.0,"Objects":[{"StartTime":34640.0,"EndTime":34640.0,"Column":0}]},{"RandomW":273071671,"RandomX":842502087,"RandomY":3579807591,"RandomZ":273326509,"StartTime":34640.0,"Objects":[{"StartTime":34640.0,"EndTime":34640.0,"Column":2}]},{"RandomW":273071671,"RandomX":842502087,"RandomY":3579807591,"RandomZ":273326509,"StartTime":34878.0,"Objects":[{"StartTime":34878.0,"EndTime":34878.0,"Column":1}]},{"RandomW":273071671,"RandomX":842502087,"RandomY":3579807591,"RandomZ":273326509,"StartTime":35116.0,"Objects":[{"StartTime":35116.0,"EndTime":35116.0,"Column":1}]},{"RandomW":273071671,"RandomX":842502087,"RandomY":3579807591,"RandomZ":273326509,"StartTime":35116.0,"Objects":[{"StartTime":35116.0,"EndTime":35116.0,"Column":0}]},{"RandomW":273071671,"RandomX":842502087,"RandomY":3579807591,"RandomZ":273326509,"StartTime":35354.0,"Objects":[{"StartTime":35354.0,"EndTime":35354.0,"Column":2}]},{"RandomW":273071671,"RandomX":842502087,"RandomY":3579807591,"RandomZ":273326509,"StartTime":35592.0,"Objects":[{"StartTime":35592.0,"EndTime":35592.0,"Column":0}]},{"RandomW":273071671,"RandomX":842502087,"RandomY":3579807591,"RandomZ":273326509,"StartTime":35593.0,"Objects":[{"StartTime":35593.0,"EndTime":35831.0,"Column":3}]},{"RandomW":273071671,"RandomX":842502087,"RandomY":3579807591,"RandomZ":273326509,"StartTime":35831.0,"Objects":[{"StartTime":35831.0,"EndTime":35831.0,"Column":1}]},{"RandomW":273071671,"RandomX":842502087,"RandomY":3579807591,"RandomZ":273326509,"StartTime":36068.0,"Objects":[{"StartTime":36068.0,"EndTime":36068.0,"Column":0}]},{"RandomW":273071671,"RandomX":842502087,"RandomY":3579807591,"RandomZ":273326509,"StartTime":36068.0,"Objects":[{"StartTime":36068.0,"EndTime":36306.0,"Column":2}]},{"RandomW":273071671,"RandomX":842502087,"RandomY":3579807591,"RandomZ":273326509,"StartTime":36544.0,"Objects":[{"StartTime":36544.0,"EndTime":36544.0,"Column":0}]},{"RandomW":273071671,"RandomX":842502087,"RandomY":3579807591,"RandomZ":273326509,"StartTime":36545.0,"Objects":[{"StartTime":36545.0,"EndTime":36783.0,"Column":3}]},{"RandomW":273071671,"RandomX":842502087,"RandomY":3579807591,"RandomZ":273326509,"StartTime":36783.0,"Objects":[{"StartTime":36783.0,"EndTime":36783.0,"Column":1}]},{"RandomW":273071671,"RandomX":842502087,"RandomY":3579807591,"RandomZ":273326509,"StartTime":37021.0,"Objects":[{"StartTime":37021.0,"EndTime":37259.0,"Column":2}]},{"RandomW":273071671,"RandomX":842502087,"RandomY":3579807591,"RandomZ":273326509,"StartTime":37021.0,"Objects":[{"StartTime":37021.0,"EndTime":37021.0,"Column":0}]},{"RandomW":273071671,"RandomX":842502087,"RandomY":3579807591,"RandomZ":273326509,"StartTime":37497.0,"Objects":[{"StartTime":37497.0,"EndTime":37497.0,"Column":0}]},{"RandomW":273071671,"RandomX":842502087,"RandomY":3579807591,"RandomZ":273326509,"StartTime":37497.0,"Objects":[{"StartTime":37497.0,"EndTime":37735.0,"Column":3}]},{"RandomW":273071671,"RandomX":842502087,"RandomY":3579807591,"RandomZ":273326509,"StartTime":37854.0,"Objects":[{"StartTime":37854.0,"EndTime":37854.0,"Column":0}]},{"RandomW":273071671,"RandomX":842502087,"RandomY":3579807591,"RandomZ":273326509,"StartTime":37973.0,"Objects":[{"StartTime":37973.0,"EndTime":38211.0,"Column":2}]},{"RandomW":273071671,"RandomX":842502087,"RandomY":3579807591,"RandomZ":273326509,"StartTime":38212.0,"Objects":[{"StartTime":38212.0,"EndTime":38212.0,"Column":0}]},{"RandomW":273071671,"RandomX":842502087,"RandomY":3579807591,"RandomZ":273326509,"StartTime":38450.0,"Objects":[{"StartTime":38450.0,"EndTime":38450.0,"Column":2}]},{"RandomW":273071671,"RandomX":842502087,"RandomY":3579807591,"RandomZ":273326509,"StartTime":38450.0,"Objects":[{"StartTime":38450.0,"EndTime":38450.0,"Column":0}]},{"RandomW":273071671,"RandomX":842502087,"RandomY":3579807591,"RandomZ":273326509,"StartTime":38688.0,"Objects":[{"StartTime":38688.0,"EndTime":38688.0,"Column":1}]},{"RandomW":273071671,"RandomX":842502087,"RandomY":3579807591,"RandomZ":273326509,"StartTime":38926.0,"Objects":[{"StartTime":38926.0,"EndTime":38926.0,"Column":0}]},{"RandomW":273071671,"RandomX":842502087,"RandomY":3579807591,"RandomZ":273326509,"StartTime":38926.0,"Objects":[{"StartTime":38926.0,"EndTime":38926.0,"Column":1}]},{"RandomW":273071671,"RandomX":842502087,"RandomY":3579807591,"RandomZ":273326509,"StartTime":39164.0,"Objects":[{"StartTime":39164.0,"EndTime":39164.0,"Column":2}]},{"RandomW":273071671,"RandomX":842502087,"RandomY":3579807591,"RandomZ":273326509,"StartTime":39402.0,"Objects":[{"StartTime":39402.0,"EndTime":39402.0,"Column":0}]},{"RandomW":273071671,"RandomX":842502087,"RandomY":3579807591,"RandomZ":273326509,"StartTime":39402.0,"Objects":[{"StartTime":39402.0,"EndTime":39640.0,"Column":2}]},{"RandomW":273071671,"RandomX":842502087,"RandomY":3579807591,"RandomZ":273326509,"StartTime":39878.0,"Objects":[{"StartTime":39878.0,"EndTime":39878.0,"Column":0}]},{"RandomW":273071671,"RandomX":842502087,"RandomY":3579807591,"RandomZ":273326509,"StartTime":39879.0,"Objects":[{"StartTime":39879.0,"EndTime":40117.0,"Column":3}]},{"RandomW":273071671,"RandomX":842502087,"RandomY":3579807591,"RandomZ":273326509,"StartTime":40116.0,"Objects":[{"StartTime":40116.0,"EndTime":40116.0,"Column":1}]},{"RandomW":273071671,"RandomX":842502087,"RandomY":3579807591,"RandomZ":273326509,"StartTime":40354.0,"Objects":[{"StartTime":40354.0,"EndTime":40592.0,"Column":2}]},{"RandomW":273071671,"RandomX":842502087,"RandomY":3579807591,"RandomZ":273326509,"StartTime":40354.0,"Objects":[{"StartTime":40354.0,"EndTime":40354.0,"Column":0}]},{"RandomW":273071671,"RandomX":842502087,"RandomY":3579807591,"RandomZ":273326509,"StartTime":40831.0,"Objects":[{"StartTime":40831.0,"EndTime":41069.0,"Column":3}]},{"RandomW":273071671,"RandomX":842502087,"RandomY":3579807591,"RandomZ":273326509,"StartTime":40831.0,"Objects":[{"StartTime":40831.0,"EndTime":40831.0,"Column":0}]},{"RandomW":273071671,"RandomX":842502087,"RandomY":3579807591,"RandomZ":273326509,"StartTime":41069.0,"Objects":[{"StartTime":41069.0,"EndTime":41069.0,"Column":1}]},{"RandomW":273071671,"RandomX":842502087,"RandomY":3579807591,"RandomZ":273326509,"StartTime":41307.0,"Objects":[{"StartTime":41307.0,"EndTime":41545.0,"Column":2}]},{"RandomW":273071671,"RandomX":842502087,"RandomY":3579807591,"RandomZ":273326509,"StartTime":41307.0,"Objects":[{"StartTime":41307.0,"EndTime":41307.0,"Column":0}]},{"RandomW":273071671,"RandomX":842502087,"RandomY":3579807591,"RandomZ":273326509,"StartTime":41783.0,"Objects":[{"StartTime":41783.0,"EndTime":42021.0,"Column":3}]},{"RandomW":273071671,"RandomX":842502087,"RandomY":3579807591,"RandomZ":273326509,"StartTime":41783.0,"Objects":[{"StartTime":41783.0,"EndTime":41783.0,"Column":0}]},{"RandomW":273071671,"RandomX":842502087,"RandomY":3579807591,"RandomZ":273326509,"StartTime":42259.0,"Objects":[{"StartTime":42259.0,"EndTime":42259.0,"Column":0}]},{"RandomW":273071671,"RandomX":842502087,"RandomY":3579807591,"RandomZ":273326509,"StartTime":42259.0,"Objects":[{"StartTime":42259.0,"EndTime":42259.0,"Column":1}]},{"RandomW":273071671,"RandomX":842502087,"RandomY":3579807591,"RandomZ":273326509,"StartTime":42497.0,"Objects":[{"StartTime":42497.0,"EndTime":42497.0,"Column":1}]},{"RandomW":273071671,"RandomX":842502087,"RandomY":3579807591,"RandomZ":273326509,"StartTime":42735.0,"Objects":[{"StartTime":42735.0,"EndTime":42735.0,"Column":0}]},{"RandomW":273071671,"RandomX":842502087,"RandomY":3579807591,"RandomZ":273326509,"StartTime":42735.0,"Objects":[{"StartTime":42735.0,"EndTime":42735.0,"Column":2}]},{"RandomW":273071671,"RandomX":842502087,"RandomY":3579807591,"RandomZ":273326509,"StartTime":42974.0,"Objects":[{"StartTime":42974.0,"EndTime":42974.0,"Column":2}]},{"RandomW":273071671,"RandomX":842502087,"RandomY":3579807591,"RandomZ":273326509,"StartTime":43212.0,"Objects":[{"StartTime":43212.0,"EndTime":43450.0,"Column":2}]},{"RandomW":273071671,"RandomX":842502087,"RandomY":3579807591,"RandomZ":273326509,"StartTime":43212.0,"Objects":[{"StartTime":43212.0,"EndTime":43212.0,"Column":0}]},{"RandomW":273071671,"RandomX":842502087,"RandomY":3579807591,"RandomZ":273326509,"StartTime":43687.0,"Objects":[{"StartTime":43687.0,"EndTime":43925.0,"Column":3}]},{"RandomW":273071671,"RandomX":842502087,"RandomY":3579807591,"RandomZ":273326509,"StartTime":43688.0,"Objects":[{"StartTime":43688.0,"EndTime":43688.0,"Column":0}]},{"RandomW":273071671,"RandomX":842502087,"RandomY":3579807591,"RandomZ":273326509,"StartTime":43926.0,"Objects":[{"StartTime":43926.0,"EndTime":43926.0,"Column":1}]},{"RandomW":273071671,"RandomX":842502087,"RandomY":3579807591,"RandomZ":273326509,"StartTime":44164.0,"Objects":[{"StartTime":44164.0,"EndTime":44402.0,"Column":2}]},{"RandomW":273071671,"RandomX":842502087,"RandomY":3579807591,"RandomZ":273326509,"StartTime":44164.0,"Objects":[{"StartTime":44164.0,"EndTime":44164.0,"Column":0}]},{"RandomW":273071671,"RandomX":842502087,"RandomY":3579807591,"RandomZ":273326509,"StartTime":44639.0,"Objects":[{"StartTime":44639.0,"EndTime":44877.0,"Column":3}]},{"RandomW":273071671,"RandomX":842502087,"RandomY":3579807591,"RandomZ":273326509,"StartTime":44640.0,"Objects":[{"StartTime":44640.0,"EndTime":44640.0,"Column":0}]},{"RandomW":273071671,"RandomX":842502087,"RandomY":3579807591,"RandomZ":273326509,"StartTime":44878.0,"Objects":[{"StartTime":44878.0,"EndTime":44878.0,"Column":1}]},{"RandomW":273071671,"RandomX":842502087,"RandomY":3579807591,"RandomZ":273326509,"StartTime":45116.0,"Objects":[{"StartTime":45116.0,"EndTime":45116.0,"Column":0}]},{"RandomW":273071671,"RandomX":842502087,"RandomY":3579807591,"RandomZ":273326509,"StartTime":45116.0,"Objects":[{"StartTime":45116.0,"EndTime":45354.0,"Column":2}]},{"RandomW":273071671,"RandomX":842502087,"RandomY":3579807591,"RandomZ":273326509,"StartTime":45593.0,"Objects":[{"StartTime":45593.0,"EndTime":45593.0,"Column":0}]},{"RandomW":273071671,"RandomX":842502087,"RandomY":3579807591,"RandomZ":273326509,"StartTime":45593.0,"Objects":[{"StartTime":45593.0,"EndTime":45831.0,"Column":3}]},{"RandomW":273071671,"RandomX":842502087,"RandomY":3579807591,"RandomZ":273326509,"StartTime":45831.0,"Objects":[{"StartTime":45831.0,"EndTime":47497.0,"Column":0}]},{"RandomW":273071671,"RandomX":842502087,"RandomY":3579807591,"RandomZ":273326509,"StartTime":45831.0,"Objects":[{"StartTime":45831.0,"EndTime":45831.0,"Column":1}]},{"RandomW":273071671,"RandomX":842502087,"RandomY":3579807591,"RandomZ":273326509,"StartTime":46069.0,"Objects":[{"StartTime":46069.0,"EndTime":46069.0,"Column":3}]},{"RandomW":273071671,"RandomX":842502087,"RandomY":3579807591,"RandomZ":273326509,"StartTime":46069.0,"Objects":[{"StartTime":46069.0,"EndTime":46069.0,"Column":1}]},{"RandomW":273071671,"RandomX":842502087,"RandomY":3579807591,"RandomZ":273326509,"StartTime":46307.0,"Objects":[{"StartTime":46307.0,"EndTime":46307.0,"Column":3}]},{"RandomW":273071671,"RandomX":842502087,"RandomY":3579807591,"RandomZ":273326509,"StartTime":46545.0,"Objects":[{"StartTime":46545.0,"EndTime":46545.0,"Column":1}]},{"RandomW":273071671,"RandomX":842502087,"RandomY":3579807591,"RandomZ":273326509,"StartTime":46545.0,"Objects":[{"StartTime":46545.0,"EndTime":46545.0,"Column":2}]},{"RandomW":273071671,"RandomX":842502087,"RandomY":3579807591,"RandomZ":273326509,"StartTime":46783.0,"Objects":[{"StartTime":46783.0,"EndTime":46783.0,"Column":2}]},{"RandomW":273071671,"RandomX":842502087,"RandomY":3579807591,"RandomZ":273326509,"StartTime":47021.0,"Objects":[{"StartTime":47021.0,"EndTime":47259.0,"Column":2}]},{"RandomW":273071671,"RandomX":842502087,"RandomY":3579807591,"RandomZ":273326509,"StartTime":47021.0,"Objects":[{"StartTime":47021.0,"EndTime":47021.0,"Column":1}]},{"RandomW":273071671,"RandomX":842502087,"RandomY":3579807591,"RandomZ":273326509,"StartTime":47497.0,"Objects":[{"StartTime":47497.0,"EndTime":47497.0,"Column":1}]},{"RandomW":273071671,"RandomX":842502087,"RandomY":3579807591,"RandomZ":273326509,"StartTime":47497.0,"Objects":[{"StartTime":47497.0,"EndTime":47735.0,"Column":3}]},{"RandomW":273071671,"RandomX":842502087,"RandomY":3579807591,"RandomZ":273326509,"StartTime":47735.0,"Objects":[{"StartTime":47735.0,"EndTime":47735.0,"Column":2}]},{"RandomW":273071671,"RandomX":842502087,"RandomY":3579807591,"RandomZ":273326509,"StartTime":47795.0,"Objects":[{"StartTime":47795.0,"EndTime":48449.0,"Column":0}]},{"RandomW":273071671,"RandomX":842502087,"RandomY":3579807591,"RandomZ":273326509,"StartTime":47974.0,"Objects":[{"StartTime":47974.0,"EndTime":48212.0,"Column":2}]},{"RandomW":273071671,"RandomX":842502087,"RandomY":3579807591,"RandomZ":273326509,"StartTime":47974.0,"Objects":[{"StartTime":47974.0,"EndTime":47974.0,"Column":1}]},{"RandomW":273071671,"RandomX":842502087,"RandomY":3579807591,"RandomZ":273326509,"StartTime":48450.0,"Objects":[{"StartTime":48450.0,"EndTime":48688.0,"Column":3}]},{"RandomW":273071671,"RandomX":842502087,"RandomY":3579807591,"RandomZ":273326509,"StartTime":48450.0,"Objects":[{"StartTime":48450.0,"EndTime":48450.0,"Column":1}]},{"RandomW":273071671,"RandomX":842502087,"RandomY":3579807591,"RandomZ":273326509,"StartTime":48688.0,"Objects":[{"StartTime":48688.0,"EndTime":48688.0,"Column":0}]},{"RandomW":273071671,"RandomX":842502087,"RandomY":3579807591,"RandomZ":273326509,"StartTime":48688.0,"Objects":[{"StartTime":48688.0,"EndTime":48688.0,"Column":2}]},{"RandomW":273071671,"RandomX":842502087,"RandomY":3579807591,"RandomZ":273326509,"StartTime":48926.0,"Objects":[{"StartTime":48926.0,"EndTime":49164.0,"Column":3}]},{"RandomW":273071671,"RandomX":842502087,"RandomY":3579807591,"RandomZ":273326509,"StartTime":48926.0,"Objects":[{"StartTime":48926.0,"EndTime":48926.0,"Column":1}]},{"RandomW":273071671,"RandomX":842502087,"RandomY":3579807591,"RandomZ":273326509,"StartTime":49164.0,"Objects":[{"StartTime":49164.0,"EndTime":49402.0,"Column":0}]},{"RandomW":273071671,"RandomX":842502087,"RandomY":3579807591,"RandomZ":273326509,"StartTime":49402.0,"Objects":[{"StartTime":49402.0,"EndTime":49402.0,"Column":1}]},{"RandomW":273071671,"RandomX":842502087,"RandomY":3579807591,"RandomZ":273326509,"StartTime":49402.0,"Objects":[{"StartTime":49402.0,"EndTime":49640.0,"Column":2}]},{"RandomW":273071671,"RandomX":842502087,"RandomY":3579807591,"RandomZ":273326509,"StartTime":49640.0,"Objects":[{"StartTime":49640.0,"EndTime":51306.0,"Column":0}]},{"RandomW":273071671,"RandomX":842502087,"RandomY":3579807591,"RandomZ":273326509,"StartTime":49878.0,"Objects":[{"StartTime":49878.0,"EndTime":49878.0,"Column":1}]},{"RandomW":273071671,"RandomX":842502087,"RandomY":3579807591,"RandomZ":273326509,"StartTime":49878.0,"Objects":[{"StartTime":49878.0,"EndTime":49878.0,"Column":2}]},{"RandomW":273071671,"RandomX":842502087,"RandomY":3579807591,"RandomZ":273326509,"StartTime":50116.0,"Objects":[{"StartTime":50116.0,"EndTime":50116.0,"Column":2}]},{"RandomW":273071671,"RandomX":842502087,"RandomY":3579807591,"RandomZ":273326509,"StartTime":50354.0,"Objects":[{"StartTime":50354.0,"EndTime":50354.0,"Column":1}]},{"RandomW":273071671,"RandomX":842502087,"RandomY":3579807591,"RandomZ":273326509,"StartTime":50354.0,"Objects":[{"StartTime":50354.0,"EndTime":50354.0,"Column":3}]},{"RandomW":273071671,"RandomX":842502087,"RandomY":3579807591,"RandomZ":273326509,"StartTime":50593.0,"Objects":[{"StartTime":50593.0,"EndTime":50593.0,"Column":3}]},{"RandomW":273071671,"RandomX":842502087,"RandomY":3579807591,"RandomZ":273326509,"StartTime":50831.0,"Objects":[{"StartTime":50831.0,"EndTime":50831.0,"Column":1}]},{"RandomW":273071671,"RandomX":842502087,"RandomY":3579807591,"RandomZ":273326509,"StartTime":50831.0,"Objects":[{"StartTime":50831.0,"EndTime":51069.0,"Column":2}]},{"RandomW":273071671,"RandomX":842502087,"RandomY":3579807591,"RandomZ":273326509,"StartTime":51307.0,"Objects":[{"StartTime":51307.0,"EndTime":51307.0,"Column":1}]},{"RandomW":273071671,"RandomX":842502087,"RandomY":3579807591,"RandomZ":273326509,"StartTime":51307.0,"Objects":[{"StartTime":51307.0,"EndTime":51545.0,"Column":3}]},{"RandomW":273071671,"RandomX":842502087,"RandomY":3579807591,"RandomZ":273326509,"StartTime":51545.0,"Objects":[{"StartTime":51545.0,"EndTime":52259.0,"Column":0}]},{"RandomW":273071671,"RandomX":842502087,"RandomY":3579807591,"RandomZ":273326509,"StartTime":51545.0,"Objects":[{"StartTime":51545.0,"EndTime":51545.0,"Column":2}]},{"RandomW":273071671,"RandomX":842502087,"RandomY":3579807591,"RandomZ":273326509,"StartTime":51783.0,"Objects":[{"StartTime":51783.0,"EndTime":51783.0,"Column":1}]},{"RandomW":273071671,"RandomX":842502087,"RandomY":3579807591,"RandomZ":273326509,"StartTime":51783.0,"Objects":[{"StartTime":51783.0,"EndTime":52021.0,"Column":2}]},{"RandomW":273071671,"RandomX":842502087,"RandomY":3579807591,"RandomZ":273326509,"StartTime":52259.0,"Objects":[{"StartTime":52259.0,"EndTime":52497.0,"Column":3}]},{"RandomW":273071671,"RandomX":842502087,"RandomY":3579807591,"RandomZ":273326509,"StartTime":52259.0,"Objects":[{"StartTime":52259.0,"EndTime":52259.0,"Column":1}]},{"RandomW":273071671,"RandomX":842502087,"RandomY":3579807591,"RandomZ":273326509,"StartTime":52497.0,"Objects":[{"StartTime":52497.0,"EndTime":52497.0,"Column":2}]},{"RandomW":273071671,"RandomX":842502087,"RandomY":3579807591,"RandomZ":273326509,"StartTime":52735.0,"Objects":[{"StartTime":52735.0,"EndTime":52973.0,"Column":2}]},{"RandomW":273071671,"RandomX":842502087,"RandomY":3579807591,"RandomZ":273326509,"StartTime":52735.0,"Objects":[{"StartTime":52735.0,"EndTime":52735.0,"Column":1}]},{"RandomW":273071671,"RandomX":842502087,"RandomY":3579807591,"RandomZ":273326509,"StartTime":52974.0,"Objects":[{"StartTime":52974.0,"EndTime":53212.0,"Column":0}]},{"RandomW":273071671,"RandomX":842502087,"RandomY":3579807591,"RandomZ":273326509,"StartTime":53212.0,"Objects":[{"StartTime":53212.0,"EndTime":53450.0,"Column":3}]},{"RandomW":273071671,"RandomX":842502087,"RandomY":3579807591,"RandomZ":273326509,"StartTime":53212.0,"Objects":[{"StartTime":53212.0,"EndTime":53212.0,"Column":2}]},{"RandomW":273071671,"RandomX":842502087,"RandomY":3579807591,"RandomZ":273326509,"StartTime":53450.0,"Objects":[{"StartTime":53450.0,"EndTime":53450.0,"Column":1}]},{"RandomW":273071671,"RandomX":842502087,"RandomY":3579807591,"RandomZ":273326509,"StartTime":53450.0,"Objects":[{"StartTime":53450.0,"EndTime":54164.0,"Column":0}]},{"RandomW":273071671,"RandomX":842502087,"RandomY":3579807591,"RandomZ":273326509,"StartTime":53688.0,"Objects":[{"StartTime":53688.0,"EndTime":53688.0,"Column":1}]},{"RandomW":273071671,"RandomX":842502087,"RandomY":3579807591,"RandomZ":273326509,"StartTime":53926.0,"Objects":[{"StartTime":53926.0,"EndTime":53926.0,"Column":2}]},{"RandomW":273071671,"RandomX":842502087,"RandomY":3579807591,"RandomZ":273326509,"StartTime":54164.0,"Objects":[{"StartTime":54164.0,"EndTime":54164.0,"Column":1}]},{"RandomW":273071671,"RandomX":842502087,"RandomY":3579807591,"RandomZ":273326509,"StartTime":54164.0,"Objects":[{"StartTime":54164.0,"EndTime":54164.0,"Column":2}]},{"RandomW":273071671,"RandomX":842502087,"RandomY":3579807591,"RandomZ":273326509,"StartTime":54402.0,"Objects":[{"StartTime":54402.0,"EndTime":55592.0,"Column":0}]},{"RandomW":273071671,"RandomX":842502087,"RandomY":3579807591,"RandomZ":273326509,"StartTime":54402.0,"Objects":[{"StartTime":54402.0,"EndTime":54402.0,"Column":3}]},{"RandomW":273071671,"RandomX":842502087,"RandomY":3579807591,"RandomZ":273326509,"StartTime":54640.0,"Objects":[{"StartTime":54640.0,"EndTime":54640.0,"Column":1}]},{"RandomW":273071671,"RandomX":842502087,"RandomY":3579807591,"RandomZ":273326509,"StartTime":54640.0,"Objects":[{"StartTime":54640.0,"EndTime":54878.0,"Column":3}]},{"RandomW":273071671,"RandomX":842502087,"RandomY":3579807591,"RandomZ":273326509,"StartTime":55116.0,"Objects":[{"StartTime":55116.0,"EndTime":55116.0,"Column":1}]},{"RandomW":273071671,"RandomX":842502087,"RandomY":3579807591,"RandomZ":273326509,"StartTime":55116.0,"Objects":[{"StartTime":55116.0,"EndTime":55354.0,"Column":3}]},{"RandomW":273071671,"RandomX":842502087,"RandomY":3579807591,"RandomZ":273326509,"StartTime":55354.0,"Objects":[{"StartTime":55354.0,"EndTime":55354.0,"Column":2}]},{"RandomW":273071671,"RandomX":842502087,"RandomY":3579807591,"RandomZ":273326509,"StartTime":55593.0,"Objects":[{"StartTime":55593.0,"EndTime":55593.0,"Column":1}]},{"RandomW":273071671,"RandomX":842502087,"RandomY":3579807591,"RandomZ":273326509,"StartTime":55593.0,"Objects":[{"StartTime":55593.0,"EndTime":55831.0,"Column":3}]},{"RandomW":273071671,"RandomX":842502087,"RandomY":3579807591,"RandomZ":273326509,"StartTime":56069.0,"Objects":[{"StartTime":56069.0,"EndTime":56069.0,"Column":2}]},{"RandomW":273071671,"RandomX":842502087,"RandomY":3579807591,"RandomZ":273326509,"StartTime":56069.0,"Objects":[{"StartTime":56069.0,"EndTime":56307.0,"Column":3}]},{"RandomW":273071671,"RandomX":842502087,"RandomY":3579807591,"RandomZ":273326509,"StartTime":56307.0,"Objects":[{"StartTime":56307.0,"EndTime":56307.0,"Column":1}]},{"RandomW":273071671,"RandomX":842502087,"RandomY":3579807591,"RandomZ":273326509,"StartTime":56545.0,"Objects":[{"StartTime":56545.0,"EndTime":56545.0,"Column":2}]},{"RandomW":273071671,"RandomX":842502087,"RandomY":3579807591,"RandomZ":273326509,"StartTime":56545.0,"Objects":[{"StartTime":56545.0,"EndTime":56783.0,"Column":3}]},{"RandomW":273071671,"RandomX":842502087,"RandomY":3579807591,"RandomZ":273326509,"StartTime":56783.0,"Objects":[{"StartTime":56783.0,"EndTime":56783.0,"Column":1}]},{"RandomW":273071671,"RandomX":842502087,"RandomY":3579807591,"RandomZ":273326509,"StartTime":56783.0,"Objects":[{"StartTime":56783.0,"EndTime":57021.0,"Column":0}]},{"RandomW":273071671,"RandomX":842502087,"RandomY":3579807591,"RandomZ":273326509,"StartTime":57021.0,"Objects":[{"StartTime":57021.0,"EndTime":57259.0,"Column":3}]},{"RandomW":273071671,"RandomX":842502087,"RandomY":3579807591,"RandomZ":273326509,"StartTime":57021.0,"Objects":[{"StartTime":57021.0,"EndTime":57021.0,"Column":2}]},{"RandomW":273071671,"RandomX":842502087,"RandomY":3579807591,"RandomZ":273326509,"StartTime":57259.0,"Objects":[{"StartTime":57259.0,"EndTime":57973.0,"Column":0}]},{"RandomW":273071671,"RandomX":842502087,"RandomY":3579807591,"RandomZ":273326509,"StartTime":57497.0,"Objects":[{"StartTime":57497.0,"EndTime":57497.0,"Column":1}]},{"RandomW":273071671,"RandomX":842502087,"RandomY":3579807591,"RandomZ":273326509,"StartTime":57497.0,"Objects":[{"StartTime":57497.0,"EndTime":57497.0,"Column":2}]},{"RandomW":273071671,"RandomX":842502087,"RandomY":3579807591,"RandomZ":273326509,"StartTime":57735.0,"Objects":[{"StartTime":57735.0,"EndTime":57735.0,"Column":2}]},{"RandomW":273071671,"RandomX":842502087,"RandomY":3579807591,"RandomZ":273326509,"StartTime":57974.0,"Objects":[{"StartTime":57974.0,"EndTime":57974.0,"Column":1}]},{"RandomW":273071671,"RandomX":842502087,"RandomY":3579807591,"RandomZ":273326509,"StartTime":57974.0,"Objects":[{"StartTime":57974.0,"EndTime":57974.0,"Column":3}]},{"RandomW":273071671,"RandomX":842502087,"RandomY":3579807591,"RandomZ":273326509,"StartTime":58212.0,"Objects":[{"StartTime":58212.0,"EndTime":60354.0,"Column":0}]},{"RandomW":273071671,"RandomX":842502087,"RandomY":3579807591,"RandomZ":273326509,"StartTime":58212.0,"Objects":[{"StartTime":58212.0,"EndTime":58212.0,"Column":3}]},{"RandomW":273071671,"RandomX":842502087,"RandomY":3579807591,"RandomZ":273326509,"StartTime":58450.0,"Objects":[{"StartTime":58450.0,"EndTime":58450.0,"Column":1}]},{"RandomW":273071671,"RandomX":842502087,"RandomY":3579807591,"RandomZ":273326509,"StartTime":58450.0,"Objects":[{"StartTime":58450.0,"EndTime":58688.0,"Column":2}]},{"RandomW":273071671,"RandomX":842502087,"RandomY":3579807591,"RandomZ":273326509,"StartTime":58926.0,"Objects":[{"StartTime":58926.0,"EndTime":58926.0,"Column":1}]},{"RandomW":273071671,"RandomX":842502087,"RandomY":3579807591,"RandomZ":273326509,"StartTime":58926.0,"Objects":[{"StartTime":58926.0,"EndTime":59164.0,"Column":3}]},{"RandomW":273071671,"RandomX":842502087,"RandomY":3579807591,"RandomZ":273326509,"StartTime":59164.0,"Objects":[{"StartTime":59164.0,"EndTime":59164.0,"Column":2}]},{"RandomW":273071671,"RandomX":842502087,"RandomY":3579807591,"RandomZ":273326509,"StartTime":59402.0,"Objects":[{"StartTime":59402.0,"EndTime":59402.0,"Column":1}]},{"RandomW":273071671,"RandomX":842502087,"RandomY":3579807591,"RandomZ":273326509,"StartTime":59402.0,"Objects":[{"StartTime":59402.0,"EndTime":59640.0,"Column":2}]},{"RandomW":273071671,"RandomX":842502087,"RandomY":3579807591,"RandomZ":273326509,"StartTime":59878.0,"Objects":[{"StartTime":59878.0,"EndTime":59878.0,"Column":1}]},{"RandomW":273071671,"RandomX":842502087,"RandomY":3579807591,"RandomZ":273326509,"StartTime":59878.0,"Objects":[{"StartTime":59878.0,"EndTime":60116.0,"Column":3}]},{"RandomW":273071671,"RandomX":842502087,"RandomY":3579807591,"RandomZ":273326509,"StartTime":60116.0,"Objects":[{"StartTime":60116.0,"EndTime":60116.0,"Column":2}]},{"RandomW":273071671,"RandomX":842502087,"RandomY":3579807591,"RandomZ":273326509,"StartTime":60354.0,"Objects":[{"StartTime":60354.0,"EndTime":60354.0,"Column":1}]},{"RandomW":273071671,"RandomX":842502087,"RandomY":3579807591,"RandomZ":273326509,"StartTime":60354.0,"Objects":[{"StartTime":60354.0,"EndTime":60592.0,"Column":2}]},{"RandomW":273071671,"RandomX":842502087,"RandomY":3579807591,"RandomZ":273326509,"StartTime":60354.0,"Objects":[{"StartTime":60354.0,"EndTime":60592.0,"Column":3}]},{"RandomW":273071671,"RandomX":842502087,"RandomY":3579807591,"RandomZ":273326509,"StartTime":60593.0,"Objects":[{"StartTime":60593.0,"EndTime":60593.0,"Column":1}]},{"RandomW":273071671,"RandomX":842502087,"RandomY":3579807591,"RandomZ":273326509,"StartTime":60831.0,"Objects":[{"StartTime":60831.0,"EndTime":60831.0,"Column":2}]},{"RandomW":273071671,"RandomX":842502087,"RandomY":3579807591,"RandomZ":273326509,"StartTime":60831.0,"Objects":[{"StartTime":60831.0,"EndTime":61069.0,"Column":3}]},{"RandomW":273071671,"RandomX":842502087,"RandomY":3579807591,"RandomZ":273326509,"StartTime":60831.0,"Objects":[{"StartTime":60831.0,"EndTime":60831.0,"Column":0}]},{"RandomW":273071671,"RandomX":842502087,"RandomY":3579807591,"RandomZ":273326509,"StartTime":61069.0,"Objects":[{"StartTime":61069.0,"EndTime":61307.0,"Column":2}]},{"RandomW":273071671,"RandomX":842502087,"RandomY":3579807591,"RandomZ":273326509,"StartTime":61307.0,"Objects":[{"StartTime":61307.0,"EndTime":61307.0,"Column":0}]},{"RandomW":273071671,"RandomX":842502087,"RandomY":3579807591,"RandomZ":273326509,"StartTime":61307.0,"Objects":[{"StartTime":61307.0,"EndTime":61307.0,"Column":3}]},{"RandomW":273071671,"RandomX":842502087,"RandomY":3579807591,"RandomZ":273326509,"StartTime":61545.0,"Objects":[{"StartTime":61545.0,"EndTime":61783.0,"Column":1}]},{"RandomW":273071671,"RandomX":842502087,"RandomY":3579807591,"RandomZ":273326509,"StartTime":61545.0,"Objects":[{"StartTime":61545.0,"EndTime":61545.0,"Column":3}]},{"RandomW":273071671,"RandomX":842502087,"RandomY":3579807591,"RandomZ":273326509,"StartTime":61783.0,"Objects":[{"StartTime":61783.0,"EndTime":61783.0,"Column":0}]},{"RandomW":273071671,"RandomX":842502087,"RandomY":3579807591,"RandomZ":273326509,"StartTime":61783.0,"Objects":[{"StartTime":61783.0,"EndTime":61783.0,"Column":3}]},{"RandomW":273071671,"RandomX":842502087,"RandomY":3579807591,"RandomZ":273326509,"StartTime":62021.0,"Objects":[{"StartTime":62021.0,"EndTime":62259.0,"Column":2}]},{"RandomW":273071671,"RandomX":842502087,"RandomY":3579807591,"RandomZ":273326509,"StartTime":62021.0,"Objects":[{"StartTime":62021.0,"EndTime":62021.0,"Column":3}]},{"RandomW":273071671,"RandomX":842502087,"RandomY":3579807591,"RandomZ":273326509,"StartTime":62259.0,"Objects":[{"StartTime":62259.0,"EndTime":62259.0,"Column":0}]},{"RandomW":273071671,"RandomX":842502087,"RandomY":3579807591,"RandomZ":273326509,"StartTime":62259.0,"Objects":[{"StartTime":62259.0,"EndTime":62497.0,"Column":3}]},{"RandomW":273071671,"RandomX":842502087,"RandomY":3579807591,"RandomZ":273326509,"StartTime":62497.0,"Objects":[{"StartTime":62497.0,"EndTime":62735.0,"Column":1}]},{"RandomW":273071671,"RandomX":842502087,"RandomY":3579807591,"RandomZ":273326509,"StartTime":62735.0,"Objects":[{"StartTime":62735.0,"EndTime":62735.0,"Column":0}]},{"RandomW":273071671,"RandomX":842502087,"RandomY":3579807591,"RandomZ":273326509,"StartTime":62735.0,"Objects":[{"StartTime":62735.0,"EndTime":62973.0,"Column":3}]},{"RandomW":273071671,"RandomX":842502087,"RandomY":3579807591,"RandomZ":273326509,"StartTime":62974.0,"Objects":[{"StartTime":62974.0,"EndTime":63212.0,"Column":2}]},{"RandomW":273071671,"RandomX":842502087,"RandomY":3579807591,"RandomZ":273326509,"StartTime":63212.0,"Objects":[{"StartTime":63212.0,"EndTime":63212.0,"Column":0}]},{"RandomW":273071671,"RandomX":842502087,"RandomY":3579807591,"RandomZ":273326509,"StartTime":63212.0,"Objects":[{"StartTime":63212.0,"EndTime":63450.0,"Column":3}]},{"RandomW":273071671,"RandomX":842502087,"RandomY":3579807591,"RandomZ":273326509,"StartTime":63450.0,"Objects":[{"StartTime":63450.0,"EndTime":63926.0,"Column":1}]},{"RandomW":273071671,"RandomX":842502087,"RandomY":3579807591,"RandomZ":273326509,"StartTime":63688.0,"Objects":[{"StartTime":63688.0,"EndTime":63688.0,"Column":0}]},{"RandomW":273071671,"RandomX":842502087,"RandomY":3579807591,"RandomZ":273326509,"StartTime":63688.0,"Objects":[{"StartTime":63688.0,"EndTime":63926.0,"Column":3}]},{"RandomW":273071671,"RandomX":842502087,"RandomY":3579807591,"RandomZ":273326509,"StartTime":63926.0,"Objects":[{"StartTime":63926.0,"EndTime":64164.0,"Column":2}]},{"RandomW":273071671,"RandomX":842502087,"RandomY":3579807591,"RandomZ":273326509,"StartTime":64164.0,"Objects":[{"StartTime":64164.0,"EndTime":64164.0,"Column":0}]},{"RandomW":273071671,"RandomX":842502087,"RandomY":3579807591,"RandomZ":273326509,"StartTime":64164.0,"Objects":[{"StartTime":64164.0,"EndTime":64402.0,"Column":3}]},{"RandomW":273071671,"RandomX":842502087,"RandomY":3579807591,"RandomZ":273326509,"StartTime":64402.0,"Objects":[{"StartTime":64402.0,"EndTime":64402.0,"Column":2}]},{"RandomW":273071671,"RandomX":842502087,"RandomY":3579807591,"RandomZ":273326509,"StartTime":64640.0,"Objects":[{"StartTime":64640.0,"EndTime":64640.0,"Column":1}]},{"RandomW":273071671,"RandomX":842502087,"RandomY":3579807591,"RandomZ":273326509,"StartTime":64640.0,"Objects":[{"StartTime":64640.0,"EndTime":64640.0,"Column":0}]},{"RandomW":273071671,"RandomX":842502087,"RandomY":3579807591,"RandomZ":273326509,"StartTime":64640.0,"Objects":[{"StartTime":64640.0,"EndTime":64878.0,"Column":3}]},{"RandomW":273071671,"RandomX":842502087,"RandomY":3579807591,"RandomZ":273326509,"StartTime":64878.0,"Objects":[{"StartTime":64878.0,"EndTime":65116.0,"Column":1}]},{"RandomW":273071671,"RandomX":842502087,"RandomY":3579807591,"RandomZ":273326509,"StartTime":65116.0,"Objects":[{"StartTime":65116.0,"EndTime":65116.0,"Column":0}]},{"RandomW":273071671,"RandomX":842502087,"RandomY":3579807591,"RandomZ":273326509,"StartTime":65116.0,"Objects":[{"StartTime":65116.0,"EndTime":65116.0,"Column":3}]},{"RandomW":273071671,"RandomX":842502087,"RandomY":3579807591,"RandomZ":273326509,"StartTime":65354.0,"Objects":[{"StartTime":65354.0,"EndTime":65592.0,"Column":2}]},{"RandomW":273071671,"RandomX":842502087,"RandomY":3579807591,"RandomZ":273326509,"StartTime":65354.0,"Objects":[{"StartTime":65354.0,"EndTime":65354.0,"Column":3}]},{"RandomW":273071671,"RandomX":842502087,"RandomY":3579807591,"RandomZ":273326509,"StartTime":65593.0,"Objects":[{"StartTime":65593.0,"EndTime":65593.0,"Column":0}]},{"RandomW":273071671,"RandomX":842502087,"RandomY":3579807591,"RandomZ":273326509,"StartTime":65593.0,"Objects":[{"StartTime":65593.0,"EndTime":65593.0,"Column":3}]},{"RandomW":273071671,"RandomX":842502087,"RandomY":3579807591,"RandomZ":273326509,"StartTime":65831.0,"Objects":[{"StartTime":65831.0,"EndTime":66069.0,"Column":1}]},{"RandomW":273071671,"RandomX":842502087,"RandomY":3579807591,"RandomZ":273326509,"StartTime":65831.0,"Objects":[{"StartTime":65831.0,"EndTime":65831.0,"Column":3}]},{"RandomW":273071671,"RandomX":842502087,"RandomY":3579807591,"RandomZ":273326509,"StartTime":66069.0,"Objects":[{"StartTime":66069.0,"EndTime":66069.0,"Column":0}]},{"RandomW":273071671,"RandomX":842502087,"RandomY":3579807591,"RandomZ":273326509,"StartTime":66069.0,"Objects":[{"StartTime":66069.0,"EndTime":66307.0,"Column":3}]},{"RandomW":273071671,"RandomX":842502087,"RandomY":3579807591,"RandomZ":273326509,"StartTime":66307.0,"Objects":[{"StartTime":66307.0,"EndTime":66545.0,"Column":1}]},{"RandomW":273071671,"RandomX":842502087,"RandomY":3579807591,"RandomZ":273326509,"StartTime":66545.0,"Objects":[{"StartTime":66545.0,"EndTime":66545.0,"Column":0}]},{"RandomW":273071671,"RandomX":842502087,"RandomY":3579807591,"RandomZ":273326509,"StartTime":66545.0,"Objects":[{"StartTime":66545.0,"EndTime":66783.0,"Column":3}]},{"RandomW":273071671,"RandomX":842502087,"RandomY":3579807591,"RandomZ":273326509,"StartTime":66783.0,"Objects":[{"StartTime":66783.0,"EndTime":67021.0,"Column":2}]},{"RandomW":273071671,"RandomX":842502087,"RandomY":3579807591,"RandomZ":273326509,"StartTime":67021.0,"Objects":[{"StartTime":67021.0,"EndTime":67021.0,"Column":0}]},{"RandomW":273071671,"RandomX":842502087,"RandomY":3579807591,"RandomZ":273326509,"StartTime":67021.0,"Objects":[{"StartTime":67021.0,"EndTime":67259.0,"Column":3}]},{"RandomW":273071671,"RandomX":842502087,"RandomY":3579807591,"RandomZ":273326509,"StartTime":67259.0,"Objects":[{"StartTime":67259.0,"EndTime":67497.0,"Column":1}]},{"RandomW":273071671,"RandomX":842502087,"RandomY":3579807591,"RandomZ":273326509,"StartTime":67497.0,"Objects":[{"StartTime":67497.0,"EndTime":67497.0,"Column":0}]},{"RandomW":273071671,"RandomX":842502087,"RandomY":3579807591,"RandomZ":273326509,"StartTime":67497.0,"Objects":[{"StartTime":67497.0,"EndTime":67735.0,"Column":3}]},{"RandomW":273071671,"RandomX":842502087,"RandomY":3579807591,"RandomZ":273326509,"StartTime":67735.0,"Objects":[{"StartTime":67735.0,"EndTime":67973.0,"Column":2}]},{"RandomW":273071671,"RandomX":842502087,"RandomY":3579807591,"RandomZ":273326509,"StartTime":67974.0,"Objects":[{"StartTime":67974.0,"EndTime":67974.0,"Column":0}]},{"RandomW":273071671,"RandomX":842502087,"RandomY":3579807591,"RandomZ":273326509,"StartTime":67974.0,"Objects":[{"StartTime":67974.0,"EndTime":68212.0,"Column":3}]},{"RandomW":273071671,"RandomX":842502087,"RandomY":3579807591,"RandomZ":273326509,"StartTime":68212.0,"Objects":[{"StartTime":68212.0,"EndTime":68450.0,"Column":1}]},{"RandomW":273071671,"RandomX":842502087,"RandomY":3579807591,"RandomZ":273326509,"StartTime":68331.0,"Objects":[{"StartTime":68331.0,"EndTime":68331.0,"Column":0}]},{"RandomW":273071671,"RandomX":842502087,"RandomY":3579807591,"RandomZ":273326509,"StartTime":68450.0,"Objects":[{"StartTime":68450.0,"EndTime":68688.0,"Column":3}]},{"RandomW":273071671,"RandomX":842502087,"RandomY":3579807591,"RandomZ":273326509,"StartTime":68688.0,"Objects":[{"StartTime":68688.0,"EndTime":69164.0,"Column":2}]},{"RandomW":273071671,"RandomX":842502087,"RandomY":3579807591,"RandomZ":273326509,"StartTime":68688.0,"Objects":[{"StartTime":68688.0,"EndTime":68688.0,"Column":0}]},{"RandomW":273071671,"RandomX":842502087,"RandomY":3579807591,"RandomZ":273326509,"StartTime":68926.0,"Objects":[{"StartTime":68926.0,"EndTime":68926.0,"Column":0}]},{"RandomW":273071671,"RandomX":842502087,"RandomY":3579807591,"RandomZ":273326509,"StartTime":68926.0,"Objects":[{"StartTime":68926.0,"EndTime":68926.0,"Column":3}]},{"RandomW":273071671,"RandomX":842502087,"RandomY":3579807591,"RandomZ":273326509,"StartTime":69164.0,"Objects":[{"StartTime":69164.0,"EndTime":69402.0,"Column":1}]},{"RandomW":273071671,"RandomX":842502087,"RandomY":3579807591,"RandomZ":273326509,"StartTime":69164.0,"Objects":[{"StartTime":69164.0,"EndTime":69164.0,"Column":3}]},{"RandomW":273071671,"RandomX":842502087,"RandomY":3579807591,"RandomZ":273326509,"StartTime":69402.0,"Objects":[{"StartTime":69402.0,"EndTime":69402.0,"Column":0}]},{"RandomW":273071671,"RandomX":842502087,"RandomY":3579807591,"RandomZ":273326509,"StartTime":69402.0,"Objects":[{"StartTime":69402.0,"EndTime":69402.0,"Column":3}]},{"RandomW":273071671,"RandomX":842502087,"RandomY":3579807591,"RandomZ":273326509,"StartTime":69640.0,"Objects":[{"StartTime":69640.0,"EndTime":69878.0,"Column":1}]},{"RandomW":273071671,"RandomX":842502087,"RandomY":3579807591,"RandomZ":273326509,"StartTime":69640.0,"Objects":[{"StartTime":69640.0,"EndTime":69640.0,"Column":3}]},{"RandomW":273071671,"RandomX":842502087,"RandomY":3579807591,"RandomZ":273326509,"StartTime":69878.0,"Objects":[{"StartTime":69878.0,"EndTime":69878.0,"Column":0}]},{"RandomW":273071671,"RandomX":842502087,"RandomY":3579807591,"RandomZ":273326509,"StartTime":69878.0,"Objects":[{"StartTime":69878.0,"EndTime":70116.0,"Column":3}]},{"RandomW":273071671,"RandomX":842502087,"RandomY":3579807591,"RandomZ":273326509,"StartTime":70116.0,"Objects":[{"StartTime":70116.0,"EndTime":70354.0,"Column":2}]},{"RandomW":273071671,"RandomX":842502087,"RandomY":3579807591,"RandomZ":273326509,"StartTime":70354.0,"Objects":[{"StartTime":70354.0,"EndTime":70354.0,"Column":0}]},{"RandomW":273071671,"RandomX":842502087,"RandomY":3579807591,"RandomZ":273326509,"StartTime":70354.0,"Objects":[{"StartTime":70354.0,"EndTime":70592.0,"Column":3}]},{"RandomW":273071671,"RandomX":842502087,"RandomY":3579807591,"RandomZ":273326509,"StartTime":70593.0,"Objects":[{"StartTime":70593.0,"EndTime":70831.0,"Column":1}]},{"RandomW":273071671,"RandomX":842502087,"RandomY":3579807591,"RandomZ":273326509,"StartTime":70831.0,"Objects":[{"StartTime":70831.0,"EndTime":70831.0,"Column":0}]},{"RandomW":273071671,"RandomX":842502087,"RandomY":3579807591,"RandomZ":273326509,"StartTime":70831.0,"Objects":[{"StartTime":70831.0,"EndTime":71069.0,"Column":3}]},{"RandomW":273071671,"RandomX":842502087,"RandomY":3579807591,"RandomZ":273326509,"StartTime":71069.0,"Objects":[{"StartTime":71069.0,"EndTime":71307.0,"Column":1}]},{"RandomW":273071671,"RandomX":842502087,"RandomY":3579807591,"RandomZ":273326509,"StartTime":71069.0,"Objects":[{"StartTime":71069.0,"EndTime":71307.0,"Column":2}]},{"RandomW":273071671,"RandomX":842502087,"RandomY":3579807591,"RandomZ":273326509,"StartTime":71307.0,"Objects":[{"StartTime":71307.0,"EndTime":71307.0,"Column":0}]},{"RandomW":273071671,"RandomX":842502087,"RandomY":3579807591,"RandomZ":273326509,"StartTime":71307.0,"Objects":[{"StartTime":71307.0,"EndTime":71545.0,"Column":3}]},{"RandomW":273071671,"RandomX":842502087,"RandomY":3579807591,"RandomZ":273326509,"StartTime":71545.0,"Objects":[{"StartTime":71545.0,"EndTime":71783.0,"Column":2}]},{"RandomW":273071671,"RandomX":842502087,"RandomY":3579807591,"RandomZ":273326509,"StartTime":71783.0,"Objects":[{"StartTime":71783.0,"EndTime":71783.0,"Column":0}]},{"RandomW":273071671,"RandomX":842502087,"RandomY":3579807591,"RandomZ":273326509,"StartTime":71783.0,"Objects":[{"StartTime":71783.0,"EndTime":72021.0,"Column":3}]},{"RandomW":273071671,"RandomX":842502087,"RandomY":3579807591,"RandomZ":273326509,"StartTime":72021.0,"Objects":[{"StartTime":72021.0,"EndTime":72259.0,"Column":2}]},{"RandomW":273071671,"RandomX":842502087,"RandomY":3579807591,"RandomZ":273326509,"StartTime":72259.0,"Objects":[{"StartTime":72259.0,"EndTime":72259.0,"Column":0}]},{"RandomW":273071671,"RandomX":842502087,"RandomY":3579807591,"RandomZ":273326509,"StartTime":72259.0,"Objects":[{"StartTime":72259.0,"EndTime":72497.0,"Column":3}]},{"RandomW":273071671,"RandomX":842502087,"RandomY":3579807591,"RandomZ":273326509,"StartTime":72497.0,"Objects":[{"StartTime":72497.0,"EndTime":72973.0,"Column":1}]},{"RandomW":273071671,"RandomX":842502087,"RandomY":3579807591,"RandomZ":273326509,"StartTime":72735.0,"Objects":[{"StartTime":72735.0,"EndTime":72735.0,"Column":0}]},{"RandomW":273071671,"RandomX":842502087,"RandomY":3579807591,"RandomZ":273326509,"StartTime":72735.0,"Objects":[{"StartTime":72735.0,"EndTime":72735.0,"Column":3}]},{"RandomW":273071671,"RandomX":842502087,"RandomY":3579807591,"RandomZ":273326509,"StartTime":72974.0,"Objects":[{"StartTime":72974.0,"EndTime":73212.0,"Column":2}]},{"RandomW":273071671,"RandomX":842502087,"RandomY":3579807591,"RandomZ":273326509,"StartTime":72974.0,"Objects":[{"StartTime":72974.0,"EndTime":72974.0,"Column":3}]},{"RandomW":273071671,"RandomX":842502087,"RandomY":3579807591,"RandomZ":273326509,"StartTime":73212.0,"Objects":[{"StartTime":73212.0,"EndTime":73212.0,"Column":0}]},{"RandomW":273071671,"RandomX":842502087,"RandomY":3579807591,"RandomZ":273326509,"StartTime":73212.0,"Objects":[{"StartTime":73212.0,"EndTime":73212.0,"Column":3}]},{"RandomW":273071671,"RandomX":842502087,"RandomY":3579807591,"RandomZ":273326509,"StartTime":73450.0,"Objects":[{"StartTime":73450.0,"EndTime":73688.0,"Column":2}]},{"RandomW":273071671,"RandomX":842502087,"RandomY":3579807591,"RandomZ":273326509,"StartTime":73450.0,"Objects":[{"StartTime":73450.0,"EndTime":73450.0,"Column":3}]},{"RandomW":273071671,"RandomX":842502087,"RandomY":3579807591,"RandomZ":273326509,"StartTime":73688.0,"Objects":[{"StartTime":73688.0,"EndTime":73688.0,"Column":0}]},{"RandomW":273071671,"RandomX":842502087,"RandomY":3579807591,"RandomZ":273326509,"StartTime":73688.0,"Objects":[{"StartTime":73688.0,"EndTime":73926.0,"Column":3}]},{"RandomW":273071671,"RandomX":842502087,"RandomY":3579807591,"RandomZ":273326509,"StartTime":73926.0,"Objects":[{"StartTime":73926.0,"EndTime":74164.0,"Column":1}]},{"RandomW":273071671,"RandomX":842502087,"RandomY":3579807591,"RandomZ":273326509,"StartTime":74164.0,"Objects":[{"StartTime":74164.0,"EndTime":74164.0,"Column":0}]},{"RandomW":273071671,"RandomX":842502087,"RandomY":3579807591,"RandomZ":273326509,"StartTime":74164.0,"Objects":[{"StartTime":74164.0,"EndTime":74402.0,"Column":3}]},{"RandomW":273071671,"RandomX":842502087,"RandomY":3579807591,"RandomZ":273326509,"StartTime":74402.0,"Objects":[{"StartTime":74402.0,"EndTime":75116.0,"Column":2}]},{"RandomW":273071671,"RandomX":842502087,"RandomY":3579807591,"RandomZ":273326509,"StartTime":74402.0,"Objects":[{"StartTime":74402.0,"EndTime":74402.0,"Column":1}]},{"RandomW":273071671,"RandomX":842502087,"RandomY":3579807591,"RandomZ":273326509,"StartTime":74640.0,"Objects":[{"StartTime":74640.0,"EndTime":74640.0,"Column":0}]},{"RandomW":273071671,"RandomX":842502087,"RandomY":3579807591,"RandomZ":273326509,"StartTime":74640.0,"Objects":[{"StartTime":74640.0,"EndTime":74878.0,"Column":3}]},{"RandomW":273071671,"RandomX":842502087,"RandomY":3579807591,"RandomZ":273326509,"StartTime":75116.0,"Objects":[{"StartTime":75116.0,"EndTime":75116.0,"Column":0}]},{"RandomW":273071671,"RandomX":842502087,"RandomY":3579807591,"RandomZ":273326509,"StartTime":75116.0,"Objects":[{"StartTime":75116.0,"EndTime":75354.0,"Column":3}]},{"RandomW":273071671,"RandomX":842502087,"RandomY":3579807591,"RandomZ":273326509,"StartTime":75354.0,"Objects":[{"StartTime":75354.0,"EndTime":75830.0,"Column":1}]},{"RandomW":273071671,"RandomX":842502087,"RandomY":3579807591,"RandomZ":273326509,"StartTime":75593.0,"Objects":[{"StartTime":75593.0,"EndTime":75593.0,"Column":0}]},{"RandomW":273071671,"RandomX":842502087,"RandomY":3579807591,"RandomZ":273326509,"StartTime":75593.0,"Objects":[{"StartTime":75593.0,"EndTime":75831.0,"Column":3}]},{"RandomW":273071671,"RandomX":842502087,"RandomY":3579807591,"RandomZ":273326509,"StartTime":75831.0,"Objects":[{"StartTime":75831.0,"EndTime":75831.0,"Column":2}]},{"RandomW":273071671,"RandomX":842502087,"RandomY":3579807591,"RandomZ":273326509,"StartTime":75950.0,"Objects":[{"StartTime":75950.0,"EndTime":75950.0,"Column":0}]},{"RandomW":273071671,"RandomX":842502087,"RandomY":3579807591,"RandomZ":273326509,"StartTime":76069.0,"Objects":[{"StartTime":76069.0,"EndTime":76307.0,"Column":3}]},{"RandomW":273071671,"RandomX":842502087,"RandomY":3579807591,"RandomZ":273326509,"StartTime":76069.0,"Objects":[{"StartTime":76069.0,"EndTime":76069.0,"Column":2}]},{"RandomW":273071671,"RandomX":842502087,"RandomY":3579807591,"RandomZ":273326509,"StartTime":76307.0,"Objects":[{"StartTime":76307.0,"EndTime":76545.0,"Column":1}]},{"RandomW":273071671,"RandomX":842502087,"RandomY":3579807591,"RandomZ":273326509,"StartTime":76307.0,"Objects":[{"StartTime":76307.0,"EndTime":76307.0,"Column":0}]},{"RandomW":273071671,"RandomX":842502087,"RandomY":3579807591,"RandomZ":273326509,"StartTime":76545.0,"Objects":[{"StartTime":76545.0,"EndTime":76545.0,"Column":0}]},{"RandomW":273071671,"RandomX":842502087,"RandomY":3579807591,"RandomZ":273326509,"StartTime":76545.0,"Objects":[{"StartTime":76545.0,"EndTime":76783.0,"Column":3}]},{"RandomW":273071671,"RandomX":842502087,"RandomY":3579807591,"RandomZ":273326509,"StartTime":76783.0,"Objects":[{"StartTime":76783.0,"EndTime":77021.0,"Column":2}]},{"RandomW":273071671,"RandomX":842502087,"RandomY":3579807591,"RandomZ":273326509,"StartTime":77021.0,"Objects":[{"StartTime":77021.0,"EndTime":77021.0,"Column":0}]},{"RandomW":273071671,"RandomX":842502087,"RandomY":3579807591,"RandomZ":273326509,"StartTime":77021.0,"Objects":[{"StartTime":77021.0,"EndTime":77259.0,"Column":3}]},{"RandomW":273071671,"RandomX":842502087,"RandomY":3579807591,"RandomZ":273326509,"StartTime":77259.0,"Objects":[{"StartTime":77259.0,"EndTime":77497.0,"Column":1}]},{"RandomW":273071671,"RandomX":842502087,"RandomY":3579807591,"RandomZ":273326509,"StartTime":77497.0,"Objects":[{"StartTime":77497.0,"EndTime":77735.0,"Column":3}]},{"RandomW":273071671,"RandomX":842502087,"RandomY":3579807591,"RandomZ":273326509,"StartTime":77498.0,"Objects":[{"StartTime":77498.0,"EndTime":77498.0,"Column":0}]},{"RandomW":273071671,"RandomX":842502087,"RandomY":3579807591,"RandomZ":273326509,"StartTime":77735.0,"Objects":[{"StartTime":77735.0,"EndTime":78211.0,"Column":2}]},{"RandomW":273071671,"RandomX":842502087,"RandomY":3579807591,"RandomZ":273326509,"StartTime":77974.0,"Objects":[{"StartTime":77974.0,"EndTime":77974.0,"Column":0}]},{"RandomW":273071671,"RandomX":842502087,"RandomY":3579807591,"RandomZ":273326509,"StartTime":77974.0,"Objects":[{"StartTime":77974.0,"EndTime":78212.0,"Column":3}]},{"RandomW":273071671,"RandomX":842502087,"RandomY":3579807591,"RandomZ":273326509,"StartTime":78212.0,"Objects":[{"StartTime":78212.0,"EndTime":78450.0,"Column":1}]},{"RandomW":273071671,"RandomX":842502087,"RandomY":3579807591,"RandomZ":273326509,"StartTime":78450.0,"Objects":[{"StartTime":78450.0,"EndTime":78450.0,"Column":3}]},{"RandomW":273071671,"RandomX":842502087,"RandomY":3579807591,"RandomZ":273326509,"StartTime":78450.0,"Objects":[{"StartTime":78450.0,"EndTime":78450.0,"Column":0}]},{"RandomW":273071671,"RandomX":842502087,"RandomY":3579807591,"RandomZ":273326509,"StartTime":78688.0,"Objects":[{"StartTime":78688.0,"EndTime":78688.0,"Column":3}]},{"RandomW":273071671,"RandomX":842502087,"RandomY":3579807591,"RandomZ":273326509,"StartTime":78688.0,"Objects":[{"StartTime":78688.0,"EndTime":78926.0,"Column":2}]},{"RandomW":273071671,"RandomX":842502087,"RandomY":3579807591,"RandomZ":273326509,"StartTime":78926.0,"Objects":[{"StartTime":78926.0,"EndTime":78926.0,"Column":3}]},{"RandomW":273071671,"RandomX":842502087,"RandomY":3579807591,"RandomZ":273326509,"StartTime":78926.0,"Objects":[{"StartTime":78926.0,"EndTime":78926.0,"Column":0}]},{"RandomW":273071671,"RandomX":842502087,"RandomY":3579807591,"RandomZ":273326509,"StartTime":79164.0,"Objects":[{"StartTime":79164.0,"EndTime":79164.0,"Column":3}]},{"RandomW":273071671,"RandomX":842502087,"RandomY":3579807591,"RandomZ":273326509,"StartTime":79164.0,"Objects":[{"StartTime":79164.0,"EndTime":79402.0,"Column":1}]},{"RandomW":273071671,"RandomX":842502087,"RandomY":3579807591,"RandomZ":273326509,"StartTime":79402.0,"Objects":[{"StartTime":79402.0,"EndTime":79640.0,"Column":3}]},{"RandomW":273071671,"RandomX":842502087,"RandomY":3579807591,"RandomZ":273326509,"StartTime":79402.0,"Objects":[{"StartTime":79402.0,"EndTime":79402.0,"Column":0}]},{"RandomW":273071671,"RandomX":842502087,"RandomY":3579807591,"RandomZ":273326509,"StartTime":79640.0,"Objects":[{"StartTime":79640.0,"EndTime":79640.0,"Column":2}]},{"RandomW":273071671,"RandomX":842502087,"RandomY":3579807591,"RandomZ":273326509,"StartTime":79878.0,"Objects":[{"StartTime":79878.0,"EndTime":79878.0,"Column":0}]},{"RandomW":273071671,"RandomX":842502087,"RandomY":3579807591,"RandomZ":273326509,"StartTime":79878.0,"Objects":[{"StartTime":79878.0,"EndTime":80116.0,"Column":3}]},{"RandomW":273071671,"RandomX":842502087,"RandomY":3579807591,"RandomZ":273326509,"StartTime":79878.0,"Objects":[{"StartTime":79878.0,"EndTime":79878.0,"Column":2}]},{"RandomW":273071671,"RandomX":842502087,"RandomY":3579807591,"RandomZ":273326509,"StartTime":80117.0,"Objects":[{"StartTime":80117.0,"EndTime":80355.0,"Column":1}]},{"RandomW":273071671,"RandomX":842502087,"RandomY":3579807591,"RandomZ":273326509,"StartTime":80355.0,"Objects":[{"StartTime":80355.0,"EndTime":80593.0,"Column":3}]},{"RandomW":273071671,"RandomX":842502087,"RandomY":3579807591,"RandomZ":273326509,"StartTime":80355.0,"Objects":[{"StartTime":80355.0,"EndTime":80355.0,"Column":0}]},{"RandomW":273071671,"RandomX":842502087,"RandomY":3579807591,"RandomZ":273326509,"StartTime":80593.0,"Objects":[{"StartTime":80593.0,"EndTime":80831.0,"Column":1}]},{"RandomW":273071671,"RandomX":842502087,"RandomY":3579807591,"RandomZ":273326509,"StartTime":80831.0,"Objects":[{"StartTime":80831.0,"EndTime":81069.0,"Column":3}]},{"RandomW":273071671,"RandomX":842502087,"RandomY":3579807591,"RandomZ":273326509,"StartTime":80831.0,"Objects":[{"StartTime":80831.0,"EndTime":80831.0,"Column":0}]},{"RandomW":273071671,"RandomX":842502087,"RandomY":3579807591,"RandomZ":273326509,"StartTime":81069.0,"Objects":[{"StartTime":81069.0,"EndTime":81307.0,"Column":2}]},{"RandomW":273071671,"RandomX":842502087,"RandomY":3579807591,"RandomZ":273326509,"StartTime":81307.0,"Objects":[{"StartTime":81307.0,"EndTime":81545.0,"Column":3}]},{"RandomW":273071671,"RandomX":842502087,"RandomY":3579807591,"RandomZ":273326509,"StartTime":81307.0,"Objects":[{"StartTime":81307.0,"EndTime":81307.0,"Column":0}]},{"RandomW":273071671,"RandomX":842502087,"RandomY":3579807591,"RandomZ":273326509,"StartTime":81545.0,"Objects":[{"StartTime":81545.0,"EndTime":81783.0,"Column":2}]},{"RandomW":273071671,"RandomX":842502087,"RandomY":3579807591,"RandomZ":273326509,"StartTime":81783.0,"Objects":[{"StartTime":81783.0,"EndTime":82021.0,"Column":3}]},{"RandomW":273071671,"RandomX":842502087,"RandomY":3579807591,"RandomZ":273326509,"StartTime":81783.0,"Objects":[{"StartTime":81783.0,"EndTime":81783.0,"Column":0}]},{"RandomW":273071671,"RandomX":842502087,"RandomY":3579807591,"RandomZ":273326509,"StartTime":82021.0,"Objects":[{"StartTime":82021.0,"EndTime":82497.0,"Column":1}]},{"RandomW":273071671,"RandomX":842502087,"RandomY":3579807591,"RandomZ":273326509,"StartTime":82259.0,"Objects":[{"StartTime":82259.0,"EndTime":82259.0,"Column":0}]},{"RandomW":273071671,"RandomX":842502087,"RandomY":3579807591,"RandomZ":273326509,"StartTime":82259.0,"Objects":[{"StartTime":82259.0,"EndTime":82259.0,"Column":3}]},{"RandomW":273071671,"RandomX":842502087,"RandomY":3579807591,"RandomZ":273326509,"StartTime":82498.0,"Objects":[{"StartTime":82498.0,"EndTime":82736.0,"Column":2}]},{"RandomW":273071671,"RandomX":842502087,"RandomY":3579807591,"RandomZ":273326509,"StartTime":82498.0,"Objects":[{"StartTime":82498.0,"EndTime":82498.0,"Column":3}]},{"RandomW":273071671,"RandomX":842502087,"RandomY":3579807591,"RandomZ":273326509,"StartTime":82736.0,"Objects":[{"StartTime":82736.0,"EndTime":82736.0,"Column":3}]},{"RandomW":273071671,"RandomX":842502087,"RandomY":3579807591,"RandomZ":273326509,"StartTime":82736.0,"Objects":[{"StartTime":82736.0,"EndTime":82736.0,"Column":0}]},{"RandomW":273071671,"RandomX":842502087,"RandomY":3579807591,"RandomZ":273326509,"StartTime":82974.0,"Objects":[{"StartTime":82974.0,"EndTime":83212.0,"Column":2}]},{"RandomW":273071671,"RandomX":842502087,"RandomY":3579807591,"RandomZ":273326509,"StartTime":82974.0,"Objects":[{"StartTime":82974.0,"EndTime":82974.0,"Column":3}]},{"RandomW":273071671,"RandomX":842502087,"RandomY":3579807591,"RandomZ":273326509,"StartTime":83212.0,"Objects":[{"StartTime":83212.0,"EndTime":83450.0,"Column":3}]},{"RandomW":273071671,"RandomX":842502087,"RandomY":3579807591,"RandomZ":273326509,"StartTime":83212.0,"Objects":[{"StartTime":83212.0,"EndTime":83212.0,"Column":0}]},{"RandomW":273071671,"RandomX":842502087,"RandomY":3579807591,"RandomZ":273326509,"StartTime":83450.0,"Objects":[{"StartTime":83450.0,"EndTime":83688.0,"Column":1}]},{"RandomW":273071671,"RandomX":842502087,"RandomY":3579807591,"RandomZ":273326509,"StartTime":83569.0,"Objects":[{"StartTime":83569.0,"EndTime":83569.0,"Column":0}]},{"RandomW":273071671,"RandomX":842502087,"RandomY":3579807591,"RandomZ":273326509,"StartTime":83688.0,"Objects":[{"StartTime":83688.0,"EndTime":83926.0,"Column":3}]},{"RandomW":273071671,"RandomX":842502087,"RandomY":3579807591,"RandomZ":273326509,"StartTime":83926.0,"Objects":[{"StartTime":83926.0,"EndTime":84402.0,"Column":2}]},{"RandomW":273071671,"RandomX":842502087,"RandomY":3579807591,"RandomZ":273326509,"StartTime":83926.0,"Objects":[{"StartTime":83926.0,"EndTime":83926.0,"Column":0}]},{"RandomW":273071671,"RandomX":842502087,"RandomY":3579807591,"RandomZ":273326509,"StartTime":84164.0,"Objects":[{"StartTime":84164.0,"EndTime":84402.0,"Column":3}]},{"RandomW":273071671,"RandomX":842502087,"RandomY":3579807591,"RandomZ":273326509,"StartTime":84164.0,"Objects":[{"StartTime":84164.0,"EndTime":84164.0,"Column":0}]},{"RandomW":273071671,"RandomX":842502087,"RandomY":3579807591,"RandomZ":273326509,"StartTime":84402.0,"Objects":[{"StartTime":84402.0,"EndTime":84640.0,"Column":1}]},{"RandomW":273071671,"RandomX":842502087,"RandomY":3579807591,"RandomZ":273326509,"StartTime":84640.0,"Objects":[{"StartTime":84640.0,"EndTime":84878.0,"Column":3}]},{"RandomW":273071671,"RandomX":842502087,"RandomY":3579807591,"RandomZ":273326509,"StartTime":84640.0,"Objects":[{"StartTime":84640.0,"EndTime":84640.0,"Column":0}]},{"RandomW":273071671,"RandomX":842502087,"RandomY":3579807591,"RandomZ":273326509,"StartTime":84878.0,"Objects":[{"StartTime":84878.0,"EndTime":85354.0,"Column":1}]},{"RandomW":273071671,"RandomX":842502087,"RandomY":3579807591,"RandomZ":273326509,"StartTime":85117.0,"Objects":[{"StartTime":85117.0,"EndTime":85117.0,"Column":0}]},{"RandomW":273071671,"RandomX":842502087,"RandomY":3579807591,"RandomZ":273326509,"StartTime":85117.0,"Objects":[{"StartTime":85117.0,"EndTime":85355.0,"Column":3}]},{"RandomW":273071671,"RandomX":842502087,"RandomY":3579807591,"RandomZ":273326509,"StartTime":85354.0,"Objects":[{"StartTime":85354.0,"EndTime":85592.0,"Column":2}]},{"RandomW":273071671,"RandomX":842502087,"RandomY":3579807591,"RandomZ":273326509,"StartTime":85593.0,"Objects":[{"StartTime":85593.0,"EndTime":85831.0,"Column":3}]},{"RandomW":273071671,"RandomX":842502087,"RandomY":3579807591,"RandomZ":273326509,"StartTime":85593.0,"Objects":[{"StartTime":85593.0,"EndTime":85593.0,"Column":0}]},{"RandomW":273071671,"RandomX":842502087,"RandomY":3579807591,"RandomZ":273326509,"StartTime":85831.0,"Objects":[{"StartTime":85831.0,"EndTime":86069.0,"Column":1}]},{"RandomW":273071671,"RandomX":842502087,"RandomY":3579807591,"RandomZ":273326509,"StartTime":86069.0,"Objects":[{"StartTime":86069.0,"EndTime":86069.0,"Column":0}]},{"RandomW":273071671,"RandomX":842502087,"RandomY":3579807591,"RandomZ":273326509,"StartTime":86069.0,"Objects":[{"StartTime":86069.0,"EndTime":86307.0,"Column":3}]},{"RandomW":273071671,"RandomX":842502087,"RandomY":3579807591,"RandomZ":273326509,"StartTime":86307.0,"Objects":[{"StartTime":86307.0,"EndTime":86545.0,"Column":2}]},{"RandomW":273071671,"RandomX":842502087,"RandomY":3579807591,"RandomZ":273326509,"StartTime":86307.0,"Objects":[{"StartTime":86307.0,"EndTime":86545.0,"Column":1}]},{"RandomW":273071671,"RandomX":842502087,"RandomY":3579807591,"RandomZ":273326509,"StartTime":86545.0,"Objects":[{"StartTime":86545.0,"EndTime":86545.0,"Column":0}]},{"RandomW":273071671,"RandomX":842502087,"RandomY":3579807591,"RandomZ":273326509,"StartTime":86545.0,"Objects":[{"StartTime":86545.0,"EndTime":86783.0,"Column":3}]},{"RandomW":273071671,"RandomX":842502087,"RandomY":3579807591,"RandomZ":273326509,"StartTime":86783.0,"Objects":[{"StartTime":86783.0,"EndTime":87021.0,"Column":1}]},{"RandomW":273071671,"RandomX":842502087,"RandomY":3579807591,"RandomZ":273326509,"StartTime":87021.0,"Objects":[{"StartTime":87021.0,"EndTime":87021.0,"Column":0}]},{"RandomW":273071671,"RandomX":842502087,"RandomY":3579807591,"RandomZ":273326509,"StartTime":87021.0,"Objects":[{"StartTime":87021.0,"EndTime":87259.0,"Column":3}]},{"RandomW":273071671,"RandomX":842502087,"RandomY":3579807591,"RandomZ":273326509,"StartTime":87259.0,"Objects":[{"StartTime":87259.0,"EndTime":87497.0,"Column":2}]},{"RandomW":273071671,"RandomX":842502087,"RandomY":3579807591,"RandomZ":273326509,"StartTime":87497.0,"Objects":[{"StartTime":87497.0,"EndTime":87497.0,"Column":0}]},{"RandomW":273071671,"RandomX":842502087,"RandomY":3579807591,"RandomZ":273326509,"StartTime":87497.0,"Objects":[{"StartTime":87497.0,"EndTime":87735.0,"Column":3}]},{"RandomW":273071671,"RandomX":842502087,"RandomY":3579807591,"RandomZ":273326509,"StartTime":87735.0,"Objects":[{"StartTime":87735.0,"EndTime":88211.0,"Column":2}]},{"RandomW":273071671,"RandomX":842502087,"RandomY":3579807591,"RandomZ":273326509,"StartTime":87973.0,"Objects":[{"StartTime":87973.0,"EndTime":87973.0,"Column":0}]},{"RandomW":273071671,"RandomX":842502087,"RandomY":3579807591,"RandomZ":273326509,"StartTime":87974.0,"Objects":[{"StartTime":87974.0,"EndTime":87974.0,"Column":3}]},{"RandomW":273071671,"RandomX":842502087,"RandomY":3579807591,"RandomZ":273326509,"StartTime":88212.0,"Objects":[{"StartTime":88212.0,"EndTime":88450.0,"Column":1}]},{"RandomW":273071671,"RandomX":842502087,"RandomY":3579807591,"RandomZ":273326509,"StartTime":88212.0,"Objects":[{"StartTime":88212.0,"EndTime":88212.0,"Column":3}]},{"RandomW":273071671,"RandomX":842502087,"RandomY":3579807591,"RandomZ":273326509,"StartTime":88450.0,"Objects":[{"StartTime":88450.0,"EndTime":88450.0,"Column":0}]},{"RandomW":273071671,"RandomX":842502087,"RandomY":3579807591,"RandomZ":273326509,"StartTime":88450.0,"Objects":[{"StartTime":88450.0,"EndTime":88450.0,"Column":3}]},{"RandomW":273071671,"RandomX":842502087,"RandomY":3579807591,"RandomZ":273326509,"StartTime":88688.0,"Objects":[{"StartTime":88688.0,"EndTime":88926.0,"Column":1}]},{"RandomW":273071671,"RandomX":842502087,"RandomY":3579807591,"RandomZ":273326509,"StartTime":88688.0,"Objects":[{"StartTime":88688.0,"EndTime":88688.0,"Column":3}]},{"RandomW":273071671,"RandomX":842502087,"RandomY":3579807591,"RandomZ":273326509,"StartTime":88926.0,"Objects":[{"StartTime":88926.0,"EndTime":88926.0,"Column":0}]},{"RandomW":273071671,"RandomX":842502087,"RandomY":3579807591,"RandomZ":273326509,"StartTime":88926.0,"Objects":[{"StartTime":88926.0,"EndTime":89164.0,"Column":3}]},{"RandomW":273071671,"RandomX":842502087,"RandomY":3579807591,"RandomZ":273326509,"StartTime":89164.0,"Objects":[{"StartTime":89164.0,"EndTime":89402.0,"Column":2}]},{"RandomW":273071671,"RandomX":842502087,"RandomY":3579807591,"RandomZ":273326509,"StartTime":89402.0,"Objects":[{"StartTime":89402.0,"EndTime":89640.0,"Column":3}]},{"RandomW":273071671,"RandomX":842502087,"RandomY":3579807591,"RandomZ":273326509,"StartTime":89402.0,"Objects":[{"StartTime":89402.0,"EndTime":89402.0,"Column":0}]},{"RandomW":273071671,"RandomX":842502087,"RandomY":3579807591,"RandomZ":273326509,"StartTime":89640.0,"Objects":[{"StartTime":89640.0,"EndTime":89878.0,"Column":1}]},{"RandomW":273071671,"RandomX":842502087,"RandomY":3579807591,"RandomZ":273326509,"StartTime":89878.0,"Objects":[{"StartTime":89878.0,"EndTime":89878.0,"Column":0}]},{"RandomW":273071671,"RandomX":842502087,"RandomY":3579807591,"RandomZ":273326509,"StartTime":89878.0,"Objects":[{"StartTime":89878.0,"EndTime":90116.0,"Column":3}]},{"RandomW":273071671,"RandomX":842502087,"RandomY":3579807591,"RandomZ":273326509,"StartTime":89878.0,"Objects":[{"StartTime":89878.0,"EndTime":90354.0,"Column":2}]},{"RandomW":273071671,"RandomX":842502087,"RandomY":3579807591,"RandomZ":273326509,"StartTime":90116.0,"Objects":[{"StartTime":90116.0,"EndTime":90354.0,"Column":1}]},{"RandomW":273071671,"RandomX":842502087,"RandomY":3579807591,"RandomZ":273326509,"StartTime":90354.0,"Objects":[{"StartTime":90354.0,"EndTime":90354.0,"Column":0}]},{"RandomW":273071671,"RandomX":842502087,"RandomY":3579807591,"RandomZ":273326509,"StartTime":90354.0,"Objects":[{"StartTime":90354.0,"EndTime":90592.0,"Column":3}]},{"RandomW":273071671,"RandomX":842502087,"RandomY":3579807591,"RandomZ":273326509,"StartTime":90593.0,"Objects":[{"StartTime":90593.0,"EndTime":90831.0,"Column":2}]},{"RandomW":273071671,"RandomX":842502087,"RandomY":3579807591,"RandomZ":273326509,"StartTime":90831.0,"Objects":[{"StartTime":90831.0,"EndTime":90831.0,"Column":0}]},{"RandomW":273071671,"RandomX":842502087,"RandomY":3579807591,"RandomZ":273326509,"StartTime":90831.0,"Objects":[{"StartTime":90831.0,"EndTime":91069.0,"Column":3}]},{"RandomW":273071671,"RandomX":842502087,"RandomY":3579807591,"RandomZ":273326509,"StartTime":91069.0,"Objects":[{"StartTime":91069.0,"EndTime":91307.0,"Column":2}]},{"RandomW":273071671,"RandomX":842502087,"RandomY":3579807591,"RandomZ":273326509,"StartTime":91307.0,"Objects":[{"StartTime":91307.0,"EndTime":91545.0,"Column":3}]},{"RandomW":273071671,"RandomX":842502087,"RandomY":3579807591,"RandomZ":273326509,"StartTime":91307.0,"Objects":[{"StartTime":91307.0,"EndTime":91307.0,"Column":0}]},{"RandomW":2659271247,"RandomX":3579807591,"RandomY":273326509,"RandomZ":273071671,"StartTime":91545.0,"Objects":[{"StartTime":91545.0,"EndTime":92735.0,"Column":0}]},{"RandomW":2659271247,"RandomX":3579807591,"RandomY":273326509,"RandomZ":273071671,"StartTime":91545.0,"Objects":[{"StartTime":91545.0,"EndTime":91545.0,"Column":1}]},{"RandomW":2659271247,"RandomX":3579807591,"RandomY":273326509,"RandomZ":273071671,"StartTime":91783.0,"Objects":[{"StartTime":91783.0,"EndTime":91783.0,"Column":3}]},{"RandomW":2659271247,"RandomX":3579807591,"RandomY":273326509,"RandomZ":273071671,"StartTime":91783.0,"Objects":[{"StartTime":91783.0,"EndTime":91783.0,"Column":1}]},{"RandomW":2659271247,"RandomX":3579807591,"RandomY":273326509,"RandomZ":273071671,"StartTime":92021.0,"Objects":[{"StartTime":92021.0,"EndTime":92021.0,"Column":3}]},{"RandomW":2659271247,"RandomX":3579807591,"RandomY":273326509,"RandomZ":273071671,"StartTime":92259.0,"Objects":[{"StartTime":92259.0,"EndTime":92259.0,"Column":3}]},{"RandomW":2659271247,"RandomX":3579807591,"RandomY":273326509,"RandomZ":273071671,"StartTime":92259.0,"Objects":[{"StartTime":92259.0,"EndTime":92259.0,"Column":1}]},{"RandomW":2659271247,"RandomX":3579807591,"RandomY":273326509,"RandomZ":273071671,"StartTime":92497.0,"Objects":[{"StartTime":92497.0,"EndTime":92497.0,"Column":3}]},{"RandomW":2659271247,"RandomX":3579807591,"RandomY":273326509,"RandomZ":273071671,"StartTime":92735.0,"Objects":[{"StartTime":92735.0,"EndTime":92973.0,"Column":3}]},{"RandomW":2659271247,"RandomX":3579807591,"RandomY":273326509,"RandomZ":273071671,"StartTime":92735.0,"Objects":[{"StartTime":92735.0,"EndTime":92735.0,"Column":1}]},{"RandomW":2659271247,"RandomX":3579807591,"RandomY":273326509,"RandomZ":273071671,"StartTime":92974.0,"Objects":[{"StartTime":92974.0,"EndTime":93212.0,"Column":1}]},{"RandomW":2659271247,"RandomX":3579807591,"RandomY":273326509,"RandomZ":273071671,"StartTime":93212.0,"Objects":[{"StartTime":93212.0,"EndTime":93450.0,"Column":2}]},{"RandomW":2659271247,"RandomX":3579807591,"RandomY":273326509,"RandomZ":273071671,"StartTime":93212.0,"Objects":[{"StartTime":93212.0,"EndTime":93212.0,"Column":0}]},{"RandomW":2659271247,"RandomX":3579807591,"RandomY":273326509,"RandomZ":273071671,"StartTime":93450.0,"Objects":[{"StartTime":93450.0,"EndTime":93450.0,"Column":3}]},{"RandomW":2659271247,"RandomX":3579807591,"RandomY":273326509,"RandomZ":273071671,"StartTime":93688.0,"Objects":[{"StartTime":93688.0,"EndTime":93688.0,"Column":0}]},{"RandomW":2659271247,"RandomX":3579807591,"RandomY":273326509,"RandomZ":273071671,"StartTime":93688.0,"Objects":[{"StartTime":93688.0,"EndTime":93926.0,"Column":3}]},{"RandomW":2659271247,"RandomX":3579807591,"RandomY":273326509,"RandomZ":273071671,"StartTime":93688.0,"Objects":[{"StartTime":93688.0,"EndTime":94164.0,"Column":1}]},{"RandomW":2659271247,"RandomX":3579807591,"RandomY":273326509,"RandomZ":273071671,"StartTime":94164.0,"Objects":[{"StartTime":94164.0,"EndTime":94402.0,"Column":2}]},{"RandomW":2659271247,"RandomX":3579807591,"RandomY":273326509,"RandomZ":273071671,"StartTime":94164.0,"Objects":[{"StartTime":94164.0,"EndTime":94164.0,"Column":0}]},{"RandomW":2659271247,"RandomX":3579807591,"RandomY":273326509,"RandomZ":273071671,"StartTime":94402.0,"Objects":[{"StartTime":94402.0,"EndTime":94402.0,"Column":1}]},{"RandomW":2659271247,"RandomX":3579807591,"RandomY":273326509,"RandomZ":273071671,"StartTime":94402.0,"Objects":[{"StartTime":94402.0,"EndTime":94402.0,"Column":3}]},{"RandomW":2659271247,"RandomX":3579807591,"RandomY":273326509,"RandomZ":273071671,"StartTime":94640.0,"Objects":[{"StartTime":94640.0,"EndTime":94640.0,"Column":0}]},{"RandomW":2659271247,"RandomX":3579807591,"RandomY":273326509,"RandomZ":273071671,"StartTime":94640.0,"Objects":[{"StartTime":94640.0,"EndTime":94878.0,"Column":3}]},{"RandomW":2659271247,"RandomX":3579807591,"RandomY":273326509,"RandomZ":273071671,"StartTime":94640.0,"Objects":[{"StartTime":94640.0,"EndTime":94640.0,"Column":2}]},{"RandomW":2659271247,"RandomX":3579807591,"RandomY":273326509,"RandomZ":273071671,"StartTime":95116.0,"Objects":[{"StartTime":95116.0,"EndTime":95592.0,"Column":1}]},{"RandomW":2659271247,"RandomX":3579807591,"RandomY":273326509,"RandomZ":273071671,"StartTime":95116.0,"Objects":[{"StartTime":95116.0,"EndTime":95116.0,"Column":0}]},{"RandomW":2659271247,"RandomX":3579807591,"RandomY":273326509,"RandomZ":273071671,"StartTime":95116.0,"Objects":[{"StartTime":95116.0,"EndTime":95354.0,"Column":2}]},{"RandomW":2659271247,"RandomX":3579807591,"RandomY":273326509,"RandomZ":273071671,"StartTime":95593.0,"Objects":[{"StartTime":95593.0,"EndTime":95593.0,"Column":0}]},{"RandomW":2659271247,"RandomX":3579807591,"RandomY":273326509,"RandomZ":273071671,"StartTime":95593.0,"Objects":[{"StartTime":95593.0,"EndTime":95593.0,"Column":3}]},{"RandomW":2659271247,"RandomX":3579807591,"RandomY":273326509,"RandomZ":273071671,"StartTime":95831.0,"Objects":[{"StartTime":95831.0,"EndTime":95831.0,"Column":3}]},{"RandomW":2659271247,"RandomX":3579807591,"RandomY":273326509,"RandomZ":273071671,"StartTime":96069.0,"Objects":[{"StartTime":96069.0,"EndTime":96069.0,"Column":0}]},{"RandomW":2659271247,"RandomX":3579807591,"RandomY":273326509,"RandomZ":273071671,"StartTime":96069.0,"Objects":[{"StartTime":96069.0,"EndTime":96069.0,"Column":2}]},{"RandomW":2659271247,"RandomX":3579807591,"RandomY":273326509,"RandomZ":273071671,"StartTime":96307.0,"Objects":[{"StartTime":96307.0,"EndTime":96307.0,"Column":2}]},{"RandomW":2659271247,"RandomX":3579807591,"RandomY":273326509,"RandomZ":273071671,"StartTime":96545.0,"Objects":[{"StartTime":96545.0,"EndTime":96545.0,"Column":0}]},{"RandomW":2659271247,"RandomX":3579807591,"RandomY":273326509,"RandomZ":273071671,"StartTime":96545.0,"Objects":[{"StartTime":96545.0,"EndTime":96783.0,"Column":3}]},{"RandomW":2659271247,"RandomX":3579807591,"RandomY":273326509,"RandomZ":273071671,"StartTime":96783.0,"Objects":[{"StartTime":96783.0,"EndTime":97259.0,"Column":1}]},{"RandomW":2659271247,"RandomX":3579807591,"RandomY":273326509,"RandomZ":273071671,"StartTime":97021.0,"Objects":[{"StartTime":97021.0,"EndTime":97021.0,"Column":0}]},{"RandomW":2659271247,"RandomX":3579807591,"RandomY":273326509,"RandomZ":273071671,"StartTime":97021.0,"Objects":[{"StartTime":97021.0,"EndTime":97259.0,"Column":2}]},{"RandomW":2659271247,"RandomX":3579807591,"RandomY":273326509,"RandomZ":273071671,"StartTime":97259.0,"Objects":[{"StartTime":97259.0,"EndTime":97259.0,"Column":3}]},{"RandomW":2659271247,"RandomX":3579807591,"RandomY":273326509,"RandomZ":273071671,"StartTime":97497.0,"Objects":[{"StartTime":97497.0,"EndTime":97497.0,"Column":0}]},{"RandomW":2659271247,"RandomX":3579807591,"RandomY":273326509,"RandomZ":273071671,"StartTime":97497.0,"Objects":[{"StartTime":97497.0,"EndTime":97497.0,"Column":2}]},{"RandomW":2659271247,"RandomX":3579807591,"RandomY":273326509,"RandomZ":273071671,"StartTime":97497.0,"Objects":[{"StartTime":97497.0,"EndTime":97735.0,"Column":3}]},{"RandomW":2659271247,"RandomX":3579807591,"RandomY":273326509,"RandomZ":273071671,"StartTime":97735.0,"Objects":[{"StartTime":97735.0,"EndTime":98211.0,"Column":1}]},{"RandomW":2659271247,"RandomX":3579807591,"RandomY":273326509,"RandomZ":273071671,"StartTime":97974.0,"Objects":[{"StartTime":97974.0,"EndTime":97974.0,"Column":0}]},{"RandomW":2659271247,"RandomX":3579807591,"RandomY":273326509,"RandomZ":273071671,"StartTime":97974.0,"Objects":[{"StartTime":97974.0,"EndTime":98212.0,"Column":2}]},{"RandomW":2659271247,"RandomX":3579807591,"RandomY":273326509,"RandomZ":273071671,"StartTime":98212.0,"Objects":[{"StartTime":98212.0,"EndTime":98212.0,"Column":3}]},{"RandomW":2659271247,"RandomX":3579807591,"RandomY":273326509,"RandomZ":273071671,"StartTime":98450.0,"Objects":[{"StartTime":98450.0,"EndTime":98450.0,"Column":2}]},{"RandomW":2659271247,"RandomX":3579807591,"RandomY":273326509,"RandomZ":273071671,"StartTime":98450.0,"Objects":[{"StartTime":98450.0,"EndTime":98450.0,"Column":0}]},{"RandomW":2659271247,"RandomX":3579807591,"RandomY":273326509,"RandomZ":273071671,"StartTime":98450.0,"Objects":[{"StartTime":98450.0,"EndTime":98688.0,"Column":3}]},{"RandomW":2659271247,"RandomX":3579807591,"RandomY":273326509,"RandomZ":273071671,"StartTime":98747.0,"Objects":[{"StartTime":98747.0,"EndTime":98747.0,"Column":0}]},{"RandomW":2659271247,"RandomX":3579807591,"RandomY":273326509,"RandomZ":273071671,"StartTime":98926.0,"Objects":[{"StartTime":98926.0,"EndTime":99640.0,"Column":1}]},{"RandomW":2659271247,"RandomX":3579807591,"RandomY":273326509,"RandomZ":273071671,"StartTime":98926.0,"Objects":[{"StartTime":98926.0,"EndTime":99164.0,"Column":2}]},{"RandomW":2659271247,"RandomX":3579807591,"RandomY":273326509,"RandomZ":273071671,"StartTime":99164.0,"Objects":[{"StartTime":99164.0,"EndTime":99164.0,"Column":0}]},{"RandomW":2659271247,"RandomX":3579807591,"RandomY":273326509,"RandomZ":273071671,"StartTime":99402.0,"Objects":[{"StartTime":99402.0,"EndTime":99402.0,"Column":0}]},{"RandomW":2659271247,"RandomX":3579807591,"RandomY":273326509,"RandomZ":273071671,"StartTime":99402.0,"Objects":[{"StartTime":99402.0,"EndTime":99402.0,"Column":3}]},{"RandomW":2659271247,"RandomX":3579807591,"RandomY":273326509,"RandomZ":273071671,"StartTime":99640.0,"Objects":[{"StartTime":99640.0,"EndTime":99640.0,"Column":3}]},{"RandomW":2659271247,"RandomX":3579807591,"RandomY":273326509,"RandomZ":273071671,"StartTime":99878.0,"Objects":[{"StartTime":99878.0,"EndTime":99878.0,"Column":0}]},{"RandomW":2659271247,"RandomX":3579807591,"RandomY":273326509,"RandomZ":273071671,"StartTime":99878.0,"Objects":[{"StartTime":99878.0,"EndTime":99878.0,"Column":2}]},{"RandomW":2659271247,"RandomX":3579807591,"RandomY":273326509,"RandomZ":273071671,"StartTime":100116.0,"Objects":[{"StartTime":100116.0,"EndTime":100116.0,"Column":2}]},{"RandomW":2659271247,"RandomX":3579807591,"RandomY":273326509,"RandomZ":273071671,"StartTime":100354.0,"Objects":[{"StartTime":100354.0,"EndTime":100354.0,"Column":0}]},{"RandomW":2659271247,"RandomX":3579807591,"RandomY":273326509,"RandomZ":273071671,"StartTime":100354.0,"Objects":[{"StartTime":100354.0,"EndTime":100830.0,"Column":1}]},{"RandomW":2659271247,"RandomX":3579807591,"RandomY":273326509,"RandomZ":273071671,"StartTime":100354.0,"Objects":[{"StartTime":100354.0,"EndTime":100592.0,"Column":3}]},{"RandomW":2659271247,"RandomX":3579807591,"RandomY":273326509,"RandomZ":273071671,"StartTime":100831.0,"Objects":[{"StartTime":100831.0,"EndTime":101069.0,"Column":2}]},{"RandomW":2659271247,"RandomX":3579807591,"RandomY":273326509,"RandomZ":273071671,"StartTime":100831.0,"Objects":[{"StartTime":100831.0,"EndTime":100831.0,"Column":0}]},{"RandomW":2659271247,"RandomX":3579807591,"RandomY":273326509,"RandomZ":273071671,"StartTime":101069.0,"Objects":[{"StartTime":101069.0,"EndTime":101069.0,"Column":3}]},{"RandomW":2659271247,"RandomX":3579807591,"RandomY":273326509,"RandomZ":273071671,"StartTime":101307.0,"Objects":[{"StartTime":101307.0,"EndTime":101545.0,"Column":3}]},{"RandomW":2659271247,"RandomX":3579807591,"RandomY":273326509,"RandomZ":273071671,"StartTime":101307.0,"Objects":[{"StartTime":101307.0,"EndTime":101783.0,"Column":1}]},{"RandomW":2659271247,"RandomX":3579807591,"RandomY":273326509,"RandomZ":273071671,"StartTime":101307.0,"Objects":[{"StartTime":101307.0,"EndTime":101307.0,"Column":0}]},{"RandomW":2659271247,"RandomX":3579807591,"RandomY":273326509,"RandomZ":273071671,"StartTime":101783.0,"Objects":[{"StartTime":101783.0,"EndTime":102021.0,"Column":2}]},{"RandomW":2659271247,"RandomX":3579807591,"RandomY":273326509,"RandomZ":273071671,"StartTime":101783.0,"Objects":[{"StartTime":101783.0,"EndTime":101783.0,"Column":0}]},{"RandomW":2659271247,"RandomX":3579807591,"RandomY":273326509,"RandomZ":273071671,"StartTime":102021.0,"Objects":[{"StartTime":102021.0,"EndTime":102021.0,"Column":1}]},{"RandomW":2659271247,"RandomX":3579807591,"RandomY":273326509,"RandomZ":273071671,"StartTime":102021.0,"Objects":[{"StartTime":102021.0,"EndTime":102021.0,"Column":3}]},{"RandomW":2659271247,"RandomX":3579807591,"RandomY":273326509,"RandomZ":273071671,"StartTime":102259.0,"Objects":[{"StartTime":102259.0,"EndTime":102497.0,"Column":2}]},{"RandomW":2659271247,"RandomX":3579807591,"RandomY":273326509,"RandomZ":273071671,"StartTime":102259.0,"Objects":[{"StartTime":102259.0,"EndTime":102497.0,"Column":3}]},{"RandomW":2659271247,"RandomX":3579807591,"RandomY":273326509,"RandomZ":273071671,"StartTime":102259.0,"Objects":[{"StartTime":102259.0,"EndTime":102259.0,"Column":0}]},{"RandomW":2659271247,"RandomX":3579807591,"RandomY":273326509,"RandomZ":273071671,"StartTime":102735.0,"Objects":[{"StartTime":102735.0,"EndTime":103449.0,"Column":1}]},{"RandomW":2659271247,"RandomX":3579807591,"RandomY":273326509,"RandomZ":273071671,"StartTime":102735.0,"Objects":[{"StartTime":102735.0,"EndTime":102973.0,"Column":2}]},{"RandomW":2659271247,"RandomX":3579807591,"RandomY":273326509,"RandomZ":273071671,"StartTime":102735.0,"Objects":[{"StartTime":102735.0,"EndTime":102735.0,"Column":0}]},{"RandomW":2659271247,"RandomX":3579807591,"RandomY":273326509,"RandomZ":273071671,"StartTime":103212.0,"Objects":[{"StartTime":103212.0,"EndTime":103212.0,"Column":0}]},{"RandomW":2659271247,"RandomX":3579807591,"RandomY":273326509,"RandomZ":273071671,"StartTime":103212.0,"Objects":[{"StartTime":103212.0,"EndTime":103212.0,"Column":3}]},{"RandomW":2659271247,"RandomX":3579807591,"RandomY":273326509,"RandomZ":273071671,"StartTime":103450.0,"Objects":[{"StartTime":103450.0,"EndTime":103450.0,"Column":3}]},{"RandomW":2659271247,"RandomX":3579807591,"RandomY":273326509,"RandomZ":273071671,"StartTime":103688.0,"Objects":[{"StartTime":103688.0,"EndTime":103688.0,"Column":0}]},{"RandomW":2659271247,"RandomX":3579807591,"RandomY":273326509,"RandomZ":273071671,"StartTime":103688.0,"Objects":[{"StartTime":103688.0,"EndTime":103688.0,"Column":2}]},{"RandomW":2659271247,"RandomX":3579807591,"RandomY":273326509,"RandomZ":273071671,"StartTime":103926.0,"Objects":[{"StartTime":103926.0,"EndTime":103926.0,"Column":2}]},{"RandomW":2659271247,"RandomX":3579807591,"RandomY":273326509,"RandomZ":273071671,"StartTime":104164.0,"Objects":[{"StartTime":104164.0,"EndTime":104164.0,"Column":0}]},{"RandomW":2659271247,"RandomX":3579807591,"RandomY":273326509,"RandomZ":273071671,"StartTime":104164.0,"Objects":[{"StartTime":104164.0,"EndTime":104402.0,"Column":3}]},{"RandomW":2659271247,"RandomX":3579807591,"RandomY":273326509,"RandomZ":273071671,"StartTime":104164.0,"Objects":[{"StartTime":104164.0,"EndTime":104164.0,"Column":1}]},{"RandomW":2659271247,"RandomX":3579807591,"RandomY":273326509,"RandomZ":273071671,"StartTime":104402.0,"Objects":[{"StartTime":104402.0,"EndTime":104640.0,"Column":1}]},{"RandomW":2659271247,"RandomX":3579807591,"RandomY":273326509,"RandomZ":273071671,"StartTime":104640.0,"Objects":[{"StartTime":104640.0,"EndTime":104640.0,"Column":0}]},{"RandomW":2659271247,"RandomX":3579807591,"RandomY":273326509,"RandomZ":273071671,"StartTime":104640.0,"Objects":[{"StartTime":104640.0,"EndTime":104878.0,"Column":2}]},{"RandomW":2659271247,"RandomX":3579807591,"RandomY":273326509,"RandomZ":273071671,"StartTime":104878.0,"Objects":[{"StartTime":104878.0,"EndTime":104878.0,"Column":3}]},{"RandomW":2659271247,"RandomX":3579807591,"RandomY":273326509,"RandomZ":273071671,"StartTime":105116.0,"Objects":[{"StartTime":105116.0,"EndTime":105354.0,"Column":3}]},{"RandomW":2659271247,"RandomX":3579807591,"RandomY":273326509,"RandomZ":273071671,"StartTime":105116.0,"Objects":[{"StartTime":105116.0,"EndTime":105116.0,"Column":2}]},{"RandomW":2659271247,"RandomX":3579807591,"RandomY":273326509,"RandomZ":273071671,"StartTime":105116.0,"Objects":[{"StartTime":105116.0,"EndTime":105116.0,"Column":0}]},{"RandomW":2659271247,"RandomX":3579807591,"RandomY":273326509,"RandomZ":273071671,"StartTime":105593.0,"Objects":[{"StartTime":105593.0,"EndTime":105593.0,"Column":0}]},{"RandomW":2659271247,"RandomX":3579807591,"RandomY":273326509,"RandomZ":273071671,"StartTime":105593.0,"Objects":[{"StartTime":105593.0,"EndTime":105831.0,"Column":2}]},{"RandomW":2659271247,"RandomX":3579807591,"RandomY":273326509,"RandomZ":273071671,"StartTime":105593.0,"Objects":[{"StartTime":105593.0,"EndTime":105593.0,"Column":1}]},{"RandomW":2659271247,"RandomX":3579807591,"RandomY":273326509,"RandomZ":273071671,"StartTime":105831.0,"Objects":[{"StartTime":105831.0,"EndTime":105831.0,"Column":1}]},{"RandomW":2659271247,"RandomX":3579807591,"RandomY":273326509,"RandomZ":273071671,"StartTime":105831.0,"Objects":[{"StartTime":105831.0,"EndTime":105831.0,"Column":3}]},{"RandomW":2659271247,"RandomX":3579807591,"RandomY":273326509,"RandomZ":273071671,"StartTime":106069.0,"Objects":[{"StartTime":106069.0,"EndTime":106307.0,"Column":3}]},{"RandomW":2659271247,"RandomX":3579807591,"RandomY":273326509,"RandomZ":273071671,"StartTime":106069.0,"Objects":[{"StartTime":106069.0,"EndTime":106069.0,"Column":1}]},{"RandomW":2659271247,"RandomX":3579807591,"RandomY":273326509,"RandomZ":273071671,"StartTime":106069.0,"Objects":[{"StartTime":106069.0,"EndTime":106069.0,"Column":0}]},{"RandomW":2659271247,"RandomX":3579807591,"RandomY":273326509,"RandomZ":273071671,"StartTime":106307.0,"Objects":[{"StartTime":106307.0,"EndTime":106307.0,"Column":2}]},{"RandomW":2659271247,"RandomX":3579807591,"RandomY":273326509,"RandomZ":273071671,"StartTime":106426.0,"Objects":[{"StartTime":106426.0,"EndTime":106426.0,"Column":0}]},{"RandomW":2659271247,"RandomX":3579807591,"RandomY":273326509,"RandomZ":273071671,"StartTime":106545.0,"Objects":[{"StartTime":106545.0,"EndTime":108449.0,"Column":1}]},{"RandomW":2659271247,"RandomX":3579807591,"RandomY":273326509,"RandomZ":273071671,"StartTime":106545.0,"Objects":[{"StartTime":106545.0,"EndTime":106783.0,"Column":2}]},{"RandomW":2659271247,"RandomX":3579807591,"RandomY":273326509,"RandomZ":273071671,"StartTime":106783.0,"Objects":[{"StartTime":106783.0,"EndTime":106783.0,"Column":0}]},{"RandomW":2659271247,"RandomX":3579807591,"RandomY":273326509,"RandomZ":273071671,"StartTime":107021.0,"Objects":[{"StartTime":107021.0,"EndTime":107021.0,"Column":0}]},{"RandomW":2659271247,"RandomX":3579807591,"RandomY":273326509,"RandomZ":273071671,"StartTime":107021.0,"Objects":[{"StartTime":107021.0,"EndTime":107021.0,"Column":3}]},{"RandomW":2659271247,"RandomX":3579807591,"RandomY":273326509,"RandomZ":273071671,"StartTime":107259.0,"Objects":[{"StartTime":107259.0,"EndTime":107259.0,"Column":3}]},{"RandomW":2659271247,"RandomX":3579807591,"RandomY":273326509,"RandomZ":273071671,"StartTime":107497.0,"Objects":[{"StartTime":107497.0,"EndTime":107497.0,"Column":0}]},{"RandomW":2659271247,"RandomX":3579807591,"RandomY":273326509,"RandomZ":273071671,"StartTime":107497.0,"Objects":[{"StartTime":107497.0,"EndTime":107497.0,"Column":2}]},{"RandomW":2659271247,"RandomX":3579807591,"RandomY":273326509,"RandomZ":273071671,"StartTime":107735.0,"Objects":[{"StartTime":107735.0,"EndTime":107735.0,"Column":2}]},{"RandomW":2659271247,"RandomX":3579807591,"RandomY":273326509,"RandomZ":273071671,"StartTime":107974.0,"Objects":[{"StartTime":107974.0,"EndTime":107974.0,"Column":0}]},{"RandomW":2659271247,"RandomX":3579807591,"RandomY":273326509,"RandomZ":273071671,"StartTime":107974.0,"Objects":[{"StartTime":107974.0,"EndTime":108212.0,"Column":3}]},{"RandomW":2659271247,"RandomX":3579807591,"RandomY":273326509,"RandomZ":273071671,"StartTime":108450.0,"Objects":[{"StartTime":108450.0,"EndTime":108450.0,"Column":0}]},{"RandomW":2659271247,"RandomX":3579807591,"RandomY":273326509,"RandomZ":273071671,"StartTime":108450.0,"Objects":[{"StartTime":108450.0,"EndTime":108688.0,"Column":2}]},{"RandomW":2659271247,"RandomX":3579807591,"RandomY":273326509,"RandomZ":273071671,"StartTime":108688.0,"Objects":[{"StartTime":108688.0,"EndTime":108688.0,"Column":3}]},{"RandomW":2659271247,"RandomX":3579807591,"RandomY":273326509,"RandomZ":273071671,"StartTime":108926.0,"Objects":[{"StartTime":108926.0,"EndTime":108926.0,"Column":0}]},{"RandomW":2659271247,"RandomX":3579807591,"RandomY":273326509,"RandomZ":273071671,"StartTime":108926.0,"Objects":[{"StartTime":108926.0,"EndTime":109164.0,"Column":3}]},{"RandomW":2659271247,"RandomX":3579807591,"RandomY":273326509,"RandomZ":273071671,"StartTime":109164.0,"Objects":[{"StartTime":109164.0,"EndTime":109640.0,"Column":1}]},{"RandomW":2659271247,"RandomX":3579807591,"RandomY":273326509,"RandomZ":273071671,"StartTime":109402.0,"Objects":[{"StartTime":109402.0,"EndTime":109402.0,"Column":0}]},{"RandomW":2659271247,"RandomX":3579807591,"RandomY":273326509,"RandomZ":273071671,"StartTime":109402.0,"Objects":[{"StartTime":109402.0,"EndTime":109640.0,"Column":2}]},{"RandomW":2659271247,"RandomX":3579807591,"RandomY":273326509,"RandomZ":273071671,"StartTime":109640.0,"Objects":[{"StartTime":109640.0,"EndTime":109640.0,"Column":3}]},{"RandomW":2659271247,"RandomX":3579807591,"RandomY":273326509,"RandomZ":273071671,"StartTime":109878.0,"Objects":[{"StartTime":109878.0,"EndTime":109878.0,"Column":0}]},{"RandomW":2659271247,"RandomX":3579807591,"RandomY":273326509,"RandomZ":273071671,"StartTime":109878.0,"Objects":[{"StartTime":109878.0,"EndTime":110116.0,"Column":3}]},{"RandomW":2659271247,"RandomX":3579807591,"RandomY":273326509,"RandomZ":273071671,"StartTime":110116.0,"Objects":[{"StartTime":110116.0,"EndTime":110116.0,"Column":2}]},{"RandomW":2659271247,"RandomX":3579807591,"RandomY":273326509,"RandomZ":273071671,"StartTime":110354.0,"Objects":[{"StartTime":110354.0,"EndTime":110354.0,"Column":0}]},{"RandomW":2659271247,"RandomX":3579807591,"RandomY":273326509,"RandomZ":273071671,"StartTime":110354.0,"Objects":[{"StartTime":110354.0,"EndTime":110592.0,"Column":2}]},{"RandomW":2659271247,"RandomX":3579807591,"RandomY":273326509,"RandomZ":273071671,"StartTime":110593.0,"Objects":[{"StartTime":110593.0,"EndTime":111307.0,"Column":1}]},{"RandomW":2659271247,"RandomX":3579807591,"RandomY":273326509,"RandomZ":273071671,"StartTime":110831.0,"Objects":[{"StartTime":110831.0,"EndTime":110831.0,"Column":0}]},{"RandomW":2659271247,"RandomX":3579807591,"RandomY":273326509,"RandomZ":273071671,"StartTime":110831.0,"Objects":[{"StartTime":110831.0,"EndTime":110831.0,"Column":3}]},{"RandomW":2659271247,"RandomX":3579807591,"RandomY":273326509,"RandomZ":273071671,"StartTime":111069.0,"Objects":[{"StartTime":111069.0,"EndTime":111069.0,"Column":3}]},{"RandomW":2659271247,"RandomX":3579807591,"RandomY":273326509,"RandomZ":273071671,"StartTime":111307.0,"Objects":[{"StartTime":111307.0,"EndTime":111307.0,"Column":0}]},{"RandomW":2659271247,"RandomX":3579807591,"RandomY":273326509,"RandomZ":273071671,"StartTime":111307.0,"Objects":[{"StartTime":111307.0,"EndTime":111307.0,"Column":2}]},{"RandomW":2659271247,"RandomX":3579807591,"RandomY":273326509,"RandomZ":273071671,"StartTime":111545.0,"Objects":[{"StartTime":111545.0,"EndTime":112259.0,"Column":1}]},{"RandomW":2659271247,"RandomX":3579807591,"RandomY":273326509,"RandomZ":273071671,"StartTime":111545.0,"Objects":[{"StartTime":111545.0,"EndTime":111545.0,"Column":2}]},{"RandomW":2659271247,"RandomX":3579807591,"RandomY":273326509,"RandomZ":273071671,"StartTime":111783.0,"Objects":[{"StartTime":111783.0,"EndTime":111783.0,"Column":0}]},{"RandomW":2659271247,"RandomX":3579807591,"RandomY":273326509,"RandomZ":273071671,"StartTime":111783.0,"Objects":[{"StartTime":111783.0,"EndTime":112021.0,"Column":3}]},{"RandomW":2659271247,"RandomX":3579807591,"RandomY":273326509,"RandomZ":273071671,"StartTime":112259.0,"Objects":[{"StartTime":112259.0,"EndTime":112259.0,"Column":0}]},{"RandomW":2659271247,"RandomX":3579807591,"RandomY":273326509,"RandomZ":273071671,"StartTime":112259.0,"Objects":[{"StartTime":112259.0,"EndTime":112497.0,"Column":2}]},{"RandomW":2659271247,"RandomX":3579807591,"RandomY":273326509,"RandomZ":273071671,"StartTime":112497.0,"Objects":[{"StartTime":112497.0,"EndTime":113449.0,"Column":1}]},{"RandomW":2659271247,"RandomX":3579807591,"RandomY":273326509,"RandomZ":273071671,"StartTime":112497.0,"Objects":[{"StartTime":112497.0,"EndTime":112497.0,"Column":3}]},{"RandomW":2659271247,"RandomX":3579807591,"RandomY":273326509,"RandomZ":273071671,"StartTime":112735.0,"Objects":[{"StartTime":112735.0,"EndTime":112735.0,"Column":0}]},{"RandomW":2659271247,"RandomX":3579807591,"RandomY":273326509,"RandomZ":273071671,"StartTime":112735.0,"Objects":[{"StartTime":112735.0,"EndTime":112973.0,"Column":3}]},{"RandomW":2659271247,"RandomX":3579807591,"RandomY":273326509,"RandomZ":273071671,"StartTime":113212.0,"Objects":[{"StartTime":113212.0,"EndTime":113212.0,"Column":0}]},{"RandomW":2659271247,"RandomX":3579807591,"RandomY":273326509,"RandomZ":273071671,"StartTime":113212.0,"Objects":[{"StartTime":113212.0,"EndTime":113450.0,"Column":2}]},{"RandomW":2659271247,"RandomX":3579807591,"RandomY":273326509,"RandomZ":273071671,"StartTime":113450.0,"Objects":[{"StartTime":113450.0,"EndTime":113450.0,"Column":3}]},{"RandomW":2659271247,"RandomX":3579807591,"RandomY":273326509,"RandomZ":273071671,"StartTime":113688.0,"Objects":[{"StartTime":113688.0,"EndTime":113688.0,"Column":0}]},{"RandomW":2659271247,"RandomX":3579807591,"RandomY":273326509,"RandomZ":273071671,"StartTime":113688.0,"Objects":[{"StartTime":113688.0,"EndTime":113926.0,"Column":3}]},{"RandomW":2659271247,"RandomX":3579807591,"RandomY":273326509,"RandomZ":273071671,"StartTime":113926.0,"Objects":[{"StartTime":113926.0,"EndTime":113926.0,"Column":1}]},{"RandomW":2659271247,"RandomX":3579807591,"RandomY":273326509,"RandomZ":273071671,"StartTime":113985.0,"Objects":[{"StartTime":113985.0,"EndTime":113985.0,"Column":0}]},{"RandomW":2659271247,"RandomX":3579807591,"RandomY":273326509,"RandomZ":273071671,"StartTime":114164.0,"Objects":[{"StartTime":114164.0,"EndTime":114402.0,"Column":2}]},{"RandomW":2659271247,"RandomX":3579807591,"RandomY":273326509,"RandomZ":273071671,"StartTime":114402.0,"Objects":[{"StartTime":114402.0,"EndTime":114402.0,"Column":0}]},{"RandomW":2659271247,"RandomX":3579807591,"RandomY":273326509,"RandomZ":273071671,"StartTime":114402.0,"Objects":[{"StartTime":114402.0,"EndTime":115116.0,"Column":1}]},{"RandomW":2659271247,"RandomX":3579807591,"RandomY":273326509,"RandomZ":273071671,"StartTime":114640.0,"Objects":[{"StartTime":114640.0,"EndTime":114640.0,"Column":0}]},{"RandomW":2659271247,"RandomX":3579807591,"RandomY":273326509,"RandomZ":273071671,"StartTime":114640.0,"Objects":[{"StartTime":114640.0,"EndTime":114640.0,"Column":3}]},{"RandomW":2659271247,"RandomX":3579807591,"RandomY":273326509,"RandomZ":273071671,"StartTime":114878.0,"Objects":[{"StartTime":114878.0,"EndTime":114878.0,"Column":3}]},{"RandomW":2659271247,"RandomX":3579807591,"RandomY":273326509,"RandomZ":273071671,"StartTime":115116.0,"Objects":[{"StartTime":115116.0,"EndTime":115116.0,"Column":0}]},{"RandomW":2659271247,"RandomX":3579807591,"RandomY":273326509,"RandomZ":273071671,"StartTime":115116.0,"Objects":[{"StartTime":115116.0,"EndTime":115116.0,"Column":2}]},{"RandomW":2659271247,"RandomX":3579807591,"RandomY":273326509,"RandomZ":273071671,"StartTime":115354.0,"Objects":[{"StartTime":115354.0,"EndTime":115354.0,"Column":2}]},{"RandomW":2659271247,"RandomX":3579807591,"RandomY":273326509,"RandomZ":273071671,"StartTime":115354.0,"Objects":[{"StartTime":115354.0,"EndTime":116306.0,"Column":1}]},{"RandomW":2659271247,"RandomX":3579807591,"RandomY":273326509,"RandomZ":273071671,"StartTime":115593.0,"Objects":[{"StartTime":115593.0,"EndTime":115831.0,"Column":3}]},{"RandomW":2659271247,"RandomX":3579807591,"RandomY":273326509,"RandomZ":273071671,"StartTime":115593.0,"Objects":[{"StartTime":115593.0,"EndTime":115593.0,"Column":0}]},{"RandomW":2659271247,"RandomX":3579807591,"RandomY":273326509,"RandomZ":273071671,"StartTime":116069.0,"Objects":[{"StartTime":116069.0,"EndTime":116307.0,"Column":2}]},{"RandomW":2659271247,"RandomX":3579807591,"RandomY":273326509,"RandomZ":273071671,"StartTime":116069.0,"Objects":[{"StartTime":116069.0,"EndTime":116069.0,"Column":0}]},{"RandomW":2659271247,"RandomX":3579807591,"RandomY":273326509,"RandomZ":273071671,"StartTime":116307.0,"Objects":[{"StartTime":116307.0,"EndTime":116307.0,"Column":3}]},{"RandomW":2659271247,"RandomX":3579807591,"RandomY":273326509,"RandomZ":273071671,"StartTime":116545.0,"Objects":[{"StartTime":116545.0,"EndTime":116783.0,"Column":3}]},{"RandomW":2659271247,"RandomX":3579807591,"RandomY":273326509,"RandomZ":273071671,"StartTime":116545.0,"Objects":[{"StartTime":116545.0,"EndTime":117021.0,"Column":2}]},{"RandomW":2659271247,"RandomX":3579807591,"RandomY":273326509,"RandomZ":273071671,"StartTime":116545.0,"Objects":[{"StartTime":116545.0,"EndTime":116545.0,"Column":0}]},{"RandomW":2659271247,"RandomX":3579807591,"RandomY":273326509,"RandomZ":273071671,"StartTime":117021.0,"Objects":[{"StartTime":117021.0,"EndTime":117021.0,"Column":1}]},{"RandomW":2659271247,"RandomX":3579807591,"RandomY":273326509,"RandomZ":273071671,"StartTime":117021.0,"Objects":[{"StartTime":117021.0,"EndTime":117259.0,"Column":3}]},{"RandomW":2659271247,"RandomX":3579807591,"RandomY":273326509,"RandomZ":273071671,"StartTime":117021.0,"Objects":[{"StartTime":117021.0,"EndTime":117021.0,"Column":0}]},{"RandomW":2659271247,"RandomX":3579807591,"RandomY":273326509,"RandomZ":273071671,"StartTime":117259.0,"Objects":[{"StartTime":117259.0,"EndTime":117259.0,"Column":2}]},{"RandomW":2659271247,"RandomX":3579807591,"RandomY":273326509,"RandomZ":273071671,"StartTime":117259.0,"Objects":[{"StartTime":117259.0,"EndTime":117497.0,"Column":1}]},{"RandomW":2659271247,"RandomX":3579807591,"RandomY":273326509,"RandomZ":273071671,"StartTime":117497.0,"Objects":[{"StartTime":117497.0,"EndTime":117497.0,"Column":0}]},{"RandomW":2659271247,"RandomX":3579807591,"RandomY":273326509,"RandomZ":273071671,"StartTime":117497.0,"Objects":[{"StartTime":117497.0,"EndTime":117735.0,"Column":3}]},{"RandomW":2659271247,"RandomX":3579807591,"RandomY":273326509,"RandomZ":273071671,"StartTime":117735.0,"Objects":[{"StartTime":117735.0,"EndTime":117973.0,"Column":2}]},{"RandomW":2659271247,"RandomX":3579807591,"RandomY":273326509,"RandomZ":273071671,"StartTime":117974.0,"Objects":[{"StartTime":117974.0,"EndTime":117974.0,"Column":0}]},{"RandomW":2659271247,"RandomX":3579807591,"RandomY":273326509,"RandomZ":273071671,"StartTime":117974.0,"Objects":[{"StartTime":117974.0,"EndTime":118212.0,"Column":3}]},{"RandomW":2659271247,"RandomX":3579807591,"RandomY":273326509,"RandomZ":273071671,"StartTime":118212.0,"Objects":[{"StartTime":118212.0,"EndTime":118926.0,"Column":1}]},{"RandomW":2659271247,"RandomX":3579807591,"RandomY":273326509,"RandomZ":273071671,"StartTime":118450.0,"Objects":[{"StartTime":118450.0,"EndTime":118450.0,"Column":0}]},{"RandomW":2659271247,"RandomX":3579807591,"RandomY":273326509,"RandomZ":273071671,"StartTime":118450.0,"Objects":[{"StartTime":118450.0,"EndTime":118450.0,"Column":2}]},{"RandomW":2659271247,"RandomX":3579807591,"RandomY":273326509,"RandomZ":273071671,"StartTime":118450.0,"Objects":[{"StartTime":118450.0,"EndTime":118450.0,"Column":3}]},{"RandomW":2659271247,"RandomX":3579807591,"RandomY":273326509,"RandomZ":273071671,"StartTime":118688.0,"Objects":[{"StartTime":118688.0,"EndTime":118688.0,"Column":2}]},{"RandomW":2659271247,"RandomX":3579807591,"RandomY":273326509,"RandomZ":273071671,"StartTime":118688.0,"Objects":[{"StartTime":118688.0,"EndTime":118688.0,"Column":3}]},{"RandomW":2659271247,"RandomX":3579807591,"RandomY":273326509,"RandomZ":273071671,"StartTime":118926.0,"Objects":[{"StartTime":118926.0,"EndTime":118926.0,"Column":0}]},{"RandomW":2659271247,"RandomX":3579807591,"RandomY":273326509,"RandomZ":273071671,"StartTime":118926.0,"Objects":[{"StartTime":118926.0,"EndTime":118926.0,"Column":3}]},{"RandomW":2659271247,"RandomX":3579807591,"RandomY":273326509,"RandomZ":273071671,"StartTime":119164.0,"Objects":[{"StartTime":119164.0,"EndTime":120830.0,"Column":2}]},{"RandomW":2659271247,"RandomX":3579807591,"RandomY":273326509,"RandomZ":273071671,"StartTime":119164.0,"Objects":[{"StartTime":119164.0,"EndTime":119164.0,"Column":3}]},{"RandomW":2659271247,"RandomX":3579807591,"RandomY":273326509,"RandomZ":273071671,"StartTime":119402.0,"Objects":[{"StartTime":119402.0,"EndTime":119402.0,"Column":0}]},{"RandomW":2659271247,"RandomX":3579807591,"RandomY":273326509,"RandomZ":273071671,"StartTime":119402.0,"Objects":[{"StartTime":119402.0,"EndTime":119640.0,"Column":3}]},{"RandomW":2659271247,"RandomX":3579807591,"RandomY":273326509,"RandomZ":273071671,"StartTime":119878.0,"Objects":[{"StartTime":119878.0,"EndTime":119878.0,"Column":0}]},{"RandomW":2659271247,"RandomX":3579807591,"RandomY":273326509,"RandomZ":273071671,"StartTime":119878.0,"Objects":[{"StartTime":119878.0,"EndTime":120116.0,"Column":3}]},{"RandomW":2659271247,"RandomX":3579807591,"RandomY":273326509,"RandomZ":273071671,"StartTime":120116.0,"Objects":[{"StartTime":120116.0,"EndTime":120116.0,"Column":1}]},{"RandomW":2659271247,"RandomX":3579807591,"RandomY":273326509,"RandomZ":273071671,"StartTime":120354.0,"Objects":[{"StartTime":120354.0,"EndTime":120354.0,"Column":0}]},{"RandomW":2659271247,"RandomX":3579807591,"RandomY":273326509,"RandomZ":273071671,"StartTime":120354.0,"Objects":[{"StartTime":120354.0,"EndTime":120592.0,"Column":3}]},{"RandomW":2659271247,"RandomX":3579807591,"RandomY":273326509,"RandomZ":273071671,"StartTime":120831.0,"Objects":[{"StartTime":120831.0,"EndTime":120831.0,"Column":0}]},{"RandomW":2659271247,"RandomX":3579807591,"RandomY":273326509,"RandomZ":273071671,"StartTime":120831.0,"Objects":[{"StartTime":120831.0,"EndTime":121069.0,"Column":3}]},{"RandomW":2659271247,"RandomX":3579807591,"RandomY":273326509,"RandomZ":273071671,"StartTime":121069.0,"Objects":[{"StartTime":121069.0,"EndTime":121307.0,"Column":1}]},{"RandomW":2659271247,"RandomX":3579807591,"RandomY":273326509,"RandomZ":273071671,"StartTime":121307.0,"Objects":[{"StartTime":121307.0,"EndTime":121307.0,"Column":0}]},{"RandomW":2659271247,"RandomX":3579807591,"RandomY":273326509,"RandomZ":273071671,"StartTime":121307.0,"Objects":[{"StartTime":121307.0,"EndTime":121545.0,"Column":3}]},{"RandomW":2659271247,"RandomX":3579807591,"RandomY":273326509,"RandomZ":273071671,"StartTime":121545.0,"Objects":[{"StartTime":121545.0,"EndTime":121545.0,"Column":2}]},{"RandomW":2659271247,"RandomX":3579807591,"RandomY":273326509,"RandomZ":273071671,"StartTime":121664.0,"Objects":[{"StartTime":121664.0,"EndTime":121664.0,"Column":0}]},{"RandomW":2659271247,"RandomX":3579807591,"RandomY":273326509,"RandomZ":273071671,"StartTime":121783.0,"Objects":[{"StartTime":121783.0,"EndTime":122021.0,"Column":3}]},{"RandomW":2659271247,"RandomX":3579807591,"RandomY":273326509,"RandomZ":273071671,"StartTime":121783.0,"Objects":[{"StartTime":121783.0,"EndTime":121783.0,"Column":2}]},{"RandomW":2659271247,"RandomX":3579807591,"RandomY":273326509,"RandomZ":273071671,"StartTime":122021.0,"Objects":[{"StartTime":122021.0,"EndTime":122259.0,"Column":1}]},{"RandomW":2659271247,"RandomX":3579807591,"RandomY":273326509,"RandomZ":273071671,"StartTime":122021.0,"Objects":[{"StartTime":122021.0,"EndTime":122021.0,"Column":0}]},{"RandomW":2659271247,"RandomX":3579807591,"RandomY":273326509,"RandomZ":273071671,"StartTime":122259.0,"Objects":[{"StartTime":122259.0,"EndTime":122259.0,"Column":3}]},{"RandomW":2659271247,"RandomX":3579807591,"RandomY":273326509,"RandomZ":273071671,"StartTime":122260.0,"Objects":[{"StartTime":122260.0,"EndTime":122260.0,"Column":0}]},{"RandomW":2659271247,"RandomX":3579807591,"RandomY":273326509,"RandomZ":273071671,"StartTime":122497.0,"Objects":[{"StartTime":122497.0,"EndTime":122735.0,"Column":2}]},{"RandomW":2659271247,"RandomX":3579807591,"RandomY":273326509,"RandomZ":273071671,"StartTime":122497.0,"Objects":[{"StartTime":122497.0,"EndTime":122497.0,"Column":3}]},{"RandomW":2659271247,"RandomX":3579807591,"RandomY":273326509,"RandomZ":273071671,"StartTime":122736.0,"Objects":[{"StartTime":122736.0,"EndTime":122736.0,"Column":3}]},{"RandomW":2659271247,"RandomX":3579807591,"RandomY":273326509,"RandomZ":273071671,"StartTime":122736.0,"Objects":[{"StartTime":122736.0,"EndTime":122736.0,"Column":0}]},{"RandomW":2659271247,"RandomX":3579807591,"RandomY":273326509,"RandomZ":273071671,"StartTime":122974.0,"Objects":[{"StartTime":122974.0,"EndTime":122974.0,"Column":3}]},{"RandomW":2659271247,"RandomX":3579807591,"RandomY":273326509,"RandomZ":273071671,"StartTime":122974.0,"Objects":[{"StartTime":122974.0,"EndTime":123212.0,"Column":1}]},{"RandomW":2659271247,"RandomX":3579807591,"RandomY":273326509,"RandomZ":273071671,"StartTime":123212.0,"Objects":[{"StartTime":123212.0,"EndTime":123450.0,"Column":3}]},{"RandomW":2659271247,"RandomX":3579807591,"RandomY":273326509,"RandomZ":273071671,"StartTime":123212.0,"Objects":[{"StartTime":123212.0,"EndTime":123212.0,"Column":0}]},{"RandomW":2659271247,"RandomX":3579807591,"RandomY":273326509,"RandomZ":273071671,"StartTime":123450.0,"Objects":[{"StartTime":123450.0,"EndTime":123688.0,"Column":2}]},{"RandomW":2659271247,"RandomX":3579807591,"RandomY":273326509,"RandomZ":273071671,"StartTime":123688.0,"Objects":[{"StartTime":123688.0,"EndTime":123688.0,"Column":0}]},{"RandomW":2659271247,"RandomX":3579807591,"RandomY":273326509,"RandomZ":273071671,"StartTime":123688.0,"Objects":[{"StartTime":123688.0,"EndTime":123926.0,"Column":3}]},{"RandomW":2659271247,"RandomX":3579807591,"RandomY":273326509,"RandomZ":273071671,"StartTime":123926.0,"Objects":[{"StartTime":123926.0,"EndTime":124164.0,"Column":1}]},{"RandomW":2659271247,"RandomX":3579807591,"RandomY":273326509,"RandomZ":273071671,"StartTime":124164.0,"Objects":[{"StartTime":124164.0,"EndTime":124164.0,"Column":0}]},{"RandomW":2659271247,"RandomX":3579807591,"RandomY":273326509,"RandomZ":273071671,"StartTime":124164.0,"Objects":[{"StartTime":124164.0,"EndTime":124402.0,"Column":3}]},{"RandomW":2659271247,"RandomX":3579807591,"RandomY":273326509,"RandomZ":273071671,"StartTime":124403.0,"Objects":[{"StartTime":124403.0,"EndTime":124641.0,"Column":2}]},{"RandomW":2659271247,"RandomX":3579807591,"RandomY":273326509,"RandomZ":273071671,"StartTime":124641.0,"Objects":[{"StartTime":124641.0,"EndTime":124879.0,"Column":3}]},{"RandomW":2659271247,"RandomX":3579807591,"RandomY":273326509,"RandomZ":273071671,"StartTime":124641.0,"Objects":[{"StartTime":124641.0,"EndTime":124641.0,"Column":0}]},{"RandomW":2659271247,"RandomX":3579807591,"RandomY":273326509,"RandomZ":273071671,"StartTime":124879.0,"Objects":[{"StartTime":124879.0,"EndTime":125117.0,"Column":1}]},{"RandomW":2659271247,"RandomX":3579807591,"RandomY":273326509,"RandomZ":273071671,"StartTime":125116.0,"Objects":[{"StartTime":125116.0,"EndTime":125354.0,"Column":3}]},{"RandomW":2659271247,"RandomX":3579807591,"RandomY":273326509,"RandomZ":273071671,"StartTime":125117.0,"Objects":[{"StartTime":125117.0,"EndTime":125117.0,"Column":0}]},{"RandomW":2659271247,"RandomX":3579807591,"RandomY":273326509,"RandomZ":273071671,"StartTime":125354.0,"Objects":[{"StartTime":125354.0,"EndTime":125354.0,"Column":2}]},{"RandomW":2659271247,"RandomX":3579807591,"RandomY":273326509,"RandomZ":273071671,"StartTime":125593.0,"Objects":[{"StartTime":125593.0,"EndTime":125593.0,"Column":0}]},{"RandomW":2659271247,"RandomX":3579807591,"RandomY":273326509,"RandomZ":273071671,"StartTime":125593.0,"Objects":[{"StartTime":125593.0,"EndTime":125831.0,"Column":3}]},{"RandomW":2659271247,"RandomX":3579807591,"RandomY":273326509,"RandomZ":273071671,"StartTime":125593.0,"Objects":[{"StartTime":125593.0,"EndTime":125593.0,"Column":2}]},{"RandomW":2659271247,"RandomX":3579807591,"RandomY":273326509,"RandomZ":273071671,"StartTime":125831.0,"Objects":[{"StartTime":125831.0,"EndTime":126069.0,"Column":2}]},{"RandomW":2659271247,"RandomX":3579807591,"RandomY":273326509,"RandomZ":273071671,"StartTime":126069.0,"Objects":[{"StartTime":126069.0,"EndTime":126069.0,"Column":3}]},{"RandomW":2659271247,"RandomX":3579807591,"RandomY":273326509,"RandomZ":273071671,"StartTime":126069.0,"Objects":[{"StartTime":126069.0,"EndTime":126069.0,"Column":0}]},{"RandomW":2659271247,"RandomX":3579807591,"RandomY":273326509,"RandomZ":273071671,"StartTime":126307.0,"Objects":[{"StartTime":126307.0,"EndTime":126545.0,"Column":1}]},{"RandomW":2659271247,"RandomX":3579807591,"RandomY":273326509,"RandomZ":273071671,"StartTime":126307.0,"Objects":[{"StartTime":126307.0,"EndTime":126307.0,"Column":3}]},{"RandomW":2659271247,"RandomX":3579807591,"RandomY":273326509,"RandomZ":273071671,"StartTime":126545.0,"Objects":[{"StartTime":126545.0,"EndTime":126545.0,"Column":0}]},{"RandomW":2659271247,"RandomX":3579807591,"RandomY":273326509,"RandomZ":273071671,"StartTime":126545.0,"Objects":[{"StartTime":126545.0,"EndTime":126545.0,"Column":3}]},{"RandomW":2659271247,"RandomX":3579807591,"RandomY":273326509,"RandomZ":273071671,"StartTime":126783.0,"Objects":[{"StartTime":126783.0,"EndTime":127021.0,"Column":2}]},{"RandomW":2659271247,"RandomX":3579807591,"RandomY":273326509,"RandomZ":273071671,"StartTime":126783.0,"Objects":[{"StartTime":126783.0,"EndTime":126783.0,"Column":3}]},{"RandomW":2659271247,"RandomX":3579807591,"RandomY":273326509,"RandomZ":273071671,"StartTime":127021.0,"Objects":[{"StartTime":127021.0,"EndTime":127259.0,"Column":3}]},{"RandomW":2659271247,"RandomX":3579807591,"RandomY":273326509,"RandomZ":273071671,"StartTime":127022.0,"Objects":[{"StartTime":127022.0,"EndTime":127022.0,"Column":0}]},{"RandomW":2659271247,"RandomX":3579807591,"RandomY":273326509,"RandomZ":273071671,"StartTime":127260.0,"Objects":[{"StartTime":127260.0,"EndTime":127498.0,"Column":1}]},{"RandomW":2659271247,"RandomX":3579807591,"RandomY":273326509,"RandomZ":273071671,"StartTime":127498.0,"Objects":[{"StartTime":127498.0,"EndTime":127736.0,"Column":3}]},{"RandomW":2659271247,"RandomX":3579807591,"RandomY":273326509,"RandomZ":273071671,"StartTime":127498.0,"Objects":[{"StartTime":127498.0,"EndTime":127498.0,"Column":0}]},{"RandomW":2659271247,"RandomX":3579807591,"RandomY":273326509,"RandomZ":273071671,"StartTime":127736.0,"Objects":[{"StartTime":127736.0,"EndTime":128212.0,"Column":1}]},{"RandomW":2659271247,"RandomX":3579807591,"RandomY":273326509,"RandomZ":273071671,"StartTime":127974.0,"Objects":[{"StartTime":127974.0,"EndTime":128212.0,"Column":3}]},{"RandomW":2659271247,"RandomX":3579807591,"RandomY":273326509,"RandomZ":273071671,"StartTime":127974.0,"Objects":[{"StartTime":127974.0,"EndTime":127974.0,"Column":0}]},{"RandomW":2659271247,"RandomX":3579807591,"RandomY":273326509,"RandomZ":273071671,"StartTime":128212.0,"Objects":[{"StartTime":128212.0,"EndTime":128450.0,"Column":2}]},{"RandomW":2659271247,"RandomX":3579807591,"RandomY":273326509,"RandomZ":273071671,"StartTime":128450.0,"Objects":[{"StartTime":128450.0,"EndTime":128688.0,"Column":3}]},{"RandomW":2659271247,"RandomX":3579807591,"RandomY":273326509,"RandomZ":273071671,"StartTime":128450.0,"Objects":[{"StartTime":128450.0,"EndTime":128450.0,"Column":0}]},{"RandomW":2659271247,"RandomX":3579807591,"RandomY":273326509,"RandomZ":273071671,"StartTime":128688.0,"Objects":[{"StartTime":128688.0,"EndTime":128926.0,"Column":1}]},{"RandomW":2659271247,"RandomX":3579807591,"RandomY":273326509,"RandomZ":273071671,"StartTime":128926.0,"Objects":[{"StartTime":128926.0,"EndTime":129164.0,"Column":3}]},{"RandomW":2659271247,"RandomX":3579807591,"RandomY":273326509,"RandomZ":273071671,"StartTime":128926.0,"Objects":[{"StartTime":128926.0,"EndTime":128926.0,"Column":0}]},{"RandomW":2659271247,"RandomX":3579807591,"RandomY":273326509,"RandomZ":273071671,"StartTime":129164.0,"Objects":[{"StartTime":129164.0,"EndTime":129402.0,"Column":2}]},{"RandomW":2659271247,"RandomX":3579807591,"RandomY":273326509,"RandomZ":273071671,"StartTime":129283.0,"Objects":[{"StartTime":129283.0,"EndTime":129283.0,"Column":0}]},{"RandomW":2659271247,"RandomX":3579807591,"RandomY":273326509,"RandomZ":273071671,"StartTime":129403.0,"Objects":[{"StartTime":129403.0,"EndTime":129641.0,"Column":3}]},{"RandomW":2659271247,"RandomX":3579807591,"RandomY":273326509,"RandomZ":273071671,"StartTime":129640.0,"Objects":[{"StartTime":129640.0,"EndTime":130116.0,"Column":2}]},{"RandomW":2659271247,"RandomX":3579807591,"RandomY":273326509,"RandomZ":273071671,"StartTime":129640.0,"Objects":[{"StartTime":129640.0,"EndTime":129640.0,"Column":0}]},{"RandomW":2659271247,"RandomX":3579807591,"RandomY":273326509,"RandomZ":273071671,"StartTime":129878.0,"Objects":[{"StartTime":129878.0,"EndTime":129878.0,"Column":3}]},{"RandomW":2659271247,"RandomX":3579807591,"RandomY":273326509,"RandomZ":273071671,"StartTime":129879.0,"Objects":[{"StartTime":129879.0,"EndTime":129879.0,"Column":0}]},{"RandomW":2659271247,"RandomX":3579807591,"RandomY":273326509,"RandomZ":273071671,"StartTime":130116.0,"Objects":[{"StartTime":130116.0,"EndTime":130116.0,"Column":3}]},{"RandomW":2659271247,"RandomX":3579807591,"RandomY":273326509,"RandomZ":273071671,"StartTime":130116.0,"Objects":[{"StartTime":130116.0,"EndTime":130354.0,"Column":1}]},{"RandomW":2659271247,"RandomX":3579807591,"RandomY":273326509,"RandomZ":273071671,"StartTime":130354.0,"Objects":[{"StartTime":130354.0,"EndTime":130354.0,"Column":0}]},{"RandomW":2659271247,"RandomX":3579807591,"RandomY":273326509,"RandomZ":273071671,"StartTime":130355.0,"Objects":[{"StartTime":130355.0,"EndTime":130355.0,"Column":3}]},{"RandomW":2659271247,"RandomX":3579807591,"RandomY":273326509,"RandomZ":273071671,"StartTime":130593.0,"Objects":[{"StartTime":130593.0,"EndTime":130831.0,"Column":2}]},{"RandomW":2659271247,"RandomX":3579807591,"RandomY":273326509,"RandomZ":273071671,"StartTime":130593.0,"Objects":[{"StartTime":130593.0,"EndTime":130593.0,"Column":3}]},{"RandomW":2659271247,"RandomX":3579807591,"RandomY":273326509,"RandomZ":273071671,"StartTime":130831.0,"Objects":[{"StartTime":130831.0,"EndTime":130831.0,"Column":0}]},{"RandomW":2659271247,"RandomX":3579807591,"RandomY":273326509,"RandomZ":273071671,"StartTime":130831.0,"Objects":[{"StartTime":130831.0,"EndTime":131069.0,"Column":3}]},{"RandomW":2659271247,"RandomX":3579807591,"RandomY":273326509,"RandomZ":273071671,"StartTime":131069.0,"Objects":[{"StartTime":131069.0,"EndTime":131307.0,"Column":1}]},{"RandomW":2659271247,"RandomX":3579807591,"RandomY":273326509,"RandomZ":273071671,"StartTime":131307.0,"Objects":[{"StartTime":131307.0,"EndTime":131545.0,"Column":3}]},{"RandomW":2659271247,"RandomX":3579807591,"RandomY":273326509,"RandomZ":273071671,"StartTime":131307.0,"Objects":[{"StartTime":131307.0,"EndTime":131307.0,"Column":0}]},{"RandomW":2659271247,"RandomX":3579807591,"RandomY":273326509,"RandomZ":273071671,"StartTime":131545.0,"Objects":[{"StartTime":131545.0,"EndTime":131783.0,"Column":2}]},{"RandomW":2659271247,"RandomX":3579807591,"RandomY":273326509,"RandomZ":273071671,"StartTime":131783.0,"Objects":[{"StartTime":131783.0,"EndTime":132021.0,"Column":3}]},{"RandomW":2659271247,"RandomX":3579807591,"RandomY":273326509,"RandomZ":273071671,"StartTime":131783.0,"Objects":[{"StartTime":131783.0,"EndTime":131783.0,"Column":0}]},{"RandomW":2659271247,"RandomX":3579807591,"RandomY":273326509,"RandomZ":273071671,"StartTime":132021.0,"Objects":[{"StartTime":132021.0,"EndTime":132259.0,"Column":2}]},{"RandomW":2659271247,"RandomX":3579807591,"RandomY":273326509,"RandomZ":273071671,"StartTime":132022.0,"Objects":[{"StartTime":132022.0,"EndTime":132260.0,"Column":1}]},{"RandomW":2659271247,"RandomX":3579807591,"RandomY":273326509,"RandomZ":273071671,"StartTime":132260.0,"Objects":[{"StartTime":132260.0,"EndTime":132498.0,"Column":3}]},{"RandomW":2659271247,"RandomX":3579807591,"RandomY":273326509,"RandomZ":273071671,"StartTime":132260.0,"Objects":[{"StartTime":132260.0,"EndTime":132260.0,"Column":0}]},{"RandomW":2659271247,"RandomX":3579807591,"RandomY":273326509,"RandomZ":273071671,"StartTime":132497.0,"Objects":[{"StartTime":132497.0,"EndTime":132735.0,"Column":2}]},{"RandomW":2659271247,"RandomX":3579807591,"RandomY":273326509,"RandomZ":273071671,"StartTime":132498.0,"Objects":[{"StartTime":132498.0,"EndTime":132736.0,"Column":1}]},{"RandomW":2659271247,"RandomX":3579807591,"RandomY":273326509,"RandomZ":273071671,"StartTime":132736.0,"Objects":[{"StartTime":132736.0,"EndTime":132974.0,"Column":3}]},{"RandomW":2659271247,"RandomX":3579807591,"RandomY":273326509,"RandomZ":273071671,"StartTime":132736.0,"Objects":[{"StartTime":132736.0,"EndTime":132736.0,"Column":0}]},{"RandomW":2659271247,"RandomX":3579807591,"RandomY":273326509,"RandomZ":273071671,"StartTime":132974.0,"Objects":[{"StartTime":132974.0,"EndTime":133212.0,"Column":2}]},{"RandomW":2659271247,"RandomX":3579807591,"RandomY":273326509,"RandomZ":273071671,"StartTime":133212.0,"Objects":[{"StartTime":133212.0,"EndTime":133450.0,"Column":3}]},{"RandomW":2659271247,"RandomX":3579807591,"RandomY":273326509,"RandomZ":273071671,"StartTime":133212.0,"Objects":[{"StartTime":133212.0,"EndTime":133212.0,"Column":0}]},{"RandomW":2659271247,"RandomX":3579807591,"RandomY":273326509,"RandomZ":273071671,"StartTime":133450.0,"Objects":[{"StartTime":133450.0,"EndTime":133926.0,"Column":1}]},{"RandomW":2659271247,"RandomX":3579807591,"RandomY":273326509,"RandomZ":273071671,"StartTime":133688.0,"Objects":[{"StartTime":133688.0,"EndTime":133688.0,"Column":0}]},{"RandomW":2659271247,"RandomX":3579807591,"RandomY":273326509,"RandomZ":273071671,"StartTime":133688.0,"Objects":[{"StartTime":133688.0,"EndTime":133688.0,"Column":3}]},{"RandomW":2659271247,"RandomX":3579807591,"RandomY":273326509,"RandomZ":273071671,"StartTime":133926.0,"Objects":[{"StartTime":133926.0,"EndTime":133926.0,"Column":3}]},{"RandomW":2659271247,"RandomX":3579807591,"RandomY":273326509,"RandomZ":273071671,"StartTime":133926.0,"Objects":[{"StartTime":133926.0,"EndTime":134164.0,"Column":2}]},{"RandomW":2659271247,"RandomX":3579807591,"RandomY":273326509,"RandomZ":273071671,"StartTime":134164.0,"Objects":[{"StartTime":134164.0,"EndTime":134164.0,"Column":3}]},{"RandomW":2659271247,"RandomX":3579807591,"RandomY":273326509,"RandomZ":273071671,"StartTime":134164.0,"Objects":[{"StartTime":134164.0,"EndTime":134164.0,"Column":0}]},{"RandomW":2659271247,"RandomX":3579807591,"RandomY":273326509,"RandomZ":273071671,"StartTime":134403.0,"Objects":[{"StartTime":134403.0,"EndTime":134403.0,"Column":3}]},{"RandomW":2659271247,"RandomX":3579807591,"RandomY":273326509,"RandomZ":273071671,"StartTime":134403.0,"Objects":[{"StartTime":134403.0,"EndTime":134641.0,"Column":1}]},{"RandomW":2659271247,"RandomX":3579807591,"RandomY":273326509,"RandomZ":273071671,"StartTime":134640.0,"Objects":[{"StartTime":134640.0,"EndTime":134878.0,"Column":3}]},{"RandomW":2659271247,"RandomX":3579807591,"RandomY":273326509,"RandomZ":273071671,"StartTime":134641.0,"Objects":[{"StartTime":134641.0,"EndTime":134641.0,"Column":0}]},{"RandomW":2659271247,"RandomX":3579807591,"RandomY":273326509,"RandomZ":273071671,"StartTime":134878.0,"Objects":[{"StartTime":134878.0,"EndTime":135116.0,"Column":2}]},{"RandomW":2659271247,"RandomX":3579807591,"RandomY":273326509,"RandomZ":273071671,"StartTime":135117.0,"Objects":[{"StartTime":135117.0,"EndTime":135355.0,"Column":3}]},{"RandomW":2659271247,"RandomX":3579807591,"RandomY":273326509,"RandomZ":273071671,"StartTime":135117.0,"Objects":[{"StartTime":135117.0,"EndTime":135117.0,"Column":0}]},{"RandomW":2659271247,"RandomX":3579807591,"RandomY":273326509,"RandomZ":273071671,"StartTime":135354.0,"Objects":[{"StartTime":135354.0,"EndTime":136068.0,"Column":1}]},{"RandomW":2659271247,"RandomX":3579807591,"RandomY":273326509,"RandomZ":273071671,"StartTime":135354.0,"Objects":[{"StartTime":135354.0,"EndTime":135354.0,"Column":2}]},{"RandomW":2659271247,"RandomX":3579807591,"RandomY":273326509,"RandomZ":273071671,"StartTime":135593.0,"Objects":[{"StartTime":135593.0,"EndTime":135593.0,"Column":0}]},{"RandomW":2659271247,"RandomX":3579807591,"RandomY":273326509,"RandomZ":273071671,"StartTime":135593.0,"Objects":[{"StartTime":135593.0,"EndTime":135831.0,"Column":3}]},{"RandomW":2659271247,"RandomX":3579807591,"RandomY":273326509,"RandomZ":273071671,"StartTime":136069.0,"Objects":[{"StartTime":136069.0,"EndTime":136307.0,"Column":3}]},{"RandomW":2659271247,"RandomX":3579807591,"RandomY":273326509,"RandomZ":273071671,"StartTime":136069.0,"Objects":[{"StartTime":136069.0,"EndTime":136069.0,"Column":0}]},{"RandomW":2659271247,"RandomX":3579807591,"RandomY":273326509,"RandomZ":273071671,"StartTime":136307.0,"Objects":[{"StartTime":136307.0,"EndTime":136783.0,"Column":2}]},{"RandomW":2659271247,"RandomX":3579807591,"RandomY":273326509,"RandomZ":273071671,"StartTime":136545.0,"Objects":[{"StartTime":136545.0,"EndTime":136783.0,"Column":3}]},{"RandomW":2659271247,"RandomX":3579807591,"RandomY":273326509,"RandomZ":273071671,"StartTime":136545.0,"Objects":[{"StartTime":136545.0,"EndTime":136545.0,"Column":0}]},{"RandomW":2659271247,"RandomX":3579807591,"RandomY":273326509,"RandomZ":273071671,"StartTime":136783.0,"Objects":[{"StartTime":136783.0,"EndTime":136783.0,"Column":1}]},{"RandomW":2659271247,"RandomX":3579807591,"RandomY":273326509,"RandomZ":273071671,"StartTime":136902.0,"Objects":[{"StartTime":136902.0,"EndTime":136902.0,"Column":0}]},{"RandomW":2659271247,"RandomX":3579807591,"RandomY":273326509,"RandomZ":273071671,"StartTime":137021.0,"Objects":[{"StartTime":137021.0,"EndTime":137021.0,"Column":1}]},{"RandomW":2659271247,"RandomX":3579807591,"RandomY":273326509,"RandomZ":273071671,"StartTime":137022.0,"Objects":[{"StartTime":137022.0,"EndTime":137260.0,"Column":3}]},{"RandomW":2659271247,"RandomX":3579807591,"RandomY":273326509,"RandomZ":273071671,"StartTime":137259.0,"Objects":[{"StartTime":137259.0,"EndTime":137497.0,"Column":2}]},{"RandomW":2659271247,"RandomX":3579807591,"RandomY":273326509,"RandomZ":273071671,"StartTime":137259.0,"Objects":[{"StartTime":137259.0,"EndTime":137259.0,"Column":0}]},{"RandomW":2659271247,"RandomX":3579807591,"RandomY":273326509,"RandomZ":273071671,"StartTime":137497.0,"Objects":[{"StartTime":137497.0,"EndTime":137497.0,"Column":3}]},{"RandomW":2659271247,"RandomX":3579807591,"RandomY":273326509,"RandomZ":273071671,"StartTime":137497.0,"Objects":[{"StartTime":137497.0,"EndTime":137497.0,"Column":0}]},{"RandomW":2659271247,"RandomX":3579807591,"RandomY":273326509,"RandomZ":273071671,"StartTime":137735.0,"Objects":[{"StartTime":137735.0,"EndTime":137735.0,"Column":3}]},{"RandomW":2659271247,"RandomX":3579807591,"RandomY":273326509,"RandomZ":273071671,"StartTime":137736.0,"Objects":[{"StartTime":137736.0,"EndTime":137974.0,"Column":1}]},{"RandomW":2659271247,"RandomX":3579807591,"RandomY":273326509,"RandomZ":273071671,"StartTime":137974.0,"Objects":[{"StartTime":137974.0,"EndTime":137974.0,"Column":0}]},{"RandomW":2659271247,"RandomX":3579807591,"RandomY":273326509,"RandomZ":273071671,"StartTime":137974.0,"Objects":[{"StartTime":137974.0,"EndTime":137974.0,"Column":3}]},{"RandomW":2659271247,"RandomX":3579807591,"RandomY":273326509,"RandomZ":273071671,"StartTime":138212.0,"Objects":[{"StartTime":138212.0,"EndTime":138450.0,"Column":2}]},{"RandomW":2659271247,"RandomX":3579807591,"RandomY":273326509,"RandomZ":273071671,"StartTime":138212.0,"Objects":[{"StartTime":138212.0,"EndTime":138212.0,"Column":3}]},{"RandomW":2659271247,"RandomX":3579807591,"RandomY":273326509,"RandomZ":273071671,"StartTime":138450.0,"Objects":[{"StartTime":138450.0,"EndTime":138688.0,"Column":3}]},{"RandomW":2659271247,"RandomX":3579807591,"RandomY":273326509,"RandomZ":273071671,"StartTime":138450.0,"Objects":[{"StartTime":138450.0,"EndTime":138450.0,"Column":0}]},{"RandomW":2659271247,"RandomX":3579807591,"RandomY":273326509,"RandomZ":273071671,"StartTime":138688.0,"Objects":[{"StartTime":138688.0,"EndTime":138926.0,"Column":1}]},{"RandomW":2659271247,"RandomX":3579807591,"RandomY":273326509,"RandomZ":273071671,"StartTime":138926.0,"Objects":[{"StartTime":138926.0,"EndTime":139164.0,"Column":3}]},{"RandomW":2659271247,"RandomX":3579807591,"RandomY":273326509,"RandomZ":273071671,"StartTime":138927.0,"Objects":[{"StartTime":138927.0,"EndTime":138927.0,"Column":0}]},{"RandomW":2659271247,"RandomX":3579807591,"RandomY":273326509,"RandomZ":273071671,"StartTime":139164.0,"Objects":[{"StartTime":139164.0,"EndTime":139402.0,"Column":2}]},{"RandomW":2659271247,"RandomX":3579807591,"RandomY":273326509,"RandomZ":273071671,"StartTime":139403.0,"Objects":[{"StartTime":139403.0,"EndTime":139641.0,"Column":3}]},{"RandomW":2659271247,"RandomX":3579807591,"RandomY":273326509,"RandomZ":273071671,"StartTime":139403.0,"Objects":[{"StartTime":139403.0,"EndTime":139403.0,"Column":0}]},{"RandomW":2659271247,"RandomX":3579807591,"RandomY":273326509,"RandomZ":273071671,"StartTime":139640.0,"Objects":[{"StartTime":139640.0,"EndTime":139878.0,"Column":1}]},{"RandomW":2659271247,"RandomX":3579807591,"RandomY":273326509,"RandomZ":273071671,"StartTime":139878.0,"Objects":[{"StartTime":139878.0,"EndTime":140116.0,"Column":3}]},{"RandomW":2659271247,"RandomX":3579807591,"RandomY":273326509,"RandomZ":273071671,"StartTime":139879.0,"Objects":[{"StartTime":139879.0,"EndTime":139879.0,"Column":0}]},{"RandomW":2659271247,"RandomX":3579807591,"RandomY":273326509,"RandomZ":273071671,"StartTime":140116.0,"Objects":[{"StartTime":140116.0,"EndTime":140592.0,"Column":1}]},{"RandomW":2659271247,"RandomX":3579807591,"RandomY":273326509,"RandomZ":273071671,"StartTime":140354.0,"Objects":[{"StartTime":140354.0,"EndTime":140592.0,"Column":3}]},{"RandomW":2659271247,"RandomX":3579807591,"RandomY":273326509,"RandomZ":273071671,"StartTime":140355.0,"Objects":[{"StartTime":140355.0,"EndTime":140355.0,"Column":0}]},{"RandomW":2659271247,"RandomX":3579807591,"RandomY":273326509,"RandomZ":273071671,"StartTime":140593.0,"Objects":[{"StartTime":140593.0,"EndTime":140593.0,"Column":2}]},{"RandomW":2659271247,"RandomX":3579807591,"RandomY":273326509,"RandomZ":273071671,"StartTime":140831.0,"Objects":[{"StartTime":140831.0,"EndTime":140831.0,"Column":0}]},{"RandomW":2659271247,"RandomX":3579807591,"RandomY":273326509,"RandomZ":273071671,"StartTime":140831.0,"Objects":[{"StartTime":140831.0,"EndTime":141069.0,"Column":3}]},{"RandomW":2659271247,"RandomX":3579807591,"RandomY":273326509,"RandomZ":273071671,"StartTime":140831.0,"Objects":[{"StartTime":140831.0,"EndTime":140831.0,"Column":2}]},{"RandomW":2659271247,"RandomX":3579807591,"RandomY":273326509,"RandomZ":273071671,"StartTime":141069.0,"Objects":[{"StartTime":141069.0,"EndTime":141307.0,"Column":2}]},{"RandomW":2659271247,"RandomX":3579807591,"RandomY":273326509,"RandomZ":273071671,"StartTime":141307.0,"Objects":[{"StartTime":141307.0,"EndTime":141545.0,"Column":3}]},{"RandomW":2659271247,"RandomX":3579807591,"RandomY":273326509,"RandomZ":273071671,"StartTime":141307.0,"Objects":[{"StartTime":141307.0,"EndTime":141307.0,"Column":0}]},{"RandomW":2659271247,"RandomX":3579807591,"RandomY":273326509,"RandomZ":273071671,"StartTime":141546.0,"Objects":[{"StartTime":141546.0,"EndTime":141784.0,"Column":1}]},{"RandomW":2659271247,"RandomX":3579807591,"RandomY":273326509,"RandomZ":273071671,"StartTime":141783.0,"Objects":[{"StartTime":141783.0,"EndTime":141783.0,"Column":3}]},{"RandomW":2659271247,"RandomX":3579807591,"RandomY":273326509,"RandomZ":273071671,"StartTime":141784.0,"Objects":[{"StartTime":141784.0,"EndTime":141784.0,"Column":0}]},{"RandomW":2659271247,"RandomX":3579807591,"RandomY":273326509,"RandomZ":273071671,"StartTime":142021.0,"Objects":[{"StartTime":142021.0,"EndTime":142021.0,"Column":3}]},{"RandomW":2659271247,"RandomX":3579807591,"RandomY":273326509,"RandomZ":273071671,"StartTime":142022.0,"Objects":[{"StartTime":142022.0,"EndTime":142260.0,"Column":1}]},{"RandomW":2659271247,"RandomX":3579807591,"RandomY":273326509,"RandomZ":273071671,"StartTime":142259.0,"Objects":[{"StartTime":142259.0,"EndTime":142259.0,"Column":3}]},{"RandomW":2659271247,"RandomX":3579807591,"RandomY":273326509,"RandomZ":273071671,"StartTime":142260.0,"Objects":[{"StartTime":142260.0,"EndTime":142260.0,"Column":0}]},{"RandomW":2659271247,"RandomX":3579807591,"RandomY":273326509,"RandomZ":273071671,"StartTime":142497.0,"Objects":[{"StartTime":142497.0,"EndTime":142497.0,"Column":3}]},{"RandomW":2659271247,"RandomX":3579807591,"RandomY":273326509,"RandomZ":273071671,"StartTime":142498.0,"Objects":[{"StartTime":142498.0,"EndTime":142736.0,"Column":2}]},{"RandomW":2659271247,"RandomX":3579807591,"RandomY":273326509,"RandomZ":273071671,"StartTime":142736.0,"Objects":[{"StartTime":142736.0,"EndTime":142736.0,"Column":0}]},{"RandomW":2659271247,"RandomX":3579807591,"RandomY":273326509,"RandomZ":273071671,"StartTime":142736.0,"Objects":[{"StartTime":142736.0,"EndTime":142974.0,"Column":3}]},{"RandomW":2659271247,"RandomX":3579807591,"RandomY":273326509,"RandomZ":273071671,"StartTime":142974.0,"Objects":[{"StartTime":142974.0,"EndTime":143450.0,"Column":1}]},{"RandomW":2659271247,"RandomX":3579807591,"RandomY":273326509,"RandomZ":273071671,"StartTime":143212.0,"Objects":[{"StartTime":143212.0,"EndTime":143212.0,"Column":0}]},{"RandomW":2659271247,"RandomX":3579807591,"RandomY":273326509,"RandomZ":273071671,"StartTime":143212.0,"Objects":[{"StartTime":143212.0,"EndTime":143450.0,"Column":3}]},{"RandomW":2659271247,"RandomX":3579807591,"RandomY":273326509,"RandomZ":273071671,"StartTime":143450.0,"Objects":[{"StartTime":143450.0,"EndTime":143688.0,"Column":2}]},{"RandomW":2659271247,"RandomX":3579807591,"RandomY":273326509,"RandomZ":273071671,"StartTime":143688.0,"Objects":[{"StartTime":143688.0,"EndTime":143688.0,"Column":0}]},{"RandomW":2659271247,"RandomX":3579807591,"RandomY":273326509,"RandomZ":273071671,"StartTime":143688.0,"Objects":[{"StartTime":143688.0,"EndTime":143926.0,"Column":3}]},{"RandomW":2659271247,"RandomX":3579807591,"RandomY":273326509,"RandomZ":273071671,"StartTime":143927.0,"Objects":[{"StartTime":143927.0,"EndTime":144165.0,"Column":1}]},{"RandomW":2659271247,"RandomX":3579807591,"RandomY":273326509,"RandomZ":273071671,"StartTime":144164.0,"Objects":[{"StartTime":144164.0,"EndTime":144402.0,"Column":3}]},{"RandomW":2659271247,"RandomX":3579807591,"RandomY":273326509,"RandomZ":273071671,"StartTime":144165.0,"Objects":[{"StartTime":144165.0,"EndTime":144165.0,"Column":0}]},{"RandomW":2659271247,"RandomX":3579807591,"RandomY":273326509,"RandomZ":273071671,"StartTime":144403.0,"Objects":[{"StartTime":144403.0,"EndTime":144641.0,"Column":2}]},{"RandomW":2659271247,"RandomX":3579807591,"RandomY":273326509,"RandomZ":273071671,"StartTime":144521.0,"Objects":[{"StartTime":144521.0,"EndTime":144521.0,"Column":0}]},{"RandomW":2659271247,"RandomX":3579807591,"RandomY":273326509,"RandomZ":273071671,"StartTime":144641.0,"Objects":[{"StartTime":144641.0,"EndTime":144879.0,"Column":3}]},{"RandomW":2659271247,"RandomX":3579807591,"RandomY":273326509,"RandomZ":273071671,"StartTime":144878.0,"Objects":[{"StartTime":144878.0,"EndTime":145354.0,"Column":1}]},{"RandomW":2659271247,"RandomX":3579807591,"RandomY":273326509,"RandomZ":273071671,"StartTime":144878.0,"Objects":[{"StartTime":144878.0,"EndTime":144878.0,"Column":0}]},{"RandomW":2659271247,"RandomX":3579807591,"RandomY":273326509,"RandomZ":273071671,"StartTime":145116.0,"Objects":[{"StartTime":145116.0,"EndTime":145116.0,"Column":3}]},{"RandomW":2659271247,"RandomX":3579807591,"RandomY":273326509,"RandomZ":273071671,"StartTime":145116.0,"Objects":[{"StartTime":145116.0,"EndTime":145116.0,"Column":0}]},{"RandomW":2659271247,"RandomX":3579807591,"RandomY":273326509,"RandomZ":273071671,"StartTime":145354.0,"Objects":[{"StartTime":145354.0,"EndTime":145354.0,"Column":3}]},{"RandomW":2659271247,"RandomX":3579807591,"RandomY":273326509,"RandomZ":273071671,"StartTime":145354.0,"Objects":[{"StartTime":145354.0,"EndTime":145592.0,"Column":2}]},{"RandomW":2659271247,"RandomX":3579807591,"RandomY":273326509,"RandomZ":273071671,"StartTime":145593.0,"Objects":[{"StartTime":145593.0,"EndTime":145593.0,"Column":0}]},{"RandomW":2659271247,"RandomX":3579807591,"RandomY":273326509,"RandomZ":273071671,"StartTime":145593.0,"Objects":[{"StartTime":145593.0,"EndTime":145593.0,"Column":3}]},{"RandomW":2659271247,"RandomX":3579807591,"RandomY":273326509,"RandomZ":273071671,"StartTime":145831.0,"Objects":[{"StartTime":145831.0,"EndTime":145831.0,"Column":3}]},{"RandomW":2659271247,"RandomX":3579807591,"RandomY":273326509,"RandomZ":273071671,"StartTime":145831.0,"Objects":[{"StartTime":145831.0,"EndTime":146069.0,"Column":1}]},{"RandomW":2659271247,"RandomX":3579807591,"RandomY":273326509,"RandomZ":273071671,"StartTime":146069.0,"Objects":[{"StartTime":146069.0,"EndTime":146069.0,"Column":0}]},{"RandomW":2659271247,"RandomX":3579807591,"RandomY":273326509,"RandomZ":273071671,"StartTime":146069.0,"Objects":[{"StartTime":146069.0,"EndTime":146307.0,"Column":3}]},{"RandomW":2659271247,"RandomX":3579807591,"RandomY":273326509,"RandomZ":273071671,"StartTime":146307.0,"Objects":[{"StartTime":146307.0,"EndTime":146545.0,"Column":2}]},{"RandomW":2659271247,"RandomX":3579807591,"RandomY":273326509,"RandomZ":273071671,"StartTime":146546.0,"Objects":[{"StartTime":146546.0,"EndTime":146784.0,"Column":3}]},{"RandomW":2659271247,"RandomX":3579807591,"RandomY":273326509,"RandomZ":273071671,"StartTime":146546.0,"Objects":[{"StartTime":146546.0,"EndTime":146546.0,"Column":0}]},{"RandomW":2659271247,"RandomX":3579807591,"RandomY":273326509,"RandomZ":273071671,"StartTime":146783.0,"Objects":[{"StartTime":146783.0,"EndTime":147021.0,"Column":1}]},{"RandomW":2659271247,"RandomX":3579807591,"RandomY":273326509,"RandomZ":273071671,"StartTime":147022.0,"Objects":[{"StartTime":147022.0,"EndTime":147260.0,"Column":3}]},{"RandomW":2659271247,"RandomX":3579807591,"RandomY":273326509,"RandomZ":273071671,"StartTime":147022.0,"Objects":[{"StartTime":147022.0,"EndTime":147022.0,"Column":0}]},{"RandomW":2659271247,"RandomX":3579807591,"RandomY":273326509,"RandomZ":273071671,"StartTime":147259.0,"Objects":[{"StartTime":147259.0,"EndTime":147497.0,"Column":1}]},{"RandomW":2659271247,"RandomX":3579807591,"RandomY":273326509,"RandomZ":273071671,"StartTime":147260.0,"Objects":[{"StartTime":147260.0,"EndTime":147498.0,"Column":2}]},{"RandomW":2659271247,"RandomX":3579807591,"RandomY":273326509,"RandomZ":273071671,"StartTime":147498.0,"Objects":[{"StartTime":147498.0,"EndTime":147736.0,"Column":3}]},{"RandomW":2659271247,"RandomX":3579807591,"RandomY":273326509,"RandomZ":273071671,"StartTime":147498.0,"Objects":[{"StartTime":147498.0,"EndTime":147498.0,"Column":0}]},{"RandomW":2659271247,"RandomX":3579807591,"RandomY":273326509,"RandomZ":273071671,"StartTime":147736.0,"Objects":[{"StartTime":147736.0,"EndTime":147974.0,"Column":1}]},{"RandomW":2659271247,"RandomX":3579807591,"RandomY":273326509,"RandomZ":273071671,"StartTime":147974.0,"Objects":[{"StartTime":147974.0,"EndTime":148212.0,"Column":3}]},{"RandomW":2659271247,"RandomX":3579807591,"RandomY":273326509,"RandomZ":273071671,"StartTime":147974.0,"Objects":[{"StartTime":147974.0,"EndTime":147974.0,"Column":0}]},{"RandomW":2659271247,"RandomX":3579807591,"RandomY":273326509,"RandomZ":273071671,"StartTime":148212.0,"Objects":[{"StartTime":148212.0,"EndTime":148450.0,"Column":2}]},{"RandomW":2659271247,"RandomX":3579807591,"RandomY":273326509,"RandomZ":273071671,"StartTime":148450.0,"Objects":[{"StartTime":148450.0,"EndTime":148688.0,"Column":3}]},{"RandomW":2659271247,"RandomX":3579807591,"RandomY":273326509,"RandomZ":273071671,"StartTime":148450.0,"Objects":[{"StartTime":148450.0,"EndTime":148450.0,"Column":0}]},{"RandomW":2659271247,"RandomX":3579807591,"RandomY":273326509,"RandomZ":273071671,"StartTime":148688.0,"Objects":[{"StartTime":148688.0,"EndTime":149164.0,"Column":2}]},{"RandomW":2659271247,"RandomX":3579807591,"RandomY":273326509,"RandomZ":273071671,"StartTime":148688.0,"Objects":[{"StartTime":148688.0,"EndTime":148688.0,"Column":1}]},{"RandomW":2659271247,"RandomX":3579807591,"RandomY":273326509,"RandomZ":273071671,"StartTime":148926.0,"Objects":[{"StartTime":148926.0,"EndTime":148926.0,"Column":0}]},{"RandomW":2659271247,"RandomX":3579807591,"RandomY":273326509,"RandomZ":273071671,"StartTime":148926.0,"Objects":[{"StartTime":148926.0,"EndTime":148926.0,"Column":3}]},{"RandomW":2659271247,"RandomX":3579807591,"RandomY":273326509,"RandomZ":273071671,"StartTime":149164.0,"Objects":[{"StartTime":149164.0,"EndTime":149402.0,"Column":1}]},{"RandomW":2659271247,"RandomX":3579807591,"RandomY":273326509,"RandomZ":273071671,"StartTime":149164.0,"Objects":[{"StartTime":149164.0,"EndTime":149164.0,"Column":3}]},{"RandomW":2659271247,"RandomX":3579807591,"RandomY":273326509,"RandomZ":273071671,"StartTime":149402.0,"Objects":[{"StartTime":149402.0,"EndTime":149402.0,"Column":0}]},{"RandomW":2659271247,"RandomX":3579807591,"RandomY":273326509,"RandomZ":273071671,"StartTime":149403.0,"Objects":[{"StartTime":149403.0,"EndTime":149403.0,"Column":3}]},{"RandomW":2659271247,"RandomX":3579807591,"RandomY":273326509,"RandomZ":273071671,"StartTime":149640.0,"Objects":[{"StartTime":149640.0,"EndTime":149878.0,"Column":2}]},{"RandomW":2659271247,"RandomX":3579807591,"RandomY":273326509,"RandomZ":273071671,"StartTime":149641.0,"Objects":[{"StartTime":149641.0,"EndTime":149641.0,"Column":3}]},{"RandomW":2659271247,"RandomX":3579807591,"RandomY":273326509,"RandomZ":273071671,"StartTime":149878.0,"Objects":[{"StartTime":149878.0,"EndTime":150116.0,"Column":3}]},{"RandomW":2659271247,"RandomX":3579807591,"RandomY":273326509,"RandomZ":273071671,"StartTime":149879.0,"Objects":[{"StartTime":149879.0,"EndTime":149879.0,"Column":0}]},{"RandomW":2659271247,"RandomX":3579807591,"RandomY":273326509,"RandomZ":273071671,"StartTime":150117.0,"Objects":[{"StartTime":150117.0,"EndTime":150355.0,"Column":1}]},{"RandomW":2659271247,"RandomX":3579807591,"RandomY":273326509,"RandomZ":273071671,"StartTime":150355.0,"Objects":[{"StartTime":150355.0,"EndTime":150593.0,"Column":3}]},{"RandomW":2659271247,"RandomX":3579807591,"RandomY":273326509,"RandomZ":273071671,"StartTime":150355.0,"Objects":[{"StartTime":150355.0,"EndTime":150355.0,"Column":0}]},{"RandomW":2659271247,"RandomX":3579807591,"RandomY":273326509,"RandomZ":273071671,"StartTime":150593.0,"Objects":[{"StartTime":150593.0,"EndTime":150831.0,"Column":2}]},{"RandomW":2659271247,"RandomX":3579807591,"RandomY":273326509,"RandomZ":273071671,"StartTime":150831.0,"Objects":[{"StartTime":150831.0,"EndTime":150831.0,"Column":0}]},{"RandomW":2659271247,"RandomX":3579807591,"RandomY":273326509,"RandomZ":273071671,"StartTime":150831.0,"Objects":[{"StartTime":150831.0,"EndTime":151069.0,"Column":3}]},{"RandomW":2659271247,"RandomX":3579807591,"RandomY":273326509,"RandomZ":273071671,"StartTime":151069.0,"Objects":[{"StartTime":151069.0,"EndTime":151307.0,"Column":1}]},{"RandomW":2659271247,"RandomX":3579807591,"RandomY":273326509,"RandomZ":273071671,"StartTime":151307.0,"Objects":[{"StartTime":151307.0,"EndTime":151545.0,"Column":3}]},{"RandomW":2659271247,"RandomX":3579807591,"RandomY":273326509,"RandomZ":273071671,"StartTime":151307.0,"Objects":[{"StartTime":151307.0,"EndTime":151307.0,"Column":0}]},{"RandomW":2659271247,"RandomX":3579807591,"RandomY":273326509,"RandomZ":273071671,"StartTime":151545.0,"Objects":[{"StartTime":151545.0,"EndTime":151783.0,"Column":2}]},{"RandomW":2659271247,"RandomX":3579807591,"RandomY":273326509,"RandomZ":273071671,"StartTime":151783.0,"Objects":[{"StartTime":151783.0,"EndTime":152021.0,"Column":3}]},{"RandomW":2659271247,"RandomX":3579807591,"RandomY":273326509,"RandomZ":273071671,"StartTime":151783.0,"Objects":[{"StartTime":151783.0,"EndTime":151783.0,"Column":0}]},{"RandomW":2659271247,"RandomX":3579807591,"RandomY":273326509,"RandomZ":273071671,"StartTime":152022.0,"Objects":[{"StartTime":152022.0,"EndTime":152260.0,"Column":1}]},{"RandomW":2659271247,"RandomX":3579807591,"RandomY":273326509,"RandomZ":273071671,"StartTime":152140.0,"Objects":[{"StartTime":152140.0,"EndTime":152140.0,"Column":0}]},{"RandomW":2659271247,"RandomX":3579807591,"RandomY":273326509,"RandomZ":273071671,"StartTime":152260.0,"Objects":[{"StartTime":152260.0,"EndTime":152498.0,"Column":3}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":152497.0,"Objects":[{"StartTime":152497.0,"EndTime":153687.0,"Column":1}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":152497.0,"Objects":[{"StartTime":152497.0,"EndTime":152497.0,"Column":0}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":152735.0,"Objects":[{"StartTime":152735.0,"EndTime":152735.0,"Column":0}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":153093.0,"Objects":[{"StartTime":153093.0,"EndTime":153093.0,"Column":0}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":153688.0,"Objects":[{"StartTime":153688.0,"EndTime":153688.0,"Column":0}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":153926.0,"Objects":[{"StartTime":153926.0,"EndTime":153926.0,"Column":1}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":154045.0,"Objects":[{"StartTime":154045.0,"EndTime":154045.0,"Column":0}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":154402.0,"Objects":[{"StartTime":154402.0,"EndTime":155116.0,"Column":2}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":154640.0,"Objects":[{"StartTime":154640.0,"EndTime":154640.0,"Column":0}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":154997.0,"Objects":[{"StartTime":154997.0,"EndTime":154997.0,"Column":0}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":155354.0,"Objects":[{"StartTime":155354.0,"EndTime":156068.0,"Column":1}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":155593.0,"Objects":[{"StartTime":155593.0,"EndTime":155593.0,"Column":0}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":155831.0,"Objects":[{"StartTime":155831.0,"EndTime":155831.0,"Column":2}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":155950.0,"Objects":[{"StartTime":155950.0,"EndTime":155950.0,"Column":0}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":156069.0,"Objects":[{"StartTime":156069.0,"EndTime":156069.0,"Column":3}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":156307.0,"Objects":[{"StartTime":156307.0,"EndTime":157021.0,"Column":1}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":156307.0,"Objects":[{"StartTime":156307.0,"EndTime":156307.0,"Column":2}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":156545.0,"Objects":[{"StartTime":156545.0,"EndTime":156545.0,"Column":0}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":156902.0,"Objects":[{"StartTime":156902.0,"EndTime":156902.0,"Column":0}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":157259.0,"Objects":[{"StartTime":157259.0,"EndTime":157973.0,"Column":2}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":157497.0,"Objects":[{"StartTime":157497.0,"EndTime":157497.0,"Column":0}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":157735.0,"Objects":[{"StartTime":157735.0,"EndTime":157735.0,"Column":1}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":157854.0,"Objects":[{"StartTime":157854.0,"EndTime":157854.0,"Column":0}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":158212.0,"Objects":[{"StartTime":158212.0,"EndTime":158926.0,"Column":1}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":158450.0,"Objects":[{"StartTime":158450.0,"EndTime":158450.0,"Column":0}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":158807.0,"Objects":[{"StartTime":158807.0,"EndTime":158807.0,"Column":0}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":159164.0,"Objects":[{"StartTime":159164.0,"EndTime":159878.0,"Column":2}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":159402.0,"Objects":[{"StartTime":159402.0,"EndTime":159402.0,"Column":0}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":159640.0,"Objects":[{"StartTime":159640.0,"EndTime":159640.0,"Column":3}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":159759.0,"Objects":[{"StartTime":159759.0,"EndTime":159759.0,"Column":0}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":159878.0,"Objects":[{"StartTime":159878.0,"EndTime":159878.0,"Column":3}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":160116.0,"Objects":[{"StartTime":160116.0,"EndTime":160830.0,"Column":1}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":160116.0,"Objects":[{"StartTime":160116.0,"EndTime":160116.0,"Column":2}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":160354.0,"Objects":[{"StartTime":160354.0,"EndTime":160354.0,"Column":0}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":160712.0,"Objects":[{"StartTime":160712.0,"EndTime":160712.0,"Column":0}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":161069.0,"Objects":[{"StartTime":161069.0,"EndTime":161783.0,"Column":2}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":161307.0,"Objects":[{"StartTime":161307.0,"EndTime":161307.0,"Column":0}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":161545.0,"Objects":[{"StartTime":161545.0,"EndTime":161545.0,"Column":1}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":161664.0,"Objects":[{"StartTime":161664.0,"EndTime":161664.0,"Column":0}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":162021.0,"Objects":[{"StartTime":162021.0,"EndTime":162735.0,"Column":1}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":162259.0,"Objects":[{"StartTime":162259.0,"EndTime":162259.0,"Column":0}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":162616.0,"Objects":[{"StartTime":162616.0,"EndTime":162616.0,"Column":0}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":162974.0,"Objects":[{"StartTime":162974.0,"EndTime":163688.0,"Column":2}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":163212.0,"Objects":[{"StartTime":163212.0,"EndTime":163212.0,"Column":0}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":163450.0,"Objects":[{"StartTime":163450.0,"EndTime":163450.0,"Column":1}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":163569.0,"Objects":[{"StartTime":163569.0,"EndTime":163569.0,"Column":0}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":163688.0,"Objects":[{"StartTime":163688.0,"EndTime":163688.0,"Column":3}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":163926.0,"Objects":[{"StartTime":163926.0,"EndTime":163926.0,"Column":2}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":163926.0,"Objects":[{"StartTime":163926.0,"EndTime":164640.0,"Column":1}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":164164.0,"Objects":[{"StartTime":164164.0,"EndTime":164164.0,"Column":0}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":164521.0,"Objects":[{"StartTime":164521.0,"EndTime":164521.0,"Column":0}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":164878.0,"Objects":[{"StartTime":164878.0,"EndTime":165592.0,"Column":2}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":165116.0,"Objects":[{"StartTime":165116.0,"EndTime":165116.0,"Column":0}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":165354.0,"Objects":[{"StartTime":165354.0,"EndTime":165354.0,"Column":1}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":165474.0,"Objects":[{"StartTime":165474.0,"EndTime":165474.0,"Column":0}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":165831.0,"Objects":[{"StartTime":165831.0,"EndTime":166545.0,"Column":1}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":166069.0,"Objects":[{"StartTime":166069.0,"EndTime":166069.0,"Column":0}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":166426.0,"Objects":[{"StartTime":166426.0,"EndTime":166426.0,"Column":0}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":166783.0,"Objects":[{"StartTime":166783.0,"EndTime":167973.0,"Column":2}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":167021.0,"Objects":[{"StartTime":167021.0,"EndTime":167021.0,"Column":0}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":167259.0,"Objects":[{"StartTime":167259.0,"EndTime":167259.0,"Column":1}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":167378.0,"Objects":[{"StartTime":167378.0,"EndTime":167378.0,"Column":0}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":167497.0,"Objects":[{"StartTime":167497.0,"EndTime":167497.0,"Column":1}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":167735.0,"Objects":[{"StartTime":167735.0,"EndTime":167973.0,"Column":1}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":167974.0,"Objects":[{"StartTime":167974.0,"EndTime":167974.0,"Column":3}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":168212.0,"Objects":[{"StartTime":168212.0,"EndTime":168212.0,"Column":3}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":168450.0,"Objects":[{"StartTime":168450.0,"EndTime":168450.0,"Column":0}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":168450.0,"Objects":[{"StartTime":168450.0,"EndTime":168450.0,"Column":1}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":168450.0,"Objects":[{"StartTime":168450.0,"EndTime":168450.0,"Column":3}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":168688.0,"Objects":[{"StartTime":168688.0,"EndTime":168688.0,"Column":3}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":168926.0,"Objects":[{"StartTime":168926.0,"EndTime":168926.0,"Column":1}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":168926.0,"Objects":[{"StartTime":168926.0,"EndTime":169164.0,"Column":3}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":169402.0,"Objects":[{"StartTime":169402.0,"EndTime":169402.0,"Column":0}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":169402.0,"Objects":[{"StartTime":169402.0,"EndTime":169402.0,"Column":2}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":169402.0,"Objects":[{"StartTime":169402.0,"EndTime":169640.0,"Column":3}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":169640.0,"Objects":[{"StartTime":169640.0,"EndTime":169640.0,"Column":1}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":169878.0,"Objects":[{"StartTime":169878.0,"EndTime":170354.0,"Column":2}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":169878.0,"Objects":[{"StartTime":169878.0,"EndTime":170116.0,"Column":3}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":170354.0,"Objects":[{"StartTime":170354.0,"EndTime":170354.0,"Column":0}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":170354.0,"Objects":[{"StartTime":170354.0,"EndTime":170592.0,"Column":3}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":170593.0,"Objects":[{"StartTime":170593.0,"EndTime":171069.0,"Column":1}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":170593.0,"Objects":[{"StartTime":170593.0,"EndTime":170593.0,"Column":2}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":170831.0,"Objects":[{"StartTime":170831.0,"EndTime":171069.0,"Column":3}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":171307.0,"Objects":[{"StartTime":171307.0,"EndTime":171307.0,"Column":0}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":171307.0,"Objects":[{"StartTime":171307.0,"EndTime":171783.0,"Column":2}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":171307.0,"Objects":[{"StartTime":171307.0,"EndTime":171545.0,"Column":3}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":171783.0,"Objects":[{"StartTime":171783.0,"EndTime":171783.0,"Column":3}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":172021.0,"Objects":[{"StartTime":172021.0,"EndTime":172021.0,"Column":3}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":172259.0,"Objects":[{"StartTime":172259.0,"EndTime":172259.0,"Column":0}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":172259.0,"Objects":[{"StartTime":172259.0,"EndTime":172259.0,"Column":1}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":172259.0,"Objects":[{"StartTime":172259.0,"EndTime":172259.0,"Column":3}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":172497.0,"Objects":[{"StartTime":172497.0,"EndTime":172497.0,"Column":3}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":172735.0,"Objects":[{"StartTime":172735.0,"EndTime":172735.0,"Column":1}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":172735.0,"Objects":[{"StartTime":172735.0,"EndTime":172973.0,"Column":3}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":173212.0,"Objects":[{"StartTime":173212.0,"EndTime":173212.0,"Column":0}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":173212.0,"Objects":[{"StartTime":173212.0,"EndTime":173212.0,"Column":2}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":173212.0,"Objects":[{"StartTime":173212.0,"EndTime":173450.0,"Column":3}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":173450.0,"Objects":[{"StartTime":173450.0,"EndTime":173450.0,"Column":2}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":173688.0,"Objects":[{"StartTime":173688.0,"EndTime":174164.0,"Column":2}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":173688.0,"Objects":[{"StartTime":173688.0,"EndTime":173688.0,"Column":1}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":173688.0,"Objects":[{"StartTime":173688.0,"EndTime":173926.0,"Column":3}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":173926.0,"Objects":[{"StartTime":173926.0,"EndTime":173926.0,"Column":1}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":174164.0,"Objects":[{"StartTime":174164.0,"EndTime":174164.0,"Column":0}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":174164.0,"Objects":[{"StartTime":174164.0,"EndTime":174164.0,"Column":1}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":174164.0,"Objects":[{"StartTime":174164.0,"EndTime":174402.0,"Column":3}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":174402.0,"Objects":[{"StartTime":174402.0,"EndTime":174878.0,"Column":2}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":174402.0,"Objects":[{"StartTime":174402.0,"EndTime":174402.0,"Column":1}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":174640.0,"Objects":[{"StartTime":174640.0,"EndTime":174640.0,"Column":1}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":174640.0,"Objects":[{"StartTime":174640.0,"EndTime":174878.0,"Column":3}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":174878.0,"Objects":[{"StartTime":174878.0,"EndTime":174878.0,"Column":1}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":175116.0,"Objects":[{"StartTime":175116.0,"EndTime":175116.0,"Column":0}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":175116.0,"Objects":[{"StartTime":175116.0,"EndTime":175592.0,"Column":2}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":175116.0,"Objects":[{"StartTime":175116.0,"EndTime":175116.0,"Column":1}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":175116.0,"Objects":[{"StartTime":175116.0,"EndTime":175354.0,"Column":3}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":175354.0,"Objects":[{"StartTime":175354.0,"EndTime":175592.0,"Column":1}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":175593.0,"Objects":[{"StartTime":175593.0,"EndTime":175593.0,"Column":3}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":175831.0,"Objects":[{"StartTime":175831.0,"EndTime":176307.0,"Column":1}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":175831.0,"Objects":[{"StartTime":175831.0,"EndTime":175831.0,"Column":3}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":176069.0,"Objects":[{"StartTime":176069.0,"EndTime":176069.0,"Column":0}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":176069.0,"Objects":[{"StartTime":176069.0,"EndTime":176069.0,"Column":2}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":176069.0,"Objects":[{"StartTime":176069.0,"EndTime":176069.0,"Column":3}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":176307.0,"Objects":[{"StartTime":176307.0,"EndTime":176307.0,"Column":3}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":176545.0,"Objects":[{"StartTime":176545.0,"EndTime":176545.0,"Column":2}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":176545.0,"Objects":[{"StartTime":176545.0,"EndTime":176783.0,"Column":3}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":177021.0,"Objects":[{"StartTime":177021.0,"EndTime":177021.0,"Column":0}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":177021.0,"Objects":[{"StartTime":177021.0,"EndTime":177021.0,"Column":2}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":177021.0,"Objects":[{"StartTime":177021.0,"EndTime":177259.0,"Column":3}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":177259.0,"Objects":[{"StartTime":177259.0,"EndTime":177259.0,"Column":1}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":177497.0,"Objects":[{"StartTime":177497.0,"EndTime":177973.0,"Column":2}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":177497.0,"Objects":[{"StartTime":177497.0,"EndTime":177497.0,"Column":1}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":177497.0,"Objects":[{"StartTime":177497.0,"EndTime":177735.0,"Column":3}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":177974.0,"Objects":[{"StartTime":177974.0,"EndTime":177974.0,"Column":0}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":177974.0,"Objects":[{"StartTime":177974.0,"EndTime":177974.0,"Column":1}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":177974.0,"Objects":[{"StartTime":177974.0,"EndTime":178212.0,"Column":3}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":178212.0,"Objects":[{"StartTime":178212.0,"EndTime":178212.0,"Column":1}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":178450.0,"Objects":[{"StartTime":178450.0,"EndTime":178450.0,"Column":1}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":178450.0,"Objects":[{"StartTime":178450.0,"EndTime":178688.0,"Column":2}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":178450.0,"Objects":[{"StartTime":178450.0,"EndTime":178688.0,"Column":3}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":178926.0,"Objects":[{"StartTime":178926.0,"EndTime":178926.0,"Column":0}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":178926.0,"Objects":[{"StartTime":178926.0,"EndTime":179402.0,"Column":2}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":178926.0,"Objects":[{"StartTime":178926.0,"EndTime":178926.0,"Column":1}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":178926.0,"Objects":[{"StartTime":178926.0,"EndTime":179164.0,"Column":3}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":179164.0,"Objects":[{"StartTime":179164.0,"EndTime":179402.0,"Column":1}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":179402.0,"Objects":[{"StartTime":179402.0,"EndTime":179402.0,"Column":3}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":179640.0,"Objects":[{"StartTime":179640.0,"EndTime":179640.0,"Column":3}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":179878.0,"Objects":[{"StartTime":179878.0,"EndTime":180354.0,"Column":1}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":179878.0,"Objects":[{"StartTime":179878.0,"EndTime":179878.0,"Column":2}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":179878.0,"Objects":[{"StartTime":179878.0,"EndTime":179878.0,"Column":3}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":180116.0,"Objects":[{"StartTime":180116.0,"EndTime":180116.0,"Column":3}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":180354.0,"Objects":[{"StartTime":180354.0,"EndTime":180354.0,"Column":2}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":180354.0,"Objects":[{"StartTime":180354.0,"EndTime":180592.0,"Column":3}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":180593.0,"Objects":[{"StartTime":180593.0,"EndTime":181069.0,"Column":1}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":180831.0,"Objects":[{"StartTime":180831.0,"EndTime":180831.0,"Column":2}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":180831.0,"Objects":[{"StartTime":180831.0,"EndTime":181069.0,"Column":3}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":181069.0,"Objects":[{"StartTime":181069.0,"EndTime":181069.0,"Column":2}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":181306.0,"Objects":[{"StartTime":181306.0,"EndTime":181782.0,"Column":2}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":181307.0,"Objects":[{"StartTime":181307.0,"EndTime":181783.0,"Column":1}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":181307.0,"Objects":[{"StartTime":181307.0,"EndTime":181545.0,"Column":3}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":181783.0,"Objects":[{"StartTime":181783.0,"EndTime":182021.0,"Column":3}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":182021.0,"Objects":[{"StartTime":182021.0,"EndTime":182497.0,"Column":2}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":182021.0,"Objects":[{"StartTime":182021.0,"EndTime":182497.0,"Column":1}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":182259.0,"Objects":[{"StartTime":182259.0,"EndTime":182497.0,"Column":3}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":182497.0,"Objects":[{"StartTime":182497.0,"EndTime":182497.0,"Column":0}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":182735.0,"Objects":[{"StartTime":182735.0,"EndTime":182735.0,"Column":0}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":182735.0,"Objects":[{"StartTime":182735.0,"EndTime":182735.0,"Column":2}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":182974.0,"Objects":[{"StartTime":182974.0,"EndTime":183212.0,"Column":2}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":183211.0,"Objects":[{"StartTime":183211.0,"EndTime":183211.0,"Column":3}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":183211.0,"Objects":[{"StartTime":183211.0,"EndTime":183211.0,"Column":0}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":183449.0,"Objects":[{"StartTime":183449.0,"EndTime":183687.0,"Column":1}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":183449.0,"Objects":[{"StartTime":183449.0,"EndTime":183449.0,"Column":3}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":183687.0,"Objects":[{"StartTime":183687.0,"EndTime":183687.0,"Column":0}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":183687.0,"Objects":[{"StartTime":183687.0,"EndTime":183687.0,"Column":3}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":183925.0,"Objects":[{"StartTime":183925.0,"EndTime":183925.0,"Column":3}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":183925.0,"Objects":[{"StartTime":183925.0,"EndTime":184163.0,"Column":2}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":184163.0,"Objects":[{"StartTime":184163.0,"EndTime":184401.0,"Column":3}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":184163.0,"Objects":[{"StartTime":184163.0,"EndTime":184163.0,"Column":0}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":184401.0,"Objects":[{"StartTime":184401.0,"EndTime":184639.0,"Column":1}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":184639.0,"Objects":[{"StartTime":184639.0,"EndTime":184639.0,"Column":0}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":184639.0,"Objects":[{"StartTime":184639.0,"EndTime":184877.0,"Column":3}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":184878.0,"Objects":[{"StartTime":184878.0,"EndTime":185116.0,"Column":2}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":185116.0,"Objects":[{"StartTime":185116.0,"EndTime":185354.0,"Column":3}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":185116.0,"Objects":[{"StartTime":185116.0,"EndTime":185116.0,"Column":0}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":185354.0,"Objects":[{"StartTime":185354.0,"EndTime":185830.0,"Column":1}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":185592.0,"Objects":[{"StartTime":185592.0,"EndTime":185592.0,"Column":0}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":185592.0,"Objects":[{"StartTime":185592.0,"EndTime":185830.0,"Column":3}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":185830.0,"Objects":[{"StartTime":185830.0,"EndTime":186068.0,"Column":2}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":186068.0,"Objects":[{"StartTime":186068.0,"EndTime":186306.0,"Column":3}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":186068.0,"Objects":[{"StartTime":186068.0,"EndTime":186068.0,"Column":0}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":186306.0,"Objects":[{"StartTime":186306.0,"EndTime":186306.0,"Column":2}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":186544.0,"Objects":[{"StartTime":186544.0,"EndTime":186782.0,"Column":3}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":186544.0,"Objects":[{"StartTime":186544.0,"EndTime":186544.0,"Column":0}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":186544.0,"Objects":[{"StartTime":186544.0,"EndTime":186544.0,"Column":1}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":186782.0,"Objects":[{"StartTime":186782.0,"EndTime":187020.0,"Column":1}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":187020.0,"Objects":[{"StartTime":187020.0,"EndTime":187020.0,"Column":3}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":187020.0,"Objects":[{"StartTime":187020.0,"EndTime":187020.0,"Column":0}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":187258.0,"Objects":[{"StartTime":187258.0,"EndTime":187258.0,"Column":3}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":187258.0,"Objects":[{"StartTime":187258.0,"EndTime":187496.0,"Column":2}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":187497.0,"Objects":[{"StartTime":187497.0,"EndTime":187497.0,"Column":3}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":187497.0,"Objects":[{"StartTime":187497.0,"EndTime":187497.0,"Column":0}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":187735.0,"Objects":[{"StartTime":187735.0,"EndTime":187735.0,"Column":3}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":187735.0,"Objects":[{"StartTime":187735.0,"EndTime":187973.0,"Column":1}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":187973.0,"Objects":[{"StartTime":187973.0,"EndTime":188211.0,"Column":3}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":187973.0,"Objects":[{"StartTime":187973.0,"EndTime":187973.0,"Column":0}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":188211.0,"Objects":[{"StartTime":188211.0,"EndTime":188449.0,"Column":2}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":188449.0,"Objects":[{"StartTime":188449.0,"EndTime":188687.0,"Column":3}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":188449.0,"Objects":[{"StartTime":188449.0,"EndTime":188449.0,"Column":0}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":188688.0,"Objects":[{"StartTime":188688.0,"EndTime":188926.0,"Column":2}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":188925.0,"Objects":[{"StartTime":188925.0,"EndTime":189163.0,"Column":3}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":188925.0,"Objects":[{"StartTime":188925.0,"EndTime":188925.0,"Column":0}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":188926.0,"Objects":[{"StartTime":188926.0,"EndTime":189164.0,"Column":1}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":189163.0,"Objects":[{"StartTime":189163.0,"EndTime":189401.0,"Column":2}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":189401.0,"Objects":[{"StartTime":189401.0,"EndTime":189639.0,"Column":3}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":189401.0,"Objects":[{"StartTime":189401.0,"EndTime":189401.0,"Column":0}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":189402.0,"Objects":[{"StartTime":189402.0,"EndTime":189878.0,"Column":1}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":189639.0,"Objects":[{"StartTime":189639.0,"EndTime":189877.0,"Column":2}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":189878.0,"Objects":[{"StartTime":189878.0,"EndTime":190116.0,"Column":3}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":189878.0,"Objects":[{"StartTime":189878.0,"EndTime":189878.0,"Column":0}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":190116.0,"Objects":[{"StartTime":190116.0,"EndTime":190354.0,"Column":1}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":190235.0,"Objects":[{"StartTime":190235.0,"EndTime":190235.0,"Column":0}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":190354.0,"Objects":[{"StartTime":190354.0,"EndTime":190592.0,"Column":3}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":190592.0,"Objects":[{"StartTime":190592.0,"EndTime":190949.0,"Column":2}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":190593.0,"Objects":[{"StartTime":190593.0,"EndTime":190593.0,"Column":0}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":190830.0,"Objects":[{"StartTime":190830.0,"EndTime":190830.0,"Column":3}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":190830.0,"Objects":[{"StartTime":190830.0,"EndTime":190830.0,"Column":0}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":191068.0,"Objects":[{"StartTime":191068.0,"EndTime":191068.0,"Column":3}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":191068.0,"Objects":[{"StartTime":191068.0,"EndTime":191306.0,"Column":1}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":191069.0,"Objects":[{"StartTime":191069.0,"EndTime":191069.0,"Column":2}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":191306.0,"Objects":[{"StartTime":191306.0,"EndTime":191306.0,"Column":3}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":191306.0,"Objects":[{"StartTime":191306.0,"EndTime":191306.0,"Column":0}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":191307.0,"Objects":[{"StartTime":191307.0,"EndTime":191307.0,"Column":2}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":191544.0,"Objects":[{"StartTime":191544.0,"EndTime":191544.0,"Column":3}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":191544.0,"Objects":[{"StartTime":191544.0,"EndTime":191782.0,"Column":1}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":191545.0,"Objects":[{"StartTime":191545.0,"EndTime":191783.0,"Column":2}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":191782.0,"Objects":[{"StartTime":191782.0,"EndTime":192020.0,"Column":3}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":191782.0,"Objects":[{"StartTime":191782.0,"EndTime":191782.0,"Column":0}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":192020.0,"Objects":[{"StartTime":192020.0,"EndTime":192258.0,"Column":2}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":192021.0,"Objects":[{"StartTime":192021.0,"EndTime":192259.0,"Column":1}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":192258.0,"Objects":[{"StartTime":192258.0,"EndTime":192496.0,"Column":3}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":192258.0,"Objects":[{"StartTime":192258.0,"EndTime":192258.0,"Column":0}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":192497.0,"Objects":[{"StartTime":192497.0,"EndTime":192735.0,"Column":1}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":192497.0,"Objects":[{"StartTime":192497.0,"EndTime":193449.0,"Column":2}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":192735.0,"Objects":[{"StartTime":192735.0,"EndTime":192973.0,"Column":3}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":192735.0,"Objects":[{"StartTime":192735.0,"EndTime":192735.0,"Column":0}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":192973.0,"Objects":[{"StartTime":192973.0,"EndTime":193211.0,"Column":1}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":193211.0,"Objects":[{"StartTime":193211.0,"EndTime":193449.0,"Column":3}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":193211.0,"Objects":[{"StartTime":193211.0,"EndTime":193211.0,"Column":0}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":193450.0,"Objects":[{"StartTime":193450.0,"EndTime":193688.0,"Column":1}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":193687.0,"Objects":[{"StartTime":193687.0,"EndTime":193925.0,"Column":3}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":193688.0,"Objects":[{"StartTime":193688.0,"EndTime":193688.0,"Column":0}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":193925.0,"Objects":[{"StartTime":193925.0,"EndTime":194163.0,"Column":2}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":194163.0,"Objects":[{"StartTime":194163.0,"EndTime":194401.0,"Column":3}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":194163.0,"Objects":[{"StartTime":194163.0,"EndTime":194163.0,"Column":0}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":194639.0,"Objects":[{"StartTime":194639.0,"EndTime":194639.0,"Column":3}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":194640.0,"Objects":[{"StartTime":194640.0,"EndTime":194878.0,"Column":1}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":194640.0,"Objects":[{"StartTime":194640.0,"EndTime":194640.0,"Column":0}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":194640.0,"Objects":[{"StartTime":194640.0,"EndTime":194640.0,"Column":2}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":194878.0,"Objects":[{"StartTime":194878.0,"EndTime":194878.0,"Column":3}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":194878.0,"Objects":[{"StartTime":194878.0,"EndTime":195116.0,"Column":2}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":195116.0,"Objects":[{"StartTime":195116.0,"EndTime":195116.0,"Column":3}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":195116.0,"Objects":[{"StartTime":195116.0,"EndTime":195116.0,"Column":0}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":195116.0,"Objects":[{"StartTime":195116.0,"EndTime":195116.0,"Column":1}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":195354.0,"Objects":[{"StartTime":195354.0,"EndTime":195354.0,"Column":3}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":195354.0,"Objects":[{"StartTime":195354.0,"EndTime":195592.0,"Column":2}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":195354.0,"Objects":[{"StartTime":195354.0,"EndTime":195592.0,"Column":1}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":195592.0,"Objects":[{"StartTime":195592.0,"EndTime":195830.0,"Column":3}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":195592.0,"Objects":[{"StartTime":195592.0,"EndTime":195592.0,"Column":0}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":195830.0,"Objects":[{"StartTime":195830.0,"EndTime":196068.0,"Column":1}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":195831.0,"Objects":[{"StartTime":195831.0,"EndTime":196069.0,"Column":2}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":196068.0,"Objects":[{"StartTime":196068.0,"EndTime":196306.0,"Column":3}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":196068.0,"Objects":[{"StartTime":196068.0,"EndTime":196068.0,"Column":0}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":196306.0,"Objects":[{"StartTime":196306.0,"EndTime":197496.0,"Column":2}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":196307.0,"Objects":[{"StartTime":196307.0,"EndTime":196545.0,"Column":1}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":196544.0,"Objects":[{"StartTime":196544.0,"EndTime":196782.0,"Column":3}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":196544.0,"Objects":[{"StartTime":196544.0,"EndTime":196544.0,"Column":0}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":197020.0,"Objects":[{"StartTime":197020.0,"EndTime":197258.0,"Column":3}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":197020.0,"Objects":[{"StartTime":197020.0,"EndTime":197020.0,"Column":0}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":197258.0,"Objects":[{"StartTime":197258.0,"EndTime":197734.0,"Column":1}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":197497.0,"Objects":[{"StartTime":197497.0,"EndTime":197735.0,"Column":3}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":197497.0,"Objects":[{"StartTime":197497.0,"EndTime":197497.0,"Column":0}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":197735.0,"Objects":[{"StartTime":197735.0,"EndTime":197735.0,"Column":2}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":197854.0,"Objects":[{"StartTime":197854.0,"EndTime":197854.0,"Column":0}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":197973.0,"Objects":[{"StartTime":197973.0,"EndTime":197973.0,"Column":1}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":197973.0,"Objects":[{"StartTime":197973.0,"EndTime":198211.0,"Column":3}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":198211.0,"Objects":[{"StartTime":198211.0,"EndTime":198449.0,"Column":1}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":198212.0,"Objects":[{"StartTime":198212.0,"EndTime":198212.0,"Column":0}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":198449.0,"Objects":[{"StartTime":198449.0,"EndTime":198687.0,"Column":3}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":198449.0,"Objects":[{"StartTime":198449.0,"EndTime":198449.0,"Column":0}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":198687.0,"Objects":[{"StartTime":198687.0,"EndTime":198925.0,"Column":2}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":198925.0,"Objects":[{"StartTime":198925.0,"EndTime":199163.0,"Column":3}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":198925.0,"Objects":[{"StartTime":198925.0,"EndTime":198925.0,"Column":0}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":199163.0,"Objects":[{"StartTime":199163.0,"EndTime":199401.0,"Column":1}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":199401.0,"Objects":[{"StartTime":199401.0,"EndTime":199639.0,"Column":3}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":199402.0,"Objects":[{"StartTime":199402.0,"EndTime":199402.0,"Column":0}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":199639.0,"Objects":[{"StartTime":199639.0,"EndTime":200115.0,"Column":2}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":199640.0,"Objects":[{"StartTime":199640.0,"EndTime":199878.0,"Column":1}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":199878.0,"Objects":[{"StartTime":199878.0,"EndTime":200116.0,"Column":3}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":199878.0,"Objects":[{"StartTime":199878.0,"EndTime":199878.0,"Column":0}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":200116.0,"Objects":[{"StartTime":200116.0,"EndTime":200354.0,"Column":1}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":200354.0,"Objects":[{"StartTime":200354.0,"EndTime":200354.0,"Column":0}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":200354.0,"Objects":[{"StartTime":200354.0,"EndTime":200354.0,"Column":3}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":200592.0,"Objects":[{"StartTime":200592.0,"EndTime":200830.0,"Column":2}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":200592.0,"Objects":[{"StartTime":200592.0,"EndTime":200592.0,"Column":3}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":200830.0,"Objects":[{"StartTime":200830.0,"EndTime":200830.0,"Column":0}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":200830.0,"Objects":[{"StartTime":200830.0,"EndTime":200830.0,"Column":3}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":201068.0,"Objects":[{"StartTime":201068.0,"EndTime":201306.0,"Column":1}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":201068.0,"Objects":[{"StartTime":201068.0,"EndTime":201068.0,"Column":3}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":201306.0,"Objects":[{"StartTime":201306.0,"EndTime":201306.0,"Column":0}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":201306.0,"Objects":[{"StartTime":201306.0,"EndTime":201544.0,"Column":3}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":201307.0,"Objects":[{"StartTime":201307.0,"EndTime":201545.0,"Column":2}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":201545.0,"Objects":[{"StartTime":201545.0,"EndTime":201545.0,"Column":1}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":201782.0,"Objects":[{"StartTime":201782.0,"EndTime":202020.0,"Column":3}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":201782.0,"Objects":[{"StartTime":201782.0,"EndTime":201782.0,"Column":0}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":201783.0,"Objects":[{"StartTime":201783.0,"EndTime":201783.0,"Column":1}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":202021.0,"Objects":[{"StartTime":202021.0,"EndTime":202259.0,"Column":1}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":202021.0,"Objects":[{"StartTime":202021.0,"EndTime":202735.0,"Column":2}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":202259.0,"Objects":[{"StartTime":202259.0,"EndTime":202259.0,"Column":0}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":202259.0,"Objects":[{"StartTime":202259.0,"EndTime":202497.0,"Column":3}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":202497.0,"Objects":[{"StartTime":202497.0,"EndTime":202735.0,"Column":1}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":202735.0,"Objects":[{"StartTime":202735.0,"EndTime":202735.0,"Column":0}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":202735.0,"Objects":[{"StartTime":202735.0,"EndTime":202973.0,"Column":3}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":202973.0,"Objects":[{"StartTime":202973.0,"EndTime":203211.0,"Column":2}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":203211.0,"Objects":[{"StartTime":203211.0,"EndTime":203211.0,"Column":0}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":203211.0,"Objects":[{"StartTime":203211.0,"EndTime":203449.0,"Column":3}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":203212.0,"Objects":[{"StartTime":203212.0,"EndTime":203450.0,"Column":1}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":203449.0,"Objects":[{"StartTime":203449.0,"EndTime":203687.0,"Column":2}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":203687.0,"Objects":[{"StartTime":203687.0,"EndTime":203687.0,"Column":0}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":203687.0,"Objects":[{"StartTime":203687.0,"EndTime":203925.0,"Column":3}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":203925.0,"Objects":[{"StartTime":203925.0,"EndTime":204401.0,"Column":2}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":203926.0,"Objects":[{"StartTime":203926.0,"EndTime":204640.0,"Column":1}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":204163.0,"Objects":[{"StartTime":204163.0,"EndTime":204163.0,"Column":3}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":204163.0,"Objects":[{"StartTime":204163.0,"EndTime":204163.0,"Column":0}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":204402.0,"Objects":[{"StartTime":204402.0,"EndTime":204402.0,"Column":3}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":204640.0,"Objects":[{"StartTime":204640.0,"EndTime":204640.0,"Column":0}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":204640.0,"Objects":[{"StartTime":204640.0,"EndTime":204640.0,"Column":3}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":204640.0,"Objects":[{"StartTime":204640.0,"EndTime":205116.0,"Column":2}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":204878.0,"Objects":[{"StartTime":204878.0,"EndTime":204878.0,"Column":3}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":204878.0,"Objects":[{"StartTime":204878.0,"EndTime":205116.0,"Column":1}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":205116.0,"Objects":[{"StartTime":205116.0,"EndTime":205116.0,"Column":0}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":205116.0,"Objects":[{"StartTime":205116.0,"EndTime":205354.0,"Column":3}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":205354.0,"Objects":[{"StartTime":205354.0,"EndTime":205592.0,"Column":1}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":205474.0,"Objects":[{"StartTime":205474.0,"EndTime":205474.0,"Column":0}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":205592.0,"Objects":[{"StartTime":205592.0,"EndTime":205830.0,"Column":3}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":205830.0,"Objects":[{"StartTime":205830.0,"EndTime":206068.0,"Column":2}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":205831.0,"Objects":[{"StartTime":205831.0,"EndTime":205831.0,"Column":0}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":206068.0,"Objects":[{"StartTime":206068.0,"EndTime":206068.0,"Column":0}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":206068.0,"Objects":[{"StartTime":206068.0,"EndTime":206306.0,"Column":3}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":206069.0,"Objects":[{"StartTime":206069.0,"EndTime":206069.0,"Column":1}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":206307.0,"Objects":[{"StartTime":206307.0,"EndTime":206545.0,"Column":1}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":206307.0,"Objects":[{"StartTime":206307.0,"EndTime":206545.0,"Column":2}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":206544.0,"Objects":[{"StartTime":206544.0,"EndTime":206544.0,"Column":0}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":206544.0,"Objects":[{"StartTime":206544.0,"EndTime":206782.0,"Column":3}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":206782.0,"Objects":[{"StartTime":206782.0,"EndTime":207020.0,"Column":1}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":206783.0,"Objects":[{"StartTime":206783.0,"EndTime":207021.0,"Column":2}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":207021.0,"Objects":[{"StartTime":207021.0,"EndTime":207259.0,"Column":3}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":207021.0,"Objects":[{"StartTime":207021.0,"EndTime":207021.0,"Column":0}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":207259.0,"Objects":[{"StartTime":207259.0,"EndTime":207497.0,"Column":2}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":207259.0,"Objects":[{"StartTime":207259.0,"EndTime":207497.0,"Column":1}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":207497.0,"Objects":[{"StartTime":207497.0,"EndTime":207735.0,"Column":3}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":207497.0,"Objects":[{"StartTime":207497.0,"EndTime":207497.0,"Column":0}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":207735.0,"Objects":[{"StartTime":207735.0,"EndTime":208449.0,"Column":1}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":207973.0,"Objects":[{"StartTime":207973.0,"EndTime":208211.0,"Column":3}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":207973.0,"Objects":[{"StartTime":207973.0,"EndTime":207973.0,"Column":0}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":208211.0,"Objects":[{"StartTime":208211.0,"EndTime":208449.0,"Column":2}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":208449.0,"Objects":[{"StartTime":208449.0,"EndTime":208687.0,"Column":3}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":208449.0,"Objects":[{"StartTime":208449.0,"EndTime":208449.0,"Column":0}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":208687.0,"Objects":[{"StartTime":208687.0,"EndTime":208925.0,"Column":1}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":208925.0,"Objects":[{"StartTime":208925.0,"EndTime":209163.0,"Column":3}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":208925.0,"Objects":[{"StartTime":208925.0,"EndTime":208925.0,"Column":0}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":209163.0,"Objects":[{"StartTime":209163.0,"EndTime":209401.0,"Column":2}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":209164.0,"Objects":[{"StartTime":209164.0,"EndTime":209640.0,"Column":1}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":209401.0,"Objects":[{"StartTime":209401.0,"EndTime":209639.0,"Column":3}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":209401.0,"Objects":[{"StartTime":209401.0,"EndTime":209401.0,"Column":0}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":209639.0,"Objects":[{"StartTime":209639.0,"EndTime":209877.0,"Column":2}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":209878.0,"Objects":[{"StartTime":209878.0,"EndTime":209878.0,"Column":3}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":209878.0,"Objects":[{"StartTime":209878.0,"EndTime":209878.0,"Column":0}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":210116.0,"Objects":[{"StartTime":210116.0,"EndTime":210116.0,"Column":3}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":210116.0,"Objects":[{"StartTime":210116.0,"EndTime":210354.0,"Column":1}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":210116.0,"Objects":[{"StartTime":210116.0,"EndTime":210354.0,"Column":2}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":210354.0,"Objects":[{"StartTime":210354.0,"EndTime":210354.0,"Column":3}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":210354.0,"Objects":[{"StartTime":210354.0,"EndTime":210354.0,"Column":0}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":210592.0,"Objects":[{"StartTime":210592.0,"EndTime":210592.0,"Column":3}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":210592.0,"Objects":[{"StartTime":210592.0,"EndTime":210830.0,"Column":1}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":210593.0,"Objects":[{"StartTime":210593.0,"EndTime":210831.0,"Column":2}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":210830.0,"Objects":[{"StartTime":210830.0,"EndTime":211068.0,"Column":3}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":210830.0,"Objects":[{"StartTime":210830.0,"EndTime":210830.0,"Column":0}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":211068.0,"Objects":[{"StartTime":211068.0,"EndTime":211306.0,"Column":2}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":211069.0,"Objects":[{"StartTime":211069.0,"EndTime":211307.0,"Column":1}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":211306.0,"Objects":[{"StartTime":211306.0,"EndTime":211306.0,"Column":0}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":211306.0,"Objects":[{"StartTime":211306.0,"EndTime":211544.0,"Column":3}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":211544.0,"Objects":[{"StartTime":211544.0,"EndTime":212258.0,"Column":2}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":211545.0,"Objects":[{"StartTime":211545.0,"EndTime":211783.0,"Column":1}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":211782.0,"Objects":[{"StartTime":211782.0,"EndTime":212020.0,"Column":3}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":211782.0,"Objects":[{"StartTime":211782.0,"EndTime":211782.0,"Column":0}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":212021.0,"Objects":[{"StartTime":212021.0,"EndTime":212259.0,"Column":1}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":212258.0,"Objects":[{"StartTime":212258.0,"EndTime":212496.0,"Column":3}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":212258.0,"Objects":[{"StartTime":212258.0,"EndTime":212258.0,"Column":0}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":212497.0,"Objects":[{"StartTime":212497.0,"EndTime":212735.0,"Column":2}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":212497.0,"Objects":[{"StartTime":212497.0,"EndTime":212735.0,"Column":1}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":212735.0,"Objects":[{"StartTime":212735.0,"EndTime":212735.0,"Column":0}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":212735.0,"Objects":[{"StartTime":212735.0,"EndTime":212973.0,"Column":3}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":212974.0,"Objects":[{"StartTime":212974.0,"EndTime":212974.0,"Column":1}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":213093.0,"Objects":[{"StartTime":213093.0,"EndTime":213093.0,"Column":0}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":213212.0,"Objects":[{"StartTime":213212.0,"EndTime":213212.0,"Column":1}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":213212.0,"Objects":[{"StartTime":213212.0,"EndTime":213450.0,"Column":3}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":213450.0,"Objects":[{"StartTime":213450.0,"EndTime":213688.0,"Column":2}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":213450.0,"Objects":[{"StartTime":213450.0,"EndTime":214402.0,"Column":1}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":213450.0,"Objects":[{"StartTime":213450.0,"EndTime":213450.0,"Column":0}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":213688.0,"Objects":[{"StartTime":213688.0,"EndTime":213688.0,"Column":0}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":213688.0,"Objects":[{"StartTime":213688.0,"EndTime":213688.0,"Column":3}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":213926.0,"Objects":[{"StartTime":213926.0,"EndTime":214164.0,"Column":2}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":213926.0,"Objects":[{"StartTime":213926.0,"EndTime":213926.0,"Column":3}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":214164.0,"Objects":[{"StartTime":214164.0,"EndTime":214164.0,"Column":3}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":214402.0,"Objects":[{"StartTime":214402.0,"EndTime":214640.0,"Column":2}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":214402.0,"Objects":[{"StartTime":214402.0,"EndTime":214402.0,"Column":0}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":214402.0,"Objects":[{"StartTime":214402.0,"EndTime":214402.0,"Column":3}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":214640.0,"Objects":[{"StartTime":214640.0,"EndTime":214640.0,"Column":0}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":214640.0,"Objects":[{"StartTime":214640.0,"EndTime":214878.0,"Column":3}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":214878.0,"Objects":[{"StartTime":214878.0,"EndTime":215116.0,"Column":1}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":215116.0,"Objects":[{"StartTime":215116.0,"EndTime":215354.0,"Column":3}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":215354.0,"Objects":[{"StartTime":215354.0,"EndTime":215592.0,"Column":2}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":215354.0,"Objects":[{"StartTime":215354.0,"EndTime":215354.0,"Column":0}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":215593.0,"Objects":[{"StartTime":215593.0,"EndTime":215593.0,"Column":0}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":215593.0,"Objects":[{"StartTime":215593.0,"EndTime":215831.0,"Column":3}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":215831.0,"Objects":[{"StartTime":215831.0,"EndTime":216307.0,"Column":1}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":216069.0,"Objects":[{"StartTime":216069.0,"EndTime":216307.0,"Column":3}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":216307.0,"Objects":[{"StartTime":216307.0,"EndTime":216545.0,"Column":2}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":216307.0,"Objects":[{"StartTime":216307.0,"EndTime":216307.0,"Column":0}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":216545.0,"Objects":[{"StartTime":216545.0,"EndTime":216545.0,"Column":0}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":216545.0,"Objects":[{"StartTime":216545.0,"EndTime":216783.0,"Column":3}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":216783.0,"Objects":[{"StartTime":216783.0,"EndTime":216783.0,"Column":1}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":217021.0,"Objects":[{"StartTime":217021.0,"EndTime":217259.0,"Column":3}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":217259.0,"Objects":[{"StartTime":217259.0,"EndTime":217497.0,"Column":2}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":217259.0,"Objects":[{"StartTime":217259.0,"EndTime":217497.0,"Column":1}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":217497.0,"Objects":[{"StartTime":217497.0,"EndTime":217497.0,"Column":0}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":217497.0,"Objects":[{"StartTime":217497.0,"EndTime":217497.0,"Column":3}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":217735.0,"Objects":[{"StartTime":217735.0,"EndTime":217973.0,"Column":1}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":217735.0,"Objects":[{"StartTime":217735.0,"EndTime":217735.0,"Column":3}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":217974.0,"Objects":[{"StartTime":217974.0,"EndTime":217974.0,"Column":3}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":218212.0,"Objects":[{"StartTime":218212.0,"EndTime":218450.0,"Column":2}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":218212.0,"Objects":[{"StartTime":218212.0,"EndTime":218212.0,"Column":3}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":218450.0,"Objects":[{"StartTime":218450.0,"EndTime":218450.0,"Column":0}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":218450.0,"Objects":[{"StartTime":218450.0,"EndTime":218688.0,"Column":3}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":218688.0,"Objects":[{"StartTime":218688.0,"EndTime":218926.0,"Column":1}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":218926.0,"Objects":[{"StartTime":218926.0,"EndTime":219164.0,"Column":3}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":219164.0,"Objects":[{"StartTime":219164.0,"EndTime":219402.0,"Column":2}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":219164.0,"Objects":[{"StartTime":219164.0,"EndTime":219164.0,"Column":0}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":219283.0,"Objects":[{"StartTime":219283.0,"EndTime":219283.0,"Column":1}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":219402.0,"Objects":[{"StartTime":219402.0,"EndTime":219402.0,"Column":0}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":219402.0,"Objects":[{"StartTime":219402.0,"EndTime":219640.0,"Column":3}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":219640.0,"Objects":[{"StartTime":219640.0,"EndTime":219878.0,"Column":1}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":219878.0,"Objects":[{"StartTime":219878.0,"EndTime":220116.0,"Column":3}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":220116.0,"Objects":[{"StartTime":220116.0,"EndTime":220354.0,"Column":2}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":220116.0,"Objects":[{"StartTime":220116.0,"EndTime":220116.0,"Column":0}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":220354.0,"Objects":[{"StartTime":220354.0,"EndTime":220592.0,"Column":3}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":220593.0,"Objects":[{"StartTime":220593.0,"EndTime":220831.0,"Column":1}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":220593.0,"Objects":[{"StartTime":220593.0,"EndTime":220593.0,"Column":0}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":220831.0,"Objects":[{"StartTime":220831.0,"EndTime":220831.0,"Column":0}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":220831.0,"Objects":[{"StartTime":220831.0,"EndTime":221069.0,"Column":3}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":221069.0,"Objects":[{"StartTime":221069.0,"EndTime":221545.0,"Column":2}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":221069.0,"Objects":[{"StartTime":221069.0,"EndTime":221307.0,"Column":0}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":221307.0,"Objects":[{"StartTime":221307.0,"EndTime":221307.0,"Column":3}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":221545.0,"Objects":[{"StartTime":221545.0,"EndTime":221783.0,"Column":1}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":221545.0,"Objects":[{"StartTime":221545.0,"EndTime":221545.0,"Column":3}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":221783.0,"Objects":[{"StartTime":221783.0,"EndTime":221783.0,"Column":3}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":222021.0,"Objects":[{"StartTime":222021.0,"EndTime":222259.0,"Column":2}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":222021.0,"Objects":[{"StartTime":222021.0,"EndTime":222021.0,"Column":0}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":222021.0,"Objects":[{"StartTime":222021.0,"EndTime":222021.0,"Column":3}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":222259.0,"Objects":[{"StartTime":222259.0,"EndTime":222497.0,"Column":3}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":222497.0,"Objects":[{"StartTime":222497.0,"EndTime":222735.0,"Column":1}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":222735.0,"Objects":[{"StartTime":222735.0,"EndTime":222973.0,"Column":3}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":222974.0,"Objects":[{"StartTime":222974.0,"EndTime":223212.0,"Column":2}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":222974.0,"Objects":[{"StartTime":222974.0,"EndTime":222974.0,"Column":0}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":223212.0,"Objects":[{"StartTime":223212.0,"EndTime":223212.0,"Column":0}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":223212.0,"Objects":[{"StartTime":223212.0,"EndTime":223450.0,"Column":3}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":223450.0,"Objects":[{"StartTime":223450.0,"EndTime":223688.0,"Column":1}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":223688.0,"Objects":[{"StartTime":223688.0,"EndTime":223688.0,"Column":2}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":223688.0,"Objects":[{"StartTime":223688.0,"EndTime":223926.0,"Column":3}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":223926.0,"Objects":[{"StartTime":223926.0,"EndTime":224164.0,"Column":1}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":223926.0,"Objects":[{"StartTime":223926.0,"EndTime":224164.0,"Column":2}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":223926.0,"Objects":[{"StartTime":223926.0,"EndTime":223926.0,"Column":0}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":224164.0,"Objects":[{"StartTime":224164.0,"EndTime":224402.0,"Column":3}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":224402.0,"Objects":[{"StartTime":224402.0,"EndTime":224640.0,"Column":1}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":224640.0,"Objects":[{"StartTime":224640.0,"EndTime":224878.0,"Column":3}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":224878.0,"Objects":[{"StartTime":224878.0,"EndTime":226306.0,"Column":2}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":225116.0,"Objects":[{"StartTime":225116.0,"EndTime":225116.0,"Column":0}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":225116.0,"Objects":[{"StartTime":225116.0,"EndTime":225116.0,"Column":3}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":225354.0,"Objects":[{"StartTime":225354.0,"EndTime":225592.0,"Column":1}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":225354.0,"Objects":[{"StartTime":225354.0,"EndTime":225354.0,"Column":3}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":225593.0,"Objects":[{"StartTime":225593.0,"EndTime":225593.0,"Column":3}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":225831.0,"Objects":[{"StartTime":225831.0,"EndTime":226069.0,"Column":1}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":225831.0,"Objects":[{"StartTime":225831.0,"EndTime":225831.0,"Column":3}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":226069.0,"Objects":[{"StartTime":226069.0,"EndTime":226069.0,"Column":0}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":226069.0,"Objects":[{"StartTime":226069.0,"EndTime":226307.0,"Column":3}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":226307.0,"Objects":[{"StartTime":226307.0,"EndTime":226545.0,"Column":1}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":226545.0,"Objects":[{"StartTime":226545.0,"EndTime":226783.0,"Column":3}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":226783.0,"Objects":[{"StartTime":226783.0,"EndTime":227021.0,"Column":2}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":226783.0,"Objects":[{"StartTime":226783.0,"EndTime":226783.0,"Column":0}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":226902.0,"Objects":[{"StartTime":226902.0,"EndTime":226902.0,"Column":1}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":227021.0,"Objects":[{"StartTime":227021.0,"EndTime":227021.0,"Column":0}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":227021.0,"Objects":[{"StartTime":227021.0,"EndTime":227259.0,"Column":3}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":227259.0,"Objects":[{"StartTime":227259.0,"EndTime":227497.0,"Column":1}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":227497.0,"Objects":[{"StartTime":227497.0,"EndTime":227735.0,"Column":3}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":227735.0,"Objects":[{"StartTime":227735.0,"EndTime":227973.0,"Column":2}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":227735.0,"Objects":[{"StartTime":227735.0,"EndTime":227735.0,"Column":0}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":227974.0,"Objects":[{"StartTime":227974.0,"EndTime":228212.0,"Column":3}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":228212.0,"Objects":[{"StartTime":228212.0,"EndTime":228450.0,"Column":1}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":228450.0,"Objects":[{"StartTime":228450.0,"EndTime":228688.0,"Column":3}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":228688.0,"Objects":[{"StartTime":228688.0,"EndTime":229878.0,"Column":2}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":228688.0,"Objects":[{"StartTime":228688.0,"EndTime":229402.0,"Column":0}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":230116.0,"Objects":[{"StartTime":230116.0,"EndTime":230116.0,"Column":0}]},{"RandomW":3083635271,"RandomX":273326509,"RandomY":273071671,"RandomZ":2659271247,"StartTime":230593.0,"Objects":[{"StartTime":230593.0,"EndTime":231307.0,"Column":3}]},{"RandomW":4073591514,"RandomX":273071671,"RandomY":2659271247,"RandomZ":3083635271,"StartTime":231545.0,"Objects":[{"StartTime":231545.0,"EndTime":232974.0,"Column":3}]}]} \ No newline at end of file diff --git a/osu.Game.Rulesets.Mania.Tests/Resources/Testing/Beatmaps/4869637.osu b/osu.Game.Rulesets.Mania.Tests/Resources/Testing/Beatmaps/4869637.osu new file mode 100644 index 0000000000..5c08994072 --- /dev/null +++ b/osu.Game.Rulesets.Mania.Tests/Resources/Testing/Beatmaps/4869637.osu @@ -0,0 +1,1442 @@ +osu file format v10 + +[General] +Mode: 3 + +[Difficulty] +HPDrainRate:5 +CircleSize:4 +OverallDifficulty:5 +ApproachRate:0 +SliderMultiplier:2.6 +SliderTickRate:1 + +[TimingPoints] +355,476.190476190476,4,2,1,60,1,0 +60652,-100,4,2,1,60,0,1 +92735,-100,4,2,1,60,0,0 +121485,-100,4,2,1,60,0,1 +153688,-100,4,2,1,60,0,0 +182497,-100,4,2,1,60,0,1 +213688,-100,4,2,1,60,0,0 + +[HitObjects] +192,120,355,1,0,0:0 +192,300,712,1,0,0:0 +320,288,1307,1,0,0:0 +320,164,1664,1,0,0:0 +448,208,2259,1,0,0:0 +320,208,2616,1,0,0:0 +320,344,3212,1,0,0:0 +448,344,3569,1,0,0:0 +192,120,4164,1,0,0:0 +320,120,4521,1,0,0:0 +320,288,5117,1,0,0:0 +192,288,5474,1,0,0:0 +448,208,6069,1,0,0:0 +320,208,6426,1,0,0:0 +320,296,7022,1,0,0:0 +448,296,7378,1,0,0:0 +192,120,7974,1,0,0:0 +64,128,7974,1,0,0:0 +64,232,8450,1,0,0:0 +320,120,8450,1,0,0:0 +64,232,8926,1,0,0:0 +320,288,8927,1,0,0:0 +192,288,9402,1,0,0:0 +64,232,9402,1,0,0:0 +64,232,9878,1,0,0:0 +448,208,9879,1,0,0:0 +320,208,10354,1,0,0:0 +64,232,10354,1,0,0:0 +64,232,10831,1,0,0:0 +320,296,10832,1,0,0:0 +448,296,11307,1,0,0:0 +64,232,11307,1,0,0:0 +256,192,11783,12,0,15116,0:0 +448,228,11783,1,0,0:0 +448,228,12259,1,0,0:0 +448,228,12735,1,0,0:0 +448,228,13212,1,0,0:0 +448,228,13688,1,0,0:0 +448,228,14164,1,0,0:0 +448,228,14640,1,0,0:0 +192,252,15116,6,0,B|192:200,2,32.5 +64,216,15593,1,0,0:0 +64,304,15831,1,0,0:0 +192,324,16069,1,0,0:0 +64,112,16307,1,0,0:0 +320,68,16545,2,0,B|320:220,1,130 +448,160,17021,2,0,B|448:292,1,130 +192,232,17259,1,0,0:0 +320,272,17497,2,0,B|320:112,1,130 +448,76,17974,2,0,B|448:248,1,130 +192,176,18212,1,0,0:0 +320,104,18450,2,0,B|320:244,1,130 +448,144,18926,2,0,B|448:280,1,130 +64,336,19402,1,0,0:0 +192,176,19640,1,0,0:0 +192,244,19878,1,0,0:0 +64,200,20116,1,0,0:0 +320,260,20354,2,0,B|320:128,1,130 +448,152,20831,2,0,B|448:292,1,130 +192,176,21069,1,0,0:0 +320,156,21307,2,0,B|320:292,1,130 +448,176,21783,2,0,B|448:312,1,130 +192,176,22021,1,0,0:0 +320,232,22259,2,0,C|320:328|320:328|320:288,1,130 +448,312,22735,2,0,B|448:176,1,130 +64,156,23212,1,0,0:0 +64,264,23450,1,0,0:0 +192,176,23688,1,0,0:0 +192,228,23926,1,0,0:0 +320,260,24164,2,0,B|320:128,1,130 +448,152,24641,2,0,B|448:292,1,130 +192,176,24878,1,0,0:0 +320,156,25117,2,0,B|320:292,1,130 +448,176,25593,2,0,B|448:312,1,130 +192,136,25831,1,0,0:0 +320,260,26069,2,0,B|320:128,1,130 +448,176,26545,2,0,B|448:312,1,130 +192,136,27021,1,0,0:0 +192,244,27259,1,0,0:0 +64,156,27497,1,0,0:0 +64,208,27735,1,0,0:0 +320,180,27974,2,0,B|320:316,1,130 +448,264,28450,2,0,B|448:132,1,130 +192,168,28688,1,0,0:0 +320,188,28926,2,0,B|320:324,1,130 +448,272,29402,2,0,B|448:140,1,130 +192,168,29640,1,0,0:0 +320,188,29878,2,0,B|320:324,1,130 +448,272,30354,2,0,B|448:140,1,130 +64,200,30831,1,0,0:0 +320,260,30831,1,0,0:0 +192,168,31069,1,0,0:0 +192,264,31307,1,0,0:0 +64,200,31307,1,0,0:0 +320,320,31545,1,0,0:0 +64,200,31783,1,0,0:0 +448,264,31783,2,0,B|448:132,1,130 +192,168,32021,1,0,0:0 +320,188,32259,2,0,B|320:324,1,130 +64,200,32259,1,0,0:0 +64,200,32735,1,0,0:0 +448,28,32735,2,0,B|448:164,1,130 +192,168,32974,1,0,0:0 +320,172,33212,2,0,B|320:308,1,130 +64,200,33212,1,0,0:0 +64,200,33688,1,0,0:0 +448,208,33688,2,0,B|448:344,1,130 +320,188,34164,2,0,B|320:324,1,130 +64,200,34164,1,0,0:0 +64,200,34640,1,0,0:0 +320,260,34640,1,0,0:0 +192,168,34878,1,0,0:0 +192,264,35116,1,0,0:0 +64,300,35116,1,0,0:0 +320,320,35354,1,0,0:0 +64,200,35592,1,0,0:0 +448,264,35593,2,0,B|448:132,1,130 +192,168,35831,1,0,0:0 +64,200,36068,1,0,0:0 +320,224,36068,2,0,B|320:360,1,130 +64,200,36544,1,0,0:0 +448,208,36545,2,0,B|448:344,1,130 +192,168,36783,1,0,0:0 +320,172,37021,2,0,B|320:308,1,130 +64,200,37021,1,0,0:0 +64,200,37497,1,0,0:0 +448,208,37497,2,0,B|448:344,1,130 +64,120,37854,1,0,0:0 +320,188,37973,2,0,B|320:324,1,130 +64,200,38212,1,0,0:0 +320,260,38450,1,0,0:0 +64,120,38450,1,0,0:0 +192,168,38688,1,0,0:0 +64,200,38926,1,0,0:0 +192,264,38926,1,0,0:0 +320,320,39164,1,0,0:0 +64,200,39402,1,0,0:0 +320,192,39402,2,0,B|320:328,1,130 +64,200,39878,1,0,0:0 +448,264,39879,2,0,B|448:132,1,130 +192,168,40116,1,0,0:0 +320,228,40354,2,0,B|320:364,1,130 +64,200,40354,1,0,0:0 +448,208,40831,2,0,B|448:344,1,130 +64,200,40831,1,0,0:0 +192,164,41069,1,0,0:0 +320,172,41307,2,0,B|320:308,1,130 +64,200,41307,1,0,0:0 +448,208,41783,2,0,B|448:344,1,130 +64,200,41783,1,0,0:0 +64,200,42259,1,0,0:0 +192,204,42259,1,0,0:0 +192,204,42497,1,0,0:0 +64,200,42735,1,0,0:0 +320,244,42735,1,0,0:0 +320,244,42974,1,0,0:0 +320,256,43212,2,0,B|320:124,1,130 +64,200,43212,1,0,0:0 +448,180,43687,2,0,B|448:316,1,130 +64,200,43688,1,0,0:0 +192,128,43926,1,0,0:0 +320,344,44164,2,0,B|320:212,1,130 +64,200,44164,1,0,0:0 +448,180,44639,2,0,B|448:316,1,130 +64,200,44640,1,0,0:0 +192,128,44878,1,0,0:0 +64,112,45116,1,0,0:0 +320,228,45116,2,0,B|320:380,1,130 +64,200,45593,1,0,0:0 +448,36,45593,2,0,B|448:180,1,130 +64,348,45831,2,0,L|64:340|64:340|64:156|64:380|64:-128|64:-128,1,910 +192,260,45831,1,0,0:0 +448,192,46069,1,0,0:0 +192,324,46069,1,0,0:0 +448,192,46307,1,0,0:0 +192,324,46545,1,0,0:0 +320,208,46545,1,0,0:0 +320,208,46783,1,0,0:0 +320,344,47021,2,0,B|320:204,1,130 +192,324,47021,1,0,0:0 +192,216,47497,1,0,0:0 +448,40,47497,2,0,B|448:184,1,130 +320,208,47735,1,0,0:0 +64,239,47795,2,0,B|64:471|64:31,1,357.5 +320,112,47974,2,0,B|320:252,1,130 +192,216,47974,1,0,0:0 +448,304,48450,2,0,B|448:160,1,130 +192,163,48450,1,0,0:0 +64,332,48688,1,0,0:0 +320,208,48688,1,0,0:0 +448,48,48926,2,0,B|448:188,1,130 +192,320,48926,1,0,0:0 +64,74,49164,2,0,B|64:226,1,130 +192,320,49402,1,0,0:0 +320,133,49402,2,0,B|320:268,1,130 +64,356,49640,2,0,B|294:236|120:80|120:80|64:312|64:312|64:-4,1,910 +192,320,49878,1,0,0:0 +320,331,49878,1,0,0:0 +320,331,50116,1,0,0:0 +192,320,50354,1,0,0:0 +448,140,50354,1,0,0:0 +448,140,50593,1,0,0:0 +192,320,50831,1,0,0:0 +320,119,50831,2,0,B|320:264,1,130 +192,320,51307,1,0,0:0 +448,304,51307,2,0,B|448:170,1,130 +64,121,51545,2,0,B|64:293|64:293|64:57,1,390 +320,188,51545,1,0,0:0 +192,320,51783,1,0,0:0 +320,295,51783,2,0,B|320:161,1,130 +448,248,52259,2,0,B|448:118,1,130 +192,172,52259,1,0,0:0 +320,188,52497,1,0,0:0 +320,246,52735,2,0,B|320:113,1,130 +192,172,52735,1,0,0:0 +64,300,52974,2,0,B|64:143,1,130 +448,327,53212,2,0,B|448:198,1,130 +320,188,53212,1,0,0:0 +192,304,53450,1,0,0:0 +64,313,53450,2,0,B|64:29|64:358|64:358|64:268,1,390 +192,172,53688,1,0,0:0 +320,122,53926,1,0,0:0 +192,172,54164,1,0,0:0 +320,122,54164,1,0,0:0 +64,20,54402,2,0,B|64:299|64:299|64:98|64:98|64:274,1,650 +448,153,54402,1,0,0:0 +192,172,54640,1,0,0:0 +448,93,54640,2,0,B|448:245,1,130 +192,172,55116,1,0,0:0 +448,321,55116,2,0,B|448:174,1,130 +320,276,55354,1,0,0:0 +192,288,55593,1,0,0:0 +448,44,55593,2,0,B|448:187,1,130 +320,164,56069,1,0,0:0 +448,344,56069,2,0,B|448:199,1,130 +192,172,56307,1,0,0:0 +320,28,56545,1,0,0:0 +448,45,56545,2,0,B|448:183,1,130 +192,96,56783,1,0,0:0 +64,341,56783,2,0,B|64:192,1,130 +448,321,57021,2,0,B|448:172,1,130 +320,66,57021,1,0,0:0 +64,26,57259,2,0,B|64:162|64:162|64:296|64:296|64:164,1,390 +192,239,57497,1,0,0:0 +320,332,57497,1,0,0:0 +320,248,57735,1,0,0:0 +192,239,57974,1,0,0:0 +448,265,57974,1,0,0:0 +64,352,58212,2,0,B|64:-37|64:-37|425:177|425:177|64:144,1,1170 +448,265,58212,1,0,0:0 +192,239,58450,1,0,0:0 +320,327,58450,2,0,B|320:170,1,130 +192,239,58926,5,0,0:0 +448,66,58926,2,0,B|448:204,1,130 +320,197,59164,1,0,0:0 +192,239,59402,1,0,0:0 +320,327,59402,2,0,B|320:192,1,130 +192,239,59878,1,0,0:0 +448,330,59878,2,0,B|448:189,1,130 +320,197,60116,1,0,0:0 +192,239,60354,1,0,0:0 +320,328,60354,2,0,B|320:193,1,130 +448,28,60354,2,0,B|448:183,1,130 +192,158,60593,1,0,0:0 +320,133,60831,1,0,0:0 +448,282,60831,2,0,B|448:134,1,130 +64,233,60831,1,0,0:0 +320,272,61069,2,0,B|320:128,1,130 +64,233,61307,1,0,0:0 +448,94,61307,1,0,0:0 +192,319,61545,2,0,B|192:184,1,130 +448,94,61545,1,0,0:0 +64,233,61783,1,0,0:0 +448,94,61783,1,0,0:0 +320,334,62021,2,0,B|320:195,1,130 +448,173,62021,1,0,0:0 +64,233,62259,1,0,0:0 +448,345,62259,2,0,B|448:208,1,130 +192,93,62497,2,0,B|192:224,1,130 +64,233,62735,1,0,0:0 +448,345,62735,2,0,B|448:190,1,130 +320,265,62974,2,0,B|320:119,1,130 +64,233,63212,1,0,0:0 +448,345,63212,2,0,B|448:189,1,130 +192,334,63450,2,0,B|192:66,1,260 +64,233,63688,1,0,0:0 +448,345,63688,2,0,B|448:191,1,130 +320,239,63926,2,0,B|320:85,1,130 +64,233,64164,1,0,0:0 +448,345,64164,2,0,B|448:192,1,130 +320,263,64402,1,0,0:0 +192,264,64640,1,0,0:0 +64,233,64640,1,0,0:0 +448,345,64640,2,0,B|448:192,1,130 +192,185,64878,2,0,B|192:34,1,130 +64,233,65116,1,0,0:0 +448,62,65116,1,0,0:0 +320,296,65354,2,0,B|320:154,1,130 +448,62,65354,1,0,0:0 +64,233,65593,1,0,0:0 +448,62,65593,1,0,0:0 +192,338,65831,2,0,B|192:201,1,130 +448,62,65831,1,0,0:0 +64,233,66069,1,0,0:0 +448,341,66069,2,0,B|448:194,1,130 +192,33,66307,2,0,B|192:186,1,130 +64,233,66545,1,0,0:0 +448,341,66545,2,0,B|448:200,1,130 +320,292,66783,2,0,B|320:140,1,130 +64,233,67021,1,0,0:0 +448,341,67021,2,0,B|448:195,1,130 +192,53,67259,2,0,B|192:195,1,130 +64,233,67497,1,0,0:0 +448,341,67497,2,0,B|448:206,1,130 +320,354,67735,2,0,B|320:203,1,130 +64,233,67974,1,0,0:0 +448,341,67974,2,0,B|448:204,1,130 +192,344,68212,2,0,B|192:203,1,130 +64,152,68331,1,0,0:0 +448,341,68450,2,0,B|448:191,1,130 +320,232,68688,2,0,B|320:-36,1,260 +64,152,68688,1,0,0:0 +64,233,68926,1,0,0:0 +448,76,68926,1,0,0:0 +192,280,69164,2,0,B|192:144,1,130 +448,76,69164,1,0,0:0 +64,233,69402,1,0,0:0 +448,76,69402,1,0,0:0 +192,14,69640,2,0,B|192:154,1,130 +448,76,69640,1,0,0:0 +64,233,69878,1,0,0:0 +448,340,69878,2,0,B|448:206,1,130 +320,319,70116,2,0,B|320:164,1,130 +64,233,70354,1,0,0:0 +448,340,70354,2,0,B|448:192,1,130 +192,204,70593,2,0,B|192:45,1,130 +64,233,70831,1,0,0:0 +448,340,70831,2,0,B|448:186,1,130 +192,346,71069,2,0,B|192:205,1,130 +320,296,71069,2,0,B|320:152,1,130 +64,233,71307,1,0,0:0 +448,340,71307,2,0,B|448:184,1,130 +320,36,71545,2,0,B|320:170,1,130 +64,233,71783,1,0,0:0 +448,340,71783,2,0,B|448:194,1,130 +320,327,72021,2,0,B|320:172,1,130 +64,233,72259,1,0,0:0 +448,340,72259,2,0,B|448:181,1,130 +192,312,72497,2,0,B|192:26,1,260 +64,233,72735,1,0,0:0 +448,83,72735,1,0,0:0 +320,277,72974,2,0,B|320:144,1,130 +448,83,72974,1,0,0:0 +64,233,73212,1,0,0:0 +448,83,73212,1,0,0:0 +320,22,73450,2,0,B|320:176,1,130 +448,83,73450,1,0,0:0 +64,233,73688,1,0,0:0 +448,338,73688,2,0,B|448:196,1,130 +192,36,73926,2,0,B|192:179,1,130 +64,233,74164,1,0,0:0 +448,338,74164,2,0,B|448:193,1,130 +320,333,74402,2,0,B|320:100|320:100|320:280,1,390 +192,247,74402,1,0,0:0 +64,233,74640,1,0,0:0 +448,338,74640,2,0,B|448:193,1,130 +64,233,75116,1,0,0:0 +448,338,75116,2,0,B|448:208,1,130 +192,330,75354,2,0,B|192:46,1,260 +64,233,75593,1,0,0:0 +448,338,75593,2,0,B|448:203,1,130 +320,130,75831,1,0,0:0 +64,156,75950,1,0,0:0 +448,338,76069,2,0,B|448:184,1,130 +320,210,76069,1,0,0:0 +192,207,76307,2,0,B|192:63,1,130 +64,156,76307,1,0,0:0 +64,233,76545,1,0,0:0 +448,338,76545,2,0,B|448:200,1,130 +320,320,76783,2,0,B|320:168,1,130 +64,233,77021,1,0,0:0 +448,338,77021,2,0,B|448:188,1,130 +192,328,77259,2,0,B|192:168,1,130 +448,338,77497,2,0,B|448:184,1,130 +64,233,77498,1,0,0:0 +320,272,77735,2,0,B|320:8,1,260 +64,233,77974,1,0,0:0 +448,338,77974,2,0,B|448:200,1,130 +192,312,78212,2,0,B|192:168,1,130 +448,76,78450,1,0,0:0 +64,233,78450,1,0,0:0 +448,76,78688,1,0,0:0 +320,276,78688,2,0,B|320:140,1,130 +448,76,78926,1,0,0:0 +64,233,78926,1,0,0:0 +448,76,79164,1,0,0:0 +192,14,79164,2,0,B|192:154,1,130 +448,340,79402,2,0,B|448:206,1,130 +64,233,79402,1,0,0:0 +320,296,79640,1,0,0:0 +64,233,79878,1,0,0:0 +448,340,79878,2,0,B|448:192,1,130 +320,296,79878,1,0,0:0 +192,204,80117,2,0,B|192:45,1,130 +448,340,80355,2,0,B|448:186,1,130 +64,233,80355,1,0,0:0 +192,346,80593,2,0,B|192:205,1,130 +448,340,80831,2,0,B|448:184,1,130 +64,233,80831,1,0,0:0 +320,36,81069,2,0,B|320:170,1,130 +448,340,81307,2,0,B|448:194,1,130 +64,233,81307,1,0,0:0 +320,327,81545,2,0,B|320:172,1,130 +448,340,81783,2,0,B|448:181,1,130 +64,233,81783,1,0,0:0 +192,312,82021,2,0,B|192:26,1,260 +64,233,82259,1,0,0:0 +448,83,82259,1,0,0:0 +320,277,82498,2,0,B|320:144,1,130 +448,83,82498,1,0,0:0 +448,83,82736,1,0,0:0 +64,233,82736,1,0,0:0 +320,22,82974,2,0,B|320:176,1,130 +448,83,82974,1,0,0:0 +448,338,83212,2,0,B|448:196,1,130 +64,233,83212,1,0,0:0 +192,36,83450,2,0,B|192:179,1,130 +64,233,83569,1,0,0:0 +448,338,83688,2,0,B|448:193,1,130 +320,384,83926,2,0,B|320:227|320:227|320:331,1,260 +64,148,83926,1,0,0:0 +448,338,84164,2,0,B|448:193,1,130 +64,233,84164,1,0,0:0 +192,207,84402,2,0,B|192:52,1,130 +448,338,84640,2,0,B|448:208,1,130 +64,233,84640,1,0,0:0 +192,330,84878,2,0,B|192:46,1,260 +64,233,85117,1,0,0:0 +448,338,85117,2,0,B|448:203,1,130 +320,124,85354,2,0,B|320:260,1,130 +448,338,85593,2,0,B|448:184,1,130 +64,246,85593,1,0,0:0 +192,208,85831,2,0,B|192:64,1,130 +64,233,86069,1,0,0:0 +448,338,86069,2,0,B|448:192,1,130 +320,344,86307,2,0,B|320:188,1,130 +192,320,86307,2,0,B|192:172,1,130 +64,233,86545,1,0,0:0 +448,338,86545,2,0,B|448:204,1,130 +192,204,86783,2,0,B|192:56,1,130 +64,233,87021,1,0,0:0 +448,338,87021,2,0,B|448:200,1,130 +320,344,87259,2,0,B|320:200,1,130 +64,233,87497,1,0,0:0 +448,338,87497,2,0,B|448:204,1,130 +320,344,87735,2,0,B|320:80,1,260 +64,233,87973,1,0,0:0 +448,68,87974,1,0,0:0 +192,204,88212,2,0,B|192:45,1,130 +448,68,88212,1,0,0:0 +64,233,88450,1,0,0:0 +448,68,88450,1,0,0:0 +192,346,88688,2,0,B|192:205,1,130 +448,68,88688,1,0,0:0 +64,233,88926,1,0,0:0 +448,340,88926,2,0,B|448:184,1,130 +320,36,89164,2,0,B|320:170,1,130 +448,340,89402,2,0,B|448:194,1,130 +64,233,89402,1,0,0:0 +192,320,89640,2,0,B|192:165,1,130 +64,233,89878,1,0,0:0 +448,340,89878,2,0,B|448:181,1,130 +320,332,89878,2,0,B|320:46,1,260 +192,104,90116,2,0,B|192:248,1,130 +64,233,90354,1,0,0:0 +448,340,90354,2,0,B|448:208,1,130 +320,277,90593,2,0,B|320:144,1,130 +64,233,90831,1,0,0:0 +448,340,90831,2,0,B|448:204,1,130 +320,22,91069,2,0,B|320:176,1,130 +448,338,91307,2,0,B|448:196,1,130 +64,233,91307,1,0,0:0 +256,192,91545,12,0,92735,0:0 +192,232,91545,1,0,0:0 +448,64,91783,1,0,0:0 +192,180,91783,1,0,0:0 +448,64,92021,1,0,0:0 +448,64,92259,1,0,0:0 +192,184,92259,1,0,0:0 +448,64,92497,1,0,0:0 +448,336,92735,2,0,B|448:176,1,130 +192,180,92735,1,0,0:0 +192,324,92974,2,0,B|192:168,1,130 +320,316,93212,2,0,B|320:160,1,130 +64,160,93212,1,0,0:0 +448,132,93450,1,0,0:0 +64,160,93688,1,0,0:0 +448,336,93688,2,0,B|448:192,1,130 +192,328,93688,2,0,B|192:64,1,260 +320,320,94164,2,0,B|320:160,1,130 +64,160,94164,1,0,0:0 +192,224,94402,1,0,0:0 +448,132,94402,1,0,0:0 +64,160,94640,1,0,0:0 +448,336,94640,2,0,B|448:184,1,130 +320,100,94640,1,0,0:0 +192,328,95116,2,0,B|192:56,1,260 +64,160,95116,1,0,0:0 +320,320,95116,2,0,B|320:164,1,130 +64,160,95593,1,0,0:0 +448,300,95593,1,0,0:0 +448,300,95831,1,0,0:0 +64,160,96069,1,0,0:0 +320,320,96069,1,0,0:0 +320,320,96307,1,0,0:0 +64,160,96545,1,0,0:0 +448,300,96545,2,0,B|448:168,1,130 +192,340,96783,2,0,B|192:56,1,260 +64,160,97021,1,0,0:0 +320,320,97021,2,0,B|320:176,1,130 +448,96,97259,1,0,0:0 +64,160,97497,1,0,0:0 +320,224,97497,1,0,0:0 +448,296,97497,2,0,B|448:136,1,130 +192,296,97735,2,0,B|192:28,1,260 +64,160,97974,1,0,0:0 +320,104,97974,2,0,B|320:256,1,130 +448,96,98212,1,0,0:0 +320,180,98450,1,0,0:0 +64,160,98450,1,0,0:0 +448,296,98450,2,0,B|448:160,1,130 +64,160,98747,1,0,0:0 +192,320,98926,2,0,B|6:242|192:188|346:133|192:24,1,390 +320,312,98926,2,0,B|320:168,1,130 +64,160,99164,1,0,0:0 +64,160,99402,1,0,0:0 +448,296,99402,1,0,0:0 +448,296,99640,1,0,0:0 +64,160,99878,1,0,0:0 +320,312,99878,1,0,0:0 +320,312,100116,1,0,0:0 +64,160,100354,1,0,0:0 +192,308,100354,2,0,B|146:207|146:207|192:140|192:140|192:56,1,260 +448,296,100354,2,0,B|448:144,1,130 +320,312,100831,2,0,B|320:176,1,130 +64,160,100831,1,0,0:0 +448,80,101069,1,0,0:0 +448,296,101307,2,0,B|448:156,1,130 +192,308,101307,2,0,B|192:44,1,260 +64,160,101307,1,0,0:0 +320,176,101783,2,0,B|320:40,1,130 +64,160,101783,1,0,0:0 +192,196,102021,1,0,0:0 +448,80,102021,1,0,0:0 +320,304,102259,2,0,B|320:168,1,130 +448,300,102259,2,0,B|448:148,1,130 +64,160,102259,1,0,0:0 +192,256,102735,2,0,B|389:228|389:228|192:144,1,390 +320,304,102735,2,0,B|320:144,1,130 +64,160,102735,1,0,0:0 +64,160,103212,1,0,0:0 +448,300,103212,1,0,0:0 +448,300,103450,1,0,0:0 +64,160,103688,1,0,0:0 +320,304,103688,1,0,0:0 +320,304,103926,1,0,0:0 +64,160,104164,1,0,0:0 +448,300,104164,2,0,B|448:164,1,130 +192,264,104164,1,0,0:0 +192,264,104402,2,0,B|192:120,1,130 +64,160,104640,1,0,0:0 +320,304,104640,2,0,B|320:164,1,130 +448,68,104878,1,0,0:0 +448,300,105116,2,0,B|448:168,1,130 +320,216,105116,1,0,0:0 +64,160,105116,1,0,0:0 +64,160,105593,1,0,0:0 +320,304,105593,2,0,B|320:164,1,130 +192,176,105593,1,0,0:0 +192,176,105831,1,0,0:0 +448,68,105831,1,0,0:0 +448,300,106069,2,0,B|448:168,1,130 +192,208,106069,1,0,0:0 +64,160,106069,1,0,0:0 +320,248,106307,1,0,0:0 +64,160,106426,1,0,0:0 +192,304,106545,2,0,B|83:196|83:196|380:273|380:273|433:170|433:170|493:76|422:20|422:20|192:252,1,1040 +320,304,106545,2,0,B|320:164,1,130 +64,160,106783,1,0,0:0 +64,160,107021,1,0,0:0 +448,300,107021,1,0,0:0 +448,300,107259,1,0,0:0 +64,160,107497,1,0,0:0 +320,304,107497,1,0,0:0 +320,304,107735,1,0,0:0 +64,160,107974,1,0,0:0 +448,300,107974,2,0,B|448:156,1,130 +64,160,108450,1,0,0:0 +320,304,108450,2,0,B|320:160,1,130 +448,68,108688,1,0,0:0 +64,160,108926,1,0,0:0 +448,300,108926,2,0,B|448:164,1,130 +192,280,109164,2,0,B|192:0,1,260 +64,160,109402,1,0,0:0 +320,280,109402,2,0,B|320:132,1,130 +448,68,109640,1,0,0:0 +64,160,109878,1,0,0:0 +448,300,109878,2,0,B|448:152,1,130 +320,280,110116,1,0,0:0 +64,160,110354,1,0,0:0 +320,280,110354,2,0,B|320:124,1,130 +192,276,110593,2,0,B|192:-158|192:234,1,390 +64,160,110831,1,0,0:0 +448,300,110831,1,0,0:0 +448,300,111069,1,0,0:0 +64,160,111307,1,0,0:0 +320,280,111307,1,0,0:0 +192,344,111545,2,0,B|192:32|192:-60|192:-60,1,390 +320,280,111545,1,0,0:0 +64,160,111783,1,0,0:0 +448,300,111783,2,0,B|448:160,1,130 +64,160,112259,1,0,0:0 +320,280,112259,2,0,B|320:136,1,130 +192,340,112497,2,0,B|344:340|354:170|354:170|277:34|277:34|192:84|192:84,1,520 +448,68,112497,1,0,0:0 +64,160,112735,1,0,0:0 +448,300,112735,2,0,B|448:160,1,130 +64,160,113212,1,0,0:0 +320,280,113212,2,0,B|320:132,1,130 +448,68,113450,1,0,0:0 +64,160,113688,1,0,0:0 +448,300,113688,2,0,B|448:164,1,130 +192,340,113926,1,0,0:0 +64,160,113985,1,0,0:0 +320,280,114164,2,0,B|320:136,1,130 +64,160,114402,1,0,0:0 +192,340,114402,2,0,B|449:220|192:288|192:36,1,390 +64,160,114640,1,0,0:0 +448,300,114640,1,0,0:0 +448,300,114878,1,0,0:0 +64,160,115116,1,0,0:0 +320,280,115116,1,0,0:0 +320,280,115354,1,0,0:0 +192,340,115354,2,0,B|446:222|446:222|192:156,1,520 +448,300,115593,2,0,B|448:160,1,130 +64,160,115593,1,0,0:0 +320,280,116069,2,0,B|320:132,1,130 +64,160,116069,1,0,0:0 +448,68,116307,1,0,0:0 +448,300,116545,2,0,B|448:144,1,130 +320,280,116545,2,0,B|320:16,1,260 +64,160,116545,1,0,0:0 +192,252,117021,1,0,0:0 +448,300,117021,2,0,B|448:160,1,130 +64,160,117021,1,0,0:0 +320,208,117259,1,0,0:0 +192,176,117259,2,0,B|192:32,1,130 +64,160,117497,1,0,0:0 +448,300,117497,2,0,B|448:156,1,130 +320,280,117735,2,0,B|320:120,1,130 +64,160,117974,1,0,0:0 +448,300,117974,2,0,B|448:152,1,130 +192,336,118212,2,0,B|462:215|192:80,1,390 +64,160,118450,1,0,0:0 +320,312,118450,1,0,0:0 +448,56,118450,1,0,0:0 +320,312,118688,1,0,0:0 +448,56,118688,1,0,0:0 +64,160,118926,1,0,0:0 +448,300,118926,1,0,0:0 +320,312,119164,2,0,L|450:178|320:-64|320:204|320:24|136:186,1,910 +448,300,119164,1,0,0:0 +64,160,119402,1,0,0:0 +448,300,119402,2,0,B|448:160,1,130 +64,160,119878,1,0,0:0 +448,300,119878,2,0,B|448:160,1,130 +192,124,120116,1,0,0:0 +64,160,120354,1,0,0:0 +448,300,120354,2,0,B|448:160,1,130 +64,160,120831,1,0,0:0 +448,300,120831,2,0,B|448:160,1,130 +192,324,121069,2,0,B|192:168,1,130 +64,160,121307,1,0,0:0 +448,300,121307,2,0,B|448:164,1,130 +320,324,121545,1,0,0:0 +64,160,121664,1,0,0:0 +448,300,121783,2,0,B|448:160,1,130 +320,324,121783,1,0,0:0 +192,319,122021,2,0,B|192:168,1,130 +64,160,122021,1,0,0:0 +448,94,122259,1,0,0:0 +64,233,122260,1,0,0:0 +320,252,122497,2,0,B|320:120,1,130 +448,94,122497,1,0,0:0 +448,94,122736,1,0,0:0 +64,233,122736,1,0,0:0 +448,173,122974,1,0,0:0 +192,336,122974,2,0,B|192:180,1,130 +448,345,123212,2,0,B|448:208,1,130 +64,233,123212,1,0,0:0 +320,334,123450,2,0,B|320:195,1,130 +64,233,123688,1,0,0:0 +448,345,123688,2,0,B|448:190,1,130 +192,93,123926,2,0,B|192:224,1,130 +64,233,124164,1,0,0:0 +448,345,124164,2,0,B|448:204,1,130 +320,265,124403,2,0,B|320:119,1,130 +448,345,124641,2,0,B|448:189,1,130 +64,233,124641,1,0,0:0 +192,334,124879,2,0,B|192:176,1,130 +448,345,125116,2,0,B|448:192,1,130 +64,233,125117,1,0,0:0 +320,124,125354,1,0,0:0 +64,233,125593,1,0,0:0 +448,345,125593,2,0,B|448:184,1,130 +320,124,125593,1,0,0:0 +320,348,125831,2,0,B|320:200,1,130 +448,80,126069,1,0,0:0 +64,233,126069,1,0,0:0 +192,185,126307,2,0,B|192:34,1,130 +448,80,126307,1,0,0:0 +64,233,126545,1,0,0:0 +448,80,126545,1,0,0:0 +320,296,126783,2,0,B|320:154,1,130 +448,80,126783,1,0,0:0 +448,341,127021,2,0,B|448:196,1,130 +64,233,127022,1,0,0:0 +192,338,127260,2,0,B|192:201,1,130 +448,341,127498,2,0,B|448:194,1,130 +64,233,127498,1,0,0:0 +192,33,127736,2,0,B|192:300,1,260 +448,341,127974,2,0,B|448:200,1,130 +64,233,127974,1,0,0:0 +320,292,128212,2,0,B|320:140,1,130 +448,341,128450,2,0,B|448:195,1,130 +64,233,128450,1,0,0:0 +192,53,128688,2,0,B|192:195,1,130 +448,341,128926,2,0,B|448:206,1,130 +64,233,128926,1,0,0:0 +320,354,129164,2,0,B|320:203,1,130 +64,233,129283,1,0,0:0 +448,341,129403,2,0,B|448:204,1,130 +320,300,129640,2,0,B|320:32,1,260 +64,220,129640,1,0,0:0 +448,148,129878,1,0,0:0 +64,233,129879,1,0,0:0 +448,148,130116,1,0,0:0 +192,308,130116,2,0,B|192:148,1,130 +64,233,130354,1,0,0:0 +448,76,130355,1,0,0:0 +320,284,130593,2,0,B|320:148,1,130 +448,76,130593,1,0,0:0 +64,233,130831,1,0,0:0 +448,340,130831,2,0,B|448:196,1,130 +192,14,131069,2,0,B|192:154,1,130 +448,340,131307,2,0,B|448:206,1,130 +64,233,131307,1,0,0:0 +320,319,131545,2,0,B|320:164,1,130 +448,340,131783,2,0,B|448:192,1,130 +64,233,131783,1,0,0:0 +320,264,132021,2,0,B|320:120,1,130 +192,204,132022,2,0,B|192:45,1,130 +448,340,132260,2,0,B|448:186,1,130 +64,233,132260,1,0,0:0 +320,264,132497,2,0,B|320:124,1,130 +192,346,132498,2,0,B|192:205,1,130 +448,340,132736,2,0,B|448:184,1,130 +64,233,132736,1,0,0:0 +320,36,132974,2,0,B|320:170,1,130 +448,340,133212,2,0,B|448:194,1,130 +64,233,133212,1,0,0:0 +192,312,133450,2,0,B|192:26,1,260 +64,233,133688,1,0,0:0 +448,83,133688,1,0,0:0 +448,83,133926,1,0,0:0 +320,327,133926,2,0,B|320:172,1,130 +448,83,134164,1,0,0:0 +64,233,134164,1,0,0:0 +448,83,134403,1,0,0:0 +192,276,134403,2,0,B|192:143,1,130 +448,338,134640,2,0,B|448:196,1,130 +64,233,134641,1,0,0:0 +320,22,134878,2,0,B|320:176,1,130 +448,338,135117,2,0,B|448:196,1,130 +64,233,135117,1,0,0:0 +192,328,135354,2,0,B|192:95|192:95|192:275,1,390 +320,152,135354,1,0,0:0 +64,233,135593,1,0,0:0 +448,338,135593,2,0,B|448:193,1,130 +448,338,136069,2,0,B|448:193,1,130 +64,233,136069,1,0,0:0 +320,320,136307,2,0,B|320:48,1,260 +448,338,136545,2,0,B|448:208,1,130 +64,233,136545,1,0,0:0 +192,296,136783,1,0,0:0 +64,233,136902,1,0,0:0 +192,296,137021,1,0,0:0 +448,338,137022,2,0,B|448:203,1,130 +320,248,137259,2,0,B|320:112,1,130 +64,176,137259,1,0,0:0 +448,96,137497,1,0,0:0 +64,176,137497,1,0,0:0 +448,96,137735,1,0,0:0 +192,207,137736,2,0,B|192:63,1,130 +64,233,137974,1,0,0:0 +448,96,137974,1,0,0:0 +320,320,138212,2,0,B|320:168,1,130 +448,96,138212,1,0,0:0 +448,338,138450,2,0,B|448:188,1,130 +64,233,138450,1,0,0:0 +192,328,138688,2,0,B|192:168,1,130 +448,338,138926,2,0,B|448:184,1,130 +64,233,138927,1,0,0:0 +320,316,139164,2,0,B|320:176,1,130 +448,338,139403,2,0,B|448:200,1,130 +64,233,139403,1,0,0:0 +192,328,139640,2,0,B|192:172,1,130 +448,338,139878,2,0,B|448:184,1,130 +64,233,139879,1,0,0:0 +192,296,140116,2,0,B|192:36,1,260 +448,338,140354,2,0,B|448:200,1,130 +64,233,140355,1,0,0:0 +320,144,140593,1,0,0:0 +64,233,140831,1,0,0:0 +448,340,140831,2,0,B|448:206,1,130 +320,144,140831,1,0,0:0 +320,319,141069,2,0,B|320:164,1,130 +448,340,141307,2,0,B|448:192,1,130 +64,233,141307,1,0,0:0 +192,204,141546,2,0,B|192:45,1,130 +448,104,141783,1,0,0:0 +64,233,141784,1,0,0:0 +448,104,142021,1,0,0:0 +192,346,142022,2,0,B|192:205,1,130 +448,104,142259,1,0,0:0 +64,233,142260,1,0,0:0 +448,104,142497,1,0,0:0 +320,36,142498,2,0,B|320:170,1,130 +64,233,142736,1,0,0:0 +448,340,142736,2,0,B|448:194,1,130 +192,312,142974,2,0,B|192:26,1,260 +64,233,143212,1,0,0:0 +448,340,143212,2,0,B|448:181,1,130 +320,336,143450,2,0,B|320:200,1,130 +64,233,143688,1,0,0:0 +448,340,143688,2,0,B|448:204,1,130 +192,284,143927,2,0,B|192:151,1,130 +448,340,144164,2,0,B|448:204,1,130 +64,233,144165,1,0,0:0 +320,22,144403,2,0,B|320:176,1,130 +64,233,144521,1,0,0:0 +448,338,144641,2,0,B|448:196,1,130 +192,328,144878,2,0,B|192:171|192:171|192:275,1,260 +64,160,144878,1,0,0:0 +448,88,145116,1,0,0:0 +64,233,145116,1,0,0:0 +448,88,145354,1,0,0:0 +320,316,145354,2,0,B|320:168,1,130 +64,233,145593,1,0,0:0 +448,88,145593,1,0,0:0 +448,88,145831,1,0,0:0 +192,288,145831,2,0,B|192:152,1,130 +64,233,146069,1,0,0:0 +448,338,146069,2,0,B|448:208,1,130 +320,328,146307,2,0,B|320:174,1,130 +448,338,146546,2,0,B|448:203,1,130 +64,233,146546,1,0,0:0 +192,300,146783,2,0,B|192:140,1,130 +448,338,147022,2,0,B|448:184,1,130 +64,246,147022,1,0,0:0 +192,100,147259,2,0,B|192:240,1,130 +320,236,147260,2,0,B|320:92,1,130 +448,338,147498,2,0,B|448:192,1,130 +64,233,147498,1,0,0:0 +192,336,147736,2,0,B|192:180,1,130 +448,338,147974,2,0,B|448:204,1,130 +64,233,147974,1,0,0:0 +320,280,148212,2,0,B|320:132,1,130 +448,338,148450,2,0,B|448:200,1,130 +64,233,148450,1,0,0:0 +320,344,148688,2,0,B|320:80,1,260 +192,148,148688,1,0,0:0 +64,233,148926,1,0,0:0 +448,68,148926,1,0,0:0 +192,204,149164,2,0,B|192:45,1,130 +448,68,149164,1,0,0:0 +64,233,149402,1,0,0:0 +448,68,149403,1,0,0:0 +320,280,149640,2,0,B|320:148,1,130 +448,68,149641,1,0,0:0 +448,340,149878,2,0,B|448:196,1,130 +64,233,149879,1,0,0:0 +192,346,150117,2,0,B|192:205,1,130 +448,340,150355,2,0,B|448:184,1,130 +64,233,150355,1,0,0:0 +320,36,150593,2,0,B|320:170,1,130 +64,233,150831,1,0,0:0 +448,340,150831,2,0,B|448:194,1,130 +192,232,151069,2,0,B|192:77,1,130 +448,340,151307,2,0,B|448:181,1,130 +64,233,151307,1,0,0:0 +320,320,151545,2,0,B|320:160,1,130 +448,340,151783,2,0,B|448:208,1,130 +64,233,151783,1,0,0:0 +192,280,152022,2,0,B|192:147,1,130 +64,233,152140,1,0,0:0 +448,340,152260,2,0,B|448:204,1,130 +256,192,152497,12,0,153687,0:0 +64,176,152497,1,0,0:0 +64,260,152735,1,0,0:0 +64,304,153093,1,0,0:0 +64,264,153688,1,0,0:0 +192,232,153926,1,0,0:0 +64,288,154045,1,0,0:0 +320,320,154402,2,0,B|320:120|320:120|320:324,1,390 +64,264,154640,1,0,0:0 +64,264,154997,1,0,0:0 +192,324,155354,2,0,B|192:88|192:88|192:256,1,390 +64,288,155593,1,0,0:0 +320,240,155831,1,0,0:0 +64,264,155950,1,0,0:0 +448,240,156069,1,0,0:0 +192,324,156307,2,0,C|192:88|192:88|192:256,1,390 +320,240,156307,1,0,0:0 +64,144,156545,1,0,0:0 +64,144,156902,1,0,0:0 +320,316,157259,2,0,C|320:80|320:80|320:248,1,390 +64,144,157497,1,0,0:0 +192,168,157735,1,0,0:0 +64,144,157854,1,0,0:0 +192,324,158212,2,0,L|192:88|192:88|192:256,1,390 +64,144,158450,1,0,0:0 +64,144,158807,1,0,0:0 +320,384,159164,2,0,L|320:148|320:148|320:316,1,390 +64,144,159402,1,0,0:0 +448,152,159640,1,0,0:0 +64,144,159759,1,0,0:0 +448,108,159878,1,0,0:0 +192,344,160116,2,0,L|192:108|192:108|192:276,1,390 +320,168,160116,1,0,0:0 +64,144,160354,1,0,0:0 +64,144,160712,1,0,0:0 +320,336,161069,2,0,B|320:100|320:100|320:268,1,390 +64,144,161307,1,0,0:0 +192,180,161545,1,0,0:0 +64,144,161664,1,0,0:0 +192,324,162021,2,0,B|192:88|192:88|192:256,1,390 +64,144,162259,1,0,0:0 +64,144,162616,1,0,0:0 +320,324,162974,2,0,B|320:88|320:88|320:256,1,390 +64,144,163212,1,0,0:0 +192,184,163450,1,0,0:0 +64,144,163569,1,0,0:0 +448,260,163688,1,0,0:0 +320,200,163926,1,0,0:0 +192,324,163926,2,0,B|192:88|192:88|192:256,1,390 +64,144,164164,1,0,0:0 +64,144,164521,1,0,0:0 +320,324,164878,2,0,B|320:88|320:88|320:256,1,390 +64,144,165116,1,0,0:0 +192,172,165354,1,0,0:0 +64,144,165474,1,0,0:0 +192,324,165831,2,0,B|192:88|192:88|192:256,1,390 +64,144,166069,1,0,0:0 +64,144,166426,1,0,0:0 +320,324,166783,2,0,B|320:224|242:196|242:196|320:88|320:88|420:209|420:209|320:380,1,650 +64,144,167021,1,0,0:0 +192,176,167259,1,0,0:0 +64,144,167378,1,0,0:0 +192,176,167497,1,0,0:0 +192,320,167735,2,0,B|192:168,1,130 +448,288,167974,1,0,0:0 +448,288,168212,1,0,0:0 +64,144,168450,1,0,0:0 +192,176,168450,1,0,0:0 +448,288,168450,1,0,0:0 +448,288,168688,1,0,0:0 +192,176,168926,1,0,0:0 +448,72,168926,2,0,B|448:224,1,130 +64,144,169402,1,0,0:0 +320,228,169402,1,0,0:0 +448,72,169402,2,0,B|448:216,1,130 +192,128,169640,1,0,0:0 +320,336,169878,2,0,B|320:60,1,260 +448,72,169878,2,0,B|448:216,1,130 +64,144,170354,1,0,0:0 +448,72,170354,2,0,B|448:220,1,130 +192,304,170593,2,0,B|192:44,1,260 +320,152,170593,1,0,0:0 +448,72,170831,2,0,B|448:220,1,130 +64,144,171307,1,0,0:0 +320,328,171307,2,0,B|320:64,1,260 +448,72,171307,2,0,B|448:216,1,130 +448,308,171783,1,0,0:0 +448,308,172021,1,0,0:0 +64,144,172259,1,0,0:0 +192,188,172259,1,0,0:0 +448,308,172259,1,0,0:0 +448,308,172497,1,0,0:0 +192,188,172735,1,0,0:0 +448,72,172735,2,0,B|448:136,4,32.5 +64,144,173212,1,0,0:0 +320,240,173212,1,0,0:0 +448,72,173212,2,0,B|448:216,1,130 +320,136,173450,1,0,0:0 +320,240,173688,2,0,B|320:-28,1,260 +192,188,173688,1,0,0:0 +448,72,173688,2,0,B|448:208,1,130 +192,148,173926,1,0,0:0 +64,144,174164,1,0,0:0 +192,188,174164,1,0,0:0 +448,72,174164,2,0,B|448:208,1,130 +320,320,174402,2,0,B|320:48,1,260 +192,188,174402,1,0,0:0 +192,188,174640,1,0,0:0 +448,72,174640,2,0,B|448:212,1,130 +192,188,174878,1,0,0:0 +64,144,175116,1,0,0:0 +320,40,175116,2,0,B|320:312,1,260 +192,148,175116,1,0,0:0 +448,72,175116,2,0,B|448:208,1,130 +192,264,175354,2,0,B|192:120,1,130 +448,304,175593,1,0,0:0 +192,320,175831,2,0,B|192:60,1,260 +448,304,175831,1,0,0:0 +64,144,176069,1,0,0:0 +320,220,176069,1,0,0:0 +448,304,176069,1,0,0:0 +448,304,176307,1,0,0:0 +320,184,176545,1,0,0:0 +448,72,176545,2,0,B|448:208,1,130 +64,144,177021,1,0,0:0 +320,184,177021,1,0,0:0 +448,72,177021,2,0,B|448:216,1,130 +192,272,177259,1,0,0:0 +320,328,177497,2,0,B|320:60,1,260 +192,204,177497,1,0,0:0 +448,72,177497,2,0,B|448:208,1,130 +64,144,177974,1,0,0:0 +192,184,177974,1,0,0:0 +448,72,177974,2,0,B|448:212,1,130 +192,232,178212,1,0,0:0 +192,184,178450,1,0,0:0 +320,300,178450,2,0,B|320:144,1,130 +448,72,178450,2,0,B|448:208,1,130 +64,144,178926,1,0,0:0 +320,56,178926,2,0,B|320:328,1,260 +192,120,178926,1,0,0:0 +448,72,178926,2,0,B|448:208,1,130 +192,336,179164,2,0,B|192:184,1,130 +448,304,179402,1,0,0:0 +448,304,179640,1,0,0:0 +192,332,179878,2,0,B|192:56,1,260 +320,176,179878,1,0,0:0 +448,304,179878,1,0,0:0 +448,304,180116,1,0,0:0 +320,176,180354,1,0,0:0 +448,76,180354,2,0,B|448:212,1,130 +192,72,180593,2,0,B|192:344,1,260 +320,176,180831,1,0,0:0 +448,76,180831,2,0,B|448:220,1,130 +320,192,181069,1,0,0:0 +320,332,181306,2,0,B|320:72,1,260 +192,344,181307,2,0,B|192:76,1,260 +448,76,181307,2,0,B|448:212,1,130 +448,76,181783,2,0,B|448:216,1,130 +320,356,182021,2,0,B|320:80,1,260 +192,72,182021,2,0,B|192:340,1,260 +448,76,182259,2,0,B|448:220,1,130 +64,136,182497,5,0,0:0 +64,328,182735,5,0,0:0 +320,192,182735,1,0,0:0 +320,272,182974,2,0,B|320:120,1,130 +448,94,183211,1,0,0:0 +64,272,183211,1,0,0:0 +192,319,183449,2,0,B|192:184,1,130 +448,94,183449,1,0,0:0 +64,233,183687,1,0,0:0 +448,94,183687,1,0,0:0 +448,173,183925,1,0,0:0 +320,334,183925,2,0,B|320:195,1,130 +448,345,184163,2,0,B|448:208,1,130 +64,233,184163,1,0,0:0 +192,93,184401,2,0,B|192:224,1,130 +64,233,184639,1,0,0:0 +448,345,184639,2,0,B|448:190,1,130 +320,265,184878,2,0,B|320:119,1,130 +448,345,185116,2,0,B|448:189,1,130 +64,233,185116,1,0,0:0 +192,334,185354,2,0,B|192:66,1,260 +64,233,185592,1,0,0:0 +448,345,185592,2,0,B|448:191,1,130 +320,239,185830,2,0,B|320:85,1,130 +448,345,186068,2,0,B|448:192,1,130 +64,233,186068,1,0,0:0 +320,263,186306,1,0,0:0 +448,345,186544,2,0,B|448:192,1,130 +64,233,186544,1,0,0:0 +192,264,186544,1,0,0:0 +192,185,186782,2,0,B|192:34,1,130 +448,62,187020,1,0,0:0 +64,233,187020,1,0,0:0 +448,62,187258,1,0,0:0 +320,296,187258,2,0,B|320:154,1,130 +448,62,187497,1,0,0:0 +64,233,187497,1,0,0:0 +448,62,187735,1,0,0:0 +192,338,187735,2,0,B|192:201,1,130 +448,341,187973,2,0,B|448:194,1,130 +64,233,187973,1,0,0:0 +320,100,188211,2,0,B|320:253,1,130 +448,341,188449,2,0,B|448:200,1,130 +64,233,188449,1,0,0:0 +320,292,188688,2,0,B|320:140,1,130 +448,341,188925,2,0,B|448:195,1,130 +64,233,188925,1,0,0:0 +192,60,188926,2,0,B|192:216,1,130 +320,92,189163,2,0,B|320:234,1,130 +448,341,189401,2,0,B|448:206,1,130 +64,233,189401,1,0,0:0 +192,88,189402,2,0,B|192:360,1,260 +320,354,189639,2,0,B|320:203,1,130 +448,341,189878,2,0,B|448:204,1,130 +64,233,189878,1,0,0:0 +192,344,190116,2,0,B|192:203,1,130 +64,233,190235,1,0,0:0 +448,341,190354,2,0,B|448:191,1,130 +320,232,190592,2,0,B|320:22,1,195 +64,233,190593,1,0,0:0 +448,76,190830,1,0,0:0 +64,233,190830,1,0,0:0 +448,76,191068,1,0,0:0 +192,280,191068,2,0,B|192:144,1,130 +320,144,191069,1,0,0:0 +448,76,191306,1,0,0:0 +64,233,191306,1,0,0:0 +320,144,191307,1,0,0:0 +448,76,191544,1,0,0:0 +192,14,191544,2,0,B|192:154,1,130 +320,92,191545,2,0,B|320:244,1,130 +448,340,191782,2,0,B|448:206,1,130 +64,233,191782,1,0,0:0 +320,319,192020,2,0,B|320:164,1,130 +192,104,192021,2,0,B|192:256,1,130 +448,340,192258,2,0,B|448:192,1,130 +64,233,192258,1,0,0:0 +192,204,192497,2,0,B|192:45,1,130 +320,376,192497,2,0,B|320:72|320:72|320:111|320:111|320:292|320:292,1,520 +448,340,192735,2,0,B|448:186,1,130 +64,233,192735,1,0,0:0 +192,346,192973,2,0,B|192:205,1,130 +448,340,193211,2,0,B|448:184,1,130 +64,233,193211,1,0,0:0 +192,276,193450,2,0,B|192:124,1,130 +448,340,193687,2,0,B|448:194,1,130 +64,233,193688,1,0,0:0 +320,327,193925,2,0,B|320:172,1,130 +448,340,194163,2,0,B|448:181,1,130 +64,233,194163,1,0,0:0 +448,83,194639,1,0,0:0 +192,312,194640,2,0,B|192:160,1,130 +64,233,194640,1,0,0:0 +320,208,194640,1,0,0:0 +448,83,194878,1,0,0:0 +320,277,194878,2,0,B|320:144,1,130 +448,83,195116,1,0,0:0 +64,233,195116,1,0,0:0 +192,160,195116,1,0,0:0 +448,83,195354,1,0,0:0 +320,22,195354,2,0,B|320:176,1,130 +192,316,195354,2,0,B|192:168,1,130 +448,338,195592,2,0,B|448:196,1,130 +64,233,195592,1,0,0:0 +192,36,195830,2,0,B|192:179,1,130 +320,104,195831,2,0,B|320:260,1,130 +448,338,196068,2,0,B|448:193,1,130 +64,233,196068,1,0,0:0 +320,333,196306,2,0,B|320:-16|320:-16|320:284|320:284|320:280,1,650 +192,272,196307,2,0,B|192:128,1,130 +448,338,196544,2,0,B|448:193,1,130 +64,233,196544,1,0,0:0 +448,338,197020,2,0,B|448:208,1,130 +64,233,197020,1,0,0:0 +192,330,197258,2,0,B|192:46,1,260 +448,338,197497,2,0,B|448:203,1,130 +64,233,197497,1,0,0:0 +320,130,197735,1,0,0:0 +64,246,197854,1,0,0:0 +192,204,197973,1,0,0:0 +448,338,197973,2,0,B|448:184,1,130 +192,207,198211,2,0,B|192:63,1,130 +64,233,198212,1,0,0:0 +448,338,198449,2,0,B|448:200,1,130 +64,233,198449,1,0,0:0 +320,320,198687,2,0,B|320:168,1,130 +448,338,198925,2,0,B|448:188,1,130 +64,233,198925,1,0,0:0 +192,352,199163,2,0,B|192:192,1,130 +448,338,199401,2,0,B|448:184,1,130 +64,233,199402,1,0,0:0 +320,312,199639,2,0,B|320:48,1,260 +192,224,199640,2,0,B|192:368,1,130 +448,338,199878,2,0,B|448:200,1,130 +64,233,199878,1,0,0:0 +192,92,200116,2,0,B|192:232,1,130 +64,233,200354,1,0,0:0 +448,76,200354,1,0,0:0 +320,352,200592,2,0,B|320:216,1,130 +448,76,200592,1,0,0:0 +64,233,200830,1,0,0:0 +448,76,200830,1,0,0:0 +192,14,201068,2,0,B|192:154,1,130 +448,76,201068,1,0,0:0 +64,233,201306,1,0,0:0 +448,340,201306,2,0,B|448:206,1,130 +320,288,201307,2,0,B|320:128,1,130 +192,144,201545,1,0,0:0 +448,340,201782,2,0,B|448:192,1,130 +64,233,201782,1,0,0:0 +192,144,201783,1,0,0:0 +192,204,202021,2,0,B|192:45,1,130 +320,356,202021,2,0,B|320:192|320:192|320:-64,1,390 +64,233,202259,1,0,0:0 +448,340,202259,2,0,B|448:186,1,130 +192,346,202497,2,0,B|192:205,1,130 +64,233,202735,1,0,0:0 +448,340,202735,2,0,B|448:184,1,130 +320,36,202973,2,0,B|320:170,1,130 +64,233,203211,1,0,0:0 +448,340,203211,2,0,B|448:194,1,130 +192,304,203212,2,0,B|192:156,1,130 +320,327,203449,2,0,B|320:172,1,130 +64,233,203687,1,0,0:0 +448,340,203687,2,0,B|448:181,1,130 +320,384,203925,2,0,B|320:98,1,260 +192,356,203926,2,0,B|265:192|265:192|192:-12,1,390 +448,83,204163,1,0,0:0 +64,233,204163,1,0,0:0 +448,83,204402,1,0,0:0 +64,233,204640,1,0,0:0 +448,83,204640,1,0,0:0 +320,72,204640,2,0,B|320:336,1,260 +448,83,204878,1,0,0:0 +192,44,204878,2,0,B|192:198,1,130 +64,233,205116,1,0,0:0 +448,338,205116,2,0,B|448:196,1,130 +192,36,205354,2,0,B|192:179,1,130 +64,233,205474,1,0,0:0 +448,338,205592,2,0,B|448:193,1,130 +320,333,205830,2,0,B|320:228|320:228|320:280,1,130 +64,233,205831,1,0,0:0 +64,233,206068,1,0,0:0 +448,338,206068,2,0,B|448:193,1,130 +192,136,206069,1,0,0:0 +192,276,206307,2,0,B|192:128,1,130 +320,128,206307,2,0,B|320:284,1,130 +64,233,206544,1,0,0:0 +448,338,206544,2,0,B|448:208,1,130 +192,196,206782,2,0,B|192:46,1,130 +320,88,206783,2,0,B|320:240,1,130 +448,338,207021,2,0,B|448:203,1,130 +64,233,207021,1,0,0:0 +320,128,207259,2,0,B|320:272,1,130 +192,328,207259,2,0,B|192:188,1,130 +448,338,207497,2,0,B|448:184,1,130 +64,246,207497,1,0,0:0 +192,336,207735,2,0,B|251:210|251:210|192:-36,1,390 +448,338,207973,2,0,B|448:192,1,130 +64,233,207973,1,0,0:0 +320,344,208211,2,0,B|320:188,1,130 +448,338,208449,2,0,B|448:204,1,130 +64,233,208449,1,0,0:0 +192,204,208687,2,0,B|192:56,1,130 +448,338,208925,2,0,B|448:200,1,130 +64,233,208925,1,0,0:0 +320,344,209163,2,0,B|320:200,1,130 +192,336,209164,2,0,B|192:56,1,260 +448,338,209401,2,0,B|448:204,1,130 +64,233,209401,1,0,0:0 +320,344,209639,2,0,B|320:184,1,130 +448,68,209878,1,0,0:0 +64,233,209878,1,0,0:0 +448,68,210116,1,0,0:0 +192,204,210116,2,0,B|192:45,1,130 +320,214,210116,2,0,B|320:76,1,130 +448,68,210354,1,0,0:0 +64,233,210354,1,0,0:0 +448,68,210592,1,0,0:0 +192,346,210592,2,0,B|192:205,1,130 +320,264,210593,2,0,B|320:120,1,130 +448,340,210830,2,0,B|448:184,1,130 +64,233,210830,1,0,0:0 +320,36,211068,2,0,B|320:170,1,130 +192,264,211069,2,0,B|192:112,1,130 +64,233,211306,1,0,0:0 +448,340,211306,2,0,B|448:194,1,130 +320,327,211544,2,0,B|403:173|403:173|320:-40,1,390 +192,200,211545,2,0,B|192:56,1,130 +448,340,211782,2,0,B|448:181,1,130 +64,233,211782,1,0,0:0 +192,328,212021,2,0,B|192:168,1,130 +448,340,212258,2,0,B|448:208,1,130 +64,233,212258,1,0,0:0 +320,277,212497,2,0,C|320:144,1,130 +192,252,212497,2,0,B|192:112,1,130 +64,233,212735,1,0,0:0 +448,340,212735,2,0,B|448:200,1,130 +192,300,212974,1,0,0:0 +64,160,213093,1,0,0:0 +192,204,213212,1,0,0:0 +448,340,213212,2,0,B|448:192,1,130 +320,324,213450,2,0,B|320:172,1,130 +192,360,213450,2,0,B|280:158|280:158|192:-128,1,520 +64,160,213450,1,0,0:0 +64,160,213688,1,0,0:0 +448,120,213688,1,0,0:0 +320,128,213926,2,0,B|320:276,1,130 +448,120,213926,1,0,0:0 +448,120,214164,1,0,0:0 +320,348,214402,2,0,B|320:192,1,130 +64,204,214402,1,0,0:0 +448,120,214402,1,0,0:0 +64,204,214640,1,0,0:0 +448,340,214640,2,0,B|448:184,1,130 +192,328,214878,2,0,B|192:176,1,130 +448,340,215116,2,0,B|448:192,1,130 +320,280,215354,2,0,B|320:144,1,130 +64,184,215354,1,0,0:0 +64,184,215593,1,0,0:0 +448,340,215593,2,0,B|448:192,1,130 +192,304,215831,2,0,B|192:40,1,260 +448,340,216069,2,0,B|448:204,1,130 +320,340,216307,2,0,B|320:180,1,130 +64,184,216307,1,0,0:0 +64,184,216545,1,0,0:0 +448,340,216545,2,0,B|448:196,1,130 +192,184,216783,1,0,0:0 +448,340,217021,2,0,B|448:200,1,130 +320,276,217259,2,0,B|320:128,1,130 +192,296,217259,2,0,B|192:148,1,130 +64,184,217497,1,0,0:0 +448,92,217497,1,0,0:0 +192,88,217735,2,0,B|192:228,1,130 +448,92,217735,1,0,0:0 +448,92,217974,1,0,0:0 +320,336,218212,2,0,B|320:184,1,130 +448,92,218212,1,0,0:0 +64,184,218450,1,0,0:0 +448,340,218450,2,0,B|448:184,1,130 +192,296,218688,2,0,B|192:136,1,130 +448,340,218926,2,0,B|448:192,1,130 +320,328,219164,2,0,B|320:176,1,130 +64,144,219164,1,0,0:0 +192,128,219283,1,0,0:0 +64,184,219402,1,0,0:0 +448,340,219402,2,0,B|448:200,1,130 +192,344,219640,2,0,B|192:204,1,130 +448,340,219878,2,0,B|448:192,1,130 +320,328,220116,2,0,B|320:192,1,130 +64,184,220116,1,0,0:0 +448,340,220354,2,0,B|448:200,1,130 +192,288,220593,2,0,B|192:132,1,130 +64,232,220593,1,0,0:0 +64,240,220831,1,0,0:0 +448,340,220831,2,0,B|448:200,1,130 +320,352,221069,2,0,B|320:88,1,260 +64,304,221069,2,0,B|64:152,1,130 +448,96,221307,1,0,0:0 +192,336,221545,2,0,B|192:176,1,130 +448,96,221545,1,0,0:0 +448,96,221783,1,0,0:0 +320,320,222021,2,0,B|320:184,1,130 +64,156,222021,1,0,0:0 +448,96,222021,1,0,0:0 +448,344,222259,2,0,B|448:200,1,130 +192,300,222497,2,0,B|192:152,1,130 +448,344,222735,2,0,B|448:204,1,130 +320,328,222974,2,0,B|320:188,1,130 +64,156,222974,1,0,0:0 +64,156,223212,1,0,0:0 +448,344,223212,2,0,B|448:192,1,130 +192,336,223450,2,0,B|192:180,1,130 +320,168,223688,1,0,0:0 +448,344,223688,2,0,B|448:200,1,130 +192,112,223926,2,0,B|192:252,1,130 +320,100,223926,2,0,B|320:232,1,130 +64,156,223926,1,0,0:0 +448,344,224164,2,0,B|448:204,1,130 +192,308,224402,2,0,B|192:156,1,130 +448,344,224640,2,0,B|448:200,1,130 +320,296,224878,2,0,B|320:104|320:104|320:340|320:340|320:-12,1,780 +64,176,225116,1,0,0:0 +448,96,225116,1,0,0:0 +192,312,225354,2,0,B|192:160,1,130 +448,96,225354,1,0,0:0 +448,96,225593,1,0,0:0 +192,252,225831,2,0,B|192:116,1,130 +448,96,225831,1,0,0:0 +64,176,226069,1,0,0:0 +448,344,226069,2,0,B|448:188,1,130 +192,328,226307,2,0,B|192:176,1,130 +448,344,226545,2,0,B|448:200,1,130 +320,300,226783,2,0,B|320:148,1,130 +64,176,226783,1,0,0:0 +192,168,226902,1,0,0:0 +64,136,227021,1,0,0:0 +448,344,227021,2,0,B|448:184,1,130 +192,288,227259,2,0,B|192:152,1,130 +448,344,227497,2,0,B|448:192,1,130 +320,312,227735,2,0,B|320:176,1,130 +64,176,227735,1,0,0:0 +448,344,227974,2,0,B|448:204,1,130 +192,264,228212,2,0,B|192:116,1,130 +448,344,228450,2,0,B|448:196,1,130 +320,328,228688,2,0,B|372:262|372:262|233:179|233:179|320:136|320:136|438:177|438:177|320:32,1,650 +64,336,228688,2,0,B|64:-56,1,390 +64,240,230116,1,0,0:0 +448,320,230593,2,0,B|299:178|299:178|448:48,1,390 +256,192,231545,12,0,232974,0:0 \ No newline at end of file From d597232c2a06d3338adbce224b57fd883af73d4e Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Fri, 13 Dec 2024 16:08:12 +0900 Subject: [PATCH 026/290] Fix incorrect `lastPattern` value In particular, mania-specific beatmaps that normally go via the "passthrough" generator should not adjust the stored pattern value. The "spinner" generator, which was previously intended to be used for non-mania-specific beatmaps, is now valid even for mania-specific beatmaps, and uses this value. In other words, another way of writing this would be: ```csharp if (conversion is SpinnerPatternGenerator || conversion is PassThroughPatternGenerator) ? lastPattern : newPattern; ``` --- osu.Game.Rulesets.Mania/Beatmaps/ManiaBeatmapConverter.cs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Mania/Beatmaps/ManiaBeatmapConverter.cs b/osu.Game.Rulesets.Mania/Beatmaps/ManiaBeatmapConverter.cs index 79234a3ba2..96550618c0 100644 --- a/osu.Game.Rulesets.Mania/Beatmaps/ManiaBeatmapConverter.cs +++ b/osu.Game.Rulesets.Mania/Beatmaps/ManiaBeatmapConverter.cs @@ -220,8 +220,11 @@ namespace osu.Game.Rulesets.Mania.Beatmaps foreach (var newPattern in conversion.Generate()) { - lastPattern = conversion is SpinnerPatternGenerator ? lastPattern : newPattern; - lastStair = (conversion as HitCirclePatternGenerator)?.StairType ?? lastStair; + if (conversion is HitCirclePatternGenerator circleGenerator) + lastStair = circleGenerator.StairType; + + if (conversion is HitCirclePatternGenerator || conversion is SliderPatternGenerator) + lastPattern = newPattern; foreach (var obj in newPattern.HitObjects) yield return obj; From a6e00d6eac9ee5e14436aec06f456cb61c7753ab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 16 Dec 2024 10:49:19 +0900 Subject: [PATCH 027/290] Implement ability to mark beatmap as played Reported at https://osu.ppy.sh/community/forums/topics/2015478?n=1. Would you believe it that this button that has been there for literal years never did anything? Implemented at a per-beatmap level. Also additionally added to context menu (at @peppy's suggestion), and also copy reworded from "Delete from unplayed" to "Mark as played" because double negation hurt my tiny brain. --- osu.Game/Beatmaps/BeatmapManager.cs | 10 ++++++++++ .../Screens/Select/Carousel/DrawableCarouselBeatmap.cs | 8 +++++++- osu.Game/Screens/Select/SongSelect.cs | 2 +- 3 files changed, 18 insertions(+), 2 deletions(-) diff --git a/osu.Game/Beatmaps/BeatmapManager.cs b/osu.Game/Beatmaps/BeatmapManager.cs index 148bd90f28..aa67d3c548 100644 --- a/osu.Game/Beatmaps/BeatmapManager.cs +++ b/osu.Game/Beatmaps/BeatmapManager.cs @@ -533,6 +533,16 @@ namespace osu.Game.Beatmaps } } + public void MarkPlayed(BeatmapInfo beatmapSetInfo) => Realm.Run(r => + { + using var transaction = r.BeginWrite(); + + var beatmap = r.Find(beatmapSetInfo.ID)!; + beatmap.LastPlayed = DateTimeOffset.Now; + + transaction.Commit(); + }); + #region Implementation of ICanAcceptFiles public Task Import(params string[] paths) => beatmapImporter.Import(paths); diff --git a/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmap.cs b/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmap.cs index 75c13c1be6..4451cfcf32 100644 --- a/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmap.cs +++ b/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmap.cs @@ -88,6 +88,9 @@ namespace osu.Game.Screens.Select.Carousel [Resolved] private OsuGame? game { get; set; } + [Resolved] + private BeatmapManager? manager { get; set; } + private IBindable starDifficultyBindable = null!; private CancellationTokenSource? starDifficultyCancellationSource; @@ -98,7 +101,7 @@ namespace osu.Game.Screens.Select.Carousel } [BackgroundDependencyLoader] - private void load(BeatmapManager? manager, SongSelect? songSelect) + private void load(SongSelect? songSelect) { Header.Height = height; @@ -300,6 +303,9 @@ namespace osu.Game.Screens.Select.Carousel if (beatmapInfo.GetOnlineURL(api, ruleset.Value) is string url) items.Add(new OsuMenuItem(CommonStrings.CopyLink, MenuItemType.Standard, () => game?.CopyUrlToClipboard(url))); + if (manager != null) + items.Add(new OsuMenuItem("Mark as played", MenuItemType.Standard, () => manager.MarkPlayed(beatmapInfo))); + if (hideRequested != null) items.Add(new OsuMenuItem(WebCommonStrings.ButtonsHide.ToSentence(), MenuItemType.Destructive, () => hideRequested(beatmapInfo))); diff --git a/osu.Game/Screens/Select/SongSelect.cs b/osu.Game/Screens/Select/SongSelect.cs index 9f7a2c02ff..651a7fe4a1 100644 --- a/osu.Game/Screens/Select/SongSelect.cs +++ b/osu.Game/Screens/Select/SongSelect.cs @@ -375,7 +375,7 @@ namespace osu.Game.Screens.Select BeatmapOptions.AddButton(@"Manage", @"collections", FontAwesome.Solid.Book, colours.Green, () => manageCollectionsDialog?.Show()); BeatmapOptions.AddButton(@"Delete", @"all difficulties", FontAwesome.Solid.Trash, colours.Pink, () => DeleteBeatmap(Beatmap.Value.BeatmapSetInfo)); - BeatmapOptions.AddButton(@"Remove", @"from unplayed", FontAwesome.Regular.TimesCircle, colours.Purple, null); + BeatmapOptions.AddButton(@"Mark", @"as played", FontAwesome.Regular.TimesCircle, colours.Purple, () => beatmaps.MarkPlayed(Beatmap.Value.BeatmapInfo)); BeatmapOptions.AddButton(@"Clear", @"local scores", FontAwesome.Solid.Eraser, colours.Purple, () => ClearScores(Beatmap.Value.BeatmapInfo)); } From 85ada3275b23d49bd5dea344c07072a204cb7e07 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 16 Dec 2024 14:14:30 +0900 Subject: [PATCH 028/290] Skip the pause cooldown when in intro / break time Had a quick look at adding test coverage in `TestScenePause` but the setup to get into either of these states seems a bit annoying.. --- osu.Game/Screens/Play/Player.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs index a762d2ae82..406a59a3b6 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -1032,7 +1032,7 @@ namespace osu.Game.Screens.Play private double? lastPauseActionTime; protected bool PauseCooldownActive => - lastPauseActionTime.HasValue && GameplayClockContainer.CurrentTime < lastPauseActionTime + PauseCooldownDuration; + PlayingState.Value == LocalUserPlayingState.Playing && lastPauseActionTime.HasValue && GameplayClockContainer.CurrentTime < lastPauseActionTime + PauseCooldownDuration; /// /// A set of conditionals which defines whether the current game state and configuration allows for From bdd417c1a1cd832b0433863d3ce151af60f99093 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 16 Dec 2024 15:18:39 +0900 Subject: [PATCH 029/290] Move "global" scroll-adjusts-volume to a per-screen component-based implementation --- .../TestSceneOverlayContainer.cs | 19 +++++-- .../UserInterface/TestSceneVolumeOverlay.cs | 18 +++--- osu.Game/OsuGame.cs | 20 ++++--- .../Volume/GlobalScrollAdjustsVolume.cs | 40 +++++++++++++ .../Overlays/Volume/VolumeControlReceptor.cs | 57 ------------------- osu.Game/Screens/Menu/MainMenu.cs | 2 + osu.Game/Screens/Play/Player.cs | 7 ++- osu.Game/Screens/Play/PlayerLoader.cs | 2 + osu.Game/Screens/Select/SongSelect.cs | 3 + 9 files changed, 92 insertions(+), 76 deletions(-) create mode 100644 osu.Game/Overlays/Volume/GlobalScrollAdjustsVolume.cs delete mode 100644 osu.Game/Overlays/Volume/VolumeControlReceptor.cs diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneOverlayContainer.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneOverlayContainer.cs index bb94912c83..e544fb127d 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneOverlayContainer.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneOverlayContainer.cs @@ -6,6 +6,7 @@ using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Colour; using osu.Framework.Graphics.Containers; +using osu.Framework.Input.Events; using osu.Game.Graphics.Containers; using osu.Game.Graphics.Sprites; using osu.Game.Overlays.Volume; @@ -59,13 +60,12 @@ namespace osu.Game.Tests.Visual.UserInterface [Test] public void TestAltScrollNotBlocked() { - bool scrollReceived = false; + TestGlobalScrollAdjustsVolume volumeAdjust = null!; - AddStep("add volume control receptor", () => Add(new VolumeControlReceptor + AddStep("add volume control receptor", () => Add(volumeAdjust = new TestGlobalScrollAdjustsVolume { RelativeSizeAxes = Axes.Both, Depth = float.MaxValue, - ScrollActionRequested = (_, _, _) => scrollReceived = true, })); AddStep("hold alt", () => InputManager.PressKey(Key.AltLeft)); @@ -75,10 +75,21 @@ namespace osu.Game.Tests.Visual.UserInterface InputManager.ScrollVerticalBy(10); }); - AddAssert("receptor received scroll input", () => scrollReceived); + AddAssert("receptor received scroll input", () => volumeAdjust.ScrollReceived); AddStep("release alt", () => InputManager.ReleaseKey(Key.AltLeft)); } + public partial class TestGlobalScrollAdjustsVolume : GlobalScrollAdjustsVolume + { + public bool ScrollReceived { get; private set; } + + protected override bool OnScroll(ScrollEvent e) + { + ScrollReceived = true; + return base.OnScroll(e); + } + } + private partial class TestOverlay : OsuFocusedOverlayContainer { [BackgroundDependencyLoader] diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneVolumeOverlay.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneVolumeOverlay.cs index 52543c68ce..c2b8ec76f4 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneVolumeOverlay.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneVolumeOverlay.cs @@ -1,8 +1,7 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - +using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Game.Overlays; using osu.Game.Overlays.Volume; @@ -11,7 +10,14 @@ namespace osu.Game.Tests.Visual.UserInterface { public partial class TestSceneVolumeOverlay : OsuTestScene { - private VolumeOverlay volume; + private VolumeOverlay volume = null!; + + protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent) + { + var dependencies = new DependencyContainer(base.CreateChildDependencies(parent)); + dependencies.CacheAs(volume = new VolumeOverlay()); + return dependencies; + } protected override void LoadComplete() { @@ -19,12 +25,10 @@ namespace osu.Game.Tests.Visual.UserInterface AddRange(new Drawable[] { - volume = new VolumeOverlay(), - new VolumeControlReceptor + volume, + new GlobalScrollAdjustsVolume { RelativeSizeAxes = Axes.Both, - ActionRequested = action => volume.Adjust(action), - ScrollActionRequested = (action, amount, isPrecise) => volume.Adjust(action, amount, isPrecise), }, }); diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs index e808e570c7..60fcd17ac6 100644 --- a/osu.Game/OsuGame.cs +++ b/osu.Game/OsuGame.cs @@ -57,7 +57,6 @@ using osu.Game.Overlays.Notifications; using osu.Game.Overlays.OSD; using osu.Game.Overlays.SkinEditor; using osu.Game.Overlays.Toolbar; -using osu.Game.Overlays.Volume; using osu.Game.Rulesets.Mods; using osu.Game.Scoring; using osu.Game.Screens; @@ -980,12 +979,6 @@ namespace osu.Game AddRange(new Drawable[] { - new VolumeControlReceptor - { - RelativeSizeAxes = Axes.Both, - ActionRequested = action => volume.Adjust(action), - ScrollActionRequested = (action, amount, isPrecise) => volume.Adjust(action, amount, isPrecise), - }, ScreenOffsetContainer = new Container { RelativeSizeAxes = Axes.Both, @@ -1432,6 +1425,19 @@ 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: + + if (!e.Repeat) + return true; + + return volume.Adjust(e.Action); + case GlobalAction.ToggleFPSDisplay: fpsCounter.ToggleVisibility(); return true; diff --git a/osu.Game/Overlays/Volume/GlobalScrollAdjustsVolume.cs b/osu.Game/Overlays/Volume/GlobalScrollAdjustsVolume.cs new file mode 100644 index 0000000000..81be084d22 --- /dev/null +++ b/osu.Game/Overlays/Volume/GlobalScrollAdjustsVolume.cs @@ -0,0 +1,40 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Allocation; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Input.Events; +using osu.Game.Input.Bindings; + +namespace osu.Game.Overlays.Volume +{ + /// + /// Add to a container or screen to make scrolling anywhere in the container cause the global game volume to be adjusted. + /// + /// + /// This is generally expected behaviour in many locations in osu!stable. + /// + public partial class GlobalScrollAdjustsVolume : Container + { + [Resolved] + private VolumeOverlay? volumeOverlay { get; set; } + + public GlobalScrollAdjustsVolume() + { + RelativeSizeAxes = Axes.Both; + } + + protected override bool OnScroll(ScrollEvent e) + { + if (e.ScrollDelta.Y == 0) + return false; + + // forward any unhandled mouse scroll events to the volume control. + return volumeOverlay?.Adjust(GlobalAction.IncreaseVolume, e.ScrollDelta.Y, e.IsPrecise) ?? false; + } + + public bool OnScroll(KeyBindingScrollEvent e) => + volumeOverlay?.Adjust(e.Action, e.ScrollAmount, e.IsPrecise) ?? false; + } +} diff --git a/osu.Game/Overlays/Volume/VolumeControlReceptor.cs b/osu.Game/Overlays/Volume/VolumeControlReceptor.cs deleted file mode 100644 index 2e8d86d4c7..0000000000 --- a/osu.Game/Overlays/Volume/VolumeControlReceptor.cs +++ /dev/null @@ -1,57 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -#nullable disable - -using System; -using osu.Framework.Graphics.Containers; -using osu.Framework.Input; -using osu.Framework.Input.Bindings; -using osu.Framework.Input.Events; -using osu.Game.Input.Bindings; - -namespace osu.Game.Overlays.Volume -{ - public partial class VolumeControlReceptor : Container, IScrollBindingHandler, IHandleGlobalKeyboardInput - { - public Func ActionRequested; - public Func ScrollActionRequested; - - public bool OnPressed(KeyBindingPressEvent e) - { - switch (e.Action) - { - case GlobalAction.DecreaseVolume: - case GlobalAction.IncreaseVolume: - return ActionRequested?.Invoke(e.Action) == true; - - case GlobalAction.ToggleMute: - case GlobalAction.NextVolumeMeter: - case GlobalAction.PreviousVolumeMeter: - if (!e.Repeat) - return ActionRequested?.Invoke(e.Action) == true; - - return false; - } - - return false; - } - - public void OnReleased(KeyBindingReleaseEvent e) - { - } - - protected override bool OnScroll(ScrollEvent e) - { - if (e.ScrollDelta.Y == 0) - return false; - - // forward any unhandled mouse scroll events to the volume control. - ScrollActionRequested?.Invoke(GlobalAction.IncreaseVolume, e.ScrollDelta.Y, e.IsPrecise); - return true; - } - - public bool OnScroll(KeyBindingScrollEvent e) => - ScrollActionRequested?.Invoke(e.Action, e.ScrollAmount, e.IsPrecise) ?? false; - } -} diff --git a/osu.Game/Screens/Menu/MainMenu.cs b/osu.Game/Screens/Menu/MainMenu.cs index 0630b9612e..ae1ad4dceb 100644 --- a/osu.Game/Screens/Menu/MainMenu.cs +++ b/osu.Game/Screens/Menu/MainMenu.cs @@ -28,6 +28,7 @@ using osu.Game.Online.API; using osu.Game.Overlays; using osu.Game.Overlays.Dialog; using osu.Game.Overlays.SkinEditor; +using osu.Game.Overlays.Volume; using osu.Game.Rulesets; using osu.Game.Screens.Backgrounds; using osu.Game.Screens.Edit; @@ -124,6 +125,7 @@ namespace osu.Game.Screens.Menu AddRangeInternal(new[] { + new GlobalScrollAdjustsVolume(), buttonsContainer = new ParallaxContainer { ParallaxAmount = 0.01f, diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs index a762d2ae82..1c186485b8 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -28,6 +28,7 @@ using osu.Game.Graphics.Containers; using osu.Game.IO.Archives; using osu.Game.Online.API; using osu.Game.Overlays; +using osu.Game.Overlays.Volume; using osu.Game.Rulesets; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Scoring; @@ -251,7 +252,11 @@ namespace osu.Game.Screens.Play dependencies.CacheAs(HealthProcessor); - InternalChild = GameplayClockContainer = CreateGameplayClockContainer(Beatmap.Value, DrawableRuleset.GameplayStartTime); + InternalChildren = new Drawable[] + { + new GlobalScrollAdjustsVolume(), + GameplayClockContainer = CreateGameplayClockContainer(Beatmap.Value, DrawableRuleset.GameplayStartTime), + }; AddInternal(screenSuspension = new ScreenSuspensionHandler(GameplayClockContainer)); diff --git a/osu.Game/Screens/Play/PlayerLoader.cs b/osu.Game/Screens/Play/PlayerLoader.cs index 20985c20e0..837974a8f2 100644 --- a/osu.Game/Screens/Play/PlayerLoader.cs +++ b/osu.Game/Screens/Play/PlayerLoader.cs @@ -27,6 +27,7 @@ using osu.Game.Input; using osu.Game.Localisation; 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; @@ -190,6 +191,7 @@ namespace osu.Game.Screens.Play InternalChildren = new Drawable[] { + new GlobalScrollAdjustsVolume(), (content = new LogoTrackingContainer { Anchor = Anchor.Centre, diff --git a/osu.Game/Screens/Select/SongSelect.cs b/osu.Game/Screens/Select/SongSelect.cs index 9f7a2c02ff..210f8203f4 100644 --- a/osu.Game/Screens/Select/SongSelect.cs +++ b/osu.Game/Screens/Select/SongSelect.cs @@ -31,6 +31,7 @@ using osu.Game.Graphics.UserInterface; using osu.Game.Input.Bindings; using osu.Game.Overlays; using osu.Game.Overlays.Mods; +using osu.Game.Overlays.Volume; using osu.Game.Rulesets; using osu.Game.Rulesets.Mods; using osu.Game.Screens.Backgrounds; @@ -169,10 +170,12 @@ namespace osu.Game.Screens.Select AddRangeInternal(new Drawable[] { + new GlobalScrollAdjustsVolume(), new VerticalMaskingContainer { Children = new Drawable[] { + new GlobalScrollAdjustsVolume(), new GridContainer // used for max width implementation { RelativeSizeAxes = Axes.Both, From d97ea781364323383fa59512e45cac494387fb4b Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 16 Dec 2024 15:22:30 +0900 Subject: [PATCH 030/290] Change beat snap divisior adjust defaults to be Ctrl+Scroll instead of Ctrl+Shift+Scroll Matches stable. - [ ] Depends on https://github.com/ppy/osu/pull/31146, else this will adjust the global volume. --- osu.Game/Input/Bindings/GlobalActionContainer.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Input/Bindings/GlobalActionContainer.cs b/osu.Game/Input/Bindings/GlobalActionContainer.cs index 170d247023..c343b4e1e6 100644 --- a/osu.Game/Input/Bindings/GlobalActionContainer.cs +++ b/osu.Game/Input/Bindings/GlobalActionContainer.cs @@ -144,8 +144,8 @@ namespace osu.Game.Input.Bindings new KeyBinding(new[] { InputKey.Control, InputKey.Alt, InputKey.MouseWheelUp }, GlobalAction.EditorIncreaseDistanceSpacing), // Framework automatically converts wheel up/down to left/right when shift is held. // See https://github.com/ppy/osu-framework/blob/master/osu.Framework/Input/StateChanges/MouseScrollRelativeInput.cs#L37-L38. - new KeyBinding(new[] { InputKey.Control, InputKey.Shift, InputKey.MouseWheelRight }, GlobalAction.EditorCyclePreviousBeatSnapDivisor), - new KeyBinding(new[] { InputKey.Control, InputKey.Shift, InputKey.MouseWheelLeft }, GlobalAction.EditorCycleNextBeatSnapDivisor), + new KeyBinding(new[] { InputKey.Control, InputKey.MouseWheelRight }, GlobalAction.EditorCyclePreviousBeatSnapDivisor), + new KeyBinding(new[] { InputKey.Control, InputKey.MouseWheelLeft }, GlobalAction.EditorCycleNextBeatSnapDivisor), new KeyBinding(new[] { InputKey.Control, InputKey.R }, GlobalAction.EditorToggleRotateControl), new KeyBinding(new[] { InputKey.Control, InputKey.E }, GlobalAction.EditorToggleScaleControl), new KeyBinding(new[] { InputKey.Control, InputKey.Left }, GlobalAction.EditorSeekToPreviousHitObject), From 68a5618e81013b40eafadd7cf4bb3b8962fc9a8f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 16 Dec 2024 16:03:26 +0900 Subject: [PATCH 031/290] Add test coverage --- .../Visual/Gameplay/TestScenePause.cs | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/osu.Game.Tests/Visual/Gameplay/TestScenePause.cs b/osu.Game.Tests/Visual/Gameplay/TestScenePause.cs index 6aa2c4e40d..7855c138ab 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestScenePause.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestScenePause.cs @@ -12,6 +12,7 @@ using osu.Framework.Graphics.Containers; using osu.Framework.Logging; using osu.Framework.Screens; using osu.Framework.Testing; +using osu.Game.Beatmaps; using osu.Game.Configuration; using osu.Game.Graphics.Containers; using osu.Game.Graphics.Cursor; @@ -19,6 +20,7 @@ using osu.Game.Rulesets; using osu.Game.Rulesets.UI; using osu.Game.Screens.Play; using osu.Game.Skinning; +using osu.Game.Storyboards; using osuTK; using osuTK.Input; @@ -28,6 +30,12 @@ namespace osu.Game.Tests.Visual.Gameplay { protected new PausePlayer Player => (PausePlayer)base.Player; + protected override WorkingBeatmap CreateWorkingBeatmap(IBeatmap beatmap, Storyboard storyboard = null) + { + beatmap.AudioLeadIn = 4000; + return base.CreateWorkingBeatmap(beatmap, storyboard); + } + private readonly Container content; protected override Container Content => content; @@ -202,6 +210,7 @@ namespace osu.Game.Tests.Visual.Gameplay [Test] public void TestUserPauseDuringCooldownTooSoon() { + AddStep("seek to gameplay", () => Player.GameplayClockContainer.Seek(0)); AddStep("move cursor outside", () => InputManager.MoveMouseTo(Player.ScreenSpaceDrawQuad.TopLeft - new Vector2(10))); pauseAndConfirm(); @@ -213,9 +222,23 @@ namespace osu.Game.Tests.Visual.Gameplay confirmNotExited(); } + [Test] + public void TestUserPauseDuringIntroSkipsCooldown() + { + AddStep("seek before gameplay", () => Player.GameplayClockContainer.Seek(-5000)); + AddStep("move cursor outside", () => InputManager.MoveMouseTo(Player.ScreenSpaceDrawQuad.TopLeft - new Vector2(10))); + + pauseAndConfirm(); + + resume(); + pauseViaBackAction(); + confirmPaused(); + } + [Test] public void TestQuickExitDuringCooldownTooSoon() { + AddStep("seek to gameplay", () => Player.GameplayClockContainer.Seek(0)); AddStep("move cursor outside", () => InputManager.MoveMouseTo(Player.ScreenSpaceDrawQuad.TopLeft - new Vector2(10))); pauseAndConfirm(); From 09fc30e377ea255387059d00a5a72faa8060c0e9 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 16 Dec 2024 17:36:40 +0900 Subject: [PATCH 032/290] Hide `!mp` commands from tournament streaming chat --- .../TestSceneTournamentMatchChatDisplay.cs | 6 +++++ .../Components/TournamentMatchChatDisplay.cs | 9 ++++++- osu.Game/Online/Chat/StandAloneChatDisplay.cs | 27 ++++++++++--------- osu.Game/Overlays/Chat/DrawableChannel.cs | 9 +++++-- 4 files changed, 35 insertions(+), 16 deletions(-) diff --git a/osu.Game.Tournament.Tests/Components/TestSceneTournamentMatchChatDisplay.cs b/osu.Game.Tournament.Tests/Components/TestSceneTournamentMatchChatDisplay.cs index de91a66e56..231bd77655 100644 --- a/osu.Game.Tournament.Tests/Components/TestSceneTournamentMatchChatDisplay.cs +++ b/osu.Game.Tournament.Tests/Components/TestSceneTournamentMatchChatDisplay.cs @@ -152,6 +152,12 @@ namespace osu.Game.Tournament.Tests.Components AddStep("change channel to 2", () => chatDisplay.Channel.Value = testChannel2); AddStep("change channel to 1", () => chatDisplay.Channel.Value = testChannel); + + AddStep("!mp message (shouldn't display)", () => testChannel.AddNewMessages(new Message(nextMessageId()) + { + Sender = redUser.ToAPIUser(), + Content = "!mp wangs" + })); } private int messageId; diff --git a/osu.Game.Tournament/Components/TournamentMatchChatDisplay.cs b/osu.Game.Tournament/Components/TournamentMatchChatDisplay.cs index 0998e606e9..c04dbdcdd6 100644 --- a/osu.Game.Tournament/Components/TournamentMatchChatDisplay.cs +++ b/osu.Game.Tournament/Components/TournamentMatchChatDisplay.cs @@ -1,6 +1,7 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System; using System.Linq; using osu.Framework.Allocation; using osu.Framework.Bindables; @@ -72,7 +73,13 @@ namespace osu.Game.Tournament.Components public void Contract() => this.FadeOut(200); - protected override ChatLine CreateMessage(Message message) => new MatchMessage(message, ladderInfo); + protected override ChatLine? CreateMessage(Message message) + { + if (message.Content.StartsWith("!mp", StringComparison.Ordinal)) + return null; + + return new MatchMessage(message, ladderInfo); + } protected override StandAloneDrawableChannel CreateDrawableChannel(Channel channel) => new MatchChannel(channel); diff --git a/osu.Game/Online/Chat/StandAloneChatDisplay.cs b/osu.Game/Online/Chat/StandAloneChatDisplay.cs index 187191d232..667ef072a9 100644 --- a/osu.Game/Online/Chat/StandAloneChatDisplay.cs +++ b/osu.Game/Online/Chat/StandAloneChatDisplay.cs @@ -1,9 +1,8 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System; +using System.Diagnostics; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; @@ -21,18 +20,18 @@ using osuTK.Input; namespace osu.Game.Online.Chat { /// - /// Display a chat channel in an insolated region. + /// Display a chat channel in an isolated region. /// public partial class StandAloneChatDisplay : CompositeDrawable { [Cached] - public readonly Bindable Channel = new Bindable(); + public readonly Bindable Channel = new Bindable(); - protected readonly ChatTextBox TextBox; + protected readonly ChatTextBox? TextBox; - private ChannelManager channelManager; + private ChannelManager? channelManager; - private StandAloneDrawableChannel drawableChannel; + private StandAloneDrawableChannel? drawableChannel; private readonly bool postingTextBox; @@ -93,6 +92,8 @@ namespace osu.Game.Online.Chat private void postMessage(TextBox sender, bool newText) { + Debug.Assert(TextBox != null); + string text = TextBox.Text.Trim(); if (string.IsNullOrWhiteSpace(text)) @@ -106,9 +107,9 @@ namespace osu.Game.Online.Chat TextBox.Text = string.Empty; } - protected virtual ChatLine CreateMessage(Message message) => new StandAloneMessage(message); + protected virtual ChatLine? CreateMessage(Message message) => new StandAloneMessage(message); - private void channelChanged(ValueChangedEvent e) + private void channelChanged(ValueChangedEvent e) { drawableChannel?.Expire(); @@ -128,8 +129,8 @@ namespace osu.Game.Online.Chat public partial class ChatTextBox : HistoryTextBox { - public Action Focus; - public Action FocusLost; + public Action? Focus; + public Action? FocusLost; protected override bool OnKeyDown(KeyDownEvent e) { @@ -171,14 +172,14 @@ namespace osu.Game.Online.Chat public partial class StandAloneDrawableChannel : DrawableChannel { - public Func CreateChatLineAction; + public Func? CreateChatLineAction; public StandAloneDrawableChannel(Channel channel) : base(channel) { } - protected override ChatLine CreateChatLine(Message m) => CreateChatLineAction(m); + protected override ChatLine? CreateChatLine(Message m) => CreateChatLineAction?.Invoke(m) ?? null; protected override DaySeparator CreateDaySeparator(DateTimeOffset time) => new StandAloneDaySeparator(time); } diff --git a/osu.Game/Overlays/Chat/DrawableChannel.cs b/osu.Game/Overlays/Chat/DrawableChannel.cs index 41098ef823..b1b91f5fe3 100644 --- a/osu.Game/Overlays/Chat/DrawableChannel.cs +++ b/osu.Game/Overlays/Chat/DrawableChannel.cs @@ -155,8 +155,13 @@ namespace osu.Game.Overlays.Chat { addDaySeparatorIfRequired(lastMessage, message); - ChatLineFlow.Add(CreateChatLine(message)); - lastMessage = message; + var chatLine = CreateChatLine(message); + + if (chatLine != null) + { + ChatLineFlow.Add(chatLine); + lastMessage = message; + } } var staleMessages = chatLines.Where(c => c.LifetimeEnd == double.MaxValue).ToArray(); From c46e81d8908c2e73f6d194f1b233a8b6fd81f6aa Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Wed, 11 Dec 2024 03:31:51 -0500 Subject: [PATCH 033/290] Roll our own iOS application delegates --- osu.Game.Rulesets.Catch.Tests.iOS/AppDelegate.cs | 15 +++++++++++++++ .../{Application.cs => Program.cs} | 7 +++---- osu.Game.Rulesets.Mania.Tests.iOS/AppDelegate.cs | 15 +++++++++++++++ .../{Application.cs => Program.cs} | 7 +++---- osu.Game.Rulesets.Osu.Tests.iOS/AppDelegate.cs | 15 +++++++++++++++ .../{Application.cs => Program.cs} | 7 +++---- osu.Game.Rulesets.Taiko.Tests.iOS/AppDelegate.cs | 15 +++++++++++++++ .../{Application.cs => Program.cs} | 7 +++---- osu.Game.Tests.iOS/AppDelegate.cs | 14 ++++++++++++++ osu.Game.Tests.iOS/{Application.cs => Program.cs} | 6 +++--- osu.iOS/AppDelegate.cs | 14 ++++++++++++++ osu.iOS/{Application.cs => Program.cs} | 6 +++--- 12 files changed, 106 insertions(+), 22 deletions(-) create mode 100644 osu.Game.Rulesets.Catch.Tests.iOS/AppDelegate.cs rename osu.Game.Rulesets.Catch.Tests.iOS/{Application.cs => Program.cs} (66%) create mode 100644 osu.Game.Rulesets.Mania.Tests.iOS/AppDelegate.cs rename osu.Game.Rulesets.Mania.Tests.iOS/{Application.cs => Program.cs} (66%) create mode 100644 osu.Game.Rulesets.Osu.Tests.iOS/AppDelegate.cs rename osu.Game.Rulesets.Osu.Tests.iOS/{Application.cs => Program.cs} (66%) create mode 100644 osu.Game.Rulesets.Taiko.Tests.iOS/AppDelegate.cs rename osu.Game.Rulesets.Taiko.Tests.iOS/{Application.cs => Program.cs} (66%) create mode 100644 osu.Game.Tests.iOS/AppDelegate.cs rename osu.Game.Tests.iOS/{Application.cs => Program.cs} (69%) create mode 100644 osu.iOS/AppDelegate.cs rename osu.iOS/{Application.cs => Program.cs} (69%) diff --git a/osu.Game.Rulesets.Catch.Tests.iOS/AppDelegate.cs b/osu.Game.Rulesets.Catch.Tests.iOS/AppDelegate.cs new file mode 100644 index 0000000000..b594d28611 --- /dev/null +++ b/osu.Game.Rulesets.Catch.Tests.iOS/AppDelegate.cs @@ -0,0 +1,15 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using Foundation; +using osu.Framework.iOS; +using osu.Game.Tests; + +namespace osu.Game.Rulesets.Catch.Tests.iOS +{ + [Register("AppDelegate")] + public class AppDelegate : GameApplicationDelegate + { + protected override Framework.Game CreateGame() => new OsuTestBrowser(); + } +} diff --git a/osu.Game.Rulesets.Catch.Tests.iOS/Application.cs b/osu.Game.Rulesets.Catch.Tests.iOS/Program.cs similarity index 66% rename from osu.Game.Rulesets.Catch.Tests.iOS/Application.cs rename to osu.Game.Rulesets.Catch.Tests.iOS/Program.cs index d097c6a698..6b887ae2d4 100644 --- a/osu.Game.Rulesets.Catch.Tests.iOS/Application.cs +++ b/osu.Game.Rulesets.Catch.Tests.iOS/Program.cs @@ -1,16 +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 osu.Framework.iOS; -using osu.Game.Tests; +using UIKit; namespace osu.Game.Rulesets.Catch.Tests.iOS { - public static class Application + public static class Program { public static void Main(string[] args) { - GameApplication.Main(new OsuTestBrowser()); + UIApplication.Main(args, null, typeof(AppDelegate)); } } } diff --git a/osu.Game.Rulesets.Mania.Tests.iOS/AppDelegate.cs b/osu.Game.Rulesets.Mania.Tests.iOS/AppDelegate.cs new file mode 100644 index 0000000000..09bed3b42b --- /dev/null +++ b/osu.Game.Rulesets.Mania.Tests.iOS/AppDelegate.cs @@ -0,0 +1,15 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using Foundation; +using osu.Framework.iOS; +using osu.Game.Tests; + +namespace osu.Game.Rulesets.Mania.Tests.iOS +{ + [Register("AppDelegate")] + public class AppDelegate : GameApplicationDelegate + { + protected override Framework.Game CreateGame() => new OsuTestBrowser(); + } +} diff --git a/osu.Game.Rulesets.Mania.Tests.iOS/Application.cs b/osu.Game.Rulesets.Mania.Tests.iOS/Program.cs similarity index 66% rename from osu.Game.Rulesets.Mania.Tests.iOS/Application.cs rename to osu.Game.Rulesets.Mania.Tests.iOS/Program.cs index 75a5a73058..696816c47b 100644 --- a/osu.Game.Rulesets.Mania.Tests.iOS/Application.cs +++ b/osu.Game.Rulesets.Mania.Tests.iOS/Program.cs @@ -1,16 +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 osu.Framework.iOS; -using osu.Game.Tests; +using UIKit; namespace osu.Game.Rulesets.Mania.Tests.iOS { - public static class Application + public static class Program { public static void Main(string[] args) { - GameApplication.Main(new OsuTestBrowser()); + UIApplication.Main(args, null, typeof(AppDelegate)); } } } diff --git a/osu.Game.Rulesets.Osu.Tests.iOS/AppDelegate.cs b/osu.Game.Rulesets.Osu.Tests.iOS/AppDelegate.cs new file mode 100644 index 0000000000..77177e93f1 --- /dev/null +++ b/osu.Game.Rulesets.Osu.Tests.iOS/AppDelegate.cs @@ -0,0 +1,15 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using Foundation; +using osu.Framework.iOS; +using osu.Game.Tests; + +namespace osu.Game.Rulesets.Osu.Tests.iOS +{ + [Register("AppDelegate")] + public class AppDelegate : GameApplicationDelegate + { + protected override Framework.Game CreateGame() => new OsuTestBrowser(); + } +} diff --git a/osu.Game.Rulesets.Osu.Tests.iOS/Application.cs b/osu.Game.Rulesets.Osu.Tests.iOS/Program.cs similarity index 66% rename from osu.Game.Rulesets.Osu.Tests.iOS/Application.cs rename to osu.Game.Rulesets.Osu.Tests.iOS/Program.cs index f9059014a5..579e20e05a 100644 --- a/osu.Game.Rulesets.Osu.Tests.iOS/Application.cs +++ b/osu.Game.Rulesets.Osu.Tests.iOS/Program.cs @@ -1,16 +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 osu.Framework.iOS; -using osu.Game.Tests; +using UIKit; namespace osu.Game.Rulesets.Osu.Tests.iOS { - public static class Application + public static class Program { public static void Main(string[] args) { - GameApplication.Main(new OsuTestBrowser()); + UIApplication.Main(args, null, typeof(AppDelegate)); } } } diff --git a/osu.Game.Rulesets.Taiko.Tests.iOS/AppDelegate.cs b/osu.Game.Rulesets.Taiko.Tests.iOS/AppDelegate.cs new file mode 100644 index 0000000000..4bfc12e7e8 --- /dev/null +++ b/osu.Game.Rulesets.Taiko.Tests.iOS/AppDelegate.cs @@ -0,0 +1,15 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using Foundation; +using osu.Framework.iOS; +using osu.Game.Tests; + +namespace osu.Game.Rulesets.Taiko.Tests.iOS +{ + [Register("AppDelegate")] + public class AppDelegate : GameApplicationDelegate + { + protected override Framework.Game CreateGame() => new OsuTestBrowser(); + } +} diff --git a/osu.Game.Rulesets.Taiko.Tests.iOS/Application.cs b/osu.Game.Rulesets.Taiko.Tests.iOS/Program.cs similarity index 66% rename from osu.Game.Rulesets.Taiko.Tests.iOS/Application.cs rename to osu.Game.Rulesets.Taiko.Tests.iOS/Program.cs index 0b6a11d8c2..bf2ffecb23 100644 --- a/osu.Game.Rulesets.Taiko.Tests.iOS/Application.cs +++ b/osu.Game.Rulesets.Taiko.Tests.iOS/Program.cs @@ -1,16 +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 osu.Framework.iOS; -using osu.Game.Tests; +using UIKit; namespace osu.Game.Rulesets.Taiko.Tests.iOS { - public static class Application + public static class Program { public static void Main(string[] args) { - GameApplication.Main(new OsuTestBrowser()); + UIApplication.Main(args, null, typeof(AppDelegate)); } } } diff --git a/osu.Game.Tests.iOS/AppDelegate.cs b/osu.Game.Tests.iOS/AppDelegate.cs new file mode 100644 index 0000000000..bfad59de43 --- /dev/null +++ b/osu.Game.Tests.iOS/AppDelegate.cs @@ -0,0 +1,14 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using Foundation; +using osu.Framework.iOS; + +namespace osu.Game.Tests.iOS +{ + [Register("AppDelegate")] + public class AppDelegate : GameApplicationDelegate + { + protected override Framework.Game CreateGame() => new OsuTestBrowser(); + } +} diff --git a/osu.Game.Tests.iOS/Application.cs b/osu.Game.Tests.iOS/Program.cs similarity index 69% rename from osu.Game.Tests.iOS/Application.cs rename to osu.Game.Tests.iOS/Program.cs index e5df79f3de..35a90d7213 100644 --- a/osu.Game.Tests.iOS/Application.cs +++ b/osu.Game.Tests.iOS/Program.cs @@ -1,15 +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 osu.Framework.iOS; +using UIKit; namespace osu.Game.Tests.iOS { - public static class Application + public static class Program { public static void Main(string[] args) { - GameApplication.Main(new OsuTestBrowser()); + UIApplication.Main(args, null, typeof(AppDelegate)); } } } diff --git a/osu.iOS/AppDelegate.cs b/osu.iOS/AppDelegate.cs new file mode 100644 index 0000000000..e88b39f710 --- /dev/null +++ b/osu.iOS/AppDelegate.cs @@ -0,0 +1,14 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using Foundation; +using osu.Framework.iOS; + +namespace osu.iOS +{ + [Register("AppDelegate")] + public class AppDelegate : GameApplicationDelegate + { + protected override Framework.Game CreateGame() => new OsuGameIOS(); + } +} diff --git a/osu.iOS/Application.cs b/osu.iOS/Program.cs similarity index 69% rename from osu.iOS/Application.cs rename to osu.iOS/Program.cs index 74bd58acb8..fd24ecf419 100644 --- a/osu.iOS/Application.cs +++ b/osu.iOS/Program.cs @@ -1,15 +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 osu.Framework.iOS; +using UIKit; namespace osu.iOS { - public static class Application + public static class Program { public static void Main(string[] args) { - GameApplication.Main(new OsuGameIOS()); + UIApplication.Main(args, null, typeof(AppDelegate)); } } } From 22e74cc0ee2c83b3e52c84286523041a6c3b1b06 Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Mon, 16 Dec 2024 12:22:28 -0500 Subject: [PATCH 034/290] Fix iOS app configuration missing certain specifications --- osu.iOS/Info.plist | 2 ++ 1 file changed, 2 insertions(+) diff --git a/osu.iOS/Info.plist b/osu.iOS/Info.plist index ae36d00910..0be75fffd8 100644 --- a/osu.iOS/Info.plist +++ b/osu.iOS/Info.plist @@ -153,5 +153,7 @@ LSApplicationCategoryType public.app-category.music-games + LSSupportsOpeningDocumentsInPlace + From 47d81e7dee802a47da83732e00690bb823996718 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 17 Dec 2024 19:10:09 +0900 Subject: [PATCH 035/290] Fix null inspections on `GameplayChatDisplay` --- .../Screens/OnlinePlay/Multiplayer/GameplayChatDisplay.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/GameplayChatDisplay.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/GameplayChatDisplay.cs index 9a03a131b4..befaf115ae 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/GameplayChatDisplay.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/GameplayChatDisplay.cs @@ -19,6 +19,8 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer [Resolved(CanBeNull = true)] private ILocalUserPlayInfo? localUserInfo { get; set; } + protected new ChatTextBox TextBox => base.TextBox!; + private readonly IBindable localUserPlaying = new Bindable(); public override bool PropagatePositionalInputSubTree => localUserPlaying.Value != LocalUserPlayingState.Playing; @@ -58,7 +60,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer localUserPlaying.BindValueChanged(playing => { - // for now let's never hold focus. this avoid misdirected gameplay keys entering chat. + // for now let's never hold focus. this avoids misdirected gameplay keys entering chat. // note that this is done within this callback as it triggers an un-focus as well. TextBox.HoldFocus = false; From c68dc1141215e97cae0c2f8f27d41e54bbe028d2 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 19 Dec 2024 00:01:36 +0900 Subject: [PATCH 036/290] Fix being able to click through slider tail drag handles Closes https://github.com/ppy/osu/issues/31176. --- .../Edit/Blueprints/Sliders/SliderEndDragMarker.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderEndDragMarker.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderEndDragMarker.cs index 37383544dc..326dd82fc6 100644 --- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderEndDragMarker.cs +++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderEndDragMarker.cs @@ -76,6 +76,10 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders base.OnDragEnd(e); } + protected override bool OnMouseDown(MouseDownEvent e) => true; + + protected override bool OnClick(ClickEvent e) => true; + private void updateState() { Colour = IsHovered || IsDragged ? colours.Red : colours.Yellow; From 75d694d3dff0131e24b04deef4a34689628b1b76 Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Wed, 18 Dec 2024 12:43:20 -0500 Subject: [PATCH 037/290] Add key value for `NSBluetoothAlwaysUsageDescription` --- osu.iOS/Info.plist | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/osu.iOS/Info.plist b/osu.iOS/Info.plist index 0be75fffd8..29410938a3 100644 --- a/osu.iOS/Info.plist +++ b/osu.iOS/Info.plist @@ -34,9 +34,11 @@ CADisableMinimumFrameDurationOnPhone NSCameraUsageDescription - We don't really use the camera. + We don't use the camera. NSMicrophoneUsageDescription - We don't really use the microphone. + We don't use the microphone. + NSBluetoothAlwaysUsageDescription + We don't use Bluetooth. UISupportedInterfaceOrientations UIInterfaceOrientationLandscapeRight From 532c681e3c53c0b3f36f18201afca884ebcdf144 Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Wed, 18 Dec 2024 12:48:24 -0500 Subject: [PATCH 038/290] Update framework --- osu.Android.props | 2 +- osu.Game/osu.Game.csproj | 2 +- osu.iOS.props | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Android.props b/osu.Android.props index 632325725a..6770b0254f 100644 --- a/osu.Android.props +++ b/osu.Android.props @@ -10,7 +10,7 @@ true - + diff --git a/osu.iOS.props b/osu.iOS.props index 62a65f291d..640e6bdd94 100644 --- a/osu.iOS.props +++ b/osu.iOS.props @@ -17,6 +17,6 @@ -all - + From c7354d9c4104d0692d567c116af3ff84364986bf Mon Sep 17 00:00:00 2001 From: mini <39670899+minisbett@users.noreply.github.com> Date: Sun, 15 Dec 2024 17:31:13 +0100 Subject: [PATCH 039/290] Apply type inheritance check --- .../IO/Serialization/Converters/TypedListConverter.cs | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/osu.Game/IO/Serialization/Converters/TypedListConverter.cs b/osu.Game/IO/Serialization/Converters/TypedListConverter.cs index de25d3e30e..19ef6b8fe6 100644 --- a/osu.Game/IO/Serialization/Converters/TypedListConverter.cs +++ b/osu.Game/IO/Serialization/Converters/TypedListConverter.cs @@ -62,8 +62,12 @@ namespace osu.Game.IO.Serialization.Converters if (tok["$type"] == null) throw new JsonException("Expected $type token."); - string typeName = lookupTable[(int)tok["$type"]]; - var instance = (T)Activator.CreateInstance(Type.GetType(typeName).AsNonNull())!; + // Prevent instantiation of types that do not inherit the type targetted by this converter + Type type = Type.GetType(lookupTable[(int)tok["$type"]]).AsNonNull(); + if (!type.IsAssignableTo(typeof(T))) + continue; + + var instance = (T)Activator.CreateInstance(type)!; serializer.Populate(itemReader, instance); list.Add(instance); From dedf8ad0936927b400b9d4b8ea3f411dde7e72ad Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 19 Dec 2024 17:25:02 +0900 Subject: [PATCH 040/290] Update resources --- osu.Game/osu.Game.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj index 847c209cc4..3f9a8142ca 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -36,7 +36,7 @@ - + From 6dc681f0e9d501c2747b4a14e9b9e182c5d2aa41 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 19 Dec 2024 12:50:48 +0100 Subject: [PATCH 041/290] Annotate virtual as potentially nullable --- osu.Game/Overlays/Chat/DrawableChannel.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/osu.Game/Overlays/Chat/DrawableChannel.cs b/osu.Game/Overlays/Chat/DrawableChannel.cs index b1b91f5fe3..cb7cd03584 100644 --- a/osu.Game/Overlays/Chat/DrawableChannel.cs +++ b/osu.Game/Overlays/Chat/DrawableChannel.cs @@ -7,6 +7,7 @@ using System; using System.Collections.Generic; using System.Diagnostics; using System.Linq; +using JetBrains.Annotations; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; @@ -132,6 +133,7 @@ namespace osu.Game.Overlays.Chat Channel.PendingMessageResolved -= pendingMessageResolved; } + [CanBeNull] protected virtual ChatLine CreateChatLine(Message m) => new ChatLine(m); protected virtual DaySeparator CreateDaySeparator(DateTimeOffset time) => new DaySeparator(time); From 772ac2d3261595d1b23e97661f091ac41829bb88 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 19 Dec 2024 14:48:18 +0100 Subject: [PATCH 042/290] Fix mod display not fading out after start of play This was very weird on master - `ModDisplay` applied a fade-in on the `iconsContainer` that lasted 1000ms, and `HUDOverlay` would stack another 200ms fade-in on top if a replay was loaded. Moving that first fadeout to a higher level broke fade-out because transforms got overwritten. --- osu.Game/Screens/Play/HUDOverlay.cs | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/osu.Game/Screens/Play/HUDOverlay.cs b/osu.Game/Screens/Play/HUDOverlay.cs index 5d92fee841..f7b1a95c23 100644 --- a/osu.Game/Screens/Play/HUDOverlay.cs +++ b/osu.Game/Screens/Play/HUDOverlay.cs @@ -35,8 +35,6 @@ namespace osu.Game.Screens.Play { public const float FADE_DURATION = 300; - private const float mods_fade_duration = 1000; - public const Easing FADE_EASING = Easing.OutQuint; /// @@ -238,7 +236,7 @@ namespace osu.Game.Screens.Play { if (e.NewValue) { - ModDisplay.FadeIn(200); + ModDisplay.FadeIn(1000, FADE_EASING); InputCountController.Margin = new MarginPadding(10) { Bottom = 30 }; } else @@ -255,8 +253,6 @@ namespace osu.Game.Screens.Play { ModDisplay.ExpansionMode = ExpansionMode.ExpandOnHover; }, 1200); - - ModDisplay.FadeInFromZero(mods_fade_duration, FADE_EASING); } protected override void Update() From 7d1473c5d0d2c3a2ba2f7467cbd5d06069b01ab1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 19 Dec 2024 14:52:27 +0100 Subject: [PATCH 043/290] Simplify expand/contract code --- osu.Game/Screens/Play/HUD/ModDisplay.cs | 47 ++++++++++++------------- 1 file changed, 23 insertions(+), 24 deletions(-) diff --git a/osu.Game/Screens/Play/HUD/ModDisplay.cs b/osu.Game/Screens/Play/HUD/ModDisplay.cs index 9f42175a70..38417fae04 100644 --- a/osu.Game/Screens/Play/HUD/ModDisplay.cs +++ b/osu.Game/Screens/Play/HUD/ModDisplay.cs @@ -33,12 +33,7 @@ namespace osu.Game.Screens.Play.HUD expansionMode = value; if (IsLoaded) - { - if (expansionMode == ExpansionMode.AlwaysExpanded || (expansionMode == ExpansionMode.ExpandOnHover && IsHovered)) - expand(); - else if (expansionMode == ExpansionMode.AlwaysContracted || (expansionMode == ExpansionMode.ExpandOnHover && !IsHovered)) - contract(); - } + updateExpansionMode(); } } @@ -88,24 +83,7 @@ namespace osu.Game.Screens.Play.HUD base.LoadComplete(); Current.BindValueChanged(updateDisplay, true); - - switch (expansionMode) - { - case ExpansionMode.AlwaysExpanded: - expand(0); - break; - - case ExpansionMode.AlwaysContracted: - contract(0); - break; - - case ExpansionMode.ExpandOnHover: - if (IsHovered) - expand(0); - else - contract(0); - break; - } + updateExpansionMode(0); } private void updateDisplay(ValueChangedEvent> mods) @@ -116,6 +94,27 @@ namespace osu.Game.Screens.Play.HUD iconsContainer.Add(new ModIcon(mod, showExtendedInformation: showExtendedInformation) { Scale = new Vector2(0.6f) }); } + private void updateExpansionMode(double duration = 500) + { + switch (expansionMode) + { + case ExpansionMode.AlwaysExpanded: + expand(duration); + break; + + case ExpansionMode.AlwaysContracted: + contract(duration); + break; + + case ExpansionMode.ExpandOnHover: + if (IsHovered) + expand(duration); + else + contract(duration); + break; + } + } + private void expand(double duration = 500) { if (ExpansionMode != ExpansionMode.AlwaysContracted) From e458f540ac857d934a851094a4e03743cbf421e7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 19 Dec 2024 14:54:57 +0100 Subject: [PATCH 044/290] Adjust formatting --- osu.Game/Screens/Play/HUDOverlay.cs | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/osu.Game/Screens/Play/HUDOverlay.cs b/osu.Game/Screens/Play/HUDOverlay.cs index f7b1a95c23..c9ab754e94 100644 --- a/osu.Game/Screens/Play/HUDOverlay.cs +++ b/osu.Game/Screens/Play/HUDOverlay.cs @@ -249,10 +249,7 @@ namespace osu.Game.Screens.Play }, true); ModDisplay.ExpansionMode = ExpansionMode.AlwaysExpanded; - Scheduler.AddDelayed(() => - { - ModDisplay.ExpansionMode = ExpansionMode.ExpandOnHover; - }, 1200); + Scheduler.AddDelayed(() => ModDisplay.ExpansionMode = ExpansionMode.ExpandOnHover, 1200); } protected override void Update() From 2cab8f4e8a38f7a2da570cb792bf7ab50efa57d2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 19 Dec 2024 15:02:49 +0100 Subject: [PATCH 045/290] Add localisation support --- .../SkinnableModDisplayStrings.cs | 49 +++++++++++++++++++ osu.Game/Screens/Play/HUD/ModDisplay.cs | 6 +++ .../Screens/Play/HUD/SkinnableModDisplay.cs | 8 +-- 3 files changed, 60 insertions(+), 3 deletions(-) create mode 100644 osu.Game/Localisation/SkinComponents/SkinnableModDisplayStrings.cs diff --git a/osu.Game/Localisation/SkinComponents/SkinnableModDisplayStrings.cs b/osu.Game/Localisation/SkinComponents/SkinnableModDisplayStrings.cs new file mode 100644 index 0000000000..d3e8c0f8c8 --- /dev/null +++ b/osu.Game/Localisation/SkinComponents/SkinnableModDisplayStrings.cs @@ -0,0 +1,49 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Localisation; + +namespace osu.Game.Localisation.SkinComponents +{ + public static class SkinnableModDisplayStrings + { + private const string prefix = @"osu.Game.Resources.Localisation.SkinnableModDisplay"; + + /// + /// "Show extended information" + /// + public static LocalisableString ShowExtendedInformation => new TranslatableString(getKey(@"show_extended_information"), @"Show extended information"); + + /// + /// "Whether to show extended information for each mod." + /// + public static LocalisableString ShowExtendedInformationDescription => new TranslatableString(getKey(@"whether_to_show_extended_information"), @"Whether to show extended information for each mod."); + + /// + /// "Expansion mode" + /// + public static LocalisableString ExpansionMode => new TranslatableString(getKey(@"expansion_mode"), @"Expansion mode"); + + /// + /// "How the mod display expands when interacted with." + /// + public static LocalisableString ExpansionModeDescription => new TranslatableString(getKey(@"how_the_mod_display_expands"), @"How the mod display expands when interacted with."); + + /// + /// "Expand on hover" + /// + public static LocalisableString ExpandOnHover => new TranslatableString(getKey(@"expand_on_hover"), @"Expand on hover"); + + /// + /// "Always contracted" + /// + public static LocalisableString AlwaysContracted => new TranslatableString(getKey(@"always_contracted"), @"Always contracted"); + + /// + /// "Always expanded" + /// + public static LocalisableString AlwaysExpanded => new TranslatableString(getKey(@"always_expanded"), @"Always expanded"); + + private static string getKey(string key) => $@"{prefix}:{key}"; + } +} diff --git a/osu.Game/Screens/Play/HUD/ModDisplay.cs b/osu.Game/Screens/Play/HUD/ModDisplay.cs index 38417fae04..d076d11b1f 100644 --- a/osu.Game/Screens/Play/HUD/ModDisplay.cs +++ b/osu.Game/Screens/Play/HUD/ModDisplay.cs @@ -8,7 +8,10 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.UserInterface; using osu.Framework.Input.Events; +using osu.Framework.Localisation; using osu.Game.Graphics.Containers; +using osu.Game.Localisation; +using osu.Game.Localisation.SkinComponents; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.UI; using osuTK; @@ -145,16 +148,19 @@ namespace osu.Game.Screens.Play.HUD /// /// The will expand only when hovered. /// + [LocalisableDescription(typeof(SkinnableModDisplayStrings), nameof(SkinnableModDisplayStrings.ExpandOnHover))] ExpandOnHover, /// /// The will always be expanded. /// + [LocalisableDescription(typeof(SkinnableModDisplayStrings), nameof(SkinnableModDisplayStrings.AlwaysExpanded))] AlwaysExpanded, /// /// The will always be contracted. /// + [LocalisableDescription(typeof(SkinnableModDisplayStrings), nameof(SkinnableModDisplayStrings.AlwaysContracted))] AlwaysContracted, } } diff --git a/osu.Game/Screens/Play/HUD/SkinnableModDisplay.cs b/osu.Game/Screens/Play/HUD/SkinnableModDisplay.cs index ce4a4e978e..b81b2d1520 100644 --- a/osu.Game/Screens/Play/HUD/SkinnableModDisplay.cs +++ b/osu.Game/Screens/Play/HUD/SkinnableModDisplay.cs @@ -9,6 +9,8 @@ using osu.Framework.Graphics.Containers; using osu.Game.Configuration; using osu.Game.Rulesets.Mods; using osu.Game.Skinning; +using osu.Game.Localisation; +using osu.Game.Localisation.SkinComponents; namespace osu.Game.Screens.Play.HUD { @@ -22,11 +24,11 @@ namespace osu.Game.Screens.Play.HUD [Resolved] private Bindable> mods { get; set; } = null!; - [SettingSource("Show extended info", "Whether to show extended information for each mod.")] + [SettingSource(typeof(SkinnableModDisplayStrings), nameof(SkinnableModDisplayStrings.ShowExtendedInformation), nameof(SkinnableModDisplayStrings.ShowExtendedInformationDescription))] public Bindable ShowExtendedInformation { get; } = new Bindable(true); - [SettingSource("Expansion mode", "How the mod display expands when interacted with.")] - public Bindable ExpansionModeSetting { get; } = new Bindable(ExpansionMode.ExpandOnHover); + [SettingSource(typeof(SkinnableModDisplayStrings), nameof(SkinnableModDisplayStrings.ExpansionMode), nameof(SkinnableModDisplayStrings.ExpansionModeDescription))] + public Bindable ExpansionModeSetting { get; } = new Bindable(); [BackgroundDependencyLoader] private void load() From df607ac3ea33cd531272e35df0fb1023cf21dcfd Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 20 Dec 2024 00:38:46 +0900 Subject: [PATCH 046/290] Load seasonal backgrounds without requiring being logged in --- osu.Game/Graphics/Backgrounds/SeasonalBackgroundLoader.cs | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/osu.Game/Graphics/Backgrounds/SeasonalBackgroundLoader.cs b/osu.Game/Graphics/Backgrounds/SeasonalBackgroundLoader.cs index 6f6febb646..b4be330f9c 100644 --- a/osu.Game/Graphics/Backgrounds/SeasonalBackgroundLoader.cs +++ b/osu.Game/Graphics/Backgrounds/SeasonalBackgroundLoader.cs @@ -28,7 +28,6 @@ namespace osu.Game.Graphics.Backgrounds [Resolved] private IAPIProvider api { get; set; } - private readonly IBindable apiState = new Bindable(); private Bindable seasonalBackgroundMode; private Bindable seasonalBackgrounds; @@ -47,13 +46,12 @@ namespace osu.Game.Graphics.Backgrounds SeasonalBackgroundChanged?.Invoke(); }); - apiState.BindTo(api.State); - apiState.BindValueChanged(fetchSeasonalBackgrounds, true); + fetchSeasonalBackgrounds(); } - private void fetchSeasonalBackgrounds(ValueChangedEvent stateChanged) + private void fetchSeasonalBackgrounds() { - if (seasonalBackgrounds.Value != null || stateChanged.NewValue != APIState.Online) + if (seasonalBackgrounds.Value != null) return; var request = new GetSeasonalBackgroundsRequest(); From f9939e7f9562ed24ad83db4cf18cf19c30eba113 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 20 Dec 2024 00:50:53 +0900 Subject: [PATCH 047/290] Remove invalid test --- .../TestSceneSeasonalBackgroundLoader.cs | 18 ++---------------- 1 file changed, 2 insertions(+), 16 deletions(-) diff --git a/osu.Game.Tests/Visual/Background/TestSceneSeasonalBackgroundLoader.cs b/osu.Game.Tests/Visual/Background/TestSceneSeasonalBackgroundLoader.cs index 54a722cee0..7b22ff1d6a 100644 --- a/osu.Game.Tests/Visual/Background/TestSceneSeasonalBackgroundLoader.cs +++ b/osu.Game.Tests/Visual/Background/TestSceneSeasonalBackgroundLoader.cs @@ -131,21 +131,6 @@ namespace osu.Game.Tests.Visual.Background assertNoBackgrounds(); } - [Test] - public void TestDelayedConnectivity() - { - registerBackgroundsResponse(DateTimeOffset.Now.AddDays(30)); - setSeasonalBackgroundMode(SeasonalBackgroundMode.Always); - AddStep("go offline", () => dummyAPI.SetState(APIState.Offline)); - - createLoader(); - assertNoBackgrounds(); - - AddStep("go online", () => dummyAPI.SetState(APIState.Online)); - - assertAnyBackground(); - } - private void registerBackgroundsResponse(DateTimeOffset endDate) => AddStep("setup request handler", () => { @@ -185,7 +170,8 @@ namespace osu.Game.Tests.Visual.Background { previousBackground = (SeasonalBackground)backgroundContainer.SingleOrDefault(); background = backgroundLoader.LoadNextBackground(); - LoadComponentAsync(background, bg => backgroundContainer.Child = bg); + if (background != null) + LoadComponentAsync(background, bg => backgroundContainer.Child = bg); }); AddUntilStep("background loaded", () => background.IsLoaded); From 9f8c390735e5acc96a872dcf5f0bbca52d62cb43 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 20 Dec 2024 12:39:33 +0900 Subject: [PATCH 048/290] Update framework --- osu.Android.props | 2 +- osu.Game/osu.Game.csproj | 2 +- osu.iOS.props | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Android.props b/osu.Android.props index 632325725a..f13760bd21 100644 --- a/osu.Android.props +++ b/osu.Android.props @@ -10,7 +10,7 @@ true - + diff --git a/osu.iOS.props b/osu.iOS.props index 62a65f291d..3e618a3a74 100644 --- a/osu.iOS.props +++ b/osu.iOS.props @@ -17,6 +17,6 @@ -all - + From 7c1482366dbbc7328d987fa80922839b2bb30ec9 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 20 Dec 2024 14:07:27 +0900 Subject: [PATCH 049/290] Remove unused using statements --- osu.Game/Screens/Play/HUD/ModDisplay.cs | 1 - osu.Game/Screens/Play/HUD/SkinnableModDisplay.cs | 1 - 2 files changed, 2 deletions(-) diff --git a/osu.Game/Screens/Play/HUD/ModDisplay.cs b/osu.Game/Screens/Play/HUD/ModDisplay.cs index d076d11b1f..417ce355a5 100644 --- a/osu.Game/Screens/Play/HUD/ModDisplay.cs +++ b/osu.Game/Screens/Play/HUD/ModDisplay.cs @@ -10,7 +10,6 @@ using osu.Framework.Graphics.UserInterface; using osu.Framework.Input.Events; using osu.Framework.Localisation; using osu.Game.Graphics.Containers; -using osu.Game.Localisation; using osu.Game.Localisation.SkinComponents; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.UI; diff --git a/osu.Game/Screens/Play/HUD/SkinnableModDisplay.cs b/osu.Game/Screens/Play/HUD/SkinnableModDisplay.cs index b81b2d1520..819484e8ba 100644 --- a/osu.Game/Screens/Play/HUD/SkinnableModDisplay.cs +++ b/osu.Game/Screens/Play/HUD/SkinnableModDisplay.cs @@ -9,7 +9,6 @@ using osu.Framework.Graphics.Containers; using osu.Game.Configuration; using osu.Game.Rulesets.Mods; using osu.Game.Skinning; -using osu.Game.Localisation; using osu.Game.Localisation.SkinComponents; namespace osu.Game.Screens.Play.HUD From a94ada2ec6563bf2ca8d84444506d477677a11a4 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 20 Dec 2024 14:19:03 +0900 Subject: [PATCH 050/290] Update resources --- osu.Game/osu.Game.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj index f011b7c3d1..fe3bdbffa3 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -36,7 +36,7 @@ - + From 80ae7942dfd4e6a8c4ece991243dfcc7e5cf167a Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 19 Dec 2024 17:52:50 +0900 Subject: [PATCH 051/290] Add christmas-specific logo heartbeat --- osu.Game/Screens/Menu/OsuLogo.cs | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/osu.Game/Screens/Menu/OsuLogo.cs b/osu.Game/Screens/Menu/OsuLogo.cs index f2e2e25fa6..f3c37c6960 100644 --- a/osu.Game/Screens/Menu/OsuLogo.cs +++ b/osu.Game/Screens/Menu/OsuLogo.cs @@ -271,8 +271,16 @@ namespace osu.Game.Screens.Menu private void load(TextureStore textures, AudioManager audio) { sampleClick = audio.Samples.Get(@"Menu/osu-logo-select"); - sampleBeat = audio.Samples.Get(@"Menu/osu-logo-heartbeat"); - sampleDownbeat = audio.Samples.Get(@"Menu/osu-logo-downbeat"); + + if (SeasonalUI.ENABLED) + { + sampleDownbeat = sampleBeat = audio.Samples.Get(@"Menu/osu-logo-heartbeat-bell"); + } + else + { + sampleBeat = audio.Samples.Get(@"Menu/osu-logo-heartbeat"); + sampleDownbeat = audio.Samples.Get(@"Menu/osu-logo-downbeat"); + } logo.Texture = textures.Get(@"Menu/logo"); ripple.Texture = textures.Get(@"Menu/logo"); @@ -303,7 +311,10 @@ namespace osu.Game.Screens.Menu else { var channel = sampleBeat.GetChannel(); - channel.Frequency.Value = 0.95 + RNG.NextDouble(0.1); + if (SeasonalUI.ENABLED) + channel.Frequency.Value = 0.99 + RNG.NextDouble(0.02); + else + channel.Frequency.Value = 0.95 + RNG.NextDouble(0.1); channel.Play(); } }); From 180a381b6fb0973b04d414c6b7f4755a8958d724 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 19 Dec 2024 17:57:12 +0900 Subject: [PATCH 052/290] Adjust menu side flashes to be brighter and coloured when seasonal active --- osu.Game/Screens/Menu/MenuSideFlashes.cs | 25 +++++++++++++++--------- 1 file changed, 16 insertions(+), 9 deletions(-) diff --git a/osu.Game/Screens/Menu/MenuSideFlashes.cs b/osu.Game/Screens/Menu/MenuSideFlashes.cs index 533c39826c..cc2d22a7fa 100644 --- a/osu.Game/Screens/Menu/MenuSideFlashes.cs +++ b/osu.Game/Screens/Menu/MenuSideFlashes.cs @@ -3,22 +3,23 @@ #nullable disable -using osuTK.Graphics; +using System; using osu.Framework.Allocation; +using osu.Framework.Audio.Track; +using osu.Framework.Bindables; using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Colour; using osu.Framework.Graphics.Shapes; +using osu.Framework.Utils; using osu.Game.Beatmaps; using osu.Game.Beatmaps.ControlPoints; using osu.Game.Graphics; using osu.Game.Graphics.Containers; -using osu.Game.Skinning; using osu.Game.Online.API; -using System; -using osu.Framework.Audio.Track; -using osu.Framework.Bindables; using osu.Game.Online.API.Requests.Responses; +using osu.Game.Skinning; +using osuTK.Graphics; namespace osu.Game.Screens.Menu { @@ -67,7 +68,7 @@ namespace osu.Game.Screens.Menu Anchor = Anchor.CentreLeft, Origin = Anchor.CentreLeft, RelativeSizeAxes = Axes.Y, - Width = box_width * 2, + Width = box_width * (SeasonalUI.ENABLED ? 4 : 2), Height = 1.5f, // align off-screen to make sure our edges don't become visible during parallax. X = -box_width, @@ -79,7 +80,7 @@ namespace osu.Game.Screens.Menu Anchor = Anchor.CentreRight, Origin = Anchor.CentreRight, RelativeSizeAxes = Axes.Y, - Width = box_width * 2, + Width = box_width * (SeasonalUI.ENABLED ? 4 : 2), Height = 1.5f, X = box_width, Alpha = 0, @@ -104,7 +105,11 @@ namespace osu.Game.Screens.Menu private void flash(Drawable d, double beatLength, bool kiai, ChannelAmplitudes amplitudes) { - d.FadeTo(Math.Max(0, ((ReferenceEquals(d, leftBox) ? amplitudes.LeftChannel : amplitudes.RightChannel) - amplitude_dead_zone) / (kiai ? kiai_multiplier : alpha_multiplier)), box_fade_in_time) + if (SeasonalUI.ENABLED) + updateColour(); + + d.FadeTo(Math.Clamp(0.1f + ((ReferenceEquals(d, leftBox) ? amplitudes.LeftChannel : amplitudes.RightChannel) - amplitude_dead_zone) / (kiai ? kiai_multiplier : alpha_multiplier), 0.1f, 1), + box_fade_in_time) .Then() .FadeOut(beatLength, Easing.In); } @@ -113,7 +118,9 @@ namespace osu.Game.Screens.Menu { Color4 baseColour = colours.Blue; - if (user.Value?.IsSupporter ?? false) + if (SeasonalUI.ENABLED) + baseColour = RNG.NextBool() ? SeasonalUI.PRIMARY_COLOUR_1 : SeasonalUI.PRIMARY_COLOUR_2; + else if (user.Value?.IsSupporter ?? false) baseColour = skin.Value.GetConfig(GlobalSkinColours.MenuGlow)?.Value ?? baseColour; // linear colour looks better in this case, so let's use it for now. From a4bf29e98f4aac7306164eb90edab065d83198eb Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 19 Dec 2024 17:57:42 +0900 Subject: [PATCH 053/290] Adjust menu logo visualiser to use seasonal colours --- osu.Game/Screens/Menu/MenuLogoVisualisation.cs | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/osu.Game/Screens/Menu/MenuLogoVisualisation.cs b/osu.Game/Screens/Menu/MenuLogoVisualisation.cs index f4e992be9a..4537b79b62 100644 --- a/osu.Game/Screens/Menu/MenuLogoVisualisation.cs +++ b/osu.Game/Screens/Menu/MenuLogoVisualisation.cs @@ -3,12 +3,12 @@ #nullable disable -using osuTK.Graphics; -using osu.Game.Skinning; -using osu.Game.Online.API; using osu.Framework.Allocation; using osu.Framework.Bindables; +using osu.Game.Online.API; using osu.Game.Online.API.Requests.Responses; +using osu.Game.Skinning; +using osuTK.Graphics; namespace osu.Game.Screens.Menu { @@ -29,7 +29,9 @@ namespace osu.Game.Screens.Menu private void updateColour() { - if (user.Value?.IsSupporter ?? false) + if (SeasonalUI.ENABLED) + Colour = SeasonalUI.AMBIENT_COLOUR_1; + else if (user.Value?.IsSupporter ?? false) Colour = skin.Value.GetConfig(GlobalSkinColours.MenuGlow)?.Value ?? Color4.White; else Colour = Color4.White; From 618a9849e314a99aff70baec7f2b1ef295b4e1e0 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 19 Dec 2024 17:59:31 +0900 Subject: [PATCH 054/290] Increase intro time allowance to account for seasonal tracks with actual long intros --- osu.Game/Screens/Menu/IntroScreen.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/Menu/IntroScreen.cs b/osu.Game/Screens/Menu/IntroScreen.cs index 0dc54b321f..9885c061a9 100644 --- a/osu.Game/Screens/Menu/IntroScreen.cs +++ b/osu.Game/Screens/Menu/IntroScreen.cs @@ -207,7 +207,7 @@ namespace osu.Game.Screens.Menu Text = NotificationsStrings.AudioPlaybackIssue }); } - }, 5000); + }, 8000); } public override void OnResuming(ScreenTransitionEvent e) From 024029822ab0e74880de27ce073fe88d735659b8 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 19 Dec 2024 17:59:48 +0900 Subject: [PATCH 055/290] Add christmas intro --- .../Visual/Menus/TestSceneIntroChristmas.cs | 15 + osu.Game/Screens/Loader.cs | 3 + osu.Game/Screens/Menu/IntroChristmas.cs | 328 ++++++++++++++++++ osu.Game/Screens/SeasonalUI.cs | 21 ++ 4 files changed, 367 insertions(+) create mode 100644 osu.Game.Tests/Visual/Menus/TestSceneIntroChristmas.cs create mode 100644 osu.Game/Screens/Menu/IntroChristmas.cs create mode 100644 osu.Game/Screens/SeasonalUI.cs diff --git a/osu.Game.Tests/Visual/Menus/TestSceneIntroChristmas.cs b/osu.Game.Tests/Visual/Menus/TestSceneIntroChristmas.cs new file mode 100644 index 0000000000..13377f49df --- /dev/null +++ b/osu.Game.Tests/Visual/Menus/TestSceneIntroChristmas.cs @@ -0,0 +1,15 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using NUnit.Framework; +using osu.Game.Screens.Menu; + +namespace osu.Game.Tests.Visual.Menus +{ + [TestFixture] + public partial class TestSceneIntroChristmas : IntroTestScene + { + protected override bool IntroReliesOnTrack => true; + protected override IntroScreen CreateScreen() => new IntroChristmas(); + } +} diff --git a/osu.Game/Screens/Loader.cs b/osu.Game/Screens/Loader.cs index d71ee05b27..811e4600eb 100644 --- a/osu.Game/Screens/Loader.cs +++ b/osu.Game/Screens/Loader.cs @@ -37,6 +37,9 @@ namespace osu.Game.Screens private IntroScreen getIntroSequence() { + if (SeasonalUI.ENABLED) + return new IntroChristmas(createMainMenu); + if (introSequence == IntroSequence.Random) introSequence = (IntroSequence)RNG.Next(0, (int)IntroSequence.Random); diff --git a/osu.Game/Screens/Menu/IntroChristmas.cs b/osu.Game/Screens/Menu/IntroChristmas.cs new file mode 100644 index 0000000000..0a1cf32b85 --- /dev/null +++ b/osu.Game/Screens/Menu/IntroChristmas.cs @@ -0,0 +1,328 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using osu.Framework.Allocation; +using osu.Framework.Audio.Track; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; +using osu.Framework.Graphics.Textures; +using osu.Framework.Screens; +using osu.Framework.Timing; +using osu.Framework.Utils; +using osu.Game.Beatmaps.ControlPoints; +using osu.Game.Graphics; +using osu.Game.Graphics.Containers; +using osu.Game.Graphics.Sprites; +using osuTK; +using osuTK.Graphics; + +namespace osu.Game.Screens.Menu +{ + public partial class IntroChristmas : IntroScreen + { + protected override string BeatmapHash => "7e26183e72a496f672c3a21292e6b469fdecd084d31c259ea10a31df5b46cd77"; + + protected override string BeatmapFile => "christmas2024.osz"; + + private const double beat_length = 60000 / 172.0; + private const double offset = 5924; + + protected override string SeeyaSampleName => "Intro/Welcome/seeya"; + + private TrianglesIntroSequence intro = null!; + + public IntroChristmas(Func? createNextScreen = null) + : base(createNextScreen) + { + } + + protected override void LogoArriving(OsuLogo logo, bool resuming) + { + base.LogoArriving(logo, resuming); + + if (!resuming) + { + PrepareMenuLoad(); + + var decouplingClock = new DecouplingFramedClock(UsingThemedIntro ? Track : null); + + LoadComponentAsync(intro = new TrianglesIntroSequence(logo, () => FadeInBackground()) + { + RelativeSizeAxes = Axes.Both, + Clock = new InterpolatingFramedClock(decouplingClock), + LoadMenu = LoadMenu + }, _ => + { + AddInternal(intro); + + // There is a chance that the intro timed out before being displayed, and this scheduled callback could + // happen during the outro rather than intro. + // In such a scenario, we don't want to play the intro sample, nor attempt to start the intro track + // (that may have already been since disposed by MusicController). + if (DidLoadMenu) + return; + + // If the user has requested no theme, fallback to the same intro voice and delay as IntroCircles. + // The triangles intro voice and theme are combined which makes it impossible to use. + StartTrack(); + + // no-op for the case of themed intro, no harm in calling for both scenarios as a safety measure. + decouplingClock.Start(); + }); + } + } + + public override void OnSuspending(ScreenTransitionEvent e) + { + base.OnSuspending(e); + + // important as there is a clock attached to a track which will likely be disposed before returning to this screen. + intro.Expire(); + } + + private partial class TrianglesIntroSequence : CompositeDrawable + { + private readonly OsuLogo logo; + private readonly Action showBackgroundAction; + private OsuSpriteText welcomeText = null!; + + private Container logoContainerSecondary = null!; + private LazerLogo lazerLogo = null!; + + private Drawable triangles = null!; + + public Action LoadMenu = null!; + + [Resolved] + private OsuGameBase game { get; set; } = null!; + + public TrianglesIntroSequence(OsuLogo logo, Action showBackgroundAction) + { + this.logo = logo; + this.showBackgroundAction = showBackgroundAction; + } + + [BackgroundDependencyLoader] + private void load() + { + InternalChildren = new[] + { + welcomeText = new OsuSpriteText + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Padding = new MarginPadding { Bottom = 10 }, + Font = OsuFont.GetFont(weight: FontWeight.Light, size: 42), + Alpha = 1, + Spacing = new Vector2(5), + }, + logoContainerSecondary = new Container + { + RelativeSizeAxes = Axes.Both, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Child = lazerLogo = new LazerLogo + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre + } + }, + triangles = new CircularContainer + { + Alpha = 0, + Masking = true, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Size = new Vector2(960), + Child = new GlitchingTriangles + { + RelativeSizeAxes = Axes.Both, + }, + } + }; + } + + private static double getTimeForBeat(int beat) => offset + beat_length * beat; + + protected override void LoadComplete() + { + base.LoadComplete(); + + lazerLogo.Hide(); + + using (BeginAbsoluteSequence(0)) + { + using (BeginDelayedSequence(getTimeForBeat(-16))) + welcomeText.FadeIn().OnComplete(t => t.Text = "welcome to osu!"); + + using (BeginDelayedSequence(getTimeForBeat(-15))) + welcomeText.FadeIn().OnComplete(t => t.Text = ""); + + using (BeginDelayedSequence(getTimeForBeat(-14))) + welcomeText.FadeIn().OnComplete(t => t.Text = "welcome to osu!"); + + using (BeginDelayedSequence(getTimeForBeat(-13))) + welcomeText.FadeIn().OnComplete(t => t.Text = ""); + + using (BeginDelayedSequence(getTimeForBeat(-12))) + welcomeText.FadeIn().OnComplete(t => t.Text = "merry christmas!"); + + using (BeginDelayedSequence(getTimeForBeat(-11))) + welcomeText.FadeIn().OnComplete(t => t.Text = ""); + + using (BeginDelayedSequence(getTimeForBeat(-10))) + welcomeText.FadeIn().OnComplete(t => t.Text = "merry osumas!"); + + using (BeginDelayedSequence(getTimeForBeat(-9))) + { + welcomeText.FadeIn().OnComplete(t => t.Text = ""); + } + + lazerLogo.Scale = new Vector2(0.2f); + triangles.Scale = new Vector2(0.2f); + + for (int i = 0; i < 8; i++) + { + using (BeginDelayedSequence(getTimeForBeat(-8 + i))) + { + triangles.FadeIn(); + + lazerLogo.ScaleTo(new Vector2(0.2f + (i + 1) / 8f * 0.3f), beat_length * 1, Easing.OutQuint); + triangles.ScaleTo(new Vector2(0.2f + (i + 1) / 8f * 0.3f), beat_length * 1, Easing.OutQuint); + lazerLogo.FadeTo((i + 1) * 0.06f); + lazerLogo.TransformTo(nameof(LazerLogo.Progress), (i + 1) / 10f); + } + } + + GameWideFlash flash = new GameWideFlash(); + + using (BeginDelayedSequence(getTimeForBeat(-2))) + { + lazerLogo.FadeIn().OnComplete(_ => game.Add(flash)); + } + + flash.FadeInCompleted = () => + { + logoContainerSecondary.Remove(lazerLogo, true); + triangles.FadeOut(); + logo.FadeIn(); + showBackgroundAction(); + LoadMenu(); + }; + } + } + + private partial class GameWideFlash : Box + { + public Action? FadeInCompleted; + + public GameWideFlash() + { + Colour = Color4.White; + RelativeSizeAxes = Axes.Both; + Blending = BlendingParameters.Additive; + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + Alpha = 0; + + this.FadeTo(0.5f, beat_length * 2, Easing.In) + .OnComplete(_ => FadeInCompleted?.Invoke()); + + this.Delay(beat_length * 2) + .Then() + .FadeOutFromOne(3000, Easing.OutQuint); + } + } + + private partial class LazerLogo : CompositeDrawable + { + private LogoAnimation highlight = null!; + private LogoAnimation background = null!; + + public float Progress + { + get => background.AnimationProgress; + set + { + background.AnimationProgress = value; + highlight.AnimationProgress = value; + } + } + + public LazerLogo() + { + Size = new Vector2(960); + } + + [BackgroundDependencyLoader] + private void load(LargeTextureStore textures) + { + InternalChildren = new Drawable[] + { + highlight = new LogoAnimation + { + RelativeSizeAxes = Axes.Both, + Texture = textures.Get(@"Intro/Triangles/logo-highlight"), + Colour = Color4.White, + }, + background = new LogoAnimation + { + RelativeSizeAxes = Axes.Both, + Texture = textures.Get(@"Intro/Triangles/logo-background"), + Colour = OsuColour.Gray(0.6f), + }, + }; + } + } + + private partial class GlitchingTriangles : BeatSyncedContainer + { + private int beatsHandled; + + protected override void OnNewBeat(int beatIndex, TimingControlPoint timingPoint, EffectControlPoint effectPoint, ChannelAmplitudes amplitudes) + { + base.OnNewBeat(beatIndex, timingPoint, effectPoint, amplitudes); + + Divisor = beatsHandled < 4 ? 1 : 4; + + for (int i = 0; i < (beatsHandled + 1); i++) + { + float angle = (float)(RNG.NextDouble() * 2 * Math.PI); + float randomRadius = (float)(Math.Sqrt(RNG.NextDouble())); + + float x = 0.5f + 0.5f * randomRadius * (float)Math.Cos(angle); + float y = 0.5f + 0.5f * randomRadius * (float)Math.Sin(angle); + + Color4 christmasColour = RNG.NextBool() ? SeasonalUI.PRIMARY_COLOUR_1 : SeasonalUI.PRIMARY_COLOUR_2; + + Drawable triangle = new Triangle + { + Size = new Vector2(RNG.NextSingle() + 1.2f) * 80, + Origin = Anchor.Centre, + RelativePositionAxes = Axes.Both, + Position = new Vector2(x, y), + Colour = christmasColour + }; + + if (beatsHandled >= 10) + triangle.Blending = BlendingParameters.Additive; + + AddInternal(triangle); + triangle + .ScaleTo(0.9f) + .ScaleTo(1, beat_length / 2, Easing.Out); + triangle.FadeInFromZero(100, Easing.OutQuint); + } + + beatsHandled += 1; + } + } + } + } +} diff --git a/osu.Game/Screens/SeasonalUI.cs b/osu.Game/Screens/SeasonalUI.cs new file mode 100644 index 0000000000..ebe4d74301 --- /dev/null +++ b/osu.Game/Screens/SeasonalUI.cs @@ -0,0 +1,21 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Extensions.Color4Extensions; +using osuTK.Graphics; + +namespace osu.Game.Screens +{ + public static class SeasonalUI + { + public static readonly bool ENABLED = true; + + public static readonly Color4 PRIMARY_COLOUR_1 = Color4Extensions.FromHex("D32F2F"); + + public static readonly Color4 PRIMARY_COLOUR_2 = Color4Extensions.FromHex("388E3C"); + + public static readonly Color4 AMBIENT_COLOUR_1 = Color4Extensions.FromHex("FFC"); + + public static readonly Color4 AMBIENT_COLOUR_2 = Color4Extensions.FromHex("FFE4B5"); + } +} From 0954e0b0321d6872e16b73055a7b171f1cbbc9f9 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 19 Dec 2024 18:00:00 +0900 Subject: [PATCH 056/290] Add seasonal lighting Replaces kiai fountains for now. --- .../TestSceneMainMenuSeasonalLighting.cs | 46 +++++ osu.Game/Screens/Menu/MainMenu.cs | 4 +- .../Screens/Menu/MainMenuSeasonalLighting.cs | 188 ++++++++++++++++++ 3 files changed, 237 insertions(+), 1 deletion(-) create mode 100644 osu.Game.Tests/Visual/Menus/TestSceneMainMenuSeasonalLighting.cs create mode 100644 osu.Game/Screens/Menu/MainMenuSeasonalLighting.cs diff --git a/osu.Game.Tests/Visual/Menus/TestSceneMainMenuSeasonalLighting.cs b/osu.Game.Tests/Visual/Menus/TestSceneMainMenuSeasonalLighting.cs new file mode 100644 index 0000000000..bfdc07fba6 --- /dev/null +++ b/osu.Game.Tests/Visual/Menus/TestSceneMainMenuSeasonalLighting.cs @@ -0,0 +1,46 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Linq; +using NUnit.Framework; +using osu.Framework.Allocation; +using osu.Framework.Testing; +using osu.Game.Beatmaps; +using osu.Game.Database; +using osu.Game.Screens.Menu; + +namespace osu.Game.Tests.Visual.Menus +{ + public partial class TestSceneMainMenuSeasonalLighting : OsuTestScene + { + [Resolved] + private BeatmapManager beatmaps { get; set; } = null!; + + [Resolved] + private RealmAccess realm { get; set; } = null!; + + [SetUpSteps] + public void SetUpSteps() + { + AddStep("prepare beatmap", () => + { + var setInfo = beatmaps.QueryBeatmapSet(b => b.Protected && b.Hash == "7e26183e72a496f672c3a21292e6b469fdecd084d31c259ea10a31df5b46cd77"); + + Beatmap.Value = beatmaps.GetWorkingBeatmap(setInfo!.Value.Beatmaps.First()); + }); + + AddStep("create lighting", () => Child = new MainMenuSeasonalLighting()); + + AddStep("restart beatmap", () => + { + Beatmap.Value.Track.Start(); + Beatmap.Value.Track.Seek(4000); + }); + } + + [Test] + public void TestBasic() + { + } + } +} diff --git a/osu.Game/Screens/Menu/MainMenu.cs b/osu.Game/Screens/Menu/MainMenu.cs index 0630b9612e..42aa2342da 100644 --- a/osu.Game/Screens/Menu/MainMenu.cs +++ b/osu.Game/Screens/Menu/MainMenu.cs @@ -124,6 +124,7 @@ namespace osu.Game.Screens.Menu AddRangeInternal(new[] { + SeasonalUI.ENABLED ? new MainMenuSeasonalLighting() : Empty(), buttonsContainer = new ParallaxContainer { ParallaxAmount = 0.01f, @@ -166,7 +167,8 @@ namespace osu.Game.Screens.Menu Origin = Anchor.TopRight, Margin = new MarginPadding { Right = 15, Top = 5 } }, - new KiaiMenuFountains(), + // For now, this is too much alongside the seasonal lighting. + SeasonalUI.ENABLED ? Empty() : new KiaiMenuFountains(), bottomElementsFlow = new FillFlowContainer { AutoSizeAxes = Axes.Both, diff --git a/osu.Game/Screens/Menu/MainMenuSeasonalLighting.cs b/osu.Game/Screens/Menu/MainMenuSeasonalLighting.cs new file mode 100644 index 0000000000..7ba4e998d2 --- /dev/null +++ b/osu.Game/Screens/Menu/MainMenuSeasonalLighting.cs @@ -0,0 +1,188 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using osu.Framework.Allocation; +using osu.Framework.Bindables; +using osu.Framework.Extensions.Color4Extensions; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Effects; +using osu.Framework.Graphics.Shapes; +using osu.Framework.Timing; +using osu.Framework.Utils; +using osu.Game.Audio; +using osu.Game.Beatmaps; +using osu.Game.Rulesets; +using osu.Game.Rulesets.Objects; +using osu.Game.Rulesets.Objects.Types; +using osuTK; +using osuTK.Graphics; + +namespace osu.Game.Screens.Menu +{ + public partial class MainMenuSeasonalLighting : CompositeDrawable + { + private IBindable working = null!; + + private InterpolatingFramedClock beatmapClock = null!; + + private List hitObjects = null!; + + [Resolved] + private RulesetStore rulesets { get; set; } = null!; + + public MainMenuSeasonalLighting() + { + RelativeChildSize = new Vector2(512, 384); + + RelativeSizeAxes = Axes.X; + + Anchor = Anchor.Centre; + Origin = Anchor.Centre; + } + + [BackgroundDependencyLoader] + private void load(IBindable working) + { + this.working = working.GetBoundCopy(); + this.working.BindValueChanged(_ => Scheduler.AddOnce(updateBeatmap), true); + } + + private void updateBeatmap() + { + lastObjectIndex = null; + beatmapClock = new InterpolatingFramedClock(new FramedClock(working.Value.Track)); + hitObjects = working.Value.GetPlayableBeatmap(rulesets.GetRuleset(0)).HitObjects.SelectMany(h => h.NestedHitObjects.Prepend(h)) + .OrderBy(h => h.StartTime) + .ToList(); + } + + private int? lastObjectIndex; + + protected override void Update() + { + base.Update(); + + Height = DrawWidth / 16 * 10; + + beatmapClock.ProcessFrame(); + + // intentionally slightly early since we are doing fades on the lighting. + double time = beatmapClock.CurrentTime + 50; + + // handle seeks or OOB by skipping to current. + if (lastObjectIndex == null || lastObjectIndex >= hitObjects.Count || (lastObjectIndex >= 0 && hitObjects[lastObjectIndex.Value].StartTime > time) + || Math.Abs(beatmapClock.ElapsedFrameTime) > 500) + lastObjectIndex = hitObjects.Count(h => h.StartTime < time) - 1; + + while (lastObjectIndex < hitObjects.Count - 1) + { + var h = hitObjects[lastObjectIndex.Value + 1]; + + if (h.StartTime > time) + break; + + // Don't add lighting if the game is running too slow. + if (Clock.ElapsedFrameTime < 20) + addLight(h); + + lastObjectIndex++; + } + } + + private void addLight(HitObject h) + { + var light = new Light + { + RelativePositionAxes = Axes.Both, + Position = ((IHasPosition)h).Position + }; + + AddInternal(light); + + if (h.GetType().Name.Contains("Tick")) + { + light.Colour = SeasonalUI.AMBIENT_COLOUR_1; + light.Scale = new Vector2(0.5f); + light + .FadeInFromZero(250) + .Then() + .FadeOutFromOne(1000, Easing.Out); + + light.MoveToOffset(new Vector2(RNG.Next(-20, 20), RNG.Next(-20, 20)), 1400, Easing.Out); + } + else + { + // default green + Color4 col = SeasonalUI.PRIMARY_COLOUR_2; + + // whistle red + if (h.Samples.Any(s => s.Name == HitSampleInfo.HIT_WHISTLE)) + col = SeasonalUI.PRIMARY_COLOUR_1; + // clap is third colour + else if (h.Samples.Any(s => s.Name == HitSampleInfo.HIT_CLAP)) + col = SeasonalUI.AMBIENT_COLOUR_1; + + light.Colour = col; + + // finish larger lighting + if (h.Samples.Any(s => s.Name == HitSampleInfo.HIT_FINISH)) + light.Scale = new Vector2(3); + + light + .FadeInFromZero(150) + .Then() + .FadeOutFromOne(1000, Easing.In); + + light.Expire(); + } + } + + public partial class Light : CompositeDrawable + { + private readonly Circle circle; + + public new Color4 Colour + { + set + { + circle.Colour = value.Darken(0.8f); + circle.EdgeEffect = new EdgeEffectParameters + { + Type = EdgeEffectType.Glow, + Colour = value, + Radius = 80, + }; + } + } + + public Light() + { + InternalChildren = new Drawable[] + { + circle = new Circle + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Size = new Vector2(12), + Colour = SeasonalUI.AMBIENT_COLOUR_1, + Blending = BlendingParameters.Additive, + EdgeEffect = new EdgeEffectParameters + { + Type = EdgeEffectType.Glow, + Colour = SeasonalUI.AMBIENT_COLOUR_2, + Radius = 80, + } + } + }; + + Origin = Anchor.Centre; + Alpha = 0.5f; + } + } + } +} From 22f3831c0d46d11f7770c62c2dab4c2ee1132e36 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 19 Dec 2024 18:44:44 +0900 Subject: [PATCH 057/290] Add logo hat --- .../Visual/UserInterface/TestSceneOsuLogo.cs | 11 +++- osu.Game/Screens/Menu/OsuLogo.cs | 50 +++++++++++++++++++ 2 files changed, 60 insertions(+), 1 deletion(-) diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneOsuLogo.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneOsuLogo.cs index 62a493815b..c112d26870 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneOsuLogo.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneOsuLogo.cs @@ -4,22 +4,31 @@ using NUnit.Framework; using osu.Framework.Graphics; using osu.Game.Screens.Menu; +using osuTK; namespace osu.Game.Tests.Visual.UserInterface { public partial class TestSceneOsuLogo : OsuTestScene { + private OsuLogo? logo; + [Test] public void TestBasic() { AddStep("Add logo", () => { - Child = new OsuLogo + Child = logo = new OsuLogo { Anchor = Anchor.Centre, Origin = Anchor.Centre, }; }); + + AddSliderStep("scale", 0.1, 2, 1, scale => + { + if (logo != null) + Child.Scale = new Vector2((float)scale); + }); } } } diff --git a/osu.Game/Screens/Menu/OsuLogo.cs b/osu.Game/Screens/Menu/OsuLogo.cs index f3c37c6960..2c62a10a8f 100644 --- a/osu.Game/Screens/Menu/OsuLogo.cs +++ b/osu.Game/Screens/Menu/OsuLogo.cs @@ -4,6 +4,7 @@ #nullable disable using System; +using JetBrains.Annotations; using osu.Framework.Allocation; using osu.Framework.Audio; using osu.Framework.Audio.Sample; @@ -211,6 +212,15 @@ namespace osu.Game.Screens.Menu Anchor = Anchor.Centre, Origin = Anchor.Centre, }, + SeasonalUI.ENABLED + ? hat = new Sprite + { + BypassAutoSizeAxes = Axes.Both, + Alpha = 0, + Origin = Anchor.BottomCentre, + Scale = new Vector2(-1, 1), + } + : Empty(), } }, impactContainer = new CircularContainer @@ -284,6 +294,8 @@ namespace osu.Game.Screens.Menu logo.Texture = textures.Get(@"Menu/logo"); ripple.Texture = textures.Get(@"Menu/logo"); + if (hat != null) + hat.Texture = textures.Get(@"Menu/hat"); } private int lastBeatIndex; @@ -369,6 +381,9 @@ namespace osu.Game.Screens.Menu const float scale_adjust_cutoff = 0.4f; + if (SeasonalUI.ENABLED) + updateHat(); + if (musicController.CurrentTrack.IsRunning) { float maxAmplitude = lastBeatIndex >= 0 ? musicController.CurrentTrack.CurrentAmplitudes.Maximum : 0; @@ -382,6 +397,38 @@ namespace osu.Game.Screens.Menu } } + private bool hasHat; + + private void updateHat() + { + if (hat == null) + return; + + bool shouldHat = DrawWidth * Scale.X < 400; + + if (shouldHat != hasHat) + { + hasHat = shouldHat; + + if (hasHat) + { + hat.Delay(400) + .Then() + .MoveTo(new Vector2(120, 160)) + .RotateTo(0) + .RotateTo(-20, 500, Easing.OutQuint) + .FadeIn(250, Easing.OutQuint); + } + else + { + hat.Delay(100) + .Then() + .MoveToOffset(new Vector2(0, -5), 500, Easing.OutQuint) + .FadeOut(500, Easing.OutQuint); + } + } + } + public override bool HandlePositionalInput => base.HandlePositionalInput && Alpha > 0.2f; protected override bool OnMouseDown(MouseDownEvent e) @@ -459,6 +506,9 @@ namespace osu.Game.Screens.Menu private Container currentProxyTarget; private Drawable proxy; + [CanBeNull] + private readonly Sprite hat; + public void StopSamplePlayback() => sampleClickChannel?.Stop(); public Drawable ProxyToContainer(Container c) From 4924a35c3133345ebc314d1fea03c8c69d8665c4 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 19 Dec 2024 19:14:48 +0900 Subject: [PATCH 058/290] Fix light expiry --- osu.Game/Screens/Menu/MainMenuSeasonalLighting.cs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/osu.Game/Screens/Menu/MainMenuSeasonalLighting.cs b/osu.Game/Screens/Menu/MainMenuSeasonalLighting.cs index 7ba4e998d2..fb16e8e0bb 100644 --- a/osu.Game/Screens/Menu/MainMenuSeasonalLighting.cs +++ b/osu.Game/Screens/Menu/MainMenuSeasonalLighting.cs @@ -3,7 +3,6 @@ using System; using System.Collections.Generic; -using System.Diagnostics; using System.Linq; using osu.Framework.Allocation; using osu.Framework.Bindables; @@ -137,9 +136,9 @@ namespace osu.Game.Screens.Menu .FadeInFromZero(150) .Then() .FadeOutFromOne(1000, Easing.In); - - light.Expire(); } + + light.Expire(); } public partial class Light : CompositeDrawable From 8c7af79f9667e1cd4db2e1ec3f480f98542b5945 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 20 Dec 2024 14:21:45 +0900 Subject: [PATCH 059/290] Tidy up for pull request attempt --- .../TestSceneMainMenuSeasonalLighting.cs | 6 +-- osu.Game/Screens/Menu/IntroChristmas.cs | 5 ++- .../Screens/Menu/MainMenuSeasonalLighting.cs | 38 +++++++++++++------ osu.Game/Screens/Menu/OsuLogo.cs | 2 +- osu.Game/Screens/SeasonalUI.cs | 8 ++-- 5 files changed, 37 insertions(+), 22 deletions(-) diff --git a/osu.Game.Tests/Visual/Menus/TestSceneMainMenuSeasonalLighting.cs b/osu.Game.Tests/Visual/Menus/TestSceneMainMenuSeasonalLighting.cs index bfdc07fba6..81862da9df 100644 --- a/osu.Game.Tests/Visual/Menus/TestSceneMainMenuSeasonalLighting.cs +++ b/osu.Game.Tests/Visual/Menus/TestSceneMainMenuSeasonalLighting.cs @@ -6,7 +6,6 @@ using NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Testing; using osu.Game.Beatmaps; -using osu.Game.Database; using osu.Game.Screens.Menu; namespace osu.Game.Tests.Visual.Menus @@ -16,15 +15,12 @@ namespace osu.Game.Tests.Visual.Menus [Resolved] private BeatmapManager beatmaps { get; set; } = null!; - [Resolved] - private RealmAccess realm { get; set; } = null!; - [SetUpSteps] public void SetUpSteps() { AddStep("prepare beatmap", () => { - var setInfo = beatmaps.QueryBeatmapSet(b => b.Protected && b.Hash == "7e26183e72a496f672c3a21292e6b469fdecd084d31c259ea10a31df5b46cd77"); + var setInfo = beatmaps.QueryBeatmapSet(b => b.Protected && b.Hash == IntroChristmas.CHRISTMAS_BEATMAP_SET_HASH); Beatmap.Value = beatmaps.GetWorkingBeatmap(setInfo!.Value.Beatmaps.First()); }); diff --git a/osu.Game/Screens/Menu/IntroChristmas.cs b/osu.Game/Screens/Menu/IntroChristmas.cs index 0a1cf32b85..273baa3c52 100644 --- a/osu.Game/Screens/Menu/IntroChristmas.cs +++ b/osu.Game/Screens/Menu/IntroChristmas.cs @@ -22,7 +22,10 @@ namespace osu.Game.Screens.Menu { public partial class IntroChristmas : IntroScreen { - protected override string BeatmapHash => "7e26183e72a496f672c3a21292e6b469fdecd084d31c259ea10a31df5b46cd77"; + // nekodex - circle the halls + public const string CHRISTMAS_BEATMAP_SET_HASH = "7e26183e72a496f672c3a21292e6b469fdecd084d31c259ea10a31df5b46cd77"; + + protected override string BeatmapHash => CHRISTMAS_BEATMAP_SET_HASH; protected override string BeatmapFile => "christmas2024.osz"; diff --git a/osu.Game/Screens/Menu/MainMenuSeasonalLighting.cs b/osu.Game/Screens/Menu/MainMenuSeasonalLighting.cs index fb16e8e0bb..f46a1387ab 100644 --- a/osu.Game/Screens/Menu/MainMenuSeasonalLighting.cs +++ b/osu.Game/Screens/Menu/MainMenuSeasonalLighting.cs @@ -31,11 +31,13 @@ namespace osu.Game.Screens.Menu private List hitObjects = null!; - [Resolved] - private RulesetStore rulesets { get; set; } = null!; + private RulesetInfo? osuRuleset; + + private int? lastObjectIndex; public MainMenuSeasonalLighting() { + // match beatmap playfield RelativeChildSize = new Vector2(512, 384); RelativeSizeAxes = Axes.X; @@ -45,23 +47,37 @@ namespace osu.Game.Screens.Menu } [BackgroundDependencyLoader] - private void load(IBindable working) + private void load(IBindable working, RulesetStore rulesets) { this.working = working.GetBoundCopy(); this.working.BindValueChanged(_ => Scheduler.AddOnce(updateBeatmap), true); + + // operate in osu! ruleset to keep things simple for now. + osuRuleset = rulesets.GetRuleset(0); } private void updateBeatmap() { lastObjectIndex = null; + + if (osuRuleset == null) + { + beatmapClock = new InterpolatingFramedClock(Clock); + hitObjects = new List(); + return; + } + + // Intentionally maintain separately so the lighting is not in audio clock space (it shouldn't rewind etc.) beatmapClock = new InterpolatingFramedClock(new FramedClock(working.Value.Track)); - hitObjects = working.Value.GetPlayableBeatmap(rulesets.GetRuleset(0)).HitObjects.SelectMany(h => h.NestedHitObjects.Prepend(h)) + + hitObjects = working.Value + .GetPlayableBeatmap(osuRuleset) + .HitObjects + .SelectMany(h => h.NestedHitObjects.Prepend(h)) .OrderBy(h => h.StartTime) .ToList(); } - private int? lastObjectIndex; - protected override void Update() { base.Update(); @@ -116,19 +132,19 @@ namespace osu.Game.Screens.Menu } else { - // default green + // default are green Color4 col = SeasonalUI.PRIMARY_COLOUR_2; - // whistle red + // whistles are red if (h.Samples.Any(s => s.Name == HitSampleInfo.HIT_WHISTLE)) col = SeasonalUI.PRIMARY_COLOUR_1; - // clap is third colour + // clap is third ambient (yellow) colour else if (h.Samples.Any(s => s.Name == HitSampleInfo.HIT_CLAP)) col = SeasonalUI.AMBIENT_COLOUR_1; light.Colour = col; - // finish larger lighting + // finish results in larger lighting if (h.Samples.Any(s => s.Name == HitSampleInfo.HIT_FINISH)) light.Scale = new Vector2(3); @@ -141,7 +157,7 @@ namespace osu.Game.Screens.Menu light.Expire(); } - public partial class Light : CompositeDrawable + private partial class Light : CompositeDrawable { private readonly Circle circle; diff --git a/osu.Game/Screens/Menu/OsuLogo.cs b/osu.Game/Screens/Menu/OsuLogo.cs index 2c62a10a8f..272f53e087 100644 --- a/osu.Game/Screens/Menu/OsuLogo.cs +++ b/osu.Game/Screens/Menu/OsuLogo.cs @@ -163,7 +163,7 @@ namespace osu.Game.Screens.Menu new Container { AutoSizeAxes = Axes.Both, - Children = new Drawable[] + Children = new[] { logoContainer = new CircularContainer { diff --git a/osu.Game/Screens/SeasonalUI.cs b/osu.Game/Screens/SeasonalUI.cs index ebe4d74301..fc2303f285 100644 --- a/osu.Game/Screens/SeasonalUI.cs +++ b/osu.Game/Screens/SeasonalUI.cs @@ -10,12 +10,12 @@ namespace osu.Game.Screens { public static readonly bool ENABLED = true; - public static readonly Color4 PRIMARY_COLOUR_1 = Color4Extensions.FromHex("D32F2F"); + public static readonly Color4 PRIMARY_COLOUR_1 = Color4Extensions.FromHex(@"D32F2F"); - public static readonly Color4 PRIMARY_COLOUR_2 = Color4Extensions.FromHex("388E3C"); + public static readonly Color4 PRIMARY_COLOUR_2 = Color4Extensions.FromHex(@"388E3C"); - public static readonly Color4 AMBIENT_COLOUR_1 = Color4Extensions.FromHex("FFC"); + public static readonly Color4 AMBIENT_COLOUR_1 = Color4Extensions.FromHex(@"FFFFCC"); - public static readonly Color4 AMBIENT_COLOUR_2 = Color4Extensions.FromHex("FFE4B5"); + public static readonly Color4 AMBIENT_COLOUR_2 = Color4Extensions.FromHex(@"FFE4B5"); } } From e5dbf9ce453e359a2e07b375ba9cbdcbe159b764 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 20 Dec 2024 14:46:34 +0900 Subject: [PATCH 060/290] Subclass osu logo instead of adding much code to it --- .../TestSceneMainMenuSeasonalLighting.cs | 1 + .../Visual/UserInterface/TestSceneOsuLogo.cs | 29 ++++++- osu.Game/OsuGame.cs | 6 +- osu.Game/Screens/Loader.cs | 3 +- osu.Game/Screens/Menu/IntroChristmas.cs | 3 +- osu.Game/Screens/Menu/MainMenu.cs | 5 +- .../Screens/Menu/MenuLogoVisualisation.cs | 5 +- osu.Game/Screens/Menu/MenuSideFlashes.cs | 11 +-- osu.Game/Screens/Menu/OsuLogo.cs | 83 ++++--------------- .../MainMenuSeasonalLighting.cs | 14 ++-- osu.Game/Seasonal/OsuLogoChristmas.cs | 74 +++++++++++++++++ .../SeasonalUIConfig.cs} | 7 +- 12 files changed, 148 insertions(+), 93 deletions(-) rename osu.Game/{Screens/Menu => Seasonal}/MainMenuSeasonalLighting.cs (93%) create mode 100644 osu.Game/Seasonal/OsuLogoChristmas.cs rename osu.Game/{Screens/SeasonalUI.cs => Seasonal/SeasonalUIConfig.cs} (78%) diff --git a/osu.Game.Tests/Visual/Menus/TestSceneMainMenuSeasonalLighting.cs b/osu.Game.Tests/Visual/Menus/TestSceneMainMenuSeasonalLighting.cs index 81862da9df..bf499f1beb 100644 --- a/osu.Game.Tests/Visual/Menus/TestSceneMainMenuSeasonalLighting.cs +++ b/osu.Game.Tests/Visual/Menus/TestSceneMainMenuSeasonalLighting.cs @@ -7,6 +7,7 @@ using osu.Framework.Allocation; using osu.Framework.Testing; using osu.Game.Beatmaps; using osu.Game.Screens.Menu; +using osu.Game.Seasonal; namespace osu.Game.Tests.Visual.Menus { diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneOsuLogo.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneOsuLogo.cs index c112d26870..27d2ff97fa 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneOsuLogo.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneOsuLogo.cs @@ -4,6 +4,7 @@ using NUnit.Framework; using osu.Framework.Graphics; using osu.Game.Screens.Menu; +using osu.Game.Seasonal; using osuTK; namespace osu.Game.Tests.Visual.UserInterface @@ -12,6 +13,19 @@ namespace osu.Game.Tests.Visual.UserInterface { private OsuLogo? logo; + private float scale = 1; + + protected override void LoadComplete() + { + base.LoadComplete(); + + AddSliderStep("scale", 0.1, 2, 1, scale => + { + if (logo != null) + Child.Scale = new Vector2(this.scale = (float)scale); + }); + } + [Test] public void TestBasic() { @@ -21,13 +35,22 @@ namespace osu.Game.Tests.Visual.UserInterface { Anchor = Anchor.Centre, Origin = Anchor.Centre, + Scale = new Vector2(scale), }; }); + } - AddSliderStep("scale", 0.1, 2, 1, scale => + [Test] + public void TestChristmas() + { + AddStep("Add logo", () => { - if (logo != null) - Child.Scale = new Vector2((float)scale); + Child = logo = new OsuLogoChristmas + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Scale = new Vector2(scale), + }; }); } } diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs index e808e570c7..0dd1746aa4 100644 --- a/osu.Game/OsuGame.cs +++ b/osu.Game/OsuGame.cs @@ -69,6 +69,7 @@ using osu.Game.Screens.OnlinePlay.Multiplayer; using osu.Game.Screens.Play; using osu.Game.Screens.Ranking; using osu.Game.Screens.Select; +using osu.Game.Seasonal; using osu.Game.Skinning; using osu.Game.Updater; using osu.Game.Users; @@ -362,7 +363,10 @@ namespace osu.Game { SentryLogger.AttachUser(API.LocalUser); - dependencies.Cache(osuLogo = new OsuLogo { Alpha = 0 }); + if (SeasonalUIConfig.ENABLED) + dependencies.CacheAs(osuLogo = new OsuLogoChristmas { Alpha = 0 }); + else + dependencies.CacheAs(osuLogo = new OsuLogo { Alpha = 0 }); // bind config int to database RulesetInfo configRuleset = LocalConfig.GetBindable(OsuSetting.Ruleset); diff --git a/osu.Game/Screens/Loader.cs b/osu.Game/Screens/Loader.cs index 811e4600eb..dfa5d2c369 100644 --- a/osu.Game/Screens/Loader.cs +++ b/osu.Game/Screens/Loader.cs @@ -15,6 +15,7 @@ using osu.Framework.Screens; using osu.Framework.Threading; using osu.Game.Configuration; using osu.Game.Graphics.UserInterface; +using osu.Game.Seasonal; using IntroSequence = osu.Game.Configuration.IntroSequence; namespace osu.Game.Screens @@ -37,7 +38,7 @@ namespace osu.Game.Screens private IntroScreen getIntroSequence() { - if (SeasonalUI.ENABLED) + if (SeasonalUIConfig.ENABLED) return new IntroChristmas(createMainMenu); if (introSequence == IntroSequence.Random) diff --git a/osu.Game/Screens/Menu/IntroChristmas.cs b/osu.Game/Screens/Menu/IntroChristmas.cs index 273baa3c52..aa16f33c3d 100644 --- a/osu.Game/Screens/Menu/IntroChristmas.cs +++ b/osu.Game/Screens/Menu/IntroChristmas.cs @@ -15,6 +15,7 @@ using osu.Game.Beatmaps.ControlPoints; using osu.Game.Graphics; using osu.Game.Graphics.Containers; using osu.Game.Graphics.Sprites; +using osu.Game.Seasonal; using osuTK; using osuTK.Graphics; @@ -302,7 +303,7 @@ namespace osu.Game.Screens.Menu float x = 0.5f + 0.5f * randomRadius * (float)Math.Cos(angle); float y = 0.5f + 0.5f * randomRadius * (float)Math.Sin(angle); - Color4 christmasColour = RNG.NextBool() ? SeasonalUI.PRIMARY_COLOUR_1 : SeasonalUI.PRIMARY_COLOUR_2; + Color4 christmasColour = RNG.NextBool() ? SeasonalUIConfig.PRIMARY_COLOUR_1 : SeasonalUIConfig.PRIMARY_COLOUR_2; Drawable triangle = new Triangle { diff --git a/osu.Game/Screens/Menu/MainMenu.cs b/osu.Game/Screens/Menu/MainMenu.cs index 42aa2342da..a4b269ad0d 100644 --- a/osu.Game/Screens/Menu/MainMenu.cs +++ b/osu.Game/Screens/Menu/MainMenu.cs @@ -35,6 +35,7 @@ using osu.Game.Screens.OnlinePlay.DailyChallenge; using osu.Game.Screens.OnlinePlay.Multiplayer; using osu.Game.Screens.OnlinePlay.Playlists; using osu.Game.Screens.Select; +using osu.Game.Seasonal; using osuTK; using osuTK.Graphics; @@ -124,7 +125,7 @@ namespace osu.Game.Screens.Menu AddRangeInternal(new[] { - SeasonalUI.ENABLED ? new MainMenuSeasonalLighting() : Empty(), + SeasonalUIConfig.ENABLED ? new MainMenuSeasonalLighting() : Empty(), buttonsContainer = new ParallaxContainer { ParallaxAmount = 0.01f, @@ -168,7 +169,7 @@ namespace osu.Game.Screens.Menu Margin = new MarginPadding { Right = 15, Top = 5 } }, // For now, this is too much alongside the seasonal lighting. - SeasonalUI.ENABLED ? Empty() : new KiaiMenuFountains(), + SeasonalUIConfig.ENABLED ? Empty() : new KiaiMenuFountains(), bottomElementsFlow = new FillFlowContainer { AutoSizeAxes = Axes.Both, diff --git a/osu.Game/Screens/Menu/MenuLogoVisualisation.cs b/osu.Game/Screens/Menu/MenuLogoVisualisation.cs index 4537b79b62..32b5c706a3 100644 --- a/osu.Game/Screens/Menu/MenuLogoVisualisation.cs +++ b/osu.Game/Screens/Menu/MenuLogoVisualisation.cs @@ -7,6 +7,7 @@ using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Game.Online.API; using osu.Game.Online.API.Requests.Responses; +using osu.Game.Seasonal; using osu.Game.Skinning; using osuTK.Graphics; @@ -29,8 +30,8 @@ namespace osu.Game.Screens.Menu private void updateColour() { - if (SeasonalUI.ENABLED) - Colour = SeasonalUI.AMBIENT_COLOUR_1; + if (SeasonalUIConfig.ENABLED) + Colour = SeasonalUIConfig.AMBIENT_COLOUR_1; else if (user.Value?.IsSupporter ?? false) Colour = skin.Value.GetConfig(GlobalSkinColours.MenuGlow)?.Value ?? Color4.White; else diff --git a/osu.Game/Screens/Menu/MenuSideFlashes.cs b/osu.Game/Screens/Menu/MenuSideFlashes.cs index cc2d22a7fa..808da5dd47 100644 --- a/osu.Game/Screens/Menu/MenuSideFlashes.cs +++ b/osu.Game/Screens/Menu/MenuSideFlashes.cs @@ -18,6 +18,7 @@ using osu.Game.Graphics; using osu.Game.Graphics.Containers; using osu.Game.Online.API; using osu.Game.Online.API.Requests.Responses; +using osu.Game.Seasonal; using osu.Game.Skinning; using osuTK.Graphics; @@ -68,7 +69,7 @@ namespace osu.Game.Screens.Menu Anchor = Anchor.CentreLeft, Origin = Anchor.CentreLeft, RelativeSizeAxes = Axes.Y, - Width = box_width * (SeasonalUI.ENABLED ? 4 : 2), + Width = box_width * (SeasonalUIConfig.ENABLED ? 4 : 2), Height = 1.5f, // align off-screen to make sure our edges don't become visible during parallax. X = -box_width, @@ -80,7 +81,7 @@ namespace osu.Game.Screens.Menu Anchor = Anchor.CentreRight, Origin = Anchor.CentreRight, RelativeSizeAxes = Axes.Y, - Width = box_width * (SeasonalUI.ENABLED ? 4 : 2), + Width = box_width * (SeasonalUIConfig.ENABLED ? 4 : 2), Height = 1.5f, X = box_width, Alpha = 0, @@ -105,7 +106,7 @@ namespace osu.Game.Screens.Menu private void flash(Drawable d, double beatLength, bool kiai, ChannelAmplitudes amplitudes) { - if (SeasonalUI.ENABLED) + if (SeasonalUIConfig.ENABLED) updateColour(); d.FadeTo(Math.Clamp(0.1f + ((ReferenceEquals(d, leftBox) ? amplitudes.LeftChannel : amplitudes.RightChannel) - amplitude_dead_zone) / (kiai ? kiai_multiplier : alpha_multiplier), 0.1f, 1), @@ -118,8 +119,8 @@ namespace osu.Game.Screens.Menu { Color4 baseColour = colours.Blue; - if (SeasonalUI.ENABLED) - baseColour = RNG.NextBool() ? SeasonalUI.PRIMARY_COLOUR_1 : SeasonalUI.PRIMARY_COLOUR_2; + if (SeasonalUIConfig.ENABLED) + baseColour = RNG.NextBool() ? SeasonalUIConfig.PRIMARY_COLOUR_1 : SeasonalUIConfig.PRIMARY_COLOUR_2; else if (user.Value?.IsSupporter ?? false) baseColour = skin.Value.GetConfig(GlobalSkinColours.MenuGlow)?.Value ?? baseColour; diff --git a/osu.Game/Screens/Menu/OsuLogo.cs b/osu.Game/Screens/Menu/OsuLogo.cs index 272f53e087..dc2dfefddb 100644 --- a/osu.Game/Screens/Menu/OsuLogo.cs +++ b/osu.Game/Screens/Menu/OsuLogo.cs @@ -4,7 +4,6 @@ #nullable disable using System; -using JetBrains.Annotations; using osu.Framework.Allocation; using osu.Framework.Audio; using osu.Framework.Audio.Sample; @@ -54,8 +53,10 @@ namespace osu.Game.Screens.Menu private Sample sampleClick; private SampleChannel sampleClickChannel; - private Sample sampleBeat; - private Sample sampleDownbeat; + protected virtual double BeatSampleVariance => 0.1; + + protected Sample SampleBeat; + protected Sample SampleDownbeat; private readonly Container colourAndTriangles; private readonly TrianglesV2 triangles; @@ -160,10 +161,10 @@ namespace osu.Game.Screens.Menu Alpha = visualizer_default_alpha, Size = SCALE_ADJUST }, - new Container + LogoElements = new Container { AutoSizeAxes = Axes.Both, - Children = new[] + Children = new Drawable[] { logoContainer = new CircularContainer { @@ -212,15 +213,6 @@ namespace osu.Game.Screens.Menu Anchor = Anchor.Centre, Origin = Anchor.Centre, }, - SeasonalUI.ENABLED - ? hat = new Sprite - { - BypassAutoSizeAxes = Axes.Both, - Alpha = 0, - Origin = Anchor.BottomCentre, - Scale = new Vector2(-1, 1), - } - : Empty(), } }, impactContainer = new CircularContainer @@ -253,6 +245,8 @@ namespace osu.Game.Screens.Menu }; } + public Container LogoElements { get; private set; } + /// /// Schedule a new external animation. Handled queueing and finishing previous animations in a sane way. /// @@ -282,20 +276,11 @@ namespace osu.Game.Screens.Menu { sampleClick = audio.Samples.Get(@"Menu/osu-logo-select"); - if (SeasonalUI.ENABLED) - { - sampleDownbeat = sampleBeat = audio.Samples.Get(@"Menu/osu-logo-heartbeat-bell"); - } - else - { - sampleBeat = audio.Samples.Get(@"Menu/osu-logo-heartbeat"); - sampleDownbeat = audio.Samples.Get(@"Menu/osu-logo-downbeat"); - } + SampleBeat = audio.Samples.Get(@"Menu/osu-logo-heartbeat"); + SampleDownbeat = audio.Samples.Get(@"Menu/osu-logo-downbeat"); logo.Texture = textures.Get(@"Menu/logo"); ripple.Texture = textures.Get(@"Menu/logo"); - if (hat != null) - hat.Texture = textures.Get(@"Menu/hat"); } private int lastBeatIndex; @@ -318,15 +303,13 @@ namespace osu.Game.Screens.Menu { if (beatIndex % timingPoint.TimeSignature.Numerator == 0) { - sampleDownbeat?.Play(); + SampleDownbeat?.Play(); } else { - var channel = sampleBeat.GetChannel(); - if (SeasonalUI.ENABLED) - channel.Frequency.Value = 0.99 + RNG.NextDouble(0.02); - else - channel.Frequency.Value = 0.95 + RNG.NextDouble(0.1); + var channel = SampleBeat.GetChannel(); + + channel.Frequency.Value = 1 - BeatSampleVariance / 2 + RNG.NextDouble(BeatSampleVariance); channel.Play(); } }); @@ -381,9 +364,6 @@ namespace osu.Game.Screens.Menu const float scale_adjust_cutoff = 0.4f; - if (SeasonalUI.ENABLED) - updateHat(); - if (musicController.CurrentTrack.IsRunning) { float maxAmplitude = lastBeatIndex >= 0 ? musicController.CurrentTrack.CurrentAmplitudes.Maximum : 0; @@ -397,38 +377,6 @@ namespace osu.Game.Screens.Menu } } - private bool hasHat; - - private void updateHat() - { - if (hat == null) - return; - - bool shouldHat = DrawWidth * Scale.X < 400; - - if (shouldHat != hasHat) - { - hasHat = shouldHat; - - if (hasHat) - { - hat.Delay(400) - .Then() - .MoveTo(new Vector2(120, 160)) - .RotateTo(0) - .RotateTo(-20, 500, Easing.OutQuint) - .FadeIn(250, Easing.OutQuint); - } - else - { - hat.Delay(100) - .Then() - .MoveToOffset(new Vector2(0, -5), 500, Easing.OutQuint) - .FadeOut(500, Easing.OutQuint); - } - } - } - public override bool HandlePositionalInput => base.HandlePositionalInput && Alpha > 0.2f; protected override bool OnMouseDown(MouseDownEvent e) @@ -506,9 +454,6 @@ namespace osu.Game.Screens.Menu private Container currentProxyTarget; private Drawable proxy; - [CanBeNull] - private readonly Sprite hat; - public void StopSamplePlayback() => sampleClickChannel?.Stop(); public Drawable ProxyToContainer(Container c) diff --git a/osu.Game/Screens/Menu/MainMenuSeasonalLighting.cs b/osu.Game/Seasonal/MainMenuSeasonalLighting.cs similarity index 93% rename from osu.Game/Screens/Menu/MainMenuSeasonalLighting.cs rename to osu.Game/Seasonal/MainMenuSeasonalLighting.cs index f46a1387ab..a382785499 100644 --- a/osu.Game/Screens/Menu/MainMenuSeasonalLighting.cs +++ b/osu.Game/Seasonal/MainMenuSeasonalLighting.cs @@ -21,7 +21,7 @@ using osu.Game.Rulesets.Objects.Types; using osuTK; using osuTK.Graphics; -namespace osu.Game.Screens.Menu +namespace osu.Game.Seasonal { public partial class MainMenuSeasonalLighting : CompositeDrawable { @@ -121,7 +121,7 @@ namespace osu.Game.Screens.Menu if (h.GetType().Name.Contains("Tick")) { - light.Colour = SeasonalUI.AMBIENT_COLOUR_1; + light.Colour = SeasonalUIConfig.AMBIENT_COLOUR_1; light.Scale = new Vector2(0.5f); light .FadeInFromZero(250) @@ -133,14 +133,14 @@ namespace osu.Game.Screens.Menu else { // default are green - Color4 col = SeasonalUI.PRIMARY_COLOUR_2; + Color4 col = SeasonalUIConfig.PRIMARY_COLOUR_2; // whistles are red if (h.Samples.Any(s => s.Name == HitSampleInfo.HIT_WHISTLE)) - col = SeasonalUI.PRIMARY_COLOUR_1; + col = SeasonalUIConfig.PRIMARY_COLOUR_1; // clap is third ambient (yellow) colour else if (h.Samples.Any(s => s.Name == HitSampleInfo.HIT_CLAP)) - col = SeasonalUI.AMBIENT_COLOUR_1; + col = SeasonalUIConfig.AMBIENT_COLOUR_1; light.Colour = col; @@ -184,12 +184,12 @@ namespace osu.Game.Screens.Menu Anchor = Anchor.Centre, Origin = Anchor.Centre, Size = new Vector2(12), - Colour = SeasonalUI.AMBIENT_COLOUR_1, + Colour = SeasonalUIConfig.AMBIENT_COLOUR_1, Blending = BlendingParameters.Additive, EdgeEffect = new EdgeEffectParameters { Type = EdgeEffectType.Glow, - Colour = SeasonalUI.AMBIENT_COLOUR_2, + Colour = SeasonalUIConfig.AMBIENT_COLOUR_2, Radius = 80, } } diff --git a/osu.Game/Seasonal/OsuLogoChristmas.cs b/osu.Game/Seasonal/OsuLogoChristmas.cs new file mode 100644 index 0000000000..ec9cac94ea --- /dev/null +++ b/osu.Game/Seasonal/OsuLogoChristmas.cs @@ -0,0 +1,74 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Allocation; +using osu.Framework.Audio; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Sprites; +using osu.Framework.Graphics.Textures; +using osu.Game.Screens.Menu; +using osuTK; + +namespace osu.Game.Seasonal +{ + public partial class OsuLogoChristmas : OsuLogo + { + protected override double BeatSampleVariance => 0.02; + + private Sprite? hat; + + private bool hasHat; + + [BackgroundDependencyLoader] + private void load(TextureStore textures, AudioManager audio) + { + LogoElements.Add(hat = new Sprite + { + BypassAutoSizeAxes = Axes.Both, + Alpha = 0, + Origin = Anchor.BottomCentre, + Scale = new Vector2(-1, 1), + Texture = textures.Get(@"Menu/hat"), + }); + + // override base samples with our preferred ones. + SampleDownbeat = SampleBeat = audio.Samples.Get(@"Menu/osu-logo-heartbeat-bell"); + } + + protected override void Update() + { + base.Update(); + updateHat(); + } + + private void updateHat() + { + if (hat == null) + return; + + bool shouldHat = DrawWidth * Scale.X < 400; + + if (shouldHat != hasHat) + { + hasHat = shouldHat; + + if (hasHat) + { + hat.Delay(400) + .Then() + .MoveTo(new Vector2(120, 160)) + .RotateTo(0) + .RotateTo(-20, 500, Easing.OutQuint) + .FadeIn(250, Easing.OutQuint); + } + else + { + hat.Delay(100) + .Then() + .MoveToOffset(new Vector2(0, -5), 500, Easing.OutQuint) + .FadeOut(500, Easing.OutQuint); + } + } + } + } +} diff --git a/osu.Game/Screens/SeasonalUI.cs b/osu.Game/Seasonal/SeasonalUIConfig.cs similarity index 78% rename from osu.Game/Screens/SeasonalUI.cs rename to osu.Game/Seasonal/SeasonalUIConfig.cs index fc2303f285..060913a8bf 100644 --- a/osu.Game/Screens/SeasonalUI.cs +++ b/osu.Game/Seasonal/SeasonalUIConfig.cs @@ -4,9 +4,12 @@ using osu.Framework.Extensions.Color4Extensions; using osuTK.Graphics; -namespace osu.Game.Screens +namespace osu.Game.Seasonal { - public static class SeasonalUI + /// + /// General configuration setting for seasonal event adjustments to the game. + /// + public static class SeasonalUIConfig { public static readonly bool ENABLED = true; From 2a720ef200897f0430a630d2d565ab52c8875278 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 20 Dec 2024 14:51:33 +0900 Subject: [PATCH 061/290] Move christmas intro screen to seasonal namespace --- osu.Game.Tests/Visual/Menus/TestSceneIntroChristmas.cs | 1 + .../Visual/Menus/TestSceneMainMenuSeasonalLighting.cs | 1 - osu.Game/{Screens/Menu => Seasonal}/IntroChristmas.cs | 4 ++-- 3 files changed, 3 insertions(+), 3 deletions(-) rename osu.Game/{Screens/Menu => Seasonal}/IntroChristmas.cs (99%) diff --git a/osu.Game.Tests/Visual/Menus/TestSceneIntroChristmas.cs b/osu.Game.Tests/Visual/Menus/TestSceneIntroChristmas.cs index 13377f49df..0398b4fbb6 100644 --- a/osu.Game.Tests/Visual/Menus/TestSceneIntroChristmas.cs +++ b/osu.Game.Tests/Visual/Menus/TestSceneIntroChristmas.cs @@ -3,6 +3,7 @@ using NUnit.Framework; using osu.Game.Screens.Menu; +using osu.Game.Seasonal; namespace osu.Game.Tests.Visual.Menus { diff --git a/osu.Game.Tests/Visual/Menus/TestSceneMainMenuSeasonalLighting.cs b/osu.Game.Tests/Visual/Menus/TestSceneMainMenuSeasonalLighting.cs index bf499f1beb..11356f7eeb 100644 --- a/osu.Game.Tests/Visual/Menus/TestSceneMainMenuSeasonalLighting.cs +++ b/osu.Game.Tests/Visual/Menus/TestSceneMainMenuSeasonalLighting.cs @@ -6,7 +6,6 @@ using NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Testing; using osu.Game.Beatmaps; -using osu.Game.Screens.Menu; using osu.Game.Seasonal; namespace osu.Game.Tests.Visual.Menus diff --git a/osu.Game/Screens/Menu/IntroChristmas.cs b/osu.Game/Seasonal/IntroChristmas.cs similarity index 99% rename from osu.Game/Screens/Menu/IntroChristmas.cs rename to osu.Game/Seasonal/IntroChristmas.cs index aa16f33c3d..ac3286f277 100644 --- a/osu.Game/Screens/Menu/IntroChristmas.cs +++ b/osu.Game/Seasonal/IntroChristmas.cs @@ -15,11 +15,11 @@ using osu.Game.Beatmaps.ControlPoints; using osu.Game.Graphics; using osu.Game.Graphics.Containers; using osu.Game.Graphics.Sprites; -using osu.Game.Seasonal; +using osu.Game.Screens.Menu; using osuTK; using osuTK.Graphics; -namespace osu.Game.Screens.Menu +namespace osu.Game.Seasonal { public partial class IntroChristmas : IntroScreen { From ad4a8a1e0a345c75b0f43186f00d985e653ad7bc Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 20 Dec 2024 14:58:45 +0900 Subject: [PATCH 062/290] Subclass menu flashes instead of adding local code to it --- osu.Game/Screens/Menu/MainMenu.cs | 2 +- osu.Game/Screens/Menu/MenuSideFlashes.cs | 31 +++++++++++++------- osu.Game/Seasonal/SeasonalMenuSideFlashes.cs | 18 ++++++++++++ 3 files changed, 39 insertions(+), 12 deletions(-) create mode 100644 osu.Game/Seasonal/SeasonalMenuSideFlashes.cs diff --git a/osu.Game/Screens/Menu/MainMenu.cs b/osu.Game/Screens/Menu/MainMenu.cs index a4b269ad0d..58d97bfe16 100644 --- a/osu.Game/Screens/Menu/MainMenu.cs +++ b/osu.Game/Screens/Menu/MainMenu.cs @@ -161,7 +161,7 @@ namespace osu.Game.Screens.Menu } }, logoTarget = new Container { RelativeSizeAxes = Axes.Both, }, - sideFlashes = new MenuSideFlashes(), + sideFlashes = SeasonalUIConfig.ENABLED ? new SeasonalMenuSideFlashes() : new MenuSideFlashes(), songTicker = new SongTicker { Anchor = Anchor.TopRight, diff --git a/osu.Game/Screens/Menu/MenuSideFlashes.cs b/osu.Game/Screens/Menu/MenuSideFlashes.cs index 808da5dd47..426896825e 100644 --- a/osu.Game/Screens/Menu/MenuSideFlashes.cs +++ b/osu.Game/Screens/Menu/MenuSideFlashes.cs @@ -11,14 +11,12 @@ using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Colour; using osu.Framework.Graphics.Shapes; -using osu.Framework.Utils; using osu.Game.Beatmaps; using osu.Game.Beatmaps.ControlPoints; using osu.Game.Graphics; using osu.Game.Graphics.Containers; using osu.Game.Online.API; using osu.Game.Online.API.Requests.Responses; -using osu.Game.Seasonal; using osu.Game.Skinning; using osuTK.Graphics; @@ -26,6 +24,10 @@ namespace osu.Game.Screens.Menu { public partial class MenuSideFlashes : BeatSyncedContainer { + protected virtual bool RefreshColoursEveryFlash => false; + + protected virtual float Intensity => 2; + private readonly IBindable beatmap = new Bindable(); private Box leftBox; @@ -69,7 +71,7 @@ namespace osu.Game.Screens.Menu Anchor = Anchor.CentreLeft, Origin = Anchor.CentreLeft, RelativeSizeAxes = Axes.Y, - Width = box_width * (SeasonalUIConfig.ENABLED ? 4 : 2), + Width = box_width * Intensity, Height = 1.5f, // align off-screen to make sure our edges don't become visible during parallax. X = -box_width, @@ -81,7 +83,7 @@ namespace osu.Game.Screens.Menu Anchor = Anchor.CentreRight, Origin = Anchor.CentreRight, RelativeSizeAxes = Axes.Y, - Width = box_width * (SeasonalUIConfig.ENABLED ? 4 : 2), + Width = box_width * Intensity, Height = 1.5f, X = box_width, Alpha = 0, @@ -89,8 +91,11 @@ namespace osu.Game.Screens.Menu } }; - user.ValueChanged += _ => updateColour(); - skin.BindValueChanged(_ => updateColour(), true); + if (!RefreshColoursEveryFlash) + { + user.ValueChanged += _ => updateColour(); + skin.BindValueChanged(_ => updateColour(), true); + } } protected override void OnNewBeat(int beatIndex, TimingControlPoint timingPoint, EffectControlPoint effectPoint, ChannelAmplitudes amplitudes) @@ -106,7 +111,7 @@ namespace osu.Game.Screens.Menu private void flash(Drawable d, double beatLength, bool kiai, ChannelAmplitudes amplitudes) { - if (SeasonalUIConfig.ENABLED) + if (RefreshColoursEveryFlash) updateColour(); d.FadeTo(Math.Clamp(0.1f + ((ReferenceEquals(d, leftBox) ? amplitudes.LeftChannel : amplitudes.RightChannel) - amplitude_dead_zone) / (kiai ? kiai_multiplier : alpha_multiplier), 0.1f, 1), @@ -115,15 +120,19 @@ namespace osu.Game.Screens.Menu .FadeOut(beatLength, Easing.In); } - private void updateColour() + protected virtual Color4 GetBaseColour() { Color4 baseColour = colours.Blue; - if (SeasonalUIConfig.ENABLED) - baseColour = RNG.NextBool() ? SeasonalUIConfig.PRIMARY_COLOUR_1 : SeasonalUIConfig.PRIMARY_COLOUR_2; - else if (user.Value?.IsSupporter ?? false) + if (user.Value?.IsSupporter ?? false) baseColour = skin.Value.GetConfig(GlobalSkinColours.MenuGlow)?.Value ?? baseColour; + return baseColour; + } + + private void updateColour() + { + var baseColour = GetBaseColour(); // linear colour looks better in this case, so let's use it for now. Color4 gradientDark = baseColour.Opacity(0).ToLinear(); Color4 gradientLight = baseColour.Opacity(0.6f).ToLinear(); diff --git a/osu.Game/Seasonal/SeasonalMenuSideFlashes.cs b/osu.Game/Seasonal/SeasonalMenuSideFlashes.cs new file mode 100644 index 0000000000..46a0a973bb --- /dev/null +++ b/osu.Game/Seasonal/SeasonalMenuSideFlashes.cs @@ -0,0 +1,18 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Utils; +using osu.Game.Screens.Menu; +using osuTK.Graphics; + +namespace osu.Game.Seasonal +{ + public partial class SeasonalMenuSideFlashes : MenuSideFlashes + { + protected override bool RefreshColoursEveryFlash => true; + + protected override float Intensity => 4; + + protected override Color4 GetBaseColour() => RNG.NextBool() ? SeasonalUIConfig.PRIMARY_COLOUR_1 : SeasonalUIConfig.PRIMARY_COLOUR_2; + } +} From 8e9377914d96a4d65a96335da0cd169e3721128d Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 20 Dec 2024 15:04:37 +0900 Subject: [PATCH 063/290] Subclass menu logo visualisation --- .../Screens/Menu/MenuLogoVisualisation.cs | 19 +++++++------------ osu.Game/Screens/Menu/OsuLogo.cs | 16 +++++++++------- osu.Game/Seasonal/OsuLogoChristmas.cs | 2 ++ .../Seasonal/SeasonalMenuLogoVisualisation.cs | 12 ++++++++++++ 4 files changed, 30 insertions(+), 19 deletions(-) create mode 100644 osu.Game/Seasonal/SeasonalMenuLogoVisualisation.cs diff --git a/osu.Game/Screens/Menu/MenuLogoVisualisation.cs b/osu.Game/Screens/Menu/MenuLogoVisualisation.cs index 32b5c706a3..f152c0c93c 100644 --- a/osu.Game/Screens/Menu/MenuLogoVisualisation.cs +++ b/osu.Game/Screens/Menu/MenuLogoVisualisation.cs @@ -1,22 +1,19 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Game.Online.API; using osu.Game.Online.API.Requests.Responses; -using osu.Game.Seasonal; using osu.Game.Skinning; using osuTK.Graphics; namespace osu.Game.Screens.Menu { - internal partial class MenuLogoVisualisation : LogoVisualisation + public partial class MenuLogoVisualisation : LogoVisualisation { - private IBindable user; - private Bindable skin; + private IBindable user = null!; + private Bindable skin = null!; [BackgroundDependencyLoader] private void load(IAPIProvider api, SkinManager skinManager) @@ -24,15 +21,13 @@ namespace osu.Game.Screens.Menu user = api.LocalUser.GetBoundCopy(); skin = skinManager.CurrentSkin.GetBoundCopy(); - user.ValueChanged += _ => updateColour(); - skin.BindValueChanged(_ => updateColour(), true); + user.ValueChanged += _ => UpdateColour(); + skin.BindValueChanged(_ => UpdateColour(), true); } - private void updateColour() + protected virtual void UpdateColour() { - if (SeasonalUIConfig.ENABLED) - Colour = SeasonalUIConfig.AMBIENT_COLOUR_1; - else if (user.Value?.IsSupporter ?? false) + if (user.Value?.IsSupporter ?? false) Colour = skin.Value.GetConfig(GlobalSkinColours.MenuGlow)?.Value ?? Color4.White; else Colour = Color4.White; diff --git a/osu.Game/Screens/Menu/OsuLogo.cs b/osu.Game/Screens/Menu/OsuLogo.cs index dc2dfefddb..31f47c1349 100644 --- a/osu.Game/Screens/Menu/OsuLogo.cs +++ b/osu.Game/Screens/Menu/OsuLogo.cs @@ -53,6 +53,8 @@ namespace osu.Game.Screens.Menu private Sample sampleClick; private SampleChannel sampleClickChannel; + protected virtual MenuLogoVisualisation CreateMenuLogoVisualisation() => new MenuLogoVisualisation(); + protected virtual double BeatSampleVariance => 0.1; protected Sample SampleBeat; @@ -153,14 +155,14 @@ namespace osu.Game.Screens.Menu AutoSizeAxes = Axes.Both, Children = new Drawable[] { - visualizer = new MenuLogoVisualisation + visualizer = CreateMenuLogoVisualisation().With(v => { - RelativeSizeAxes = Axes.Both, - Origin = Anchor.Centre, - Anchor = Anchor.Centre, - Alpha = visualizer_default_alpha, - Size = SCALE_ADJUST - }, + v.RelativeSizeAxes = Axes.Both; + v.Origin = Anchor.Centre; + v.Anchor = Anchor.Centre; + v.Alpha = visualizer_default_alpha; + v.Size = SCALE_ADJUST; + }), LogoElements = new Container { AutoSizeAxes = Axes.Both, diff --git a/osu.Game/Seasonal/OsuLogoChristmas.cs b/osu.Game/Seasonal/OsuLogoChristmas.cs index ec9cac94ea..8975a69c32 100644 --- a/osu.Game/Seasonal/OsuLogoChristmas.cs +++ b/osu.Game/Seasonal/OsuLogoChristmas.cs @@ -19,6 +19,8 @@ namespace osu.Game.Seasonal private bool hasHat; + protected override MenuLogoVisualisation CreateMenuLogoVisualisation() => new SeasonalMenuLogoVisualisation(); + [BackgroundDependencyLoader] private void load(TextureStore textures, AudioManager audio) { diff --git a/osu.Game/Seasonal/SeasonalMenuLogoVisualisation.cs b/osu.Game/Seasonal/SeasonalMenuLogoVisualisation.cs new file mode 100644 index 0000000000..f00da3fe7e --- /dev/null +++ b/osu.Game/Seasonal/SeasonalMenuLogoVisualisation.cs @@ -0,0 +1,12 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Game.Screens.Menu; + +namespace osu.Game.Seasonal +{ + internal partial class SeasonalMenuLogoVisualisation : MenuLogoVisualisation + { + protected override void UpdateColour() => Colour = SeasonalUIConfig.AMBIENT_COLOUR_1; + } +} From 3fc99904113036e4edd0fbd750e17605e900d953 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 20 Dec 2024 15:28:33 +0900 Subject: [PATCH 064/290] Fix some failing tests --- .../Editor/TestSceneSliderVelocityAdjust.cs | 3 ++- .../Visual/Menus/TestSceneMainMenuSeasonalLighting.cs | 3 ++- osu.Game/Screens/Menu/IntroScreen.cs | 3 ++- 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderVelocityAdjust.cs b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderVelocityAdjust.cs index 175cbeca6e..6690d043f8 100644 --- a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderVelocityAdjust.cs +++ b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderVelocityAdjust.cs @@ -7,6 +7,7 @@ using osu.Framework.Input; using osu.Framework.Testing; using osu.Framework.Utils; using osu.Game.Beatmaps; +using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Osu.Objects; using osu.Game.Rulesets.UI; using osu.Game.Screens.Edit; @@ -29,7 +30,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor private Slider? slider => editorBeatmap.HitObjects.OfType().FirstOrDefault(); - private TimelineHitObjectBlueprint blueprint => editor.ChildrenOfType().FirstOrDefault()!; + private TimelineHitObjectBlueprint blueprint => editor.ChildrenOfType().FirstOrDefault(b => b.Item.GetEndTime() != b.Item.StartTime)!; private DifficultyPointPiece difficultyPointPiece => blueprint.ChildrenOfType().First(); diff --git a/osu.Game.Tests/Visual/Menus/TestSceneMainMenuSeasonalLighting.cs b/osu.Game.Tests/Visual/Menus/TestSceneMainMenuSeasonalLighting.cs index 11356f7eeb..46fddf823e 100644 --- a/osu.Game.Tests/Visual/Menus/TestSceneMainMenuSeasonalLighting.cs +++ b/osu.Game.Tests/Visual/Menus/TestSceneMainMenuSeasonalLighting.cs @@ -22,7 +22,8 @@ namespace osu.Game.Tests.Visual.Menus { var setInfo = beatmaps.QueryBeatmapSet(b => b.Protected && b.Hash == IntroChristmas.CHRISTMAS_BEATMAP_SET_HASH); - Beatmap.Value = beatmaps.GetWorkingBeatmap(setInfo!.Value.Beatmaps.First()); + if (setInfo != null) + Beatmap.Value = beatmaps.GetWorkingBeatmap(setInfo.Value.Beatmaps.First()); }); AddStep("create lighting", () => Child = new MainMenuSeasonalLighting()); diff --git a/osu.Game/Screens/Menu/IntroScreen.cs b/osu.Game/Screens/Menu/IntroScreen.cs index 9885c061a9..a5c2497618 100644 --- a/osu.Game/Screens/Menu/IntroScreen.cs +++ b/osu.Game/Screens/Menu/IntroScreen.cs @@ -12,6 +12,7 @@ using osu.Framework.Audio; using osu.Framework.Audio.Sample; using osu.Framework.Audio.Track; using osu.Framework.Bindables; +using osu.Framework.Development; using osu.Framework.Extensions; using osu.Framework.Graphics; using osu.Framework.Screens; @@ -200,7 +201,7 @@ namespace osu.Game.Screens.Menu PrepareMenuLoad(); LoadMenu(); - if (!Debugger.IsAttached) + if (!Debugger.IsAttached && !DebugUtils.IsNUnitRunning) { notifications.Post(new SimpleErrorNotification { From 7ebc9dd843b0b801bbfb3a1e72c1be669fff197a Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 20 Dec 2024 15:32:00 +0900 Subject: [PATCH 065/290] Disable seasonal for now --- osu.Game/Seasonal/SeasonalUIConfig.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Seasonal/SeasonalUIConfig.cs b/osu.Game/Seasonal/SeasonalUIConfig.cs index 060913a8bf..b894a42108 100644 --- a/osu.Game/Seasonal/SeasonalUIConfig.cs +++ b/osu.Game/Seasonal/SeasonalUIConfig.cs @@ -11,7 +11,7 @@ namespace osu.Game.Seasonal /// public static class SeasonalUIConfig { - public static readonly bool ENABLED = true; + public static readonly bool ENABLED = false; public static readonly Color4 PRIMARY_COLOUR_1 = Color4Extensions.FromHex(@"D32F2F"); From f5b019807730a4b1d45158939f55299d54ac5cc6 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 20 Dec 2024 16:02:43 +0900 Subject: [PATCH 066/290] Fix test faiulres when seasonal set to `true` due to non-circles intro --- osu.Game/Screens/Loader.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/osu.Game/Screens/Loader.cs b/osu.Game/Screens/Loader.cs index dfa5d2c369..9e7ff80f7c 100644 --- a/osu.Game/Screens/Loader.cs +++ b/osu.Game/Screens/Loader.cs @@ -6,6 +6,7 @@ using System.Collections.Generic; using System.Linq; using osu.Framework.Allocation; +using osu.Framework.Development; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shaders; @@ -38,7 +39,9 @@ namespace osu.Game.Screens private IntroScreen getIntroSequence() { - if (SeasonalUIConfig.ENABLED) + // Headless tests run too fast to load non-circles intros correctly. + // They will hit the "audio can't play" notification and cause random test failures. + if (SeasonalUIConfig.ENABLED && !DebugUtils.IsNUnitRunning) return new IntroChristmas(createMainMenu); if (introSequence == IntroSequence.Random) From 5d1701469848f89410d84a220e065754f003a42b Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 20 Dec 2024 16:31:06 +0900 Subject: [PATCH 067/290] Fix mouse wheel disable not working during gameplay --- .../TestSceneMouseWheelVolumeAdjust.cs | 14 +++--- .../Volume/GlobalScrollAdjustsVolume.cs | 3 -- .../Play/GameplayScrollWheelHandling.cs | 44 +++++++++++++++++++ osu.Game/Screens/Play/Player.cs | 18 +------- 4 files changed, 53 insertions(+), 26 deletions(-) create mode 100644 osu.Game/Screens/Play/GameplayScrollWheelHandling.cs diff --git a/osu.Game.Tests/Visual/Navigation/TestSceneMouseWheelVolumeAdjust.cs b/osu.Game.Tests/Visual/Navigation/TestSceneMouseWheelVolumeAdjust.cs index a89f5fb647..26a37fa211 100644 --- a/osu.Game.Tests/Visual/Navigation/TestSceneMouseWheelVolumeAdjust.cs +++ b/osu.Game.Tests/Visual/Navigation/TestSceneMouseWheelVolumeAdjust.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 NUnit.Framework; using osu.Framework.Extensions; using osu.Game.Configuration; @@ -58,7 +56,11 @@ namespace osu.Game.Tests.Visual.Navigation // First scroll makes volume controls appear, second adjusts volume. AddRepeatStep("Adjust volume using mouse wheel", () => InputManager.ScrollVerticalBy(5), 10); - AddAssert("Volume is still zero", () => Game.Audio.Volume.Value == 0); + AddAssert("Volume is still zero", () => Game.Audio.Volume.Value, () => Is.Zero); + + AddStep("Pause", () => InputManager.PressKey(Key.Escape)); + AddRepeatStep("Adjust volume using mouse wheel", () => InputManager.ScrollVerticalBy(5), 10); + AddAssert("Volume is above zero", () => Game.Audio.Volume.Value > 0); } [Test] @@ -80,8 +82,8 @@ namespace osu.Game.Tests.Visual.Navigation private void loadToPlayerNonBreakTime() { - Player player = null; - Screens.Select.SongSelect songSelect = null; + Player? player = null; + Screens.Select.SongSelect songSelect = null!; PushAndConfirm(() => songSelect = new TestSceneScreenNavigation.TestPlaySongSelect()); AddUntilStep("wait for song select", () => songSelect.BeatmapSetsLoaded); @@ -95,7 +97,7 @@ namespace osu.Game.Tests.Visual.Navigation return (player = Game.ScreenStack.CurrentScreen as Player) != null; }); - AddUntilStep("wait for play time active", () => !player.IsBreakTime.Value); + AddUntilStep("wait for play time active", () => player!.IsBreakTime.Value, () => Is.False); } } } diff --git a/osu.Game/Overlays/Volume/GlobalScrollAdjustsVolume.cs b/osu.Game/Overlays/Volume/GlobalScrollAdjustsVolume.cs index 81be084d22..f1ad88833b 100644 --- a/osu.Game/Overlays/Volume/GlobalScrollAdjustsVolume.cs +++ b/osu.Game/Overlays/Volume/GlobalScrollAdjustsVolume.cs @@ -33,8 +33,5 @@ namespace osu.Game.Overlays.Volume // forward any unhandled mouse scroll events to the volume control. return volumeOverlay?.Adjust(GlobalAction.IncreaseVolume, e.ScrollDelta.Y, e.IsPrecise) ?? false; } - - public bool OnScroll(KeyBindingScrollEvent e) => - volumeOverlay?.Adjust(e.Action, e.ScrollAmount, e.IsPrecise) ?? false; } } diff --git a/osu.Game/Screens/Play/GameplayScrollWheelHandling.cs b/osu.Game/Screens/Play/GameplayScrollWheelHandling.cs new file mode 100644 index 0000000000..73ad9ccb24 --- /dev/null +++ b/osu.Game/Screens/Play/GameplayScrollWheelHandling.cs @@ -0,0 +1,44 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Allocation; +using osu.Framework.Bindables; +using osu.Framework.Input.Events; +using osu.Game.Configuration; +using osu.Game.Overlays.Volume; + +namespace osu.Game.Screens.Play +{ + /// + /// Primarily handles volume adjustment in gameplay. + /// + /// - If the user has mouse wheel disabled, only allow during break time or when holding alt. Also block scroll from parent handling. + /// - Otherwise always allow, as per implementation. + /// + internal class GameplayScrollWheelHandling : GlobalScrollAdjustsVolume + { + private Bindable mouseWheelDisabled = null!; + + [Resolved] + private IGameplayClock gameplayClock { get; set; } = null!; + + [BackgroundDependencyLoader] + private void load(OsuConfigManager config) + { + mouseWheelDisabled = config.GetBindable(OsuSetting.MouseDisableWheel); + } + + protected override bool OnScroll(ScrollEvent e) + { + // During pause, allow global volume adjust regardless of settings. + if (gameplayClock.IsPaused.Value) + return base.OnScroll(e); + + // Block any parent handling of scroll if the user has asked for it (special case when holding "Alt"). + if (mouseWheelDisabled.Value && !e.AltPressed) + return true; + + return base.OnScroll(e); + } + } +} diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs index 1c186485b8..f6b0230714 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -15,7 +15,6 @@ using osu.Framework.Bindables; using osu.Framework.Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; -using osu.Framework.Input.Events; using osu.Framework.Logging; using osu.Framework.Screens; using osu.Framework.Threading; @@ -28,7 +27,6 @@ using osu.Game.Graphics.Containers; using osu.Game.IO.Archives; using osu.Game.Online.API; using osu.Game.Overlays; -using osu.Game.Overlays.Volume; using osu.Game.Rulesets; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Scoring; @@ -89,8 +87,6 @@ namespace osu.Game.Screens.Play private bool isRestarting; private bool skipExitTransition; - private Bindable mouseWheelDisabled; - private readonly Bindable storyboardReplacesBackground = new Bindable(); public IBindable LocalUserPlaying => localUserPlaying; @@ -229,8 +225,6 @@ namespace osu.Game.Screens.Play return; } - mouseWheelDisabled = config.GetBindable(OsuSetting.MouseDisableWheel); - if (game != null) gameActive.BindTo(game.IsActive); @@ -254,7 +248,6 @@ namespace osu.Game.Screens.Play InternalChildren = new Drawable[] { - new GlobalScrollAdjustsVolume(), GameplayClockContainer = CreateGameplayClockContainer(Beatmap.Value, DrawableRuleset.GameplayStartTime), }; @@ -271,6 +264,7 @@ namespace osu.Game.Screens.Play dependencies.CacheAs(GameplayState = new GameplayState(playableBeatmap, ruleset, gameplayMods, Score, ScoreProcessor, HealthProcessor, Beatmap.Value.Storyboard)); var rulesetSkinProvider = new RulesetSkinProvidingContainer(ruleset, playableBeatmap, Beatmap.Value.Skin); + GameplayClockContainer.Add(new GameplayScrollWheelHandling()); // load the skinning hierarchy first. // this is intentionally done in two stages to ensure things are in a loaded state before exposing the ruleset to skin sources. @@ -899,16 +893,6 @@ namespace osu.Game.Screens.Play }); } - protected override bool OnScroll(ScrollEvent e) - { - // During pause, allow global volume adjust regardless of settings. - if (GameplayClockContainer.IsPaused.Value) - return false; - - // Block global volume adjust if the user has asked for it (special case when holding "Alt"). - return mouseWheelDisabled.Value && !e.AltPressed; - } - #region Gameplay leaderboard protected readonly Bindable LeaderboardExpandedState = new BindableBool(); From 48ce68694a4d01cf57171332453d66b1393962cf Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 20 Dec 2024 17:06:47 +0900 Subject: [PATCH 068/290] Add missing partial --- osu.Game/Screens/Play/GameplayScrollWheelHandling.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/Play/GameplayScrollWheelHandling.cs b/osu.Game/Screens/Play/GameplayScrollWheelHandling.cs index 73ad9ccb24..059d5a0dd4 100644 --- a/osu.Game/Screens/Play/GameplayScrollWheelHandling.cs +++ b/osu.Game/Screens/Play/GameplayScrollWheelHandling.cs @@ -15,7 +15,7 @@ namespace osu.Game.Screens.Play /// - If the user has mouse wheel disabled, only allow during break time or when holding alt. Also block scroll from parent handling. /// - Otherwise always allow, as per implementation. /// - internal class GameplayScrollWheelHandling : GlobalScrollAdjustsVolume + internal partial class GameplayScrollWheelHandling : GlobalScrollAdjustsVolume { private Bindable mouseWheelDisabled = null!; From 25373c3f9c9b2f4b4b5d6c7d7da1bc2685885320 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 20 Dec 2024 09:50:58 +0100 Subject: [PATCH 069/290] Fix backwards repeat check --- osu.Game/OsuGame.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs index 60fcd17ac6..244b72edaa 100644 --- a/osu.Game/OsuGame.cs +++ b/osu.Game/OsuGame.cs @@ -1433,7 +1433,7 @@ namespace osu.Game case GlobalAction.NextVolumeMeter: case GlobalAction.PreviousVolumeMeter: - if (!e.Repeat) + if (e.Repeat) return true; return volume.Adjust(e.Action); From 3ec63d00cbd32b5ab31dd3b5705f9e0dedb229bf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 19 Dec 2024 13:26:52 +0100 Subject: [PATCH 070/290] Silence test that apparently can't work on CI --- osu.Game.Tests/Visual/Gameplay/TestScenePause.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/osu.Game.Tests/Visual/Gameplay/TestScenePause.cs b/osu.Game.Tests/Visual/Gameplay/TestScenePause.cs index 7855c138ab..58fe6e8e56 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestScenePause.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestScenePause.cs @@ -208,6 +208,7 @@ namespace osu.Game.Tests.Visual.Gameplay } [Test] + [Ignore("Fails on github runners if they happen to skip too far forward in time.")] public void TestUserPauseDuringCooldownTooSoon() { AddStep("seek to gameplay", () => Player.GameplayClockContainer.Seek(0)); From 139fb2cdd3a60faee550be9a9cb816c4943c9141 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 20 Dec 2024 19:44:43 +0900 Subject: [PATCH 071/290] Revert and fix some tests still --- .../Editor/TestSceneSliderVelocityAdjust.cs | 3 +-- osu.Game/Screens/Menu/IntroScreen.cs | 3 +-- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderVelocityAdjust.cs b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderVelocityAdjust.cs index 6690d043f8..175cbeca6e 100644 --- a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderVelocityAdjust.cs +++ b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderVelocityAdjust.cs @@ -7,7 +7,6 @@ using osu.Framework.Input; using osu.Framework.Testing; using osu.Framework.Utils; using osu.Game.Beatmaps; -using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Osu.Objects; using osu.Game.Rulesets.UI; using osu.Game.Screens.Edit; @@ -30,7 +29,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor private Slider? slider => editorBeatmap.HitObjects.OfType().FirstOrDefault(); - private TimelineHitObjectBlueprint blueprint => editor.ChildrenOfType().FirstOrDefault(b => b.Item.GetEndTime() != b.Item.StartTime)!; + private TimelineHitObjectBlueprint blueprint => editor.ChildrenOfType().FirstOrDefault()!; private DifficultyPointPiece difficultyPointPiece => blueprint.ChildrenOfType().First(); diff --git a/osu.Game/Screens/Menu/IntroScreen.cs b/osu.Game/Screens/Menu/IntroScreen.cs index a5c2497618..9885c061a9 100644 --- a/osu.Game/Screens/Menu/IntroScreen.cs +++ b/osu.Game/Screens/Menu/IntroScreen.cs @@ -12,7 +12,6 @@ using osu.Framework.Audio; using osu.Framework.Audio.Sample; using osu.Framework.Audio.Track; using osu.Framework.Bindables; -using osu.Framework.Development; using osu.Framework.Extensions; using osu.Framework.Graphics; using osu.Framework.Screens; @@ -201,7 +200,7 @@ namespace osu.Game.Screens.Menu PrepareMenuLoad(); LoadMenu(); - if (!Debugger.IsAttached && !DebugUtils.IsNUnitRunning) + if (!Debugger.IsAttached) { notifications.Post(new SimpleErrorNotification { From 4551d59f3922a44a9d8424048a34cfdccaa2d711 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 20 Dec 2024 12:06:26 +0100 Subject: [PATCH 072/290] Give skinnable mod display a minimum size Co-authored-by: Dean Herbert --- osu.Game/Screens/Play/HUD/ModDisplay.cs | 4 +++- osu.Game/Screens/Play/HUD/SkinnableModDisplay.cs | 10 +++++++++- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/Play/HUD/ModDisplay.cs b/osu.Game/Screens/Play/HUD/ModDisplay.cs index 417ce355a5..3ab4c15154 100644 --- a/osu.Game/Screens/Play/HUD/ModDisplay.cs +++ b/osu.Game/Screens/Play/HUD/ModDisplay.cs @@ -22,6 +22,8 @@ namespace osu.Game.Screens.Play.HUD /// public partial class ModDisplay : CompositeDrawable, IHasCurrentValue> { + public const float MOD_ICON_SCALE = 0.6f; + private ExpansionMode expansionMode = ExpansionMode.ExpandOnHover; public ExpansionMode ExpansionMode @@ -93,7 +95,7 @@ namespace osu.Game.Screens.Play.HUD iconsContainer.Clear(); foreach (Mod mod in mods.NewValue.AsOrdered()) - iconsContainer.Add(new ModIcon(mod, showExtendedInformation: showExtendedInformation) { Scale = new Vector2(0.6f) }); + iconsContainer.Add(new ModIcon(mod, showExtendedInformation: showExtendedInformation) { Scale = new Vector2(MOD_ICON_SCALE) }); } private void updateExpansionMode(double duration = 500) diff --git a/osu.Game/Screens/Play/HUD/SkinnableModDisplay.cs b/osu.Game/Screens/Play/HUD/SkinnableModDisplay.cs index 819484e8ba..ee77e38edd 100644 --- a/osu.Game/Screens/Play/HUD/SkinnableModDisplay.cs +++ b/osu.Game/Screens/Play/HUD/SkinnableModDisplay.cs @@ -10,6 +10,8 @@ using osu.Game.Configuration; using osu.Game.Rulesets.Mods; using osu.Game.Skinning; using osu.Game.Localisation.SkinComponents; +using osu.Game.Rulesets.UI; +using osuTK; namespace osu.Game.Screens.Play.HUD { @@ -32,7 +34,13 @@ namespace osu.Game.Screens.Play.HUD [BackgroundDependencyLoader] private void load() { - InternalChild = modDisplay = new ModDisplay(); + InternalChildren = new Drawable[] + { + // Provide a minimum autosize. + new Container { Size = ModIcon.MOD_ICON_SIZE * ModDisplay.MOD_ICON_SCALE }, + modDisplay = new ModDisplay(), + }; + modDisplay.Current = mods; AutoSizeAxes = Axes.Both; } From a9cf31f5d8c9f2fc3136201faa22eaa58b35a46e Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 20 Dec 2024 21:27:24 +0900 Subject: [PATCH 073/290] Usings --- osu.Game/Screens/Play/HUD/SkinnableModDisplay.cs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/osu.Game/Screens/Play/HUD/SkinnableModDisplay.cs b/osu.Game/Screens/Play/HUD/SkinnableModDisplay.cs index ee77e38edd..59bb1ade41 100644 --- a/osu.Game/Screens/Play/HUD/SkinnableModDisplay.cs +++ b/osu.Game/Screens/Play/HUD/SkinnableModDisplay.cs @@ -7,11 +7,10 @@ using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Game.Configuration; -using osu.Game.Rulesets.Mods; -using osu.Game.Skinning; using osu.Game.Localisation.SkinComponents; +using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.UI; -using osuTK; +using osu.Game.Skinning; namespace osu.Game.Screens.Play.HUD { From 1fcd953e4a55c8d6576e64c737fa05b19a40a829 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sat, 21 Dec 2024 20:17:27 +0900 Subject: [PATCH 074/290] Fetch ruleset before initialising beatmap the first time --- osu.Game/Seasonal/MainMenuSeasonalLighting.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Game/Seasonal/MainMenuSeasonalLighting.cs b/osu.Game/Seasonal/MainMenuSeasonalLighting.cs index a382785499..718dd38fe7 100644 --- a/osu.Game/Seasonal/MainMenuSeasonalLighting.cs +++ b/osu.Game/Seasonal/MainMenuSeasonalLighting.cs @@ -49,11 +49,11 @@ namespace osu.Game.Seasonal [BackgroundDependencyLoader] private void load(IBindable working, RulesetStore rulesets) { - this.working = working.GetBoundCopy(); - this.working.BindValueChanged(_ => Scheduler.AddOnce(updateBeatmap), true); - // operate in osu! ruleset to keep things simple for now. osuRuleset = rulesets.GetRuleset(0); + + this.working = working.GetBoundCopy(); + this.working.BindValueChanged(_ => Scheduler.AddOnce(updateBeatmap), true); } private void updateBeatmap() From d897a31f0c5b63534f60d165857bd67123a854e4 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sat, 21 Dec 2024 20:30:00 +0900 Subject: [PATCH 075/290] Add extra safeties against null ref when rulesets are missing --- osu.Game/Seasonal/MainMenuSeasonalLighting.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/osu.Game/Seasonal/MainMenuSeasonalLighting.cs b/osu.Game/Seasonal/MainMenuSeasonalLighting.cs index 718dd38fe7..30ad7acefe 100644 --- a/osu.Game/Seasonal/MainMenuSeasonalLighting.cs +++ b/osu.Game/Seasonal/MainMenuSeasonalLighting.cs @@ -27,7 +27,7 @@ namespace osu.Game.Seasonal { private IBindable working = null!; - private InterpolatingFramedClock beatmapClock = null!; + private InterpolatingFramedClock? beatmapClock; private List hitObjects = null!; @@ -82,6 +82,9 @@ namespace osu.Game.Seasonal { base.Update(); + if (osuRuleset == null || beatmapClock == null) + return; + Height = DrawWidth / 16 * 10; beatmapClock.ProcessFrame(); From 5f617e6697aa6e2e4f8be7e411612725a364cc0a Mon Sep 17 00:00:00 2001 From: jkh675 Date: Sat, 21 Dec 2024 20:31:12 +0800 Subject: [PATCH 076/290] Implement rename skin popover and button --- osu.Game/Localisation/SkinSettingsStrings.cs | 5 + .../Overlays/Settings/Sections/SkinSection.cs | 96 +++++++++++++++++++ 2 files changed, 101 insertions(+) diff --git a/osu.Game/Localisation/SkinSettingsStrings.cs b/osu.Game/Localisation/SkinSettingsStrings.cs index 4b6b0ce1d6..1a812ad04d 100644 --- a/osu.Game/Localisation/SkinSettingsStrings.cs +++ b/osu.Game/Localisation/SkinSettingsStrings.cs @@ -54,6 +54,11 @@ namespace osu.Game.Localisation ///
public static LocalisableString BeatmapHitsounds => new TranslatableString(getKey(@"beatmap_hitsounds"), @"Beatmap hitsounds"); + /// + /// "Rename selected skin" + /// + public static LocalisableString RenameSkinButton = new TranslatableString(getKey(@"rename_skin_button"), @"Rename selected skin"); + /// /// "Export selected skin" /// diff --git a/osu.Game/Overlays/Settings/Sections/SkinSection.cs b/osu.Game/Overlays/Settings/Sections/SkinSection.cs index 9b04f208a7..c015affcd2 100644 --- a/osu.Game/Overlays/Settings/Sections/SkinSection.cs +++ b/osu.Game/Overlays/Settings/Sections/SkinSection.cs @@ -9,17 +9,23 @@ using System.Linq; using JetBrains.Annotations; using osu.Framework.Allocation; using osu.Framework.Bindables; +using osu.Framework.Extensions; using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Cursor; using osu.Framework.Graphics.Sprites; +using osu.Framework.Graphics.UserInterface; using osu.Framework.Localisation; using osu.Framework.Logging; using osu.Game.Database; using osu.Game.Graphics; using osu.Game.Graphics.UserInterface; +using osu.Game.Graphics.UserInterfaceV2; using osu.Game.Localisation; using osu.Game.Overlays.SkinEditor; using osu.Game.Screens.Select; using osu.Game.Skinning; +using osuTK; using Realms; namespace osu.Game.Overlays.Settings.Sections @@ -69,6 +75,7 @@ namespace osu.Game.Overlays.Settings.Sections Text = SkinSettingsStrings.SkinLayoutEditor, Action = () => skinEditor?.ToggleVisibility(), }, + new RenameSkinButton(), new ExportSkinButton(), new DeleteSkinButton(), }; @@ -136,6 +143,95 @@ namespace osu.Game.Overlays.Settings.Sections } } + public partial class RenameSkinButton : SettingsButton, IHasPopover + { + [Resolved] + private SkinManager skins { get; set; } + + private Bindable currentSkin; + + [BackgroundDependencyLoader] + private void load() + { + Text = SkinSettingsStrings.RenameSkinButton; + Action = this.ShowPopover; + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + currentSkin = skins.CurrentSkin.GetBoundCopy(); + currentSkin.BindValueChanged(skin => Enabled.Value = skin.NewValue.SkinInfo.PerformRead(s => !s.Protected), true); + } + + public Popover GetPopover() + { + return new RenameSkinPopover(); + } + + public partial class RenameSkinPopover : OsuPopover + { + [Resolved] + private SkinManager skins { get; set; } + + public Action Rename { get; init; } + + private readonly FocusedTextBox textBox; + private readonly RoundedButton renameButton; + + public RenameSkinPopover() + { + AutoSizeAxes = Axes.Both; + Origin = Anchor.TopCentre; + + Child = new FillFlowContainer + { + Direction = FillDirection.Vertical, + AutoSizeAxes = Axes.Y, + Width = 250, + Spacing = new Vector2(10f), + Children = new Drawable[] + { + textBox = new FocusedTextBox + { + PlaceholderText = @"Skin name", + FontSize = OsuFont.DEFAULT_FONT_SIZE, + RelativeSizeAxes = Axes.X, + }, + renameButton = new RoundedButton + { + Height = 40, + RelativeSizeAxes = Axes.X, + MatchingFilter = true, + Text = SkinSettingsStrings.RenameSkinButton, + } + } + }; + + renameButton.Action += rename; + textBox.OnCommit += delegate (TextBox _, bool _) { rename(); }; + } + + protected override void PopIn() + { + textBox.Text = skins.CurrentSkinInfo.Value.Value.Name; + textBox.TakeFocus(); + base.PopIn(); + } + + private void rename() + { + skins.CurrentSkinInfo.Value.PerformWrite(skin => + { + skin.Name = textBox.Text; + PopOut(); + }); + } + } + } + + public partial class ExportSkinButton : SettingsButton { [Resolved] From 1174f46656510e7524af30d5218fd48b7d99b0d9 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sat, 21 Dec 2024 21:41:48 +0900 Subject: [PATCH 077/290] Add menu tip hinting at correct spelling of laser --- osu.Game/Localisation/MenuTipStrings.cs | 5 +++++ osu.Game/Screens/Menu/MenuTip.cs | 3 ++- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/osu.Game/Localisation/MenuTipStrings.cs b/osu.Game/Localisation/MenuTipStrings.cs index f97ad5fa2c..9258f5d575 100644 --- a/osu.Game/Localisation/MenuTipStrings.cs +++ b/osu.Game/Localisation/MenuTipStrings.cs @@ -119,6 +119,11 @@ namespace osu.Game.Localisation /// public static LocalisableString AutoplayBeatmapShortcut => new TranslatableString(getKey(@"autoplay_beatmap_shortcut"), @"Ctrl-Enter at song select will start a beatmap in autoplay mode!"); + /// + /// ""Lazer" it not an english word. The correct spelling for the bright light is "laser"." + /// + public static LocalisableString LazerIsNotAWord => new TranslatableString(getKey(@"lazer_is_not_a_word"), @"""Lazer"" it not an english word. The correct spelling for the bright light is ""laser""."); + /// /// "Multithreading support means that even with low "FPS" your input and judgements will be accurate!" /// diff --git a/osu.Game/Screens/Menu/MenuTip.cs b/osu.Game/Screens/Menu/MenuTip.cs index 3fc5fe57fb..af7cfde52b 100644 --- a/osu.Game/Screens/Menu/MenuTip.cs +++ b/osu.Game/Screens/Menu/MenuTip.cs @@ -122,7 +122,8 @@ namespace osu.Game.Screens.Menu MenuTipStrings.RandomSkinShortcut, MenuTipStrings.ToggleReplaySettingsShortcut, MenuTipStrings.CopyModsFromScore, - MenuTipStrings.AutoplayBeatmapShortcut + MenuTipStrings.AutoplayBeatmapShortcut, + MenuTipStrings.LazerIsNotAWord }; return tips[RNG.Next(0, tips.Length)]; From 9a0d9641ab9d608713f2a3588a2c571c8b7b2aa2 Mon Sep 17 00:00:00 2001 From: jkh675 Date: Sat, 21 Dec 2024 21:26:56 +0800 Subject: [PATCH 078/290] Select all on focus when popover just open --- osu.Game/Overlays/Settings/Sections/SkinSection.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/osu.Game/Overlays/Settings/Sections/SkinSection.cs b/osu.Game/Overlays/Settings/Sections/SkinSection.cs index c015affcd2..5ff8c88756 100644 --- a/osu.Game/Overlays/Settings/Sections/SkinSection.cs +++ b/osu.Game/Overlays/Settings/Sections/SkinSection.cs @@ -178,13 +178,14 @@ namespace osu.Game.Overlays.Settings.Sections public Action Rename { get; init; } private readonly FocusedTextBox textBox; - private readonly RoundedButton renameButton; public RenameSkinPopover() { AutoSizeAxes = Axes.Both; Origin = Anchor.TopCentre; + RoundedButton renameButton; + Child = new FillFlowContainer { Direction = FillDirection.Vertical, @@ -198,6 +199,7 @@ namespace osu.Game.Overlays.Settings.Sections PlaceholderText = @"Skin name", FontSize = OsuFont.DEFAULT_FONT_SIZE, RelativeSizeAxes = Axes.X, + SelectAllOnFocus = true, }, renameButton = new RoundedButton { @@ -231,7 +233,6 @@ namespace osu.Game.Overlays.Settings.Sections } } - public partial class ExportSkinButton : SettingsButton { [Resolved] From ae7f1a9ef104d8f18d1f1d24c8fe822e5b95bda0 Mon Sep 17 00:00:00 2001 From: jkh675 Date: Sat, 21 Dec 2024 22:27:21 +0800 Subject: [PATCH 079/290] Fix code quality --- osu.Game/Overlays/Settings/Sections/SkinSection.cs | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/osu.Game/Overlays/Settings/Sections/SkinSection.cs b/osu.Game/Overlays/Settings/Sections/SkinSection.cs index 5ff8c88756..1792c61d48 100644 --- a/osu.Game/Overlays/Settings/Sections/SkinSection.cs +++ b/osu.Game/Overlays/Settings/Sections/SkinSection.cs @@ -212,7 +212,13 @@ namespace osu.Game.Overlays.Settings.Sections }; renameButton.Action += rename; - textBox.OnCommit += delegate (TextBox _, bool _) { rename(); }; + + void onTextboxCommit(TextBox sender, bool newText) + { + rename(); + } + + textBox.OnCommit += onTextboxCommit; } protected override void PopIn() From 7cd397986687d88fb0c423c920cc40b27e3b5f70 Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Sat, 21 Dec 2024 12:58:56 -0500 Subject: [PATCH 080/290] Fix typo in main menu tip --- osu.Game/Localisation/MenuTipStrings.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Localisation/MenuTipStrings.cs b/osu.Game/Localisation/MenuTipStrings.cs index 9258f5d575..3b40d7bff5 100644 --- a/osu.Game/Localisation/MenuTipStrings.cs +++ b/osu.Game/Localisation/MenuTipStrings.cs @@ -120,9 +120,9 @@ namespace osu.Game.Localisation public static LocalisableString AutoplayBeatmapShortcut => new TranslatableString(getKey(@"autoplay_beatmap_shortcut"), @"Ctrl-Enter at song select will start a beatmap in autoplay mode!"); /// - /// ""Lazer" it not an english word. The correct spelling for the bright light is "laser"." + /// ""Lazer" is not an english word. The correct spelling for the bright light is "laser"." /// - public static LocalisableString LazerIsNotAWord => new TranslatableString(getKey(@"lazer_is_not_a_word"), @"""Lazer"" it not an english word. The correct spelling for the bright light is ""laser""."); + public static LocalisableString LazerIsNotAWord => new TranslatableString(getKey(@"lazer_is_not_a_word"), @"""Lazer"" is not an english word. The correct spelling for the bright light is ""laser""."); /// /// "Multithreading support means that even with low "FPS" your input and judgements will be accurate!" From 1c48fdb2350b2389f3d79fdaad9fb32194c9fa48 Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Sat, 21 Dec 2024 14:03:20 -0500 Subject: [PATCH 081/290] Add `Hidden` cursor state flag on all platforms --- osu.Desktop/OsuGameDesktop.cs | 1 - osu.Game/OsuGame.cs | 1 + 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Desktop/OsuGameDesktop.cs b/osu.Desktop/OsuGameDesktop.cs index 46bd894c07..2d3f4e0ed6 100644 --- a/osu.Desktop/OsuGameDesktop.cs +++ b/osu.Desktop/OsuGameDesktop.cs @@ -134,7 +134,6 @@ namespace osu.Desktop if (iconStream != null) host.Window.SetIconFromStream(iconStream); - host.Window.CursorState |= CursorState.Hidden; host.Window.Title = Name; } diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs index 244b72edaa..96899e0ddb 100644 --- a/osu.Game/OsuGame.cs +++ b/osu.Game/OsuGame.cs @@ -319,6 +319,7 @@ namespace osu.Game if (host.Window != null) { + host.Window.CursorState |= CursorState.Hidden; host.Window.DragDrop += path => { // on macOS/iOS, URL associations are handled via SDL_DROPFILE events. From ce5a2059933e48693a27797b9e9919afe191fbe2 Mon Sep 17 00:00:00 2001 From: Joseph Madamba Date: Sat, 21 Dec 2024 11:37:30 -0800 Subject: [PATCH 082/290] Capitalise English --- osu.Game/Localisation/MenuTipStrings.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Localisation/MenuTipStrings.cs b/osu.Game/Localisation/MenuTipStrings.cs index 3b40d7bff5..9d398e8e64 100644 --- a/osu.Game/Localisation/MenuTipStrings.cs +++ b/osu.Game/Localisation/MenuTipStrings.cs @@ -120,9 +120,9 @@ namespace osu.Game.Localisation public static LocalisableString AutoplayBeatmapShortcut => new TranslatableString(getKey(@"autoplay_beatmap_shortcut"), @"Ctrl-Enter at song select will start a beatmap in autoplay mode!"); /// - /// ""Lazer" is not an english word. The correct spelling for the bright light is "laser"." + /// ""Lazer" is not an English word. The correct spelling for the bright light is "laser"." /// - public static LocalisableString LazerIsNotAWord => new TranslatableString(getKey(@"lazer_is_not_a_word"), @"""Lazer"" is not an english word. The correct spelling for the bright light is ""laser""."); + public static LocalisableString LazerIsNotAWord => new TranslatableString(getKey(@"lazer_is_not_a_word"), @"""Lazer"" is not an English word. The correct spelling for the bright light is ""laser""."); /// /// "Multithreading support means that even with low "FPS" your input and judgements will be accurate!" From 431d57a8a11671d9fd787ea26a60c7ff414c9eac Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Sat, 21 Dec 2024 17:22:07 -0500 Subject: [PATCH 083/290] Make "featured artist" beatmap listing filter persist in config --- osu.Game/Configuration/OsuConfigManager.cs | 2 ++ .../BeatmapListing/BeatmapSearchGeneralFilterRow.cs | 8 ++++++++ 2 files changed, 10 insertions(+) diff --git a/osu.Game/Configuration/OsuConfigManager.cs b/osu.Game/Configuration/OsuConfigManager.cs index df0a823648..deac1a5128 100644 --- a/osu.Game/Configuration/OsuConfigManager.cs +++ b/osu.Game/Configuration/OsuConfigManager.cs @@ -57,6 +57,7 @@ namespace osu.Game.Configuration SetDefault(OsuSetting.ChatDisplayHeight, ChatOverlay.DEFAULT_HEIGHT, 0.2f, 1f, 0.01f); SetDefault(OsuSetting.BeatmapListingCardSize, BeatmapCardSize.Normal); + SetDefault(OsuSetting.BeatmapListingFeaturedArtistFilter, true); SetDefault(OsuSetting.ProfileCoverExpanded, true); @@ -450,5 +451,6 @@ namespace osu.Game.Configuration EditorAdjustExistingObjectsOnTimingChanges, AlwaysRequireHoldingForPause, MultiplayerShowInProgressFilter, + BeatmapListingFeaturedArtistFilter, } } diff --git a/osu.Game/Overlays/BeatmapListing/BeatmapSearchGeneralFilterRow.cs b/osu.Game/Overlays/BeatmapListing/BeatmapSearchGeneralFilterRow.cs index 34b7d45a77..c297e4305d 100644 --- a/osu.Game/Overlays/BeatmapListing/BeatmapSearchGeneralFilterRow.cs +++ b/osu.Game/Overlays/BeatmapListing/BeatmapSearchGeneralFilterRow.cs @@ -125,6 +125,9 @@ namespace osu.Game.Overlays.BeatmapListing [Resolved] private OsuColour colours { get; set; } = null!; + [Resolved] + private OsuConfigManager config { get; set; } = null!; + [Resolved] private SessionStatics sessionStatics { get; set; } = null!; @@ -135,7 +138,12 @@ namespace osu.Game.Overlays.BeatmapListing { base.LoadComplete(); + config.BindWith(OsuSetting.BeatmapListingFeaturedArtistFilter, Active); disclaimerShown = sessionStatics.GetBindable(Static.FeaturedArtistDisclaimerShownOnce); + + // no need to show the disclaimer if the user already had it toggled off in config. + if (!Active.Value) + disclaimerShown.Value = true; } protected override Color4 ColourNormal => colours.Orange1; From c24f690019fd1871a941abcbb5d70ca386387137 Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Sun, 22 Dec 2024 07:47:57 -0500 Subject: [PATCH 084/290] Allow disabling filter items in beatmap listing overlay --- ...BeatmapSearchMultipleSelectionFilterRow.cs | 33 +++++++++++++++---- .../Overlays/BeatmapListing/FilterTabItem.cs | 2 ++ 2 files changed, 28 insertions(+), 7 deletions(-) diff --git a/osu.Game/Overlays/BeatmapListing/BeatmapSearchMultipleSelectionFilterRow.cs b/osu.Game/Overlays/BeatmapListing/BeatmapSearchMultipleSelectionFilterRow.cs index 958297b559..50e3c0e931 100644 --- a/osu.Game/Overlays/BeatmapListing/BeatmapSearchMultipleSelectionFilterRow.cs +++ b/osu.Game/Overlays/BeatmapListing/BeatmapSearchMultipleSelectionFilterRow.cs @@ -73,7 +73,12 @@ namespace osu.Game.Overlays.BeatmapListing private void currentChanged(object? sender, NotifyCollectionChangedEventArgs e) { foreach (var c in Children) - c.Active.Value = Current.Contains(c.Value); + { + if (!c.Active.Disabled) + c.Active.Value = Current.Contains(c.Value); + else if (c.Active.Value != Current.Contains(c.Value)) + throw new InvalidOperationException($"Expected filter {c.Value} to be set to {Current.Contains(c.Value)}, but was {c.Active.Value}"); + } } /// @@ -100,8 +105,9 @@ namespace osu.Game.Overlays.BeatmapListing protected partial class MultipleSelectionFilterTabItem : FilterTabItem { - private Drawable activeContent = null!; + private Container activeContent = null!; private Circle background = null!; + private SpriteIcon icon = null!; public MultipleSelectionFilterTabItem(T value) : base(value) @@ -123,7 +129,6 @@ namespace osu.Game.Overlays.BeatmapListing Alpha = 0, Padding = new MarginPadding { - Left = -16, Right = -4, Vertical = -2 }, @@ -134,8 +139,9 @@ namespace osu.Game.Overlays.BeatmapListing Colour = Color4.White, RelativeSizeAxes = Axes.Both, }, - new SpriteIcon + icon = new SpriteIcon { + Alpha = 0f, Icon = FontAwesome.Solid.TimesCircle, Size = new Vector2(10), Colour = ColourProvider.Background4, @@ -160,13 +166,26 @@ namespace osu.Game.Overlays.BeatmapListing { Color4 colour = Active.Value ? ColourActive : ColourNormal; - if (IsHovered) + if (!Enabled.Value) + colour = colour.Darken(1f); + else if (IsHovered) colour = Active.Value ? colour.Darken(0.2f) : colour.Lighten(0.2f); if (Active.Value) { - // This just allows enough spacing for adjacent tab items to show the "x". - Padding = new MarginPadding { Left = 12 }; + if (Enabled.Value) + { + // This just allows enough spacing for adjacent tab items to show the "x". + Padding = new MarginPadding { Left = 12 }; + activeContent.Padding = activeContent.Padding with { Left = -16 }; + icon.Show(); + } + else + { + Padding = new MarginPadding(); + activeContent.Padding = activeContent.Padding with { Left = -6 }; + icon.Hide(); + } activeContent.FadeIn(200, Easing.OutQuint); background.FadeColour(colour, 200, Easing.OutQuint); diff --git a/osu.Game/Overlays/BeatmapListing/FilterTabItem.cs b/osu.Game/Overlays/BeatmapListing/FilterTabItem.cs index 8f4ecaa0f5..e357718103 100644 --- a/osu.Game/Overlays/BeatmapListing/FilterTabItem.cs +++ b/osu.Game/Overlays/BeatmapListing/FilterTabItem.cs @@ -57,7 +57,9 @@ namespace osu.Game.Overlays.BeatmapListing { base.LoadComplete(); + Enabled.BindValueChanged(_ => UpdateState()); UpdateState(); + FinishTransforms(true); } From 589e187a80b022b4ce20e265fb4e5af775b2369f Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Sun, 22 Dec 2024 07:50:08 -0500 Subject: [PATCH 085/290] Disable ability to toggle "featured artists" beatmap listing filter in iOS --- osu.Game/OsuGame.cs | 6 ++++++ .../BeatmapListing/BeatmapSearchGeneralFilterRow.cs | 12 ++++++++++++ 2 files changed, 18 insertions(+) diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs index 244b72edaa..36f7bcbb1e 100644 --- a/osu.Game/OsuGame.cs +++ b/osu.Game/OsuGame.cs @@ -220,6 +220,12 @@ namespace osu.Game private readonly List visibleBlockingOverlays = new List(); + /// + /// Whether the game should be limited from providing access to download non-featured-artist beatmaps. + /// This only affects the "featured artists" filter in the beatmap listing overlay. + /// + public bool LimitedToFeaturedArtists => RuntimeInfo.OS == RuntimeInfo.Platform.iOS && true; + public OsuGame(string[] args = null) { this.args = args; diff --git a/osu.Game/Overlays/BeatmapListing/BeatmapSearchGeneralFilterRow.cs b/osu.Game/Overlays/BeatmapListing/BeatmapSearchGeneralFilterRow.cs index c297e4305d..d7201d4df8 100644 --- a/osu.Game/Overlays/BeatmapListing/BeatmapSearchGeneralFilterRow.cs +++ b/osu.Game/Overlays/BeatmapListing/BeatmapSearchGeneralFilterRow.cs @@ -125,6 +125,9 @@ namespace osu.Game.Overlays.BeatmapListing [Resolved] private OsuColour colours { get; set; } = null!; + [Resolved] + private OsuGame game { get; set; } = null!; + [Resolved] private OsuConfigManager config { get; set; } = null!; @@ -144,6 +147,12 @@ namespace osu.Game.Overlays.BeatmapListing // no need to show the disclaimer if the user already had it toggled off in config. if (!Active.Value) disclaimerShown.Value = true; + + if (game.LimitedToFeaturedArtists) + { + Enabled.Value = false; + Active.Disabled = true; + } } protected override Color4 ColourNormal => colours.Orange1; @@ -151,6 +160,9 @@ namespace osu.Game.Overlays.BeatmapListing protected override bool OnClick(ClickEvent e) { + if (!Enabled.Value) + return true; + if (!disclaimerShown.Value && dialogOverlay != null) { dialogOverlay.Push(new FeaturedArtistConfirmDialog(() => From e716919a07599068556b3f07aab191c9c266bf8d Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sun, 22 Dec 2024 22:57:17 +0900 Subject: [PATCH 086/290] Remove redundant `&& true` Co-authored-by: Susko3 --- osu.Game/OsuGame.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs index 36f7bcbb1e..17ad67b733 100644 --- a/osu.Game/OsuGame.cs +++ b/osu.Game/OsuGame.cs @@ -224,7 +224,7 @@ namespace osu.Game /// Whether the game should be limited from providing access to download non-featured-artist beatmaps. /// This only affects the "featured artists" filter in the beatmap listing overlay. /// - public bool LimitedToFeaturedArtists => RuntimeInfo.OS == RuntimeInfo.Platform.iOS && true; + public bool LimitedToFeaturedArtists => RuntimeInfo.OS == RuntimeInfo.Platform.iOS; public OsuGame(string[] args = null) { From 0aed625bb8027bea06a98833904b2687c8619650 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sun, 22 Dec 2024 23:58:35 +0900 Subject: [PATCH 087/290] Rename variable and adjust commentary --- osu.Game/OsuGame.cs | 5 ++--- .../Overlays/BeatmapListing/BeatmapSearchGeneralFilterRow.cs | 2 +- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs index 17ad67b733..3864c518d2 100644 --- a/osu.Game/OsuGame.cs +++ b/osu.Game/OsuGame.cs @@ -221,10 +221,9 @@ namespace osu.Game private readonly List visibleBlockingOverlays = new List(); /// - /// Whether the game should be limited from providing access to download non-featured-artist beatmaps. - /// This only affects the "featured artists" filter in the beatmap listing overlay. + /// Whether the game should be limited to only display licensed content. /// - public bool LimitedToFeaturedArtists => RuntimeInfo.OS == RuntimeInfo.Platform.iOS; + public bool HideUnlicensedContent => RuntimeInfo.OS == RuntimeInfo.Platform.iOS; public OsuGame(string[] args = null) { diff --git a/osu.Game/Overlays/BeatmapListing/BeatmapSearchGeneralFilterRow.cs b/osu.Game/Overlays/BeatmapListing/BeatmapSearchGeneralFilterRow.cs index d7201d4df8..b525d8282e 100644 --- a/osu.Game/Overlays/BeatmapListing/BeatmapSearchGeneralFilterRow.cs +++ b/osu.Game/Overlays/BeatmapListing/BeatmapSearchGeneralFilterRow.cs @@ -148,7 +148,7 @@ namespace osu.Game.Overlays.BeatmapListing if (!Active.Value) disclaimerShown.Value = true; - if (game.LimitedToFeaturedArtists) + if (game.HideUnlicensedContent) { Enabled.Value = false; Active.Disabled = true; From fcfab9e53c5fdb98e38d84903120611d48fa439e Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Sun, 22 Dec 2024 10:14:52 -0500 Subject: [PATCH 088/290] Fix spacing --- .../BeatmapListing/BeatmapSearchMultipleSelectionFilterRow.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Overlays/BeatmapListing/BeatmapSearchMultipleSelectionFilterRow.cs b/osu.Game/Overlays/BeatmapListing/BeatmapSearchMultipleSelectionFilterRow.cs index 50e3c0e931..27b630d623 100644 --- a/osu.Game/Overlays/BeatmapListing/BeatmapSearchMultipleSelectionFilterRow.cs +++ b/osu.Game/Overlays/BeatmapListing/BeatmapSearchMultipleSelectionFilterRow.cs @@ -183,7 +183,7 @@ namespace osu.Game.Overlays.BeatmapListing else { Padding = new MarginPadding(); - activeContent.Padding = activeContent.Padding with { Left = -6 }; + activeContent.Padding = activeContent.Padding with { Left = -4 }; icon.Hide(); } From b3056d6114b9a3d439ae437537568fc9124c4a58 Mon Sep 17 00:00:00 2001 From: Plextora <71889427+Plextora@users.noreply.github.com> Date: Sun, 22 Dec 2024 16:58:00 -0500 Subject: [PATCH 089/290] Change score background to pink if user is friended --- osu.Game/Online/Leaderboards/LeaderboardScore.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/osu.Game/Online/Leaderboards/LeaderboardScore.cs b/osu.Game/Online/Leaderboards/LeaderboardScore.cs index 5651f01645..9aa0e0fbe2 100644 --- a/osu.Game/Online/Leaderboards/LeaderboardScore.cs +++ b/osu.Game/Online/Leaderboards/LeaderboardScore.cs @@ -101,6 +101,7 @@ namespace osu.Game.Online.Leaderboards private void load(IAPIProvider api, OsuColour colour) { var user = Score.User; + bool isUserFriend = api.Friends.Any(friend => friend.TargetID == user.OnlineID); statisticsLabels = GetStatistics(Score).Select(s => new ScoreComponentLabel(s)).ToList(); @@ -129,7 +130,7 @@ namespace osu.Game.Online.Leaderboards background = new Box { RelativeSizeAxes = Axes.Both, - Colour = user.OnlineID == api.LocalUser.Value.Id && isOnlineScope ? colour.Green : Color4.Black, + Colour = isUserFriend ? colour.Pink : (user.OnlineID == api.LocalUser.Value.Id && isOnlineScope ? colour.Green : Color4.Black), Alpha = background_alpha, }, }, From 1a7feeb4edab01db1ab6c9fa5c501b69456a78da Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 23 Dec 2024 14:39:07 +0900 Subject: [PATCH 090/290] Use `virtual` property rather than inline iOS conditional --- osu.Game/OsuGame.cs | 4 ++-- osu.iOS/OsuGameIOS.cs | 2 ++ 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs index 3864c518d2..c5c6ef8cc7 100644 --- a/osu.Game/OsuGame.cs +++ b/osu.Game/OsuGame.cs @@ -221,9 +221,9 @@ namespace osu.Game private readonly List visibleBlockingOverlays = new List(); /// - /// Whether the game should be limited to only display licensed content. + /// Whether the game should be limited to only display officially licensed content. /// - public bool HideUnlicensedContent => RuntimeInfo.OS == RuntimeInfo.Platform.iOS; + public virtual bool HideUnlicensedContent => false; public OsuGame(string[] args = null) { diff --git a/osu.iOS/OsuGameIOS.cs b/osu.iOS/OsuGameIOS.cs index c0bd77366e..a9ca1778a0 100644 --- a/osu.iOS/OsuGameIOS.cs +++ b/osu.iOS/OsuGameIOS.cs @@ -17,6 +17,8 @@ namespace osu.iOS { public override Version AssemblyVersion => new Version(NSBundle.MainBundle.InfoDictionary["CFBundleVersion"].ToString()); + public override bool HideUnlicensedContent => true; + protected override UpdateManager CreateUpdateManager() => new MobileUpdateNotifier(); protected override BatteryInfo CreateBatteryInfo() => new IOSBatteryInfo(); From f12fffd116eb3488586405de0177ed63e1fffa30 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 23 Dec 2024 14:43:36 +0900 Subject: [PATCH 091/290] Fix more than obvious test failure Please run tests please run tests please run tests. --- .../BeatmapListing/BeatmapSearchGeneralFilterRow.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/osu.Game/Overlays/BeatmapListing/BeatmapSearchGeneralFilterRow.cs b/osu.Game/Overlays/BeatmapListing/BeatmapSearchGeneralFilterRow.cs index b525d8282e..e4c663ee13 100644 --- a/osu.Game/Overlays/BeatmapListing/BeatmapSearchGeneralFilterRow.cs +++ b/osu.Game/Overlays/BeatmapListing/BeatmapSearchGeneralFilterRow.cs @@ -125,9 +125,6 @@ namespace osu.Game.Overlays.BeatmapListing [Resolved] private OsuColour colours { get; set; } = null!; - [Resolved] - private OsuGame game { get; set; } = null!; - [Resolved] private OsuConfigManager config { get; set; } = null!; @@ -137,6 +134,9 @@ namespace osu.Game.Overlays.BeatmapListing [Resolved] private IDialogOverlay? dialogOverlay { get; set; } + [Resolved] + private OsuGame? game { get; set; } + protected override void LoadComplete() { base.LoadComplete(); @@ -148,7 +148,7 @@ namespace osu.Game.Overlays.BeatmapListing if (!Active.Value) disclaimerShown.Value = true; - if (game.HideUnlicensedContent) + if (game?.HideUnlicensedContent == true) { Enabled.Value = false; Active.Disabled = true; From 097828ded208d872bf886579741fe72197781f01 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 23 Dec 2024 22:07:42 +0900 Subject: [PATCH 092/290] Fix incorrect mouse wheel mappings --- osu.Game/Input/Bindings/GlobalActionContainer.cs | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/osu.Game/Input/Bindings/GlobalActionContainer.cs b/osu.Game/Input/Bindings/GlobalActionContainer.cs index c343b4e1e6..35d2465084 100644 --- a/osu.Game/Input/Bindings/GlobalActionContainer.cs +++ b/osu.Game/Input/Bindings/GlobalActionContainer.cs @@ -142,10 +142,8 @@ namespace osu.Game.Input.Bindings new KeyBinding(new[] { InputKey.Control, InputKey.J }, GlobalAction.EditorFlipVertically), new KeyBinding(new[] { InputKey.Control, InputKey.Alt, InputKey.MouseWheelDown }, GlobalAction.EditorDecreaseDistanceSpacing), new KeyBinding(new[] { InputKey.Control, InputKey.Alt, InputKey.MouseWheelUp }, GlobalAction.EditorIncreaseDistanceSpacing), - // Framework automatically converts wheel up/down to left/right when shift is held. - // See https://github.com/ppy/osu-framework/blob/master/osu.Framework/Input/StateChanges/MouseScrollRelativeInput.cs#L37-L38. - new KeyBinding(new[] { InputKey.Control, InputKey.MouseWheelRight }, GlobalAction.EditorCyclePreviousBeatSnapDivisor), - new KeyBinding(new[] { InputKey.Control, InputKey.MouseWheelLeft }, GlobalAction.EditorCycleNextBeatSnapDivisor), + new KeyBinding(new[] { InputKey.Control, InputKey.MouseWheelUp }, GlobalAction.EditorCyclePreviousBeatSnapDivisor), + new KeyBinding(new[] { InputKey.Control, InputKey.MouseWheelDown }, GlobalAction.EditorCycleNextBeatSnapDivisor), new KeyBinding(new[] { InputKey.Control, InputKey.R }, GlobalAction.EditorToggleRotateControl), new KeyBinding(new[] { InputKey.Control, InputKey.E }, GlobalAction.EditorToggleScaleControl), new KeyBinding(new[] { InputKey.Control, InputKey.Left }, GlobalAction.EditorSeekToPreviousHitObject), From 9ff4a58fa3724904c13f1117c14ab03824963dda Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 23 Dec 2024 22:14:03 +0900 Subject: [PATCH 093/290] Add migration to update users which have previous default bindings for beat snap --- osu.Game/Database/RealmAccess.cs | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/osu.Game/Database/RealmAccess.cs b/osu.Game/Database/RealmAccess.cs index a520040ad1..e9fd82c4ff 100644 --- a/osu.Game/Database/RealmAccess.cs +++ b/osu.Game/Database/RealmAccess.cs @@ -96,7 +96,7 @@ namespace osu.Game.Database /// 43 2024-10-14 Reset keybind for toggling FPS display to avoid conflict with "convert to stream" in the editor, if not already changed by user. /// 44 2024-11-22 Removed several properties from BeatmapInfo which did not need to be persisted to realm. /// - private const int schema_version = 44; + private const int schema_version = 45; /// /// Lock object which is held during sections, blocking realm retrieval during blocking periods. @@ -1205,6 +1205,22 @@ namespace osu.Game.Database break; } + + case 45: + { + // Cycling beat snap divisors no longer requires holding shift (just control). + var keyBindings = migration.NewRealm.All(); + + var nextBeatSnapBinding = keyBindings.FirstOrDefault(k => k.ActionInt == (int)GlobalAction.EditorCycleNextBeatSnapDivisor); + if (nextBeatSnapBinding != null && nextBeatSnapBinding.KeyCombination.Keys.SequenceEqual(new[] { InputKey.Shift, InputKey.Control, InputKey.MouseWheelLeft })) + migration.NewRealm.Remove(nextBeatSnapBinding); + + var previousBeatSnapBinding = keyBindings.FirstOrDefault(k => k.ActionInt == (int)GlobalAction.EditorCyclePreviousBeatSnapDivisor); + if (previousBeatSnapBinding != null && previousBeatSnapBinding.KeyCombination.Keys.SequenceEqual(new[] { InputKey.Shift, InputKey.Control, InputKey.MouseWheelRight })) + migration.NewRealm.Remove(previousBeatSnapBinding); + + break; + } } Logger.Log($"Migration completed in {stopwatch.ElapsedMilliseconds}ms"); From 050bf9ec6033b26a4a0cb6878738dc66346ba0b7 Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Mon, 23 Dec 2024 10:52:18 -0500 Subject: [PATCH 094/290] Keep 'x' symbol visible even while disabled --- ...BeatmapSearchMultipleSelectionFilterRow.cs | 20 ++++--------------- 1 file changed, 4 insertions(+), 16 deletions(-) diff --git a/osu.Game/Overlays/BeatmapListing/BeatmapSearchMultipleSelectionFilterRow.cs b/osu.Game/Overlays/BeatmapListing/BeatmapSearchMultipleSelectionFilterRow.cs index 27b630d623..b4940d3aa1 100644 --- a/osu.Game/Overlays/BeatmapListing/BeatmapSearchMultipleSelectionFilterRow.cs +++ b/osu.Game/Overlays/BeatmapListing/BeatmapSearchMultipleSelectionFilterRow.cs @@ -107,7 +107,6 @@ namespace osu.Game.Overlays.BeatmapListing { private Container activeContent = null!; private Circle background = null!; - private SpriteIcon icon = null!; public MultipleSelectionFilterTabItem(T value) : base(value) @@ -129,6 +128,7 @@ namespace osu.Game.Overlays.BeatmapListing Alpha = 0, Padding = new MarginPadding { + Left = -16, Right = -4, Vertical = -2 }, @@ -139,9 +139,8 @@ namespace osu.Game.Overlays.BeatmapListing Colour = Color4.White, RelativeSizeAxes = Axes.Both, }, - icon = new SpriteIcon + new SpriteIcon { - Alpha = 0f, Icon = FontAwesome.Solid.TimesCircle, Size = new Vector2(10), Colour = ColourProvider.Background4, @@ -173,19 +172,8 @@ namespace osu.Game.Overlays.BeatmapListing if (Active.Value) { - if (Enabled.Value) - { - // This just allows enough spacing for adjacent tab items to show the "x". - Padding = new MarginPadding { Left = 12 }; - activeContent.Padding = activeContent.Padding with { Left = -16 }; - icon.Show(); - } - else - { - Padding = new MarginPadding(); - activeContent.Padding = activeContent.Padding with { Left = -4 }; - icon.Hide(); - } + // This just allows enough spacing for adjacent tab items to show the "x". + Padding = new MarginPadding { Left = 12 }; activeContent.FadeIn(200, Easing.OutQuint); background.FadeColour(colour, 200, Easing.OutQuint); From 7e3477f4bbfaa9cb1c01dea68b320e7267c5bbda Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Mon, 23 Dec 2024 10:54:52 -0500 Subject: [PATCH 095/290] Remove unnecessary guarding --- .../BeatmapListing/BeatmapSearchMultipleSelectionFilterRow.cs | 2 -- 1 file changed, 2 deletions(-) diff --git a/osu.Game/Overlays/BeatmapListing/BeatmapSearchMultipleSelectionFilterRow.cs b/osu.Game/Overlays/BeatmapListing/BeatmapSearchMultipleSelectionFilterRow.cs index b4940d3aa1..73af62c322 100644 --- a/osu.Game/Overlays/BeatmapListing/BeatmapSearchMultipleSelectionFilterRow.cs +++ b/osu.Game/Overlays/BeatmapListing/BeatmapSearchMultipleSelectionFilterRow.cs @@ -76,8 +76,6 @@ namespace osu.Game.Overlays.BeatmapListing { if (!c.Active.Disabled) c.Active.Value = Current.Contains(c.Value); - else if (c.Active.Value != Current.Contains(c.Value)) - throw new InvalidOperationException($"Expected filter {c.Value} to be set to {Current.Contains(c.Value)}, but was {c.Active.Value}"); } } From 6b635d588f16af12bde4340640aee476197795fd Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Mon, 23 Dec 2024 10:59:06 -0500 Subject: [PATCH 096/290] Add tooltip --- osu.Game/Localisation/BeatmapOverlayStrings.cs | 5 +++++ .../Overlays/BeatmapListing/BeatmapSearchGeneralFilterRow.cs | 5 ++++- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/osu.Game/Localisation/BeatmapOverlayStrings.cs b/osu.Game/Localisation/BeatmapOverlayStrings.cs index fc818f7596..43ffa17d93 100644 --- a/osu.Game/Localisation/BeatmapOverlayStrings.cs +++ b/osu.Game/Localisation/BeatmapOverlayStrings.cs @@ -28,6 +28,11 @@ This includes content that may not be correctly licensed for osu! usage. Browse /// public static LocalisableString UserContentConfirmButtonText => new TranslatableString(getKey(@"understood"), @"I understand"); + /// + /// "Toggling this filter is disabled in this platform." + /// + public static LocalisableString FeaturedArtistsDisabledTooltip => new TranslatableString(getKey(@"featured_artists_disabled_tooltip"), @"Toggling this filter is disabled in this platform."); + private static string getKey(string key) => $@"{prefix}:{key}"; } } diff --git a/osu.Game/Overlays/BeatmapListing/BeatmapSearchGeneralFilterRow.cs b/osu.Game/Overlays/BeatmapListing/BeatmapSearchGeneralFilterRow.cs index e4c663ee13..b9720f06e8 100644 --- a/osu.Game/Overlays/BeatmapListing/BeatmapSearchGeneralFilterRow.cs +++ b/osu.Game/Overlays/BeatmapListing/BeatmapSearchGeneralFilterRow.cs @@ -5,6 +5,7 @@ using System; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Extensions; +using osu.Framework.Graphics.Cursor; using osu.Framework.Graphics.Sprites; using osu.Framework.Input.Events; using osu.Framework.Localisation; @@ -113,7 +114,7 @@ namespace osu.Game.Overlays.BeatmapListing } } - private partial class FeaturedArtistsTabItem : MultipleSelectionFilterTabItem + private partial class FeaturedArtistsTabItem : MultipleSelectionFilterTabItem, IHasTooltip { private Bindable disclaimerShown = null!; @@ -137,6 +138,8 @@ namespace osu.Game.Overlays.BeatmapListing [Resolved] private OsuGame? game { get; set; } + public LocalisableString TooltipText => !Enabled.Value ? BeatmapOverlayStrings.FeaturedArtistsDisabledTooltip : string.Empty; + protected override void LoadComplete() { base.LoadComplete(); From 47afab8a32fb312601f8d5b18fb6a9cae6de6e97 Mon Sep 17 00:00:00 2001 From: Plextora <71889427+Plextora@users.noreply.github.com> Date: Mon, 23 Dec 2024 12:47:50 -0500 Subject: [PATCH 097/290] Use yellow instead of pink --- osu.Game/Online/Leaderboards/LeaderboardScore.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Online/Leaderboards/LeaderboardScore.cs b/osu.Game/Online/Leaderboards/LeaderboardScore.cs index 9aa0e0fbe2..32b25a866d 100644 --- a/osu.Game/Online/Leaderboards/LeaderboardScore.cs +++ b/osu.Game/Online/Leaderboards/LeaderboardScore.cs @@ -130,7 +130,7 @@ namespace osu.Game.Online.Leaderboards background = new Box { RelativeSizeAxes = Axes.Both, - Colour = isUserFriend ? colour.Pink : (user.OnlineID == api.LocalUser.Value.Id && isOnlineScope ? colour.Green : Color4.Black), + Colour = isUserFriend ? colour.Yellow : (user.OnlineID == api.LocalUser.Value.Id && isOnlineScope ? colour.Green : Color4.Black), Alpha = background_alpha, }, }, From 7e8aaa68ff11082ff60a3c8b85d54e21444553a1 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 24 Dec 2024 11:46:39 +0900 Subject: [PATCH 098/290] Add keywords for intro-related settings --- .../Settings/Sections/UserInterface/MainMenuSettings.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/osu.Game/Overlays/Settings/Sections/UserInterface/MainMenuSettings.cs b/osu.Game/Overlays/Settings/Sections/UserInterface/MainMenuSettings.cs index 5e42c3035c..c50d56b458 100644 --- a/osu.Game/Overlays/Settings/Sections/UserInterface/MainMenuSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/UserInterface/MainMenuSettings.cs @@ -36,11 +36,13 @@ namespace osu.Game.Overlays.Settings.Sections.UserInterface }, new SettingsCheckbox { + Keywords = new[] { "intro", "welcome" }, LabelText = UserInterfaceStrings.InterfaceVoices, Current = config.GetBindable(OsuSetting.MenuVoice) }, new SettingsCheckbox { + Keywords = new[] { "intro", "welcome" }, LabelText = UserInterfaceStrings.OsuMusicTheme, Current = config.GetBindable(OsuSetting.MenuMusic) }, From 282c67d14bf5d4071beb64602d0c5d3420ea864a Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 24 Dec 2024 11:59:45 +0900 Subject: [PATCH 099/290] Update resources --- osu.Game/osu.Game.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj index fe3bdbffa3..51bed31afb 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -36,7 +36,7 @@ - + From 8762e3fedb5139a70b1914dbb5e797e865a1cd85 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 24 Dec 2024 12:12:49 +0900 Subject: [PATCH 100/290] Always show tooltip, and reword to be always applicable --- osu.Game/Localisation/BeatmapOverlayStrings.cs | 4 ++-- .../Overlays/BeatmapListing/BeatmapSearchGeneralFilterRow.cs | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Game/Localisation/BeatmapOverlayStrings.cs b/osu.Game/Localisation/BeatmapOverlayStrings.cs index 43ffa17d93..f8122c1ef9 100644 --- a/osu.Game/Localisation/BeatmapOverlayStrings.cs +++ b/osu.Game/Localisation/BeatmapOverlayStrings.cs @@ -29,9 +29,9 @@ This includes content that may not be correctly licensed for osu! usage. Browse public static LocalisableString UserContentConfirmButtonText => new TranslatableString(getKey(@"understood"), @"I understand"); /// - /// "Toggling this filter is disabled in this platform." + /// "Featured Artists are music artists who have collaborated with osu! to make a selection of their tracks available for use in beatmaps. For some osu! releases, we showcase only featured artist beatmaps to better support the surrounding ecosystem." /// - public static LocalisableString FeaturedArtistsDisabledTooltip => new TranslatableString(getKey(@"featured_artists_disabled_tooltip"), @"Toggling this filter is disabled in this platform."); + public static LocalisableString FeaturedArtistsTooltip => new TranslatableString(getKey(@"featured_artists_disabled_tooltip"), @"Featured Artists are music artists who have collaborated with osu! to make a selection of their tracks available for use in beatmaps. For some osu! releases, we showcase only featured artist beatmaps to better support the surrounding ecosystem."); private static string getKey(string key) => $@"{prefix}:{key}"; } diff --git a/osu.Game/Overlays/BeatmapListing/BeatmapSearchGeneralFilterRow.cs b/osu.Game/Overlays/BeatmapListing/BeatmapSearchGeneralFilterRow.cs index b9720f06e8..b62836dfde 100644 --- a/osu.Game/Overlays/BeatmapListing/BeatmapSearchGeneralFilterRow.cs +++ b/osu.Game/Overlays/BeatmapListing/BeatmapSearchGeneralFilterRow.cs @@ -138,7 +138,7 @@ namespace osu.Game.Overlays.BeatmapListing [Resolved] private OsuGame? game { get; set; } - public LocalisableString TooltipText => !Enabled.Value ? BeatmapOverlayStrings.FeaturedArtistsDisabledTooltip : string.Empty; + public LocalisableString TooltipText => BeatmapOverlayStrings.FeaturedArtistsTooltip; protected override void LoadComplete() { From ae9c7e1b354c43fc606a75031514ea56ec648723 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 24 Dec 2024 13:17:58 +0900 Subject: [PATCH 101/290] Adjust layout and remove localisable strings for temporary buttons --- osu.Game/Localisation/SkinSettingsStrings.cs | 15 -- .../Overlays/Settings/Sections/SkinSection.cs | 150 +++++++++--------- 2 files changed, 76 insertions(+), 89 deletions(-) diff --git a/osu.Game/Localisation/SkinSettingsStrings.cs b/osu.Game/Localisation/SkinSettingsStrings.cs index 1a812ad04d..16dca7fd87 100644 --- a/osu.Game/Localisation/SkinSettingsStrings.cs +++ b/osu.Game/Localisation/SkinSettingsStrings.cs @@ -54,21 +54,6 @@ namespace osu.Game.Localisation /// public static LocalisableString BeatmapHitsounds => new TranslatableString(getKey(@"beatmap_hitsounds"), @"Beatmap hitsounds"); - /// - /// "Rename selected skin" - /// - public static LocalisableString RenameSkinButton = new TranslatableString(getKey(@"rename_skin_button"), @"Rename selected skin"); - - /// - /// "Export selected skin" - /// - public static LocalisableString ExportSkinButton => new TranslatableString(getKey(@"export_skin_button"), @"Export selected skin"); - - /// - /// "Delete selected skin" - /// - public static LocalisableString DeleteSkinButton => new TranslatableString(getKey(@"delete_skin_button"), @"Delete selected skin"); - private static string getKey(string key) => $"{prefix}:{key}"; } } diff --git a/osu.Game/Overlays/Settings/Sections/SkinSection.cs b/osu.Game/Overlays/Settings/Sections/SkinSection.cs index 1792c61d48..7fffd3693c 100644 --- a/osu.Game/Overlays/Settings/Sections/SkinSection.cs +++ b/osu.Game/Overlays/Settings/Sections/SkinSection.cs @@ -75,9 +75,21 @@ namespace osu.Game.Overlays.Settings.Sections Text = SkinSettingsStrings.SkinLayoutEditor, Action = () => skinEditor?.ToggleVisibility(), }, - new RenameSkinButton(), - new ExportSkinButton(), - new DeleteSkinButton(), + new FillFlowContainer + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Direction = FillDirection.Horizontal, + Spacing = new Vector2(5, 0), + Padding = new MarginPadding { Left = SettingsPanel.CONTENT_MARGINS, Right = SettingsPanel.CONTENT_MARGINS }, + Children = new Drawable[] + { + // This is all super-temporary until we move skin settings to their own panel / overlay. + new RenameSkinButton { Padding = new MarginPadding(), RelativeSizeAxes = Axes.None, Width = 120 }, + new ExportSkinButton { Padding = new MarginPadding(), RelativeSizeAxes = Axes.None, Width = 120 }, + new DeleteSkinButton { Padding = new MarginPadding(), RelativeSizeAxes = Axes.None, Width = 110 }, + } + }, }; } @@ -153,7 +165,7 @@ namespace osu.Game.Overlays.Settings.Sections [BackgroundDependencyLoader] private void load() { - Text = SkinSettingsStrings.RenameSkinButton; + Text = "Rename"; Action = this.ShowPopover; } @@ -169,74 +181,6 @@ namespace osu.Game.Overlays.Settings.Sections { return new RenameSkinPopover(); } - - public partial class RenameSkinPopover : OsuPopover - { - [Resolved] - private SkinManager skins { get; set; } - - public Action Rename { get; init; } - - private readonly FocusedTextBox textBox; - - public RenameSkinPopover() - { - AutoSizeAxes = Axes.Both; - Origin = Anchor.TopCentre; - - RoundedButton renameButton; - - Child = new FillFlowContainer - { - Direction = FillDirection.Vertical, - AutoSizeAxes = Axes.Y, - Width = 250, - Spacing = new Vector2(10f), - Children = new Drawable[] - { - textBox = new FocusedTextBox - { - PlaceholderText = @"Skin name", - FontSize = OsuFont.DEFAULT_FONT_SIZE, - RelativeSizeAxes = Axes.X, - SelectAllOnFocus = true, - }, - renameButton = new RoundedButton - { - Height = 40, - RelativeSizeAxes = Axes.X, - MatchingFilter = true, - Text = SkinSettingsStrings.RenameSkinButton, - } - } - }; - - renameButton.Action += rename; - - void onTextboxCommit(TextBox sender, bool newText) - { - rename(); - } - - textBox.OnCommit += onTextboxCommit; - } - - protected override void PopIn() - { - textBox.Text = skins.CurrentSkinInfo.Value.Value.Name; - textBox.TakeFocus(); - base.PopIn(); - } - - private void rename() - { - skins.CurrentSkinInfo.Value.PerformWrite(skin => - { - skin.Name = textBox.Text; - PopOut(); - }); - } - } } public partial class ExportSkinButton : SettingsButton @@ -249,7 +193,7 @@ namespace osu.Game.Overlays.Settings.Sections [BackgroundDependencyLoader] private void load() { - Text = SkinSettingsStrings.ExportSkinButton; + Text = "Export"; Action = export; } @@ -287,7 +231,7 @@ namespace osu.Game.Overlays.Settings.Sections [BackgroundDependencyLoader] private void load() { - Text = SkinSettingsStrings.DeleteSkinButton; + Text = "Delete"; Action = delete; } @@ -304,5 +248,63 @@ namespace osu.Game.Overlays.Settings.Sections dialogOverlay?.Push(new SkinDeleteDialog(currentSkin.Value)); } } + + public partial class RenameSkinPopover : OsuPopover + { + [Resolved] + private SkinManager skins { get; set; } + + private readonly FocusedTextBox textBox; + + public RenameSkinPopover() + { + AutoSizeAxes = Axes.Both; + Origin = Anchor.TopCentre; + + RoundedButton renameButton; + + Child = new FillFlowContainer + { + Direction = FillDirection.Vertical, + AutoSizeAxes = Axes.Y, + Width = 250, + Spacing = new Vector2(10f), + Children = new Drawable[] + { + textBox = new FocusedTextBox + { + PlaceholderText = @"Skin name", + FontSize = OsuFont.DEFAULT_FONT_SIZE, + RelativeSizeAxes = Axes.X, + SelectAllOnFocus = true, + }, + renameButton = new RoundedButton + { + Height = 40, + RelativeSizeAxes = Axes.X, + MatchingFilter = true, + Text = "Save", + } + } + }; + + renameButton.Action += rename; + textBox.OnCommit += (_, _) => rename(); + } + + protected override void PopIn() + { + textBox.Text = skins.CurrentSkinInfo.Value.Value.Name; + textBox.TakeFocus(); + + base.PopIn(); + } + + private void rename() => skins.CurrentSkinInfo.Value.PerformWrite(skin => + { + skin.Name = textBox.Text; + PopOut(); + }); + } } } From 378bef34efab9980bbb6de9d62726a3349ae3a6c Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 24 Dec 2024 13:42:18 +0900 Subject: [PATCH 102/290] Change order of skin layout editor button for better visual balance --- osu.Game/Overlays/Settings/Sections/SkinSection.cs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/osu.Game/Overlays/Settings/Sections/SkinSection.cs b/osu.Game/Overlays/Settings/Sections/SkinSection.cs index 7fffd3693c..a89d5e2f4a 100644 --- a/osu.Game/Overlays/Settings/Sections/SkinSection.cs +++ b/osu.Game/Overlays/Settings/Sections/SkinSection.cs @@ -70,11 +70,6 @@ namespace osu.Game.Overlays.Settings.Sections Current = skins.CurrentSkinInfo, Keywords = new[] { @"skins" }, }, - new SettingsButton - { - Text = SkinSettingsStrings.SkinLayoutEditor, - Action = () => skinEditor?.ToggleVisibility(), - }, new FillFlowContainer { RelativeSizeAxes = Axes.X, @@ -90,6 +85,11 @@ namespace osu.Game.Overlays.Settings.Sections new DeleteSkinButton { Padding = new MarginPadding(), RelativeSizeAxes = Axes.None, Width = 110 }, } }, + new SettingsButton + { + Text = SkinSettingsStrings.SkinLayoutEditor, + Action = () => skinEditor?.ToggleVisibility(), + }, }; } From a5d354d753302c318ade8cb56fbe1d884e20942a Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 24 Dec 2024 15:17:10 +0900 Subject: [PATCH 103/290] Update framework --- osu.Android.props | 2 +- osu.Game/osu.Game.csproj | 2 +- osu.iOS.props | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Android.props b/osu.Android.props index f13760bd21..84827ce76b 100644 --- a/osu.Android.props +++ b/osu.Android.props @@ -10,7 +10,7 @@ true - + diff --git a/osu.iOS.props b/osu.iOS.props index 3e618a3a74..349d6fa1d7 100644 --- a/osu.iOS.props +++ b/osu.iOS.props @@ -17,6 +17,6 @@ -all - + From b8d6bba03924ed96328d04e6c9ce7fe5041afa59 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 24 Dec 2024 16:05:44 +0900 Subject: [PATCH 104/290] Fix legacy hitcircle fallback logic being broken with recent fix I was a bit too eager to replace all calls with the new `provider` in https://github.com/ppy/osu/commit/dae380b7fa927c351e2e413c5b23834f717908d9, while it doesn't actually make sense. To handle the case that was trying to be fixed, using the `provider` to check whether the *prefix* version of the circle sprite is available is enough alone. Closes https://github.com/ppy/osu/issues/31200 --- .../Skinning/Legacy/LegacyMainCirclePiece.cs | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyMainCirclePiece.cs b/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyMainCirclePiece.cs index 0dc0f065d4..e74ffaac0c 100644 --- a/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyMainCirclePiece.cs +++ b/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyMainCirclePiece.cs @@ -61,13 +61,16 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy var drawableOsuObject = (DrawableOsuHitObject?)drawableObject; - // As a precondition, ensure that any prefix lookups are run against the skin which is providing "hitcircle". + // As a precondition, prefer that any *prefix* lookups are run against the skin which is providing "hitcircle". // This is to correctly handle a case such as: // // - Beatmap provides `hitcircle` // - User skin provides `sliderstartcircle` // // In such a case, the `hitcircle` should be used for slider start circles rather than the user's skin override. + // + // Of note, this consideration should only be used to decide whether to continue looking up the prefixed name or not. + // The final lookups must still run on the full skin hierarchy as per usual in order to correctly handle fallback cases. var provider = skin.FindProvider(s => s.GetTexture(base_lookup) != null) ?? skin; // if a base texture for the specified prefix exists, continue using it for subsequent lookups. @@ -81,7 +84,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy // expected behaviour in this scenario is not showing the overlay, rather than using hitcircleoverlay.png. InternalChildren = new[] { - CircleSprite = new LegacyKiaiFlashingDrawable(() => new Sprite { Texture = provider.GetTexture(circleName)?.WithMaximumSize(maxSize) }) + CircleSprite = new LegacyKiaiFlashingDrawable(() => new Sprite { Texture = skin.GetTexture(circleName)?.WithMaximumSize(maxSize) }) { Anchor = Anchor.Centre, Origin = Anchor.Centre, @@ -90,7 +93,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy { Anchor = Anchor.Centre, Origin = Anchor.Centre, - Child = OverlaySprite = new LegacyKiaiFlashingDrawable(() => new Sprite { Texture = provider.GetTexture(@$"{circleName}overlay")?.WithMaximumSize(maxSize) }) + Child = OverlaySprite = new LegacyKiaiFlashingDrawable(() => new Sprite { Texture = skin.GetTexture(@$"{circleName}overlay")?.WithMaximumSize(maxSize) }) { Anchor = Anchor.Centre, Origin = Anchor.Centre, From d9be172647c81972247075c5eae14608ace9f99d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 24 Dec 2024 08:17:25 +0100 Subject: [PATCH 105/290] Add explanatory comment for schema version bump --- osu.Game/Database/RealmAccess.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/osu.Game/Database/RealmAccess.cs b/osu.Game/Database/RealmAccess.cs index e9fd82c4ff..b412348595 100644 --- a/osu.Game/Database/RealmAccess.cs +++ b/osu.Game/Database/RealmAccess.cs @@ -95,6 +95,7 @@ namespace osu.Game.Database /// 42 2024-08-07 Update mania key bindings to reflect changes to ManiaAction /// 43 2024-10-14 Reset keybind for toggling FPS display to avoid conflict with "convert to stream" in the editor, if not already changed by user. /// 44 2024-11-22 Removed several properties from BeatmapInfo which did not need to be persisted to realm. + /// 45 2024-12-23 Change beat snap divisor adjust defaults to be Ctrl+Scroll instead of Ctrl+Shift+Scroll, if not already changed by user. /// private const int schema_version = 45; From d8686f55f7178bbdbee3d85a60c3f3e5c36431c8 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 24 Dec 2024 17:10:48 +0900 Subject: [PATCH 106/290] Slightly reduce background brightness at main menu when seasonal lighting is active --- osu.Game/Screens/Menu/MainMenu.cs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/Menu/MainMenu.cs b/osu.Game/Screens/Menu/MainMenu.cs index a5acc6a1c2..99bc1825f5 100644 --- a/osu.Game/Screens/Menu/MainMenu.cs +++ b/osu.Game/Screens/Menu/MainMenu.cs @@ -202,18 +202,20 @@ namespace osu.Game.Screens.Menu holdToExitGameOverlay?.CreateProxy() ?? Empty() }); + float baseDim = SeasonalUIConfig.ENABLED ? 0.84f : 1; + Buttons.StateChanged += state => { switch (state) { case ButtonSystemState.Initial: case ButtonSystemState.Exit: - ApplyToBackground(b => b.FadeColour(Color4.White, 500, Easing.OutSine)); + ApplyToBackground(b => b.FadeColour(OsuColour.Gray(baseDim), 500, Easing.OutSine)); onlineMenuBanner.State.Value = Visibility.Hidden; break; default: - ApplyToBackground(b => b.FadeColour(OsuColour.Gray(0.8f), 500, Easing.OutSine)); + ApplyToBackground(b => b.FadeColour(OsuColour.Gray(baseDim * 0.8f), 500, Easing.OutSine)); onlineMenuBanner.State.Value = Visibility.Visible; break; } From ce1eda7e54516921bc25d1a3ed6ee0c7e307ade9 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 24 Dec 2024 17:11:21 +0900 Subject: [PATCH 107/290] Fix adjusting volume using scroll wheel not working during intro --- osu.Game/Screens/Menu/IntroScreen.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/osu.Game/Screens/Menu/IntroScreen.cs b/osu.Game/Screens/Menu/IntroScreen.cs index 9885c061a9..c110c53df8 100644 --- a/osu.Game/Screens/Menu/IntroScreen.cs +++ b/osu.Game/Screens/Menu/IntroScreen.cs @@ -24,6 +24,7 @@ using osu.Game.Localisation; using osu.Game.Online.API; using osu.Game.Overlays; using osu.Game.Overlays.Notifications; +using osu.Game.Overlays.Volume; using osu.Game.Rulesets; using osu.Game.Screens.Backgrounds; using osu.Game.Skinning; @@ -174,6 +175,8 @@ namespace osu.Game.Screens.Menu return UsingThemedIntro = initialBeatmap != null; } + + AddInternal(new GlobalScrollAdjustsVolume()); } public override void OnEntering(ScreenTransitionEvent e) From 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 108/290] 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 109/290] 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 110/290] 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 111/290] 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 112/290] 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 113/290] 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 114/290] Add failing test case --- .../Beatmaps/IO/LegacyBeatmapExporterTest.cs | 24 ++++++++++++++++++ .../Archives/fractional-coordinates.olz | Bin 0 -> 556 bytes 2 files changed, 24 insertions(+) create mode 100644 osu.Game.Tests/Resources/Archives/fractional-coordinates.olz diff --git a/osu.Game.Tests/Beatmaps/IO/LegacyBeatmapExporterTest.cs b/osu.Game.Tests/Beatmaps/IO/LegacyBeatmapExporterTest.cs index 8a95d26782..cf498c7856 100644 --- a/osu.Game.Tests/Beatmaps/IO/LegacyBeatmapExporterTest.cs +++ b/osu.Game.Tests/Beatmaps/IO/LegacyBeatmapExporterTest.cs @@ -11,6 +11,7 @@ using osu.Framework.Testing; using osu.Game.Beatmaps; using osu.Game.Database; using osu.Game.IO.Archives; +using osu.Game.Rulesets.Objects.Types; using osu.Game.Tests.Resources; using osu.Game.Tests.Visual; using MemoryStream = System.IO.MemoryStream; @@ -50,6 +51,29 @@ namespace osu.Game.Tests.Beatmaps.IO AddAssert("hit object is snapped", () => beatmap.Beatmap.HitObjects[0].StartTime, () => Is.EqualTo(28519).Within(0.001)); } + [Test] + public void TestFractionalObjectCoordinatesRounded() + { + IWorkingBeatmap beatmap = null!; + MemoryStream outStream = null!; + + // Ensure importer encoding is correct + AddStep("import beatmap", () => beatmap = importBeatmapFromArchives(@"fractional-coordinates.olz")); + AddAssert("hit object has fractional position", () => ((IHasYPosition)beatmap.Beatmap.HitObjects[1]).Y, () => Is.EqualTo(383.99997).Within(0.00001)); + + // Ensure exporter legacy conversion is correct + AddStep("export", () => + { + outStream = new MemoryStream(); + + new LegacyBeatmapExporter(LocalStorage) + .ExportToStream((BeatmapSetInfo)beatmap.BeatmapInfo.BeatmapSet!, outStream, null); + }); + + AddStep("import beatmap again", () => beatmap = importBeatmapFromStream(outStream)); + AddAssert("hit object is snapped", () => ((IHasYPosition)beatmap.Beatmap.HitObjects[1]).Y, () => Is.EqualTo(384).Within(0.00001)); + } + [Test] public void TestExportStability() { diff --git a/osu.Game.Tests/Resources/Archives/fractional-coordinates.olz b/osu.Game.Tests/Resources/Archives/fractional-coordinates.olz new file mode 100644 index 0000000000000000000000000000000000000000..5c5af368c8b95fe76c9f45f0dfbc5f1e73a126a5 GIT binary patch literal 556 zcmWIWW@Zs#-~d9~@Y#_JP;iZrfkBEvfg!CZF}Wl&KQA#yH#tAQC?zv5u_U!vFTc1n zG=!IdUA-zVT^ES0fVi}Rn}Lz#1v3K!mGuF23h}>PvNx-V+F!!I-FH zEGNUKc=wUltddJxI@Uis=>AF6hpR8S=*F9W_n+N=^82&5>Dur`4C_O7B^`Tx=;4eB z)7d91n0aHOXY_(y+Z$3uQ{;*>1Fl|ljbPFiGy1G?GvkGTUvx>xoMVeSd_uK-7y9j; zw|SM=gyxfb+@Gs>GowmE~dXeXY2Xr<2s9#*=r^0|K^@- zHNB**ovbUc%0gG<_!rO2jVI{q%V0+1|qIjV5RN0_Oi>7Q6DvV1s&==x=om ze(Bb|a(pZ2t6k~;d*PMhQoX3GzO=pd%)n9y0g96VZ$>5&W<-1;%Yotx2DUVUSmeiEfHx}}$OJ|p%mC8s JfOa!5002C=(%Aq2 literal 0 HcmV?d00001 From 0d16ed028b89c4ed92aa2efd7968557a700dbfcb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 27 Dec 2024 10:56:52 +0100 Subject: [PATCH 115/290] 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 116/290] 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 117/290] 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 118/290] 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 119/290] 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 120/290] 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 121/290] 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 122/290] 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 123/290] 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 124/290] 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 125/290] 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 126/290] 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 127/290] 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 128/290] 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 129/290] 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 130/290] 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 131/290] 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 132/290] 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 133/290] 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 134/290] 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 135/290] 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 136/290] 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 137/290] 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 138/290] 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 139/290] 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 140/290] 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 141/290] 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 142/290] 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 143/290] 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 144/290] 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 145/290] 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 146/290] 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 147/290] 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 148/290] 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 149/290] 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 150/290] 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 151/290] 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 152/290] 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 153/290] 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 154/290] 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 155/290] 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 156/290] 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 157/290] 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 158/290] 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 159/290] 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 160/290] 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 161/290] 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 162/290] 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 163/290] 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 164/290] 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 165/290] 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 166/290] 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 167/290] 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 168/290] 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 169/290] 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 170/290] 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 171/290] 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 172/290] 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 173/290] 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 174/290] 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 175/290] 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 176/290] Mark recommendation test as flaky Will revisit during song select refactoring no doubt. --- .../Visual/SongSelect/TestSceneBeatmapRecommendations.cs | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapRecommendations.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapRecommendations.cs index aa452101bf..5c89e8a02c 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapRecommendations.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapRecommendations.cs @@ -12,7 +12,6 @@ using osu.Framework.Platform; using osu.Framework.Testing; using osu.Game.Beatmaps; using osu.Game.Database; -using osu.Game.Extensions; using osu.Game.Graphics.Sprites; using osu.Game.Online.API; using osu.Game.Online.API.Requests; @@ -85,6 +84,7 @@ namespace osu.Game.Tests.Visual.SongSelect } [Test] + [FlakyTest] public void TestPresentedBeatmapIsRecommended() { List beatmapSets = null; @@ -106,6 +106,7 @@ namespace osu.Game.Tests.Visual.SongSelect } [Test] + [FlakyTest] public void TestCurrentRulesetIsRecommended() { BeatmapSetInfo catchSet = null, mixedSet = null; @@ -142,6 +143,7 @@ namespace osu.Game.Tests.Visual.SongSelect } [Test] + [FlakyTest] public void TestSecondBestRulesetIsRecommended() { BeatmapSetInfo osuSet = null, mixedSet = null; @@ -159,6 +161,7 @@ namespace osu.Game.Tests.Visual.SongSelect } [Test] + [FlakyTest] public void TestCorrectStarRatingIsUsed() { BeatmapSetInfo osuSet = null, maniaSet = null; @@ -176,6 +179,7 @@ namespace osu.Game.Tests.Visual.SongSelect } [Test] + [FlakyTest] public void TestBeatmapListingFilter() { AddStep("set playmode to taiko", () => ((DummyAPIAccess)API).LocalUser.Value.PlayMode = "taiko"); @@ -245,7 +249,7 @@ namespace osu.Game.Tests.Visual.SongSelect AddStep("present beatmap", () => Game.PresentBeatmap(getImport())); AddUntilStep("wait for song select", () => Game.ScreenStack.CurrentScreen is Screens.Select.SongSelect select && select.BeatmapSetsLoaded); - AddUntilStep("recommended beatmap displayed", () => Game.Beatmap.Value.BeatmapInfo.MatchesOnlineID(getImport().Beatmaps[expectedDiff - 1])); + AddUntilStep("recommended beatmap displayed", () => Game.Beatmap.Value.BeatmapInfo.OnlineID, () => Is.EqualTo(getImport().Beatmaps[expectedDiff - 1].OnlineID)); } protected override TestOsuGame CreateTestGame() => new NoBeatmapUpdateGame(LocalStorage, API); From 51b62a6d8e6877131542d2869f91158c000dcb50 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Tue, 7 Jan 2025 19:12:31 +0900 Subject: [PATCH 177/290] Display notification on friend presence changes --- .../TestSceneFriendPresenceNotifier.cs | 129 +++++++++++++++ osu.Game/Online/API/APIAccess.cs | 9 ++ osu.Game/Online/API/DummyAPIAccess.cs | 3 + osu.Game/Online/API/IAPIProvider.cs | 7 + osu.Game/Online/FriendPresenceNotifier.cs | 148 ++++++++++++++++++ osu.Game/OsuGame.cs | 1 + .../Visual/Metadata/TestMetadataClient.cs | 3 +- 7 files changed, 299 insertions(+), 1 deletion(-) create mode 100644 osu.Game.Tests/Visual/Components/TestSceneFriendPresenceNotifier.cs create mode 100644 osu.Game/Online/FriendPresenceNotifier.cs diff --git a/osu.Game.Tests/Visual/Components/TestSceneFriendPresenceNotifier.cs b/osu.Game.Tests/Visual/Components/TestSceneFriendPresenceNotifier.cs new file mode 100644 index 0000000000..851c1141db --- /dev/null +++ b/osu.Game.Tests/Visual/Components/TestSceneFriendPresenceNotifier.cs @@ -0,0 +1,129 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Linq; +using NUnit.Framework; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Testing; +using osu.Game.Online; +using osu.Game.Online.API; +using osu.Game.Online.API.Requests.Responses; +using osu.Game.Online.Chat; +using osu.Game.Online.Metadata; +using osu.Game.Overlays; +using osu.Game.Overlays.Notifications; +using osu.Game.Tests.Visual.Metadata; +using osu.Game.Users; +using osuTK.Input; + +namespace osu.Game.Tests.Visual.Components +{ + public partial class TestSceneFriendPresenceNotifier : OsuManualInputManagerTestScene + { + private ChannelManager channelManager = null!; + private NotificationOverlay notificationOverlay = null!; + private ChatOverlay chatOverlay = null!; + private TestMetadataClient metadataClient = null!; + + [SetUp] + public void Setup() => Schedule(() => + { + Child = new DependencyProvidingContainer + { + RelativeSizeAxes = Axes.Both, + CachedDependencies = + [ + (typeof(ChannelManager), channelManager = new ChannelManager(API)), + (typeof(INotificationOverlay), notificationOverlay = new NotificationOverlay()), + (typeof(ChatOverlay), chatOverlay = new ChatOverlay()), + (typeof(MetadataClient), metadataClient = new TestMetadataClient()), + ], + Children = new Drawable[] + { + channelManager, + notificationOverlay, + chatOverlay, + metadataClient, + new FriendPresenceNotifier() + } + }; + + for (int i = 1; i <= 100; i++) + ((DummyAPIAccess)API).Friends.Add(new APIRelation { TargetID = i, TargetUser = new APIUser { Username = $"Friend {i}" } }); + }); + + [Test] + public void TestNotifications() + { + AddStep("bring friend 1 online", () => metadataClient.UserPresenceUpdated(1, new UserPresence { Status = UserStatus.Online })); + AddUntilStep("wait for notification", () => notificationOverlay.AllNotifications.Count(), () => Is.EqualTo(1)); + AddStep("bring friend 1 offline", () => metadataClient.UserPresenceUpdated(1, null)); + AddUntilStep("wait for notification", () => notificationOverlay.AllNotifications.Count(), () => Is.EqualTo(2)); + } + + [Test] + public void TestSingleUserNotificationOpensChat() + { + AddStep("bring friend 1 online", () => metadataClient.UserPresenceUpdated(1, new UserPresence { Status = UserStatus.Online })); + AddUntilStep("wait for notification", () => notificationOverlay.AllNotifications.Count(), () => Is.EqualTo(1)); + + AddStep("click notification", () => + { + InputManager.MoveMouseTo(this.ChildrenOfType().First()); + InputManager.Click(MouseButton.Left); + }); + + AddUntilStep("chat overlay opened", () => chatOverlay.State.Value, () => Is.EqualTo(Visibility.Visible)); + AddUntilStep("user channel selected", () => channelManager.CurrentChannel.Value.Name, () => Is.EqualTo(((DummyAPIAccess)API).Friends[0].TargetUser!.Username)); + } + + [Test] + public void TestMultipleUserNotificationDoesNotOpenChat() + { + AddStep("bring friends 1 & 2 online", () => + { + metadataClient.UserPresenceUpdated(1, new UserPresence { Status = UserStatus.Online }); + metadataClient.UserPresenceUpdated(2, new UserPresence { Status = UserStatus.Online }); + }); + + AddUntilStep("wait for notification", () => notificationOverlay.AllNotifications.Count(), () => Is.EqualTo(1)); + + AddStep("click notification", () => + { + InputManager.MoveMouseTo(this.ChildrenOfType().First()); + InputManager.Click(MouseButton.Left); + }); + + AddAssert("chat overlay not opened", () => chatOverlay.State.Value, () => Is.EqualTo(Visibility.Hidden)); + } + + [Test] + public void TestNonFriendsDoNotNotify() + { + AddStep("bring non-friend 1000 online", () => metadataClient.UserPresenceUpdated(1000, new UserPresence { Status = UserStatus.Online })); + AddWaitStep("wait for possible notification", 10); + AddAssert("no notification", () => notificationOverlay.AllNotifications.Count(), () => Is.Zero); + } + + [Test] + public void TestPostManyDebounced() + { + AddStep("bring friends 1-10 online", () => + { + for (int i = 1; i <= 10; i++) + metadataClient.UserPresenceUpdated(i, new UserPresence { Status = UserStatus.Online }); + }); + + AddUntilStep("wait for notification", () => notificationOverlay.AllNotifications.Count(), () => Is.EqualTo(1)); + + AddStep("bring friends 1-10 offline", () => + { + for (int i = 1; i <= 10; i++) + metadataClient.UserPresenceUpdated(i, null); + }); + + AddUntilStep("wait for notification", () => notificationOverlay.AllNotifications.Count(), () => Is.EqualTo(2)); + } + } +} diff --git a/osu.Game/Online/API/APIAccess.cs b/osu.Game/Online/API/APIAccess.cs index ec48fa2436..39c09f2a5d 100644 --- a/osu.Game/Online/API/APIAccess.cs +++ b/osu.Game/Online/API/APIAccess.cs @@ -75,6 +75,7 @@ namespace osu.Game.Online.API protected bool HasLogin => authentication.Token.Value != null || (!string.IsNullOrEmpty(ProvidedUsername) && !string.IsNullOrEmpty(password)); + private readonly Dictionary friendsMapping = new Dictionary(); private readonly CancellationTokenSource cancellationToken = new CancellationTokenSource(); private readonly Logger log; @@ -403,6 +404,8 @@ namespace osu.Game.Online.API public IChatClient GetChatClient() => new WebSocketChatClient(this); + public APIRelation GetFriend(int userId) => friendsMapping.GetValueOrDefault(userId); + public RegistrationRequest.RegistrationRequestErrors CreateAccount(string email, string username, string password) { Debug.Assert(State.Value == APIState.Offline); @@ -594,6 +597,8 @@ namespace osu.Game.Online.API Schedule(() => { setLocalUser(createGuestUser()); + + friendsMapping.Clear(); friends.Clear(); }); @@ -610,7 +615,11 @@ namespace osu.Game.Online.API friendsReq.Failure += _ => state.Value = APIState.Failing; friendsReq.Success += res => { + friendsMapping.Clear(); friends.Clear(); + + foreach (var u in res) + friendsMapping[u.TargetID] = u; friends.AddRange(res); }; diff --git a/osu.Game/Online/API/DummyAPIAccess.cs b/osu.Game/Online/API/DummyAPIAccess.cs index 5d63c04925..ca4edb3d8f 100644 --- a/osu.Game/Online/API/DummyAPIAccess.cs +++ b/osu.Game/Online/API/DummyAPIAccess.cs @@ -2,6 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using System; +using System.Linq; using System.Threading; using System.Threading.Tasks; using osu.Framework.Bindables; @@ -194,6 +195,8 @@ namespace osu.Game.Online.API public IChatClient GetChatClient() => new TestChatClientConnector(this); + public APIRelation? GetFriend(int userId) => Friends.FirstOrDefault(r => r.TargetID == userId); + public RegistrationRequest.RegistrationRequestErrors? CreateAccount(string email, string username, string password) { Thread.Sleep(200); diff --git a/osu.Game/Online/API/IAPIProvider.cs b/osu.Game/Online/API/IAPIProvider.cs index 1c4b2da742..4655b26f84 100644 --- a/osu.Game/Online/API/IAPIProvider.cs +++ b/osu.Game/Online/API/IAPIProvider.cs @@ -152,6 +152,13 @@ namespace osu.Game.Online.API /// IChatClient GetChatClient(); + /// + /// Retrieves a friend from a given user ID. + /// + /// The friend's user ID. + /// The object representing the friend, if any. + APIRelation? GetFriend(int userId); + /// /// Create a new user account. This is a blocking operation. /// diff --git a/osu.Game/Online/FriendPresenceNotifier.cs b/osu.Game/Online/FriendPresenceNotifier.cs new file mode 100644 index 0000000000..8fcf1a9f69 --- /dev/null +++ b/osu.Game/Online/FriendPresenceNotifier.cs @@ -0,0 +1,148 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Collections.Generic; +using System.Linq; +using osu.Framework.Allocation; +using osu.Framework.Bindables; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Sprites; +using osu.Game.Graphics; +using osu.Game.Online.API; +using osu.Game.Online.API.Requests.Responses; +using osu.Game.Online.Chat; +using osu.Game.Online.Metadata; +using osu.Game.Overlays; +using osu.Game.Overlays.Notifications; +using osu.Game.Users; + +namespace osu.Game.Online +{ + public partial class FriendPresenceNotifier : Component + { + [Resolved] + private INotificationOverlay notifications { get; set; } = null!; + + [Resolved] + private IAPIProvider api { get; set; } = null!; + + [Resolved] + private MetadataClient metadataClient { get; set; } = null!; + + [Resolved] + private ChannelManager channelManager { get; set; } = null!; + + [Resolved] + private ChatOverlay chatOverlay { get; set; } = null!; + + [Resolved] + private OsuColour colours { get; set; } = null!; + + private readonly IBindableDictionary userStates = new BindableDictionary(); + private readonly HashSet onlineAlertQueue = new HashSet(); + private readonly HashSet offlineAlertQueue = new HashSet(); + + private double? lastOnlineAlertTime; + private double? lastOfflineAlertTime; + + protected override void LoadComplete() + { + base.LoadComplete(); + + userStates.BindTo(metadataClient.UserStates); + userStates.BindCollectionChanged((_, args) => + { + switch (args.Action) + { + case NotifyDictionaryChangedAction.Add: + foreach ((int userId, var _) in args.NewItems!) + { + if (api.GetFriend(userId)?.TargetUser is APIUser user) + { + if (!offlineAlertQueue.Remove(user)) + { + onlineAlertQueue.Add(user); + lastOnlineAlertTime ??= Time.Current; + } + } + } + + break; + + case NotifyDictionaryChangedAction.Remove: + foreach ((int userId, var _) in args.OldItems!) + { + if (api.GetFriend(userId)?.TargetUser is APIUser user) + { + if (!onlineAlertQueue.Remove(user)) + { + offlineAlertQueue.Add(user); + lastOfflineAlertTime ??= Time.Current; + } + } + } + + break; + } + }); + } + + protected override void Update() + { + base.Update(); + + alertOnlineUsers(); + alertOfflineUsers(); + } + + private void alertOnlineUsers() + { + if (onlineAlertQueue.Count == 0) + return; + + if (lastOnlineAlertTime == null || Time.Current - lastOnlineAlertTime < 1000) + return; + + APIUser? singleUser = onlineAlertQueue.Count == 1 ? onlineAlertQueue.Single() : null; + + notifications.Post(new SimpleNotification + { + Icon = FontAwesome.Solid.UserPlus, + Text = $"Online: {string.Join(@", ", onlineAlertQueue.Select(u => u.Username))}", + IconColour = colours.Green, + Activated = () => + { + if (singleUser != null) + { + channelManager.OpenPrivateChannel(singleUser); + chatOverlay.Show(); + } + + return true; + } + }); + + onlineAlertQueue.Clear(); + lastOnlineAlertTime = null; + } + + private void alertOfflineUsers() + { + if (offlineAlertQueue.Count == 0) + return; + + if (lastOfflineAlertTime == null || Time.Current - lastOfflineAlertTime < 1000) + return; + + notifications.Post(new SimpleNotification + { + Icon = FontAwesome.Solid.UserMinus, + Text = $"Offline: {string.Join(@", ", offlineAlertQueue.Select(u => u.Username))}", + IconColour = colours.Red + }); + + offlineAlertQueue.Clear(); + lastOfflineAlertTime = null; + } + } +} diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs index c20536a1ec..329ac89a6c 100644 --- a/osu.Game/OsuGame.cs +++ b/osu.Game/OsuGame.cs @@ -1151,6 +1151,7 @@ namespace osu.Game Add(externalLinkOpener = new ExternalLinkOpener()); Add(new MusicKeyBindingHandler()); Add(new OnlineStatusNotifier(() => ScreenStack.CurrentScreen)); + Add(new FriendPresenceNotifier()); // side overlays which cancel each other. var singleDisplaySideOverlays = new OverlayContainer[] { Settings, Notifications, FirstRunOverlay }; diff --git a/osu.Game/Tests/Visual/Metadata/TestMetadataClient.cs b/osu.Game/Tests/Visual/Metadata/TestMetadataClient.cs index 4a862750bc..6dd6392b3a 100644 --- a/osu.Game/Tests/Visual/Metadata/TestMetadataClient.cs +++ b/osu.Game/Tests/Visual/Metadata/TestMetadataClient.cs @@ -2,6 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using System; +using System.Linq; using System.Threading.Tasks; using osu.Framework.Allocation; using osu.Framework.Bindables; @@ -66,7 +67,7 @@ namespace osu.Game.Tests.Visual.Metadata public override Task UserPresenceUpdated(int userId, UserPresence? presence) { - if (isWatchingUserPresence.Value) + if (isWatchingUserPresence.Value || api.Friends.Any(f => f.TargetID == userId)) { if (presence.HasValue) userStates[userId] = presence.Value; From 3c03406b45f2c2e707eab5a1a61e7ab1fa4f4815 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 7 Jan 2025 11:23:47 +0100 Subject: [PATCH 178/290] 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 179/290] 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 180/290] Remove unused protected field --- osu.Game/Screens/Edit/Components/BottomBarContainer.cs | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/osu.Game/Screens/Edit/Components/BottomBarContainer.cs b/osu.Game/Screens/Edit/Components/BottomBarContainer.cs index da71457004..37337bc79f 100644 --- a/osu.Game/Screens/Edit/Components/BottomBarContainer.cs +++ b/osu.Game/Screens/Edit/Components/BottomBarContainer.cs @@ -2,7 +2,6 @@ // See the LICENCE file in the repository root for full licence text. using osu.Framework.Allocation; -using osu.Framework.Audio.Track; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; @@ -18,8 +17,6 @@ namespace osu.Game.Screens.Edit.Components protected readonly IBindable Beatmap = new Bindable(); - protected readonly IBindable Track = new Bindable(); - public readonly Drawable Background; private readonly Container content; @@ -45,10 +42,9 @@ namespace osu.Game.Screens.Edit.Components } [BackgroundDependencyLoader] - private void load(IBindable beatmap, EditorClock clock) + private void load(IBindable beatmap) { Beatmap.BindTo(beatmap); - Track.BindTo(clock.Track); } } } From 45e0adcd253f1dfa922723c502dab365b76f51cd Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Tue, 7 Jan 2025 19:32:30 +0900 Subject: [PATCH 181/290] Add config option --- osu.Game/Configuration/OsuConfigManager.cs | 2 ++ .../Localisation/OnlineSettingsStrings.cs | 12 +++++++++++- osu.Game/Online/FriendPresenceNotifier.cs | 19 +++++++++++++++++++ .../Online/AlertsAndPrivacySettings.cs | 6 ++++++ 4 files changed, 38 insertions(+), 1 deletion(-) diff --git a/osu.Game/Configuration/OsuConfigManager.cs b/osu.Game/Configuration/OsuConfigManager.cs index dd3abb6f81..3c463f6f0c 100644 --- a/osu.Game/Configuration/OsuConfigManager.cs +++ b/osu.Game/Configuration/OsuConfigManager.cs @@ -96,6 +96,7 @@ namespace osu.Game.Configuration SetDefault(OsuSetting.NotifyOnUsernameMentioned, true); SetDefault(OsuSetting.NotifyOnPrivateMessage, true); + SetDefault(OsuSetting.NotifyOnFriendPresenceChange, true); // Audio SetDefault(OsuSetting.VolumeInactive, 0.25, 0, 1, 0.01); @@ -417,6 +418,7 @@ namespace osu.Game.Configuration IntroSequence, NotifyOnUsernameMentioned, NotifyOnPrivateMessage, + NotifyOnFriendPresenceChange, UIHoldActivationDelay, HitLighting, StarFountains, diff --git a/osu.Game/Localisation/OnlineSettingsStrings.cs b/osu.Game/Localisation/OnlineSettingsStrings.cs index 8e8c81cf59..98364a3f5a 100644 --- a/osu.Game/Localisation/OnlineSettingsStrings.cs +++ b/osu.Game/Localisation/OnlineSettingsStrings.cs @@ -29,6 +29,16 @@ namespace osu.Game.Localisation /// public static LocalisableString NotifyOnPrivateMessage => new TranslatableString(getKey(@"notify_on_private_message"), @"Show a notification when you receive a private message"); + /// + /// "Show notification popups when friends change status" + /// + public static LocalisableString NotifyOnFriendPresenceChange => new TranslatableString(getKey(@"notify_on_friend_presence_change"), @"Show notification popups when friends change status"); + + /// + /// "Notifications will be shown when friends go online/offline." + /// + public static LocalisableString NotifyOnFriendPresenceChangeTooltip => new TranslatableString(getKey(@"notify_on_friend_presence_change_tooltip"), @"Notifications will be shown when friends go online/offline."); + /// /// "Integrations" /// @@ -84,6 +94,6 @@ namespace osu.Game.Localisation /// public static LocalisableString HideCountryFlags => new TranslatableString(getKey(@"hide_country_flags"), @"Hide country flags"); - private static string getKey(string key) => $"{prefix}:{key}"; + private static string getKey(string key) => $@"{prefix}:{key}"; } } diff --git a/osu.Game/Online/FriendPresenceNotifier.cs b/osu.Game/Online/FriendPresenceNotifier.cs index 8fcf1a9f69..655a004d3e 100644 --- a/osu.Game/Online/FriendPresenceNotifier.cs +++ b/osu.Game/Online/FriendPresenceNotifier.cs @@ -7,6 +7,7 @@ using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Sprites; +using osu.Game.Configuration; using osu.Game.Graphics; using osu.Game.Online.API; using osu.Game.Online.API.Requests.Responses; @@ -38,6 +39,10 @@ namespace osu.Game.Online [Resolved] private OsuColour colours { get; set; } = null!; + [Resolved] + private OsuConfigManager config { get; set; } = null!; + + private readonly Bindable notifyOnFriendPresenceChange = new BindableBool(); private readonly IBindableDictionary userStates = new BindableDictionary(); private readonly HashSet onlineAlertQueue = new HashSet(); private readonly HashSet offlineAlertQueue = new HashSet(); @@ -49,6 +54,8 @@ namespace osu.Game.Online { base.LoadComplete(); + config.BindWith(OsuSetting.NotifyOnFriendPresenceChange, notifyOnFriendPresenceChange); + userStates.BindTo(metadataClient.UserStates); userStates.BindCollectionChanged((_, args) => { @@ -103,6 +110,12 @@ namespace osu.Game.Online if (lastOnlineAlertTime == null || Time.Current - lastOnlineAlertTime < 1000) return; + if (!notifyOnFriendPresenceChange.Value) + { + lastOnlineAlertTime = null; + return; + } + APIUser? singleUser = onlineAlertQueue.Count == 1 ? onlineAlertQueue.Single() : null; notifications.Post(new SimpleNotification @@ -134,6 +147,12 @@ namespace osu.Game.Online if (lastOfflineAlertTime == null || Time.Current - lastOfflineAlertTime < 1000) return; + if (!notifyOnFriendPresenceChange.Value) + { + lastOfflineAlertTime = null; + return; + } + notifications.Post(new SimpleNotification { Icon = FontAwesome.Solid.UserMinus, diff --git a/osu.Game/Overlays/Settings/Sections/Online/AlertsAndPrivacySettings.cs b/osu.Game/Overlays/Settings/Sections/Online/AlertsAndPrivacySettings.cs index 7bd0829add..608c6ef1b2 100644 --- a/osu.Game/Overlays/Settings/Sections/Online/AlertsAndPrivacySettings.cs +++ b/osu.Game/Overlays/Settings/Sections/Online/AlertsAndPrivacySettings.cs @@ -29,6 +29,12 @@ namespace osu.Game.Overlays.Settings.Sections.Online Current = config.GetBindable(OsuSetting.NotifyOnPrivateMessage) }, new SettingsCheckbox + { + LabelText = OnlineSettingsStrings.NotifyOnFriendPresenceChange, + TooltipText = OnlineSettingsStrings.NotifyOnFriendPresenceChangeTooltip, + Current = config.GetBindable(OsuSetting.NotifyOnFriendPresenceChange), + }, + new SettingsCheckbox { LabelText = OnlineSettingsStrings.HideCountryFlags, Current = config.GetBindable(OsuSetting.HideCountryFlags) From 98bb723438c0ce37311451e52529e86f2386777a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 7 Jan 2025 11:37:06 +0100 Subject: [PATCH 182/290] Do not expose track directly in `EditorClock` Intends to stop people from mutating it directly, and going through `EditorClock` members like `AudioAdjustments` instead. --- .../Timelines/Summary/Parts/TimelinePart.cs | 26 +++++++++------- .../Compose/Components/Timeline/Timeline.cs | 31 +++++++++++++------ osu.Game/Screens/Edit/EditorClock.cs | 6 +++- .../Edit/Timing/WaveformComparisonDisplay.cs | 24 ++++++++++---- 4 files changed, 59 insertions(+), 28 deletions(-) diff --git a/osu.Game/Screens/Edit/Components/Timelines/Summary/Parts/TimelinePart.cs b/osu.Game/Screens/Edit/Components/Timelines/Summary/Parts/TimelinePart.cs index ee7e759ebc..bec9e275cb 100644 --- a/osu.Game/Screens/Edit/Components/Timelines/Summary/Parts/TimelinePart.cs +++ b/osu.Game/Screens/Edit/Components/Timelines/Summary/Parts/TimelinePart.cs @@ -3,8 +3,8 @@ using System; using osu.Framework.Allocation; -using osu.Framework.Audio.Track; using osu.Framework.Bindables; +using osu.Framework.Extensions.ObjectExtensions; using osuTK; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; @@ -26,7 +26,8 @@ namespace osu.Game.Screens.Edit.Components.Timelines.Summary.Parts [Resolved] protected EditorBeatmap EditorBeatmap { get; private set; } = null!; - protected readonly IBindable Track = new Bindable(); + [Resolved] + private EditorClock editorClock { get; set; } = null!; private readonly Container content; @@ -35,22 +36,17 @@ namespace osu.Game.Screens.Edit.Components.Timelines.Summary.Parts public TimelinePart(Container? content = null) { AddInternal(this.content = content ?? new Container { RelativeSizeAxes = Axes.Both }); - - beatmap.ValueChanged += _ => - { - updateRelativeChildSize(); - }; - - Track.ValueChanged += _ => updateRelativeChildSize(); } [BackgroundDependencyLoader] - private void load(IBindable beatmap, EditorClock clock) + private void load(IBindable beatmap) { this.beatmap.BindTo(beatmap); LoadBeatmap(EditorBeatmap); - Track.BindTo(clock.Track); + this.beatmap.ValueChanged += _ => updateRelativeChildSize(); + editorClock.TrackChanged += updateRelativeChildSize; + updateRelativeChildSize(); } private void updateRelativeChildSize() @@ -68,5 +64,13 @@ namespace osu.Game.Screens.Edit.Components.Timelines.Summary.Parts { content.Clear(); } + + protected override void Dispose(bool isDisposing) + { + base.Dispose(isDisposing); + + if (editorClock.IsNotNull()) + editorClock.TrackChanged -= updateRelativeChildSize; + } } } diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/Timeline.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/Timeline.cs index 66621afa21..e5360e2eeb 100644 --- a/osu.Game/Screens/Edit/Compose/Components/Timeline/Timeline.cs +++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/Timeline.cs @@ -3,9 +3,9 @@ using System; using osu.Framework.Allocation; -using osu.Framework.Audio.Track; using osu.Framework.Bindables; using osu.Framework.Extensions.Color4Extensions; +using osu.Framework.Extensions.ObjectExtensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Audio; using osu.Framework.Graphics.Containers; @@ -49,6 +49,9 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline [Resolved] private EditorBeatmap editorBeatmap { get; set; } = null!; + [Resolved] + private IBindable beatmap { get; set; } = null!; + /// /// The timeline's scroll position in the last frame. /// @@ -86,8 +89,6 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline private double trackLengthForZoom; - private readonly IBindable track = new Bindable(); - public Timeline(Drawable userContent) { this.userContent = userContent; @@ -101,7 +102,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline } [BackgroundDependencyLoader] - private void load(IBindable beatmap, OsuColour colours, OverlayColourProvider colourProvider, OsuConfigManager config) + private void load(OsuColour colours, OverlayColourProvider colourProvider, OsuConfigManager config) { CentreMarker centreMarker; @@ -150,16 +151,18 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline controlPointsVisible = config.GetBindable(OsuSetting.EditorTimelineShowTimingChanges); ticksVisible = config.GetBindable(OsuSetting.EditorTimelineShowTicks); - track.BindTo(editorClock.Track); - track.BindValueChanged(_ => - { - waveform.Waveform = beatmap.Value.Waveform; - Scheduler.AddOnce(applyVisualOffset, beatmap); - }, true); + editorClock.TrackChanged += updateWaveform; + updateWaveform(); Zoom = (float)(defaultTimelineZoom * editorBeatmap.TimelineZoom); } + private void updateWaveform() + { + waveform.Waveform = beatmap.Value.Waveform; + Scheduler.AddOnce(applyVisualOffset, beatmap); + } + private void applyVisualOffset(IBindable beatmap) { waveform.RelativePositionAxes = Axes.X; @@ -334,5 +337,13 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline double time = TimeAtPosition(Content.ToLocalSpace(screenSpacePosition).X); return new SnapResult(screenSpacePosition, beatSnapProvider.SnapTime(time)); } + + protected override void Dispose(bool isDisposing) + { + base.Dispose(isDisposing); + + if (editorClock.IsNotNull()) + editorClock.TrackChanged -= updateWaveform; + } } } diff --git a/osu.Game/Screens/Edit/EditorClock.cs b/osu.Game/Screens/Edit/EditorClock.cs index 7214854b52..8b9bdb595d 100644 --- a/osu.Game/Screens/Edit/EditorClock.cs +++ b/osu.Game/Screens/Edit/EditorClock.cs @@ -6,6 +6,7 @@ using System; using System.Diagnostics; using System.Linq; +using JetBrains.Annotations; using osu.Framework.Audio; using osu.Framework.Audio.Track; using osu.Framework.Bindables; @@ -24,7 +25,8 @@ namespace osu.Game.Screens.Edit /// public partial class EditorClock : CompositeComponent, IFrameBasedClock, IAdjustableClock, ISourceChangeableClock { - public IBindable Track => track; + [CanBeNull] + public event Action TrackChanged; private readonly Bindable track = new Bindable(); @@ -59,6 +61,8 @@ namespace osu.Game.Screens.Edit underlyingClock = new FramedBeatmapClock(applyOffsets: true, requireDecoupling: true); AddInternal(underlyingClock); + + track.BindValueChanged(_ => TrackChanged?.Invoke()); } /// diff --git a/osu.Game/Screens/Edit/Timing/WaveformComparisonDisplay.cs b/osu.Game/Screens/Edit/Timing/WaveformComparisonDisplay.cs index 45213b7bdb..2df2dd7c5b 100644 --- a/osu.Game/Screens/Edit/Timing/WaveformComparisonDisplay.cs +++ b/osu.Game/Screens/Edit/Timing/WaveformComparisonDisplay.cs @@ -4,8 +4,8 @@ using System; using System.Linq; using osu.Framework.Allocation; -using osu.Framework.Audio.Track; using osu.Framework.Bindables; +using osu.Framework.Extensions.ObjectExtensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Audio; using osu.Framework.Graphics.Containers; @@ -305,7 +305,8 @@ namespace osu.Game.Screens.Edit.Timing [Resolved] private IBindable beatmap { get; set; } = null!; - private readonly IBindable track = new Bindable(); + [Resolved] + private EditorClock editorClock { get; set; } = null!; public WaveformRow(bool isMainRow) { @@ -313,7 +314,7 @@ namespace osu.Game.Screens.Edit.Timing } [BackgroundDependencyLoader] - private void load(EditorClock clock) + private void load() { InternalChildren = new Drawable[] { @@ -343,13 +344,16 @@ namespace osu.Game.Screens.Edit.Timing Colour = colourProvider.Content2 } }; - - track.BindTo(clock.Track); } protected override void LoadComplete() { - track.ValueChanged += _ => waveformGraph.Waveform = beatmap.Value.Waveform; + editorClock.TrackChanged += updateWaveform; + } + + private void updateWaveform() + { + waveformGraph.Waveform = beatmap.Value.Waveform; } public int BeatIndex { set => beatIndexText.Text = value.ToString(); } @@ -363,6 +367,14 @@ namespace osu.Game.Screens.Edit.Timing get => waveformGraph.X; set => waveformGraph.X = value; } + + protected override void Dispose(bool isDisposing) + { + base.Dispose(isDisposing); + + if (editorClock.IsNotNull()) + editorClock.TrackChanged -= updateWaveform; + } } } } From 8f4eafea4eab7a1a2e7d4b3571732477509ba0cf Mon Sep 17 00:00:00 2001 From: Andrei Zavatski Date: Tue, 7 Jan 2025 14:00:31 +0300 Subject: [PATCH 183/290] Fix combo properties multiple reassignments --- .../Objects/CatchHitObject.cs | 36 ++++++++++--------- osu.Game.Rulesets.Osu/Objects/OsuHitObject.cs | 36 ++++++++++--------- .../Objects/Types/IHasComboInformation.cs | 16 +++++---- 3 files changed, 48 insertions(+), 40 deletions(-) diff --git a/osu.Game.Rulesets.Catch/Objects/CatchHitObject.cs b/osu.Game.Rulesets.Catch/Objects/CatchHitObject.cs index 2018fd5ea9..3c7ead09af 100644 --- a/osu.Game.Rulesets.Catch/Objects/CatchHitObject.cs +++ b/osu.Game.Rulesets.Catch/Objects/CatchHitObject.cs @@ -159,27 +159,29 @@ namespace osu.Game.Rulesets.Catch.Objects { // Note that this implementation is shared with the osu! ruleset's implementation. // If a change is made here, OsuHitObject.cs should also be updated. - ComboIndex = lastObj?.ComboIndex ?? 0; - ComboIndexWithOffsets = lastObj?.ComboIndexWithOffsets ?? 0; - IndexInCurrentCombo = (lastObj?.IndexInCurrentCombo + 1) ?? 0; + int index = lastObj?.ComboIndex ?? 0; + int indexWithOffsets = lastObj?.ComboIndexWithOffsets ?? 0; + int inCurrentCombo = (lastObj?.IndexInCurrentCombo + 1) ?? 0; - if (this is BananaShower) + // For the purpose of combo colours, spinners never start a new combo even if they are flagged as doing so. + if (this is not BananaShower) { - // For the purpose of combo colours, spinners never start a new combo even if they are flagged as doing so. - return; + // At decode time, the first hitobject in the beatmap and the first hitobject after a banana shower are both enforced to be a new combo, + // but this isn't directly enforced by the editor so the extra checks against the last hitobject are duplicated here. + if (NewCombo || lastObj == null || lastObj is BananaShower) + { + inCurrentCombo = 0; + index++; + indexWithOffsets += ComboOffset + 1; + + if (lastObj != null) + lastObj.LastInCombo = true; + } } - // At decode time, the first hitobject in the beatmap and the first hitobject after a banana shower are both enforced to be a new combo, - // but this isn't directly enforced by the editor so the extra checks against the last hitobject are duplicated here. - if (NewCombo || lastObj == null || lastObj is BananaShower) - { - IndexInCurrentCombo = 0; - ComboIndex++; - ComboIndexWithOffsets += ComboOffset + 1; - - if (lastObj != null) - lastObj.LastInCombo = true; - } + ComboIndex = index; + ComboIndexWithOffsets = indexWithOffsets; + IndexInCurrentCombo = inCurrentCombo; } protected override HitWindows CreateHitWindows() => HitWindows.Empty; diff --git a/osu.Game.Rulesets.Osu/Objects/OsuHitObject.cs b/osu.Game.Rulesets.Osu/Objects/OsuHitObject.cs index 8c1bd6302e..937e0bda23 100644 --- a/osu.Game.Rulesets.Osu/Objects/OsuHitObject.cs +++ b/osu.Game.Rulesets.Osu/Objects/OsuHitObject.cs @@ -184,27 +184,29 @@ namespace osu.Game.Rulesets.Osu.Objects { // Note that this implementation is shared with the osu!catch ruleset's implementation. // If a change is made here, CatchHitObject.cs should also be updated. - ComboIndex = lastObj?.ComboIndex ?? 0; - ComboIndexWithOffsets = lastObj?.ComboIndexWithOffsets ?? 0; - IndexInCurrentCombo = (lastObj?.IndexInCurrentCombo + 1) ?? 0; + int index = lastObj?.ComboIndex ?? 0; + int indexWithOffsets = lastObj?.ComboIndexWithOffsets ?? 0; + int inCurrentCombo = (lastObj?.IndexInCurrentCombo + 1) ?? 0; - if (this is Spinner) + // For the purpose of combo colours, spinners never start a new combo even if they are flagged as doing so. + if (this is not Spinner) { - // For the purpose of combo colours, spinners never start a new combo even if they are flagged as doing so. - return; + // At decode time, the first hitobject in the beatmap and the first hitobject after a spinner are both enforced to be a new combo, + // but this isn't directly enforced by the editor so the extra checks against the last hitobject are duplicated here. + if (NewCombo || lastObj == null || lastObj is Spinner) + { + inCurrentCombo = 0; + index++; + indexWithOffsets += ComboOffset + 1; + + if (lastObj != null) + lastObj.LastInCombo = true; + } } - // At decode time, the first hitobject in the beatmap and the first hitobject after a spinner are both enforced to be a new combo, - // but this isn't directly enforced by the editor so the extra checks against the last hitobject are duplicated here. - if (NewCombo || lastObj == null || lastObj is Spinner) - { - IndexInCurrentCombo = 0; - ComboIndex++; - ComboIndexWithOffsets += ComboOffset + 1; - - if (lastObj != null) - lastObj.LastInCombo = true; - } + ComboIndex = index; + ComboIndexWithOffsets = indexWithOffsets; + IndexInCurrentCombo = inCurrentCombo; } protected override HitWindows CreateHitWindows() => new OsuHitWindows(); diff --git a/osu.Game/Rulesets/Objects/Types/IHasComboInformation.cs b/osu.Game/Rulesets/Objects/Types/IHasComboInformation.cs index 3aa68197ec..98519de981 100644 --- a/osu.Game/Rulesets/Objects/Types/IHasComboInformation.cs +++ b/osu.Game/Rulesets/Objects/Types/IHasComboInformation.cs @@ -84,19 +84,23 @@ namespace osu.Game.Rulesets.Objects.Types /// The previous hitobject, or null if this is the first object in the beatmap. void UpdateComboInformation(IHasComboInformation? lastObj) { - ComboIndex = lastObj?.ComboIndex ?? 0; - ComboIndexWithOffsets = lastObj?.ComboIndexWithOffsets ?? 0; - IndexInCurrentCombo = (lastObj?.IndexInCurrentCombo + 1) ?? 0; + int index = lastObj?.ComboIndex ?? 0; + int indexWithOffsets = lastObj?.ComboIndexWithOffsets ?? 0; + int inCurrentCombo = (lastObj?.IndexInCurrentCombo + 1) ?? 0; if (NewCombo || lastObj == null) { - IndexInCurrentCombo = 0; - ComboIndex++; - ComboIndexWithOffsets += ComboOffset + 1; + inCurrentCombo = 0; + index++; + indexWithOffsets += ComboOffset + 1; if (lastObj != null) lastObj.LastInCombo = true; } + + ComboIndex = index; + ComboIndexWithOffsets = indexWithOffsets; + IndexInCurrentCombo = inCurrentCombo; } } } From 973f606a9e48fb5d43cbbff03af514ca8a48766a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 7 Jan 2025 13:59:26 +0100 Subject: [PATCH 184/290] 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 185/290] 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 186/290] 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 187/290] 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 188/290] 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 189/290] 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 190/290] 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 191/290] 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 192/290] 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 193/290] 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 194/290] 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 195/290] 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 196/290] 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 197/290] Select closest timing point every time the timing screen is changed to No issue thread for this, was pointed out internally: https://discord.com/channels/90072389919997952/1259818301517725707/1316604605777444905 Due to the custom setup that editor has with its nested "screens-that-aren't-screens", the logic that selects the closest timing point to the current time would only fire on the first open of the screen. Seems like a good idea to have it fire every time instead. --- osu.Game/Screens/Edit/Timing/TimingScreen.cs | 33 +++++++++++++++----- 1 file changed, 26 insertions(+), 7 deletions(-) diff --git a/osu.Game/Screens/Edit/Timing/TimingScreen.cs b/osu.Game/Screens/Edit/Timing/TimingScreen.cs index 67d4429be8..cddde34aca 100644 --- a/osu.Game/Screens/Edit/Timing/TimingScreen.cs +++ b/osu.Game/Screens/Edit/Timing/TimingScreen.cs @@ -15,6 +15,8 @@ namespace osu.Game.Screens.Edit.Timing [Cached] public readonly Bindable SelectedGroup = new Bindable(); + private readonly Bindable currentEditorMode = new Bindable(); + [Resolved] private EditorClock? editorClock { get; set; } @@ -41,18 +43,35 @@ namespace osu.Game.Screens.Edit.Timing } }; + [BackgroundDependencyLoader] + private void load(Editor? editor) + { + if (editor != null) + currentEditorMode.BindTo(editor.Mode); + } + protected override void LoadComplete() { base.LoadComplete(); - if (editorClock != null) + // When entering the timing screen, let's choose the closest valid timing point. + // This will emulate the osu-stable behaviour where a metronome and timing information + // are presented on entering the screen. + currentEditorMode.BindValueChanged(mode => { - // When entering the timing screen, let's choose the closest valid timing point. - // This will emulate the osu-stable behaviour where a metronome and timing information - // are presented on entering the screen. - var nearestTimingPoint = EditorBeatmap.ControlPointInfo.TimingPointAt(editorClock.CurrentTime); - SelectedGroup.Value = EditorBeatmap.ControlPointInfo.GroupAt(nearestTimingPoint.Time); - } + if (mode.NewValue == EditorScreenMode.Timing) + selectClosestTimingPoint(); + }); + selectClosestTimingPoint(); + } + + private void selectClosestTimingPoint() + { + if (editorClock == null) + return; + + var nearestTimingPoint = EditorBeatmap.ControlPointInfo.TimingPointAt(editorClock.CurrentTime); + SelectedGroup.Value = EditorBeatmap.ControlPointInfo.GroupAt(nearestTimingPoint.Time); } protected override void ConfigureTimeline(TimelineArea timelineArea) From f4d83fe6851272375f2382ffc2dd0c0d89721f93 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Thu, 9 Jan 2025 13:23:16 +0900 Subject: [PATCH 198/290] Keep friend states when stopping watching global activity --- .../Online/Metadata/OnlineMetadataClient.cs | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/osu.Game/Online/Metadata/OnlineMetadataClient.cs b/osu.Game/Online/Metadata/OnlineMetadataClient.cs index a3041c6753..ef748f0b49 100644 --- a/osu.Game/Online/Metadata/OnlineMetadataClient.cs +++ b/osu.Game/Online/Metadata/OnlineMetadataClient.cs @@ -3,6 +3,7 @@ using System; using System.Diagnostics; +using System.Linq; using System.Threading; using System.Threading.Tasks; using Microsoft.AspNetCore.SignalR.Client; @@ -31,10 +32,11 @@ namespace osu.Game.Online.Metadata private readonly string endpoint; + [Resolved] + private IAPIProvider api { get; set; } = null!; + private IHubClientConnector? connector; - private Bindable lastQueueId = null!; - private IBindable localUser = null!; private IBindable userActivity = null!; private IBindable? userStatus; @@ -47,7 +49,7 @@ namespace osu.Game.Online.Metadata } [BackgroundDependencyLoader] - private void load(IAPIProvider api, OsuConfigManager config) + private void load(OsuConfigManager config) { // Importantly, we are intentionally not using MessagePack here to correctly support derived class serialization. // More information on the limitations / reasoning can be found in osu-server-spectator's initialisation code. @@ -226,7 +228,15 @@ namespace osu.Game.Online.Metadata throw new OperationCanceledException(); // must be scheduled before any remote calls to avoid mis-ordering. - Schedule(() => userStates.Clear()); + Schedule(() => + { + foreach (int userId in userStates.Keys.ToArray()) + { + if (api.GetFriend(userId) == null) + userStates.Remove(userId); + } + }); + Debug.Assert(connection != null); await connection.InvokeAsync(nameof(IMetadataServer.EndWatchingUserPresence)).ConfigureAwait(false); Logger.Log($@"{nameof(OnlineMetadataClient)} stopped watching user presence", LoggingTarget.Network); From 2a7a3d932edebd82d2a2fa26f20957a88ea5edc6 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 9 Jan 2025 13:24:12 +0900 Subject: [PATCH 199/290] 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 200/290] 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 201/290] Add reason for making test interactive-only --- osu.Game.Tests/Visual/Gameplay/TestSceneReplayRecorder.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneReplayRecorder.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneReplayRecorder.cs index 31af96bdf8..4ad6bc66e3 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneReplayRecorder.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneReplayRecorder.cs @@ -75,7 +75,7 @@ namespace osu.Game.Tests.Visual.Gameplay } [Test] - [Explicit] + [Explicit("Making this test work in a headless context is high effort due to rate adjustment requirements not aligning with the global fast clock. StopwatchClock usage would need to be replace with a rate adjusting clock that still reads from the parent clock. High effort for a test which likely will not see any changes to covered code for some years.")] public void TestSlowClockStillRecordsFramesInRealtime() { ScheduledDelegate moveFunction = null; From 7268b2e077ab95347a12d5374cbdf505ff8538d1 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Thu, 9 Jan 2025 17:31:01 +0900 Subject: [PATCH 202/290] Add separate path for friend presence notifications It proved to be too difficult to deal with the flow that clears user states on stopping the watching of global presence updates. It's not helped in the least that friends are updated via the API, so there's a third flow to consider (and the timings therein - both server-spectator and friends are updated concurrently). Simplest is to separate the friends flow, though this does mean some logic and state duplication. --- .../TestSceneFriendPresenceNotifier.cs | 14 +- osu.Game/Online/API/APIAccess.cs | 19 ++- osu.Game/Online/API/DummyAPIAccess.cs | 3 - osu.Game/Online/API/IAPIProvider.cs | 7 - osu.Game/Online/FriendPresenceNotifier.cs | 121 ++++++++++++------ osu.Game/Online/Metadata/IMetadataClient.cs | 5 + osu.Game/Online/Metadata/MetadataClient.cs | 8 ++ .../Online/Metadata/OnlineMetadataClient.cs | 34 +++-- .../Visual/Metadata/TestMetadataClient.cs | 16 ++- 9 files changed, 148 insertions(+), 79 deletions(-) diff --git a/osu.Game.Tests/Visual/Components/TestSceneFriendPresenceNotifier.cs b/osu.Game.Tests/Visual/Components/TestSceneFriendPresenceNotifier.cs index 851c1141db..2fe2326508 100644 --- a/osu.Game.Tests/Visual/Components/TestSceneFriendPresenceNotifier.cs +++ b/osu.Game.Tests/Visual/Components/TestSceneFriendPresenceNotifier.cs @@ -56,16 +56,16 @@ namespace osu.Game.Tests.Visual.Components [Test] public void TestNotifications() { - AddStep("bring friend 1 online", () => metadataClient.UserPresenceUpdated(1, new UserPresence { Status = UserStatus.Online })); + AddStep("bring friend 1 online", () => metadataClient.FriendPresenceUpdated(1, new UserPresence { Status = UserStatus.Online })); AddUntilStep("wait for notification", () => notificationOverlay.AllNotifications.Count(), () => Is.EqualTo(1)); - AddStep("bring friend 1 offline", () => metadataClient.UserPresenceUpdated(1, null)); + AddStep("bring friend 1 offline", () => metadataClient.FriendPresenceUpdated(1, null)); AddUntilStep("wait for notification", () => notificationOverlay.AllNotifications.Count(), () => Is.EqualTo(2)); } [Test] public void TestSingleUserNotificationOpensChat() { - AddStep("bring friend 1 online", () => metadataClient.UserPresenceUpdated(1, new UserPresence { Status = UserStatus.Online })); + AddStep("bring friend 1 online", () => metadataClient.FriendPresenceUpdated(1, new UserPresence { Status = UserStatus.Online })); AddUntilStep("wait for notification", () => notificationOverlay.AllNotifications.Count(), () => Is.EqualTo(1)); AddStep("click notification", () => @@ -83,8 +83,8 @@ namespace osu.Game.Tests.Visual.Components { AddStep("bring friends 1 & 2 online", () => { - metadataClient.UserPresenceUpdated(1, new UserPresence { Status = UserStatus.Online }); - metadataClient.UserPresenceUpdated(2, new UserPresence { Status = UserStatus.Online }); + metadataClient.FriendPresenceUpdated(1, new UserPresence { Status = UserStatus.Online }); + metadataClient.FriendPresenceUpdated(2, new UserPresence { Status = UserStatus.Online }); }); AddUntilStep("wait for notification", () => notificationOverlay.AllNotifications.Count(), () => Is.EqualTo(1)); @@ -112,7 +112,7 @@ namespace osu.Game.Tests.Visual.Components AddStep("bring friends 1-10 online", () => { for (int i = 1; i <= 10; i++) - metadataClient.UserPresenceUpdated(i, new UserPresence { Status = UserStatus.Online }); + metadataClient.FriendPresenceUpdated(i, new UserPresence { Status = UserStatus.Online }); }); AddUntilStep("wait for notification", () => notificationOverlay.AllNotifications.Count(), () => Is.EqualTo(1)); @@ -120,7 +120,7 @@ namespace osu.Game.Tests.Visual.Components AddStep("bring friends 1-10 offline", () => { for (int i = 1; i <= 10; i++) - metadataClient.UserPresenceUpdated(i, null); + metadataClient.FriendPresenceUpdated(i, null); }); AddUntilStep("wait for notification", () => notificationOverlay.AllNotifications.Count(), () => Is.EqualTo(2)); diff --git a/osu.Game/Online/API/APIAccess.cs b/osu.Game/Online/API/APIAccess.cs index 39c09f2a5d..46476ab7f0 100644 --- a/osu.Game/Online/API/APIAccess.cs +++ b/osu.Game/Online/API/APIAccess.cs @@ -6,6 +6,7 @@ using System; using System.Collections.Generic; using System.Diagnostics; +using System.Linq; using System.Net; using System.Net.Http; using System.Net.Sockets; @@ -18,6 +19,7 @@ using osu.Framework.Extensions.ObjectExtensions; using osu.Framework.Graphics; using osu.Framework.Logging; using osu.Game.Configuration; +using osu.Game.Extensions; using osu.Game.Localisation; using osu.Game.Online.API.Requests; using osu.Game.Online.API.Requests.Responses; @@ -75,7 +77,6 @@ namespace osu.Game.Online.API protected bool HasLogin => authentication.Token.Value != null || (!string.IsNullOrEmpty(ProvidedUsername) && !string.IsNullOrEmpty(password)); - private readonly Dictionary friendsMapping = new Dictionary(); private readonly CancellationTokenSource cancellationToken = new CancellationTokenSource(); private readonly Logger log; @@ -404,8 +405,6 @@ namespace osu.Game.Online.API public IChatClient GetChatClient() => new WebSocketChatClient(this); - public APIRelation GetFriend(int userId) => friendsMapping.GetValueOrDefault(userId); - public RegistrationRequest.RegistrationRequestErrors CreateAccount(string email, string username, string password) { Debug.Assert(State.Value == APIState.Offline); @@ -597,8 +596,6 @@ namespace osu.Game.Online.API Schedule(() => { setLocalUser(createGuestUser()); - - friendsMapping.Clear(); friends.Clear(); }); @@ -615,12 +612,14 @@ namespace osu.Game.Online.API friendsReq.Failure += _ => state.Value = APIState.Failing; friendsReq.Success += res => { - friendsMapping.Clear(); - friends.Clear(); + // Add new friends into local list. + HashSet friendsSet = friends.Select(f => f.TargetID).ToHashSet(); + friends.AddRange(res.Where(f => !friendsSet.Contains(f.TargetID))); - foreach (var u in res) - friendsMapping[u.TargetID] = u; - friends.AddRange(res); + // Remove non-friends from local lists. + friendsSet.Clear(); + friendsSet.AddRange(res.Select(f => f.TargetID)); + friends.RemoveAll(f => !friendsSet.Contains(f.TargetID)); }; Queue(friendsReq); diff --git a/osu.Game/Online/API/DummyAPIAccess.cs b/osu.Game/Online/API/DummyAPIAccess.cs index ca4edb3d8f..5d63c04925 100644 --- a/osu.Game/Online/API/DummyAPIAccess.cs +++ b/osu.Game/Online/API/DummyAPIAccess.cs @@ -2,7 +2,6 @@ // See the LICENCE file in the repository root for full licence text. using System; -using System.Linq; using System.Threading; using System.Threading.Tasks; using osu.Framework.Bindables; @@ -195,8 +194,6 @@ namespace osu.Game.Online.API public IChatClient GetChatClient() => new TestChatClientConnector(this); - public APIRelation? GetFriend(int userId) => Friends.FirstOrDefault(r => r.TargetID == userId); - public RegistrationRequest.RegistrationRequestErrors? CreateAccount(string email, string username, string password) { Thread.Sleep(200); diff --git a/osu.Game/Online/API/IAPIProvider.cs b/osu.Game/Online/API/IAPIProvider.cs index 4655b26f84..1c4b2da742 100644 --- a/osu.Game/Online/API/IAPIProvider.cs +++ b/osu.Game/Online/API/IAPIProvider.cs @@ -152,13 +152,6 @@ namespace osu.Game.Online.API /// IChatClient GetChatClient(); - /// - /// Retrieves a friend from a given user ID. - /// - /// The friend's user ID. - /// The object representing the friend, if any. - APIRelation? GetFriend(int userId); - /// /// Create a new user account. This is a blocking operation. /// diff --git a/osu.Game/Online/FriendPresenceNotifier.cs b/osu.Game/Online/FriendPresenceNotifier.cs index 655a004d3e..330e0a908f 100644 --- a/osu.Game/Online/FriendPresenceNotifier.cs +++ b/osu.Game/Online/FriendPresenceNotifier.cs @@ -2,6 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using System.Collections.Generic; +using System.Collections.Specialized; using System.Linq; using osu.Framework.Allocation; using osu.Framework.Bindables; @@ -43,7 +44,10 @@ namespace osu.Game.Online private OsuConfigManager config { get; set; } = null!; private readonly Bindable notifyOnFriendPresenceChange = new BindableBool(); - private readonly IBindableDictionary userStates = new BindableDictionary(); + + private readonly IBindableList friends = new BindableList(); + private readonly IBindableDictionary friendStates = new BindableDictionary(); + private readonly HashSet onlineAlertQueue = new HashSet(); private readonly HashSet offlineAlertQueue = new HashSet(); @@ -56,42 +60,11 @@ namespace osu.Game.Online config.BindWith(OsuSetting.NotifyOnFriendPresenceChange, notifyOnFriendPresenceChange); - userStates.BindTo(metadataClient.UserStates); - userStates.BindCollectionChanged((_, args) => - { - switch (args.Action) - { - case NotifyDictionaryChangedAction.Add: - foreach ((int userId, var _) in args.NewItems!) - { - if (api.GetFriend(userId)?.TargetUser is APIUser user) - { - if (!offlineAlertQueue.Remove(user)) - { - onlineAlertQueue.Add(user); - lastOnlineAlertTime ??= Time.Current; - } - } - } + friends.BindTo(api.Friends); + friends.BindCollectionChanged(onFriendsChanged, true); - break; - - case NotifyDictionaryChangedAction.Remove: - foreach ((int userId, var _) in args.OldItems!) - { - if (api.GetFriend(userId)?.TargetUser is APIUser user) - { - if (!onlineAlertQueue.Remove(user)) - { - offlineAlertQueue.Add(user); - lastOfflineAlertTime ??= Time.Current; - } - } - } - - break; - } - }); + friendStates.BindTo(metadataClient.FriendStates); + friendStates.BindCollectionChanged(onFriendStatesChanged, true); } protected override void Update() @@ -102,6 +75,82 @@ namespace osu.Game.Online alertOfflineUsers(); } + private void onFriendsChanged(object? sender, NotifyCollectionChangedEventArgs e) + { + switch (e.Action) + { + case NotifyCollectionChangedAction.Add: + foreach (APIRelation friend in e.NewItems!.Cast()) + { + if (friend.TargetUser is not APIUser user) + continue; + + if (friendStates.TryGetValue(friend.TargetID, out _)) + markUserOnline(user); + } + + break; + + case NotifyCollectionChangedAction.Remove: + foreach (APIRelation friend in e.OldItems!.Cast()) + { + if (friend.TargetUser is not APIUser user) + continue; + + onlineAlertQueue.Remove(user); + offlineAlertQueue.Remove(user); + } + + break; + } + } + + private void onFriendStatesChanged(object? sender, NotifyDictionaryChangedEventArgs e) + { + switch (e.Action) + { + case NotifyDictionaryChangedAction.Add: + foreach ((int friendId, _) in e.NewItems!) + { + APIRelation? friend = friends.FirstOrDefault(f => f.TargetID == friendId); + + if (friend?.TargetUser is APIUser user) + markUserOnline(user); + } + + break; + + case NotifyDictionaryChangedAction.Remove: + foreach ((int friendId, _) in e.OldItems!) + { + APIRelation? friend = friends.FirstOrDefault(f => f.TargetID == friendId); + + if (friend?.TargetUser is APIUser user) + markUserOffline(user); + } + + break; + } + } + + private void markUserOnline(APIUser user) + { + if (!offlineAlertQueue.Remove(user)) + { + onlineAlertQueue.Add(user); + lastOnlineAlertTime ??= Time.Current; + } + } + + private void markUserOffline(APIUser user) + { + if (!onlineAlertQueue.Remove(user)) + { + offlineAlertQueue.Add(user); + lastOfflineAlertTime ??= Time.Current; + } + } + private void alertOnlineUsers() { if (onlineAlertQueue.Count == 0) diff --git a/osu.Game/Online/Metadata/IMetadataClient.cs b/osu.Game/Online/Metadata/IMetadataClient.cs index 97c1bbde5f..a4251fae80 100644 --- a/osu.Game/Online/Metadata/IMetadataClient.cs +++ b/osu.Game/Online/Metadata/IMetadataClient.cs @@ -21,6 +21,11 @@ namespace osu.Game.Online.Metadata /// Task UserPresenceUpdated(int userId, UserPresence? status); + /// + /// Delivers and update of the of a friend with the supplied . + /// + Task FriendPresenceUpdated(int userId, UserPresence? presence); + /// /// Delivers an update of the current "daily challenge" status. /// Null value means there is no "daily challenge" currently active. diff --git a/osu.Game/Online/Metadata/MetadataClient.cs b/osu.Game/Online/Metadata/MetadataClient.cs index 8a5fe1733e..6578f70f74 100644 --- a/osu.Game/Online/Metadata/MetadataClient.cs +++ b/osu.Game/Online/Metadata/MetadataClient.cs @@ -42,6 +42,11 @@ namespace osu.Game.Online.Metadata /// public abstract IBindableDictionary UserStates { get; } + /// + /// Dictionary keyed by user ID containing all of the information about currently online friends received from the server. + /// + public abstract IBindableDictionary FriendStates { get; } + /// public abstract Task UpdateActivity(UserActivity? activity); @@ -57,6 +62,9 @@ namespace osu.Game.Online.Metadata /// public abstract Task UserPresenceUpdated(int userId, UserPresence? presence); + /// + public abstract Task FriendPresenceUpdated(int userId, UserPresence? presence); + #endregion #region Daily Challenge diff --git a/osu.Game/Online/Metadata/OnlineMetadataClient.cs b/osu.Game/Online/Metadata/OnlineMetadataClient.cs index ef748f0b49..a8a14b1c78 100644 --- a/osu.Game/Online/Metadata/OnlineMetadataClient.cs +++ b/osu.Game/Online/Metadata/OnlineMetadataClient.cs @@ -3,7 +3,6 @@ using System; using System.Diagnostics; -using System.Linq; using System.Threading; using System.Threading.Tasks; using Microsoft.AspNetCore.SignalR.Client; @@ -27,14 +26,14 @@ namespace osu.Game.Online.Metadata public override IBindableDictionary UserStates => userStates; private readonly BindableDictionary userStates = new BindableDictionary(); + public override IBindableDictionary FriendStates => friendStates; + private readonly BindableDictionary friendStates = new BindableDictionary(); + public override IBindable DailyChallengeInfo => dailyChallengeInfo; private readonly Bindable dailyChallengeInfo = new Bindable(); private readonly string endpoint; - [Resolved] - private IAPIProvider api { get; set; } = null!; - private IHubClientConnector? connector; private Bindable lastQueueId = null!; private IBindable localUser = null!; @@ -49,7 +48,7 @@ namespace osu.Game.Online.Metadata } [BackgroundDependencyLoader] - private void load(OsuConfigManager config) + private void load(IAPIProvider api, OsuConfigManager config) { // Importantly, we are intentionally not using MessagePack here to correctly support derived class serialization. // More information on the limitations / reasoning can be found in osu-server-spectator's initialisation code. @@ -63,6 +62,7 @@ namespace osu.Game.Online.Metadata // https://github.com/dotnet/aspnetcore/issues/15198 connection.On(nameof(IMetadataClient.BeatmapSetsUpdated), ((IMetadataClient)this).BeatmapSetsUpdated); connection.On(nameof(IMetadataClient.UserPresenceUpdated), ((IMetadataClient)this).UserPresenceUpdated); + connection.On(nameof(IMetadataClient.FriendPresenceUpdated), ((IMetadataClient)this).FriendPresenceUpdated); connection.On(nameof(IMetadataClient.DailyChallengeUpdated), ((IMetadataClient)this).DailyChallengeUpdated); connection.On(nameof(IMetadataClient.MultiplayerRoomScoreSet), ((IMetadataClient)this).MultiplayerRoomScoreSet); connection.On(nameof(IStatefulUserHubClient.DisconnectRequested), ((IMetadataClient)this).DisconnectRequested); @@ -108,6 +108,7 @@ namespace osu.Game.Online.Metadata { isWatchingUserPresence.Value = false; userStates.Clear(); + friendStates.Clear(); dailyChallengeInfo.Value = null; }); return; @@ -209,6 +210,19 @@ namespace osu.Game.Online.Metadata return Task.CompletedTask; } + public override Task FriendPresenceUpdated(int userId, UserPresence? presence) + { + Schedule(() => + { + if (presence?.Status != null) + friendStates[userId] = presence.Value; + else + friendStates.Remove(userId); + }); + + return Task.CompletedTask; + } + public override async Task BeginWatchingUserPresence() { if (connector?.IsConnected.Value != true) @@ -228,15 +242,7 @@ namespace osu.Game.Online.Metadata throw new OperationCanceledException(); // must be scheduled before any remote calls to avoid mis-ordering. - Schedule(() => - { - foreach (int userId in userStates.Keys.ToArray()) - { - if (api.GetFriend(userId) == null) - userStates.Remove(userId); - } - }); - + Schedule(() => userStates.Clear()); Debug.Assert(connection != null); await connection.InvokeAsync(nameof(IMetadataServer.EndWatchingUserPresence)).ConfigureAwait(false); Logger.Log($@"{nameof(OnlineMetadataClient)} stopped watching user presence", LoggingTarget.Network); diff --git a/osu.Game/Tests/Visual/Metadata/TestMetadataClient.cs b/osu.Game/Tests/Visual/Metadata/TestMetadataClient.cs index 6dd6392b3a..36f79a5adc 100644 --- a/osu.Game/Tests/Visual/Metadata/TestMetadataClient.cs +++ b/osu.Game/Tests/Visual/Metadata/TestMetadataClient.cs @@ -2,7 +2,6 @@ // See the LICENCE file in the repository root for full licence text. using System; -using System.Linq; using System.Threading.Tasks; using osu.Framework.Allocation; using osu.Framework.Bindables; @@ -23,6 +22,9 @@ namespace osu.Game.Tests.Visual.Metadata public override IBindableDictionary UserStates => userStates; private readonly BindableDictionary userStates = new BindableDictionary(); + public override IBindableDictionary FriendStates => friendStates; + private readonly BindableDictionary friendStates = new BindableDictionary(); + public override Bindable DailyChallengeInfo => dailyChallengeInfo; private readonly Bindable dailyChallengeInfo = new Bindable(); @@ -67,7 +69,7 @@ namespace osu.Game.Tests.Visual.Metadata public override Task UserPresenceUpdated(int userId, UserPresence? presence) { - if (isWatchingUserPresence.Value || api.Friends.Any(f => f.TargetID == userId)) + if (isWatchingUserPresence.Value) { if (presence.HasValue) userStates[userId] = presence.Value; @@ -78,6 +80,16 @@ namespace osu.Game.Tests.Visual.Metadata return Task.CompletedTask; } + public override Task FriendPresenceUpdated(int userId, UserPresence? presence) + { + if (presence.HasValue) + friendStates[userId] = presence.Value; + else + friendStates.Remove(userId); + + return Task.CompletedTask; + } + public override Task GetChangesSince(int queueId) => Task.FromResult(new BeatmapUpdates(Array.Empty(), queueId)); From 18f1d62182b02cecca7f8fff118c287cde6109fe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 8 Jan 2025 13:40:42 +0100 Subject: [PATCH 203/290] 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 204/290] 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 205/290] 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 206/290] Ignore realm `List` type --- osu.sln.DotSettings | 1 + 1 file changed, 1 insertion(+) diff --git a/osu.sln.DotSettings b/osu.sln.DotSettings index ccd6db354b..8f5e642f94 100644 --- a/osu.sln.DotSettings +++ b/osu.sln.DotSettings @@ -840,6 +840,7 @@ See the LICENCE file in the repository root for full licence text. True True True + True True True True From 48196949e080e1f0057d20e3bb637cfc9b4989fc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 9 Jan 2025 15:29:40 +0100 Subject: [PATCH 207/290] Add combo colour override control to editor Closes https://github.com/ppy/osu/issues/25608. Logic mostly matching stable. All operations are done on `ComboOffset` which still makes overridden combo colours weirdly relatively dependent on each other rather than them be an "absolute" choice, but alas... As per stable, two consecutive new combos can use the same colour only if they are separated by a break: https://github.com/peppy/osu-stable-reference/blob/52f3f75ed7efd7b9eb56e1e45c95bb91504337be/osu!/GameModes/Edit/Modes/EditorModeCompose.cs#L4564-L4571 This control is only available once the user has changed the combo colours from defaults; additionally, only a single new combo object must be selected for the colour selector to show up. --- .../Edit/CatchHitObjectComposer.cs | 3 +- .../Edit/OsuHitObjectComposer.cs | 2 +- osu.Game/Rulesets/Edit/HitObjectComposer.cs | 5 +- .../Objects/Types/IHasComboInformation.cs | 3 + .../TernaryButtons/NewComboTernaryButton.cs | 278 ++++++++++++++++++ .../Components/ComposeBlueprintContainer.cs | 11 +- 6 files changed, 289 insertions(+), 13 deletions(-) create mode 100644 osu.Game/Screens/Edit/Components/TernaryButtons/NewComboTernaryButton.cs diff --git a/osu.Game.Rulesets.Catch/Edit/CatchHitObjectComposer.cs b/osu.Game.Rulesets.Catch/Edit/CatchHitObjectComposer.cs index e0d80e0e64..7bb5539963 100644 --- a/osu.Game.Rulesets.Catch/Edit/CatchHitObjectComposer.cs +++ b/osu.Game.Rulesets.Catch/Edit/CatchHitObjectComposer.cs @@ -18,7 +18,6 @@ using osu.Game.Rulesets.Edit.Tools; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.UI; -using osu.Game.Screens.Edit.Components.TernaryButtons; using osu.Game.Screens.Edit.Compose.Components; using osuTK; @@ -72,7 +71,7 @@ namespace osu.Game.Rulesets.Catch.Edit protected override Drawable CreateHitObjectInspector() => new CatchHitObjectInspector(DistanceSnapProvider); - protected override IEnumerable CreateTernaryButtons() + protected override IEnumerable CreateTernaryButtons() => base.CreateTernaryButtons() .Concat(DistanceSnapProvider.CreateTernaryButtons()); diff --git a/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs b/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs index e8b9d0544e..f5e7ff6004 100644 --- a/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs +++ b/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs @@ -53,7 +53,7 @@ namespace osu.Game.Rulesets.Osu.Edit protected override Drawable CreateHitObjectInspector() => new OsuHitObjectInspector(); - protected override IEnumerable CreateTernaryButtons() + protected override IEnumerable CreateTernaryButtons() => base.CreateTernaryButtons() .Append(new DrawableTernaryButton { diff --git a/osu.Game/Rulesets/Edit/HitObjectComposer.cs b/osu.Game/Rulesets/Edit/HitObjectComposer.cs index 9f277b6190..15b60114af 100644 --- a/osu.Game/Rulesets/Edit/HitObjectComposer.cs +++ b/osu.Game/Rulesets/Edit/HitObjectComposer.cs @@ -16,6 +16,7 @@ using osu.Framework.Graphics.Shapes; using osu.Framework.Input; using osu.Framework.Input.Events; using osu.Framework.Logging; +using osu.Framework.Testing; using osu.Game.Beatmaps; using osu.Game.Configuration; using osu.Game.Graphics; @@ -370,7 +371,7 @@ namespace osu.Game.Rulesets.Edit /// /// Create all ternary states required to be displayed to the user. /// - protected virtual IEnumerable CreateTernaryButtons() => BlueprintContainer.MainTernaryStates; + protected virtual IEnumerable CreateTernaryButtons() => BlueprintContainer.MainTernaryStates; /// /// Construct a relevant blueprint container. This will manage hitobject selection/placement input handling and display logic. @@ -429,7 +430,7 @@ namespace osu.Game.Rulesets.Edit } else { - if (togglesCollection.ElementAtOrDefault(rightIndex) is DrawableTernaryButton button) + if (togglesCollection.ChildrenOfType().ElementAtOrDefault(rightIndex) is DrawableTernaryButton button) { button.Toggle(); return true; diff --git a/osu.Game/Rulesets/Objects/Types/IHasComboInformation.cs b/osu.Game/Rulesets/Objects/Types/IHasComboInformation.cs index 3aa68197ec..cc521aeab7 100644 --- a/osu.Game/Rulesets/Objects/Types/IHasComboInformation.cs +++ b/osu.Game/Rulesets/Objects/Types/IHasComboInformation.cs @@ -50,6 +50,9 @@ namespace osu.Game.Rulesets.Objects.Types /// new bool NewCombo { get; set; } + /// + new int ComboOffset { get; set; } + /// /// Bindable exposure of . /// diff --git a/osu.Game/Screens/Edit/Components/TernaryButtons/NewComboTernaryButton.cs b/osu.Game/Screens/Edit/Components/TernaryButtons/NewComboTernaryButton.cs new file mode 100644 index 0000000000..effe35c0c3 --- /dev/null +++ b/osu.Game/Screens/Edit/Components/TernaryButtons/NewComboTernaryButton.cs @@ -0,0 +1,278 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using osu.Framework.Allocation; +using osu.Framework.Bindables; +using osu.Framework.Extensions; +using osu.Framework.Extensions.Color4Extensions; +using osu.Framework.Extensions.ObjectExtensions; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Cursor; +using osu.Framework.Graphics.Shapes; +using osu.Framework.Graphics.Sprites; +using osu.Framework.Graphics.UserInterface; +using osu.Game.Graphics; +using osu.Game.Graphics.Containers; +using osu.Game.Graphics.UserInterface; +using osu.Game.Graphics.UserInterfaceV2; +using osu.Game.Overlays; +using osu.Game.Rulesets.Objects; +using osu.Game.Rulesets.Objects.Types; +using osuTK; + +namespace osu.Game.Screens.Edit.Components.TernaryButtons +{ + public partial class NewComboTernaryButton : CompositeDrawable, IHasCurrentValue + { + public Bindable Current + { + get => current.Current; + set => current.Current = value; + } + + private readonly BindableWithCurrent current = new BindableWithCurrent(); + + private readonly BindableList selectedHitObjects = new BindableList(); + private readonly BindableList comboColours = new BindableList(); + + private Container mainButtonContainer = null!; + private ColourPickerButton pickerButton = null!; + + [BackgroundDependencyLoader] + private void load(EditorBeatmap editorBeatmap) + { + RelativeSizeAxes = Axes.X; + AutoSizeAxes = Axes.Y; + InternalChildren = new Drawable[] + { + mainButtonContainer = new Container + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Padding = new MarginPadding { Right = 30 }, + Child = new DrawableTernaryButton + { + Current = Current, + Description = "New combo", + CreateIcon = () => new SpriteIcon { Icon = OsuIcon.EditorNewComboA }, + }, + }, + pickerButton = new ColourPickerButton + { + Anchor = Anchor.CentreRight, + Origin = Anchor.CentreRight, + Width = 25, + ComboColours = { BindTarget = comboColours } + } + }; + + selectedHitObjects.BindTo(editorBeatmap.SelectedHitObjects); + if (editorBeatmap.BeatmapSkin != null) + comboColours.BindTo(editorBeatmap.BeatmapSkin.ComboColours); + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + selectedHitObjects.BindCollectionChanged((_, _) => updateState()); + comboColours.BindCollectionChanged((_, _) => updateState()); + Current.BindValueChanged(_ => updateState(), true); + } + + private void updateState() + { + if (Current.Value == TernaryState.True && selectedHitObjects.Count == 1 && selectedHitObjects.Single() is IHasComboInformation hasCombo && comboColours.Count > 1) + { + mainButtonContainer.Padding = new MarginPadding { Right = 30 }; + pickerButton.SelectedHitObject.Value = hasCombo; + pickerButton.Alpha = 1; + } + else + { + mainButtonContainer.Padding = new MarginPadding(); + pickerButton.Alpha = 0; + } + } + + private partial class ColourPickerButton : OsuButton, IHasPopover + { + public BindableList ComboColours { get; } = new BindableList(); + public Bindable SelectedHitObject { get; } = new Bindable(); + + [Resolved] + private EditorBeatmap editorBeatmap { get; set; } = null!; + + [Resolved] + private OverlayColourProvider colourProvider { get; set; } = null!; + + private SpriteIcon icon = null!; + + [BackgroundDependencyLoader] + private void load() + { + Add(icon = new SpriteIcon + { + Icon = FontAwesome.Solid.Palette, + Size = new Vector2(16), + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + }); + + Action = this.ShowPopover; + } + + protected override void LoadComplete() + { + base.LoadComplete(); + ComboColours.BindCollectionChanged((_, _) => updateState()); + SelectedHitObject.BindValueChanged(val => + { + if (val.OldValue != null) + val.OldValue.ComboIndexWithOffsetsBindable.ValueChanged -= onComboIndexChanged; + + updateState(); + + if (val.NewValue != null) + val.NewValue.ComboIndexWithOffsetsBindable.ValueChanged += onComboIndexChanged; + }, true); + } + + private void onComboIndexChanged(ValueChangedEvent _) => updateState(); + + private void updateState() + { + if (SelectedHitObject.Value == null) + { + BackgroundColour = colourProvider.Background3; + icon.Colour = BackgroundColour.Darken(0.5f); + icon.Blending = BlendingParameters.Additive; + Enabled.Value = false; + } + else + { + BackgroundColour = ComboColours[comboIndexFor(SelectedHitObject.Value, ComboColours)]; + icon.Colour = OsuColour.ForegroundTextColourFor(BackgroundColour); + icon.Blending = BlendingParameters.Inherit; + Enabled.Value = true; + } + } + + public Popover GetPopover() => new ComboColourPalettePopover(ComboColours, SelectedHitObject.Value.AsNonNull(), editorBeatmap); + } + + private partial class ComboColourPalettePopover : OsuPopover + { + private readonly IReadOnlyList comboColours; + private readonly IHasComboInformation hasComboInformation; + private readonly EditorBeatmap editorBeatmap; + + public ComboColourPalettePopover(IReadOnlyList comboColours, IHasComboInformation hasComboInformation, EditorBeatmap editorBeatmap) + { + this.comboColours = comboColours; + this.hasComboInformation = hasComboInformation; + this.editorBeatmap = editorBeatmap; + + AllowableAnchors = [Anchor.CentreRight]; + } + + [BackgroundDependencyLoader] + private void load() + { + Debug.Assert(comboColours.Count > 0); + var hitObject = hasComboInformation as HitObject; + Debug.Assert(hitObject != null); + + FillFlowContainer container; + + Child = container = new FillFlowContainer + { + Width = 230, + AutoSizeAxes = Axes.Y, + Spacing = new Vector2(10), + }; + + int selectedColourIndex = comboIndexFor(hasComboInformation, comboColours); + + for (int i = 0; i < comboColours.Count; i++) + { + int index = i; + + if (getPreviousHitObjectWithCombo(editorBeatmap, hitObject) is IHasComboInformation previousHasCombo + && index == comboIndexFor(previousHasCombo, comboColours) + && !canReuseLastComboColour(editorBeatmap, hitObject)) + { + continue; + } + + container.Add(new OsuClickableContainer + { + Size = new Vector2(50), + Masking = true, + CornerRadius = 25, + Children = new[] + { + new Box + { + RelativeSizeAxes = Axes.Both, + Colour = comboColours[index], + }, + selectedColourIndex == index + ? new SpriteIcon + { + Icon = FontAwesome.Solid.Check, + Size = new Vector2(24), + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Colour = OsuColour.ForegroundTextColourFor(comboColours[index]), + } + : Empty() + }, + Action = () => + { + int comboDifference = index - selectedColourIndex; + if (comboDifference == 0) + return; + + int newOffset = hasComboInformation.ComboOffset + comboDifference; + // `newOffset` must be positive to serialise correctly - this implements the true math "modulus" rather than the built-in "remainder" % op + // which can return negative results when the first operand is negative + newOffset -= (int)Math.Floor((double)newOffset / comboColours.Count) * comboColours.Count; + + hasComboInformation.ComboOffset = newOffset; + editorBeatmap.BeginChange(); + editorBeatmap.Update((HitObject)hasComboInformation); + editorBeatmap.EndChange(); + this.HidePopover(); + } + }); + } + } + + private static IHasComboInformation? getPreviousHitObjectWithCombo(EditorBeatmap editorBeatmap, HitObject hitObject) + => editorBeatmap.HitObjects.TakeWhile(ho => ho != hitObject).LastOrDefault() as IHasComboInformation; + + private static bool canReuseLastComboColour(EditorBeatmap editorBeatmap, HitObject hitObject) + { + double? closestBreakEnd = editorBeatmap.Breaks.Select(b => b.EndTime) + .Where(t => t <= hitObject.StartTime) + .OrderBy(t => t) + .LastOrDefault(); + + if (closestBreakEnd == null) + return false; + + return editorBeatmap.HitObjects.FirstOrDefault(ho => ho.StartTime >= closestBreakEnd) == hitObject; + } + } + + // compare `EditorBeatmapSkin.updateColours()` et al. for reasoning behind the off-by-one index rotation + private static int comboIndexFor(IHasComboInformation hasComboInformation, IReadOnlyCollection comboColours) + => (hasComboInformation.ComboIndexWithOffsets + comboColours.Count - 1) % comboColours.Count; + } +} diff --git a/osu.Game/Screens/Edit/Compose/Components/ComposeBlueprintContainer.cs b/osu.Game/Screens/Edit/Compose/Components/ComposeBlueprintContainer.cs index bbb4095206..5d93c4ea9d 100644 --- a/osu.Game/Screens/Edit/Compose/Components/ComposeBlueprintContainer.cs +++ b/osu.Game/Screens/Edit/Compose/Components/ComposeBlueprintContainer.cs @@ -237,22 +237,17 @@ namespace osu.Game.Screens.Edit.Compose.Components /// /// A collection of states which will be displayed to the user in the toolbox. /// - public DrawableTernaryButton[] MainTernaryStates { get; private set; } + public Drawable[] MainTernaryStates { get; private set; } public SampleBankTernaryButton[] SampleBankTernaryStates { get; private set; } /// /// Create all ternary states required to be displayed to the user. /// - protected virtual IEnumerable CreateTernaryButtons() + protected virtual IEnumerable CreateTernaryButtons() { //TODO: this should only be enabled (visible?) for rulesets that provide combo-supporting HitObjects. - yield return new DrawableTernaryButton - { - Current = NewCombo, - Description = "New combo", - CreateIcon = () => new SpriteIcon { Icon = OsuIcon.EditorNewComboA }, - }; + yield return new NewComboTernaryButton { Current = NewCombo }; foreach (var kvp in SelectionHandler.SelectionSampleStates) { From 0d9a3428ae4b447d72e908f7fdb4f617525c0905 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 10 Jan 2025 14:13:03 +0100 Subject: [PATCH 208/290] Merge conditionals --- .../Objects/CatchHitObject.cs | 21 ++++++++----------- osu.Game.Rulesets.Osu/Objects/OsuHitObject.cs | 21 ++++++++----------- 2 files changed, 18 insertions(+), 24 deletions(-) diff --git a/osu.Game.Rulesets.Catch/Objects/CatchHitObject.cs b/osu.Game.Rulesets.Catch/Objects/CatchHitObject.cs index 3c7ead09af..deaa566864 100644 --- a/osu.Game.Rulesets.Catch/Objects/CatchHitObject.cs +++ b/osu.Game.Rulesets.Catch/Objects/CatchHitObject.cs @@ -163,20 +163,17 @@ namespace osu.Game.Rulesets.Catch.Objects int indexWithOffsets = lastObj?.ComboIndexWithOffsets ?? 0; int inCurrentCombo = (lastObj?.IndexInCurrentCombo + 1) ?? 0; - // For the purpose of combo colours, spinners never start a new combo even if they are flagged as doing so. - if (this is not BananaShower) + // - For the purpose of combo colours, spinners never start a new combo even if they are flagged as doing so. + // - At decode time, the first hitobject in the beatmap and the first hitobject after a banana shower are both enforced to be a new combo, + // but this isn't directly enforced by the editor so the extra checks against the last hitobject are duplicated here. + if (this is not BananaShower && (NewCombo || lastObj == null || lastObj is BananaShower)) { - // At decode time, the first hitobject in the beatmap and the first hitobject after a banana shower are both enforced to be a new combo, - // but this isn't directly enforced by the editor so the extra checks against the last hitobject are duplicated here. - if (NewCombo || lastObj == null || lastObj is BananaShower) - { - inCurrentCombo = 0; - index++; - indexWithOffsets += ComboOffset + 1; + inCurrentCombo = 0; + index++; + indexWithOffsets += ComboOffset + 1; - if (lastObj != null) - lastObj.LastInCombo = true; - } + if (lastObj != null) + lastObj.LastInCombo = true; } ComboIndex = index; diff --git a/osu.Game.Rulesets.Osu/Objects/OsuHitObject.cs b/osu.Game.Rulesets.Osu/Objects/OsuHitObject.cs index 937e0bda23..9623d1999b 100644 --- a/osu.Game.Rulesets.Osu/Objects/OsuHitObject.cs +++ b/osu.Game.Rulesets.Osu/Objects/OsuHitObject.cs @@ -188,20 +188,17 @@ namespace osu.Game.Rulesets.Osu.Objects int indexWithOffsets = lastObj?.ComboIndexWithOffsets ?? 0; int inCurrentCombo = (lastObj?.IndexInCurrentCombo + 1) ?? 0; - // For the purpose of combo colours, spinners never start a new combo even if they are flagged as doing so. - if (this is not Spinner) + // - For the purpose of combo colours, spinners never start a new combo even if they are flagged as doing so. + // - At decode time, the first hitobject in the beatmap and the first hitobject after a spinner are both enforced to be a new combo, + // but this isn't directly enforced by the editor so the extra checks against the last hitobject are duplicated here. + if (this is not Spinner && (NewCombo || lastObj == null || lastObj is Spinner)) { - // At decode time, the first hitobject in the beatmap and the first hitobject after a spinner are both enforced to be a new combo, - // but this isn't directly enforced by the editor so the extra checks against the last hitobject are duplicated here. - if (NewCombo || lastObj == null || lastObj is Spinner) - { - inCurrentCombo = 0; - index++; - indexWithOffsets += ComboOffset + 1; + inCurrentCombo = 0; + index++; + indexWithOffsets += ComboOffset + 1; - if (lastObj != null) - lastObj.LastInCombo = true; - } + if (lastObj != null) + lastObj.LastInCombo = true; } ComboIndex = index; From 94ea003d90f0d96ebe82ab1a80abb6e2672f060a Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sat, 11 Jan 2025 01:42:54 +0900 Subject: [PATCH 209/290] Update game `ScrollContainer` usage in line with framework changes See https://github.com/ppy/osu-framework/pull/6467. --- .../UserInterface/TestSceneSectionsContainer.cs | 2 +- osu.Game/Graphics/Containers/OsuScrollContainer.cs | 8 ++++---- osu.Game/Graphics/Containers/SectionsContainer.cs | 4 ++-- .../Containers/UserTrackingScrollContainer.cs | 4 ++-- osu.Game/Online/Leaderboards/Leaderboard.cs | 4 ++-- osu.Game/Overlays/Chat/ChannelScrollContainer.cs | 4 ++-- osu.Game/Overlays/Chat/DrawableChannel.cs | 2 +- osu.Game/Overlays/Mods/ModSelectOverlay.cs | 8 ++++---- osu.Game/Overlays/NewsOverlay.cs | 2 +- osu.Game/Overlays/OnlineOverlay.cs | 2 +- osu.Game/Overlays/OverlayScrollContainer.cs | 6 +++--- osu.Game/Overlays/WikiOverlay.cs | 2 +- .../Edit/Compose/Components/Timeline/Timeline.cs | 4 ++-- .../Components/Timeline/ZoomableScrollContainer.cs | 2 +- osu.Game/Screens/Play/HUD/GameplayLeaderboard.cs | 6 +++--- osu.Game/Screens/Ranking/ScorePanelList.cs | 4 ++-- osu.Game/Screens/Select/BeatmapCarousel.cs | 12 ++++++------ 17 files changed, 38 insertions(+), 38 deletions(-) diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneSectionsContainer.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneSectionsContainer.cs index 3a1eb554ab..7ec57c9e5e 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneSectionsContainer.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneSectionsContainer.cs @@ -138,7 +138,7 @@ namespace osu.Game.Tests.Visual.UserInterface AddUntilStep("section top is visible", () => { var scrollContainer = container.ChildrenOfType().Single(); - float sectionPosition = scrollContainer.GetChildPosInContent(container.Children[scrollIndex]); + double sectionPosition = scrollContainer.GetChildPosInContent(container.Children[scrollIndex]); return scrollContainer.Current < sectionPosition; }); } diff --git a/osu.Game/Graphics/Containers/OsuScrollContainer.cs b/osu.Game/Graphics/Containers/OsuScrollContainer.cs index a3cd5a4902..f40c91e27e 100644 --- a/osu.Game/Graphics/Containers/OsuScrollContainer.cs +++ b/osu.Game/Graphics/Containers/OsuScrollContainer.cs @@ -59,11 +59,11 @@ namespace osu.Game.Graphics.Containers /// An added amount to scroll beyond the requirement to bring the target into view. public void ScrollIntoView(Drawable d, bool animated = true, float extraScroll = 0) { - float childPos0 = GetChildPosInContent(d); - float childPos1 = GetChildPosInContent(d, d.DrawSize); + double childPos0 = GetChildPosInContent(d); + double childPos1 = GetChildPosInContent(d, d.DrawSize); - float minPos = Math.Min(childPos0, childPos1); - float maxPos = Math.Max(childPos0, childPos1); + double minPos = Math.Min(childPos0, childPos1); + double maxPos = Math.Max(childPos0, childPos1); if (minPos < Current || (minPos > Current && d.DrawSize[ScrollDim] > DisplayableContent)) ScrollTo(minPos - extraScroll, animated); diff --git a/osu.Game/Graphics/Containers/SectionsContainer.cs b/osu.Game/Graphics/Containers/SectionsContainer.cs index 9f41c4eff2..828fc9704c 100644 --- a/osu.Game/Graphics/Containers/SectionsContainer.cs +++ b/osu.Game/Graphics/Containers/SectionsContainer.cs @@ -208,7 +208,7 @@ namespace osu.Game.Graphics.Containers private float getScrollTargetForDrawable(Drawable target) { // implementation similar to ScrollIntoView but a bit more nuanced. - return scrollContainer.GetChildPosInContent(target) - scrollContainer.DisplayableContent * scroll_y_centre; + return (float)(scrollContainer.GetChildPosInContent(target) - scrollContainer.DisplayableContent * scroll_y_centre); } public void ScrollToTop() => scrollContainer.ScrollTo(0); @@ -259,7 +259,7 @@ namespace osu.Game.Graphics.Containers updateSectionsMargin(); } - float currentScroll = scrollContainer.Current; + float currentScroll = (float)scrollContainer.Current; if (currentScroll != lastKnownScroll) { diff --git a/osu.Game/Graphics/Containers/UserTrackingScrollContainer.cs b/osu.Game/Graphics/Containers/UserTrackingScrollContainer.cs index 354a57b7d2..30b9eeb74c 100644 --- a/osu.Game/Graphics/Containers/UserTrackingScrollContainer.cs +++ b/osu.Game/Graphics/Containers/UserTrackingScrollContainer.cs @@ -35,7 +35,7 @@ namespace osu.Game.Graphics.Containers { } - protected override void OnUserScroll(float value, bool animated = true, double? distanceDecay = default) + protected override void OnUserScroll(double value, bool animated = true, double? distanceDecay = default) { UserScrolling = true; base.OnUserScroll(value, animated, distanceDecay); @@ -53,7 +53,7 @@ namespace osu.Game.Graphics.Containers base.ScrollFromMouseEvent(e); } - public new void ScrollTo(float value, bool animated = true, double? distanceDecay = null) + public new void ScrollTo(double value, bool animated = true, double? distanceDecay = null) { UserScrolling = false; base.ScrollTo(value, animated, distanceDecay); diff --git a/osu.Game/Online/Leaderboards/Leaderboard.cs b/osu.Game/Online/Leaderboards/Leaderboard.cs index d76da54adf..3c25d6f789 100644 --- a/osu.Game/Online/Leaderboards/Leaderboard.cs +++ b/osu.Game/Online/Leaderboards/Leaderboard.cs @@ -375,8 +375,8 @@ namespace osu.Game.Online.Leaderboards { base.UpdateAfterChildren(); - float fadeBottom = scrollContainer.Current + scrollContainer.DrawHeight; - float fadeTop = scrollContainer.Current + LeaderboardScore.HEIGHT; + float fadeBottom = (float)(scrollContainer.Current + scrollContainer.DrawHeight); + float fadeTop = (float)(scrollContainer.Current + LeaderboardScore.HEIGHT); if (!scrollContainer.IsScrolledToEnd()) fadeBottom -= LeaderboardScore.HEIGHT; diff --git a/osu.Game/Overlays/Chat/ChannelScrollContainer.cs b/osu.Game/Overlays/Chat/ChannelScrollContainer.cs index 6d8b21a7c5..b621b555b0 100644 --- a/osu.Game/Overlays/Chat/ChannelScrollContainer.cs +++ b/osu.Game/Overlays/Chat/ChannelScrollContainer.cs @@ -41,13 +41,13 @@ namespace osu.Game.Overlays.Chat #region Scroll handling - protected override void OnUserScroll(float value, bool animated = true, double? distanceDecay = null) + protected override void OnUserScroll(double value, bool animated = true, double? distanceDecay = null) { base.OnUserScroll(value, animated, distanceDecay); updateTrackState(); } - public new void ScrollTo(float value, bool animated = true, double? distanceDecay = null) + public new void ScrollTo(double value, bool animated = true, double? distanceDecay = null) { base.ScrollTo(value, animated, distanceDecay); updateTrackState(); diff --git a/osu.Game/Overlays/Chat/DrawableChannel.cs b/osu.Game/Overlays/Chat/DrawableChannel.cs index cb7cd03584..2f0461eb40 100644 --- a/osu.Game/Overlays/Chat/DrawableChannel.cs +++ b/osu.Game/Overlays/Chat/DrawableChannel.cs @@ -117,7 +117,7 @@ namespace osu.Game.Overlays.Chat if (chatLine == null) return; - float center = scroll.GetChildPosInContent(chatLine, chatLine.DrawSize / 2) - scroll.DisplayableContent / 2; + double center = scroll.GetChildPosInContent(chatLine, chatLine.DrawSize / 2) - scroll.DisplayableContent / 2; scroll.ScrollTo(Math.Clamp(center, 0, scroll.ScrollableExtent)); chatLine.Highlight(); diff --git a/osu.Game/Overlays/Mods/ModSelectOverlay.cs b/osu.Game/Overlays/Mods/ModSelectOverlay.cs index ed73340eeb..daac925dfb 100644 --- a/osu.Game/Overlays/Mods/ModSelectOverlay.cs +++ b/osu.Game/Overlays/Mods/ModSelectOverlay.cs @@ -710,13 +710,13 @@ namespace osu.Game.Overlays.Mods // the bounds below represent the horizontal range of scroll items to be considered fully visible/active, in the scroll's internal coordinate space. // note that clamping is applied to the left scroll bound to ensure scrolling past extents does not change the set of active columns. - float leftVisibleBound = Math.Clamp(Current, 0, ScrollableExtent); - float rightVisibleBound = leftVisibleBound + DrawWidth; + double leftVisibleBound = Math.Clamp(Current, 0, ScrollableExtent); + double rightVisibleBound = leftVisibleBound + DrawWidth; // if a movement is occurring at this time, the bounds below represent the full range of columns that the scroll movement will encompass. // this will be used to ensure that columns do not change state from active to inactive back and forth until they are fully scrolled past. - float leftMovementBound = Math.Min(Current, Target); - float rightMovementBound = Math.Max(Current, Target) + DrawWidth; + double leftMovementBound = Math.Min(Current, Target); + double rightMovementBound = Math.Max(Current, Target) + DrawWidth; foreach (var column in Child) { diff --git a/osu.Game/Overlays/NewsOverlay.cs b/osu.Game/Overlays/NewsOverlay.cs index cb9d940a05..81ac67bd89 100644 --- a/osu.Game/Overlays/NewsOverlay.cs +++ b/osu.Game/Overlays/NewsOverlay.cs @@ -136,7 +136,7 @@ namespace osu.Game.Overlays { base.UpdateAfterChildren(); sidebarContainer.Height = DrawHeight; - sidebarContainer.Y = Math.Clamp(ScrollFlow.Current - Header.DrawHeight, 0, Math.Max(ScrollFlow.ScrollContent.DrawHeight - DrawHeight - Header.DrawHeight, 0)); + sidebarContainer.Y = (float)Math.Clamp(ScrollFlow.Current - Header.DrawHeight, 0, Math.Max(ScrollFlow.ScrollContent.DrawHeight - DrawHeight - Header.DrawHeight, 0)); } private void loadListing(int? year = null) diff --git a/osu.Game/Overlays/OnlineOverlay.cs b/osu.Game/Overlays/OnlineOverlay.cs index 051873b394..cc5a1b9d2d 100644 --- a/osu.Game/Overlays/OnlineOverlay.cs +++ b/osu.Game/Overlays/OnlineOverlay.cs @@ -88,7 +88,7 @@ namespace osu.Game.Overlays base.UpdateAfterChildren(); // don't block header by applying padding equal to the visible header height - loadingContainer.Padding = new MarginPadding { Top = Math.Max(0, Header.Height - ScrollFlow.Current) }; + loadingContainer.Padding = new MarginPadding { Top = (float)Math.Max(0, Header.Height - ScrollFlow.Current) }; } } } diff --git a/osu.Game/Overlays/OverlayScrollContainer.cs b/osu.Game/Overlays/OverlayScrollContainer.cs index 4328977a8d..66a8686a88 100644 --- a/osu.Game/Overlays/OverlayScrollContainer.cs +++ b/osu.Game/Overlays/OverlayScrollContainer.cs @@ -35,7 +35,7 @@ namespace osu.Game.Overlays public ScrollBackButton Button { get; private set; } - private readonly Bindable lastScrollTarget = new Bindable(); + private readonly Bindable lastScrollTarget = new Bindable(); [BackgroundDependencyLoader] private void load() @@ -63,7 +63,7 @@ namespace osu.Game.Overlays Button.State = Target > button_scroll_position || lastScrollTarget.Value != null ? Visibility.Visible : Visibility.Hidden; } - protected override void OnUserScroll(float value, bool animated = true, double? distanceDecay = default) + protected override void OnUserScroll(double value, bool animated = true, double? distanceDecay = default) { base.OnUserScroll(value, animated, distanceDecay); @@ -112,7 +112,7 @@ namespace osu.Game.Overlays private readonly Box background; private readonly SpriteIcon spriteIcon; - public Bindable LastScrollTarget = new Bindable(); + public Bindable LastScrollTarget = new Bindable(); protected override HoverSounds CreateHoverSounds(HoverSampleSet sampleSet) => new HoverSounds(); diff --git a/osu.Game/Overlays/WikiOverlay.cs b/osu.Game/Overlays/WikiOverlay.cs index 14a25a909d..ef258da82b 100644 --- a/osu.Game/Overlays/WikiOverlay.cs +++ b/osu.Game/Overlays/WikiOverlay.cs @@ -100,7 +100,7 @@ namespace osu.Game.Overlays if (articlePage != null) { articlePage.SidebarContainer.Height = DrawHeight; - articlePage.SidebarContainer.Y = Math.Clamp(ScrollFlow.Current - Header.DrawHeight, 0, Math.Max(ScrollFlow.ScrollContent.DrawHeight - DrawHeight - Header.DrawHeight, 0)); + articlePage.SidebarContainer.Y = (float)Math.Clamp(ScrollFlow.Current - Header.DrawHeight, 0, Math.Max(ScrollFlow.ScrollContent.DrawHeight - DrawHeight - Header.DrawHeight, 0)); } } diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/Timeline.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/Timeline.cs index e5360e2eeb..5f46b3d937 100644 --- a/osu.Game/Screens/Edit/Compose/Components/Timeline/Timeline.cs +++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/Timeline.cs @@ -55,7 +55,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline /// /// The timeline's scroll position in the last frame. /// - private float lastScrollPosition; + private double lastScrollPosition; /// /// The track time in the last frame. @@ -322,7 +322,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline /// public double VisibleRange => editorClock.TrackLength / Zoom; - public double TimeAtPosition(float x) + public double TimeAtPosition(double x) { return x / Content.DrawWidth * editorClock.TrackLength; } diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/ZoomableScrollContainer.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/ZoomableScrollContainer.cs index 31a0936eb4..9db14ce4c4 100644 --- a/osu.Game/Screens/Edit/Compose/Components/Timeline/ZoomableScrollContainer.cs +++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/ZoomableScrollContainer.cs @@ -182,7 +182,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline } private void transformZoomTo(float newZoom, float focusPoint, double duration = 0, Easing easing = Easing.None) - => this.TransformTo(this.PopulateTransform(new TransformZoom(focusPoint, zoomedContent.DrawWidth, Current), newZoom, duration, easing)); + => this.TransformTo(this.PopulateTransform(new TransformZoom(focusPoint, zoomedContent.DrawWidth, (float)Current), newZoom, duration, easing)); /// /// Invoked when has changed. diff --git a/osu.Game/Screens/Play/HUD/GameplayLeaderboard.cs b/osu.Game/Screens/Play/HUD/GameplayLeaderboard.cs index d2b6b834f8..f6694505dc 100644 --- a/osu.Game/Screens/Play/HUD/GameplayLeaderboard.cs +++ b/osu.Game/Screens/Play/HUD/GameplayLeaderboard.cs @@ -114,15 +114,15 @@ namespace osu.Game.Screens.Play.HUD if (requiresScroll && TrackedScore != null) { - float scrollTarget = scroll.GetChildPosInContent(TrackedScore) + TrackedScore.DrawHeight / 2 - scroll.DrawHeight / 2; + double scrollTarget = scroll.GetChildPosInContent(TrackedScore) + TrackedScore.DrawHeight / 2 - scroll.DrawHeight / 2; scroll.ScrollTo(scrollTarget); } const float panel_height = GameplayLeaderboardScore.PANEL_HEIGHT; - float fadeBottom = scroll.Current + scroll.DrawHeight; - float fadeTop = scroll.Current + panel_height; + float fadeBottom = (float)(scroll.Current + scroll.DrawHeight); + float fadeTop = (float)(scroll.Current + panel_height); if (scroll.IsScrolledToStart()) fadeTop -= panel_height; if (!scroll.IsScrolledToEnd()) fadeBottom -= panel_height; diff --git a/osu.Game/Screens/Ranking/ScorePanelList.cs b/osu.Game/Screens/Ranking/ScorePanelList.cs index e711bed729..b0e1c89121 100644 --- a/osu.Game/Screens/Ranking/ScorePanelList.cs +++ b/osu.Game/Screens/Ranking/ScorePanelList.cs @@ -334,7 +334,7 @@ namespace osu.Game.Screens.Ranking private partial class Scroll : OsuScrollContainer { - public new float Target => base.Target; + public new double Target => base.Target; public Scroll() : base(Direction.Horizontal) @@ -344,7 +344,7 @@ namespace osu.Game.Screens.Ranking /// /// The target that will be scrolled to instantaneously next frame. /// - public float? InstantScrollTarget; + public double? InstantScrollTarget; protected override void UpdateAfterChildren() { diff --git a/osu.Game/Screens/Select/BeatmapCarousel.cs b/osu.Game/Screens/Select/BeatmapCarousel.cs index 65c4133ea2..de12b36b17 100644 --- a/osu.Game/Screens/Select/BeatmapCarousel.cs +++ b/osu.Game/Screens/Select/BeatmapCarousel.cs @@ -611,12 +611,12 @@ namespace osu.Game.Screens.Select /// /// The position of the lower visible bound with respect to the current scroll position. /// - private float visibleBottomBound => Scroll.Current + DrawHeight + BleedBottom; + private float visibleBottomBound => (float)(Scroll.Current + DrawHeight + BleedBottom); /// /// The position of the upper visible bound with respect to the current scroll position. /// - private float visibleUpperBound => Scroll.Current - BleedTop; + private float visibleUpperBound => (float)(Scroll.Current - BleedTop); public void FlushPendingFilterOperations() { @@ -1006,7 +1006,7 @@ namespace osu.Game.Screens.Select // we take the difference in scroll height and apply to all visible panels. // this avoids edge cases like when the visible panels is reduced suddenly, causing ScrollContainer // to enter clamp-special-case mode where it animates completely differently to normal. - float scrollChange = scrollTarget.Value - Scroll.Current; + float scrollChange = (float)(scrollTarget.Value - Scroll.Current); Scroll.ScrollTo(scrollTarget.Value, false); foreach (var i in Scroll) i.Y += scrollChange; @@ -1217,12 +1217,12 @@ namespace osu.Game.Screens.Select private const float top_padding = 10; private const float bottom_padding = 70; - protected override float ToScrollbarPosition(float scrollPosition) + protected override float ToScrollbarPosition(double scrollPosition) { if (Precision.AlmostEquals(0, ScrollableExtent)) return 0; - return top_padding + (ScrollbarMovementExtent - (top_padding + bottom_padding)) * (scrollPosition / ScrollableExtent); + return (float)(top_padding + (ScrollbarMovementExtent - (top_padding + bottom_padding)) * (scrollPosition / ScrollableExtent)); } protected override float FromScrollbarPosition(float scrollbarPosition) @@ -1230,7 +1230,7 @@ namespace osu.Game.Screens.Select if (Precision.AlmostEquals(0, ScrollbarMovementExtent)) return 0; - return ScrollableExtent * ((scrollbarPosition - top_padding) / (ScrollbarMovementExtent - (top_padding + bottom_padding))); + return (float)(ScrollableExtent * ((scrollbarPosition - top_padding) / (ScrollbarMovementExtent - (top_padding + bottom_padding)))); } } } From 5e9a7532d31d594a36013d19772e7ea4a95a0a46 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 10 Jan 2025 20:55:53 +0900 Subject: [PATCH 210/290] Add basic implementation of new beatmap carousel --- .../SongSelect/TestSceneBeatmapCarouselV2.cs | 189 +++++++++ .../Screens/SelectV2/BeatmapCarouselV2.cs | 205 ++++++++++ osu.Game/Screens/SelectV2/Carousel.cs | 371 ++++++++++++++++++ osu.Game/Screens/SelectV2/CarouselItem.cs | 41 ++ osu.Game/Screens/SelectV2/ICarouselFilter.cs | 23 ++ osu.Game/Screens/SelectV2/ICarouselPanel.cs | 23 ++ osu.Game/Tests/Beatmaps/TestBeatmapStore.cs | 2 +- 7 files changed, 853 insertions(+), 1 deletion(-) create mode 100644 osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2.cs create mode 100644 osu.Game/Screens/SelectV2/BeatmapCarouselV2.cs create mode 100644 osu.Game/Screens/SelectV2/Carousel.cs create mode 100644 osu.Game/Screens/SelectV2/CarouselItem.cs create mode 100644 osu.Game/Screens/SelectV2/ICarouselFilter.cs create mode 100644 osu.Game/Screens/SelectV2/ICarouselPanel.cs diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2.cs new file mode 100644 index 0000000000..75223adc2b --- /dev/null +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2.cs @@ -0,0 +1,189 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using NUnit.Framework; +using osu.Framework.Allocation; +using osu.Framework.Bindables; +using osu.Framework.Extensions.ObjectExtensions; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; +using osu.Framework.Testing; +using osu.Framework.Utils; +using osu.Game.Beatmaps; +using osu.Game.Database; +using osu.Game.Graphics.Containers; +using osu.Game.Screens.SelectV2; +using osu.Game.Tests.Beatmaps; +using osu.Game.Tests.Resources; +using osuTK.Graphics; + +namespace osu.Game.Tests.Visual.SongSelect +{ + [TestFixture] + public partial class TestSceneBeatmapCarouselV2 : OsuManualInputManagerTestScene + { + private readonly BindableList beatmapSets = new BindableList(); + + [Cached(typeof(BeatmapStore))] + private BeatmapStore store; + + private OsuTextFlowContainer stats = null!; + private BeatmapCarouselV2 carousel = null!; + + private int beatmapCount; + + public TestSceneBeatmapCarouselV2() + { + store = new TestBeatmapStore + { + BeatmapSets = { BindTarget = beatmapSets } + }; + + beatmapSets.BindCollectionChanged((_, _) => + { + beatmapCount = beatmapSets.Sum(s => s.Beatmaps.Count); + }); + + Scheduler.AddDelayed(updateStats, 100, true); + } + + [SetUpSteps] + public void SetUpSteps() + { + AddStep("create components", () => + { + beatmapSets.Clear(); + + Box topBox; + Children = new Drawable[] + { + new GridContainer + { + RelativeSizeAxes = Axes.Both, + ColumnDimensions = new[] + { + new Dimension(GridSizeMode.Relative, 1), + }, + RowDimensions = new[] + { + new Dimension(GridSizeMode.Absolute, 200), + new Dimension(), + new Dimension(GridSizeMode.Absolute, 200), + }, + Content = new[] + { + new Drawable[] + { + topBox = new Box + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Colour = Color4.Cyan, + RelativeSizeAxes = Axes.Both, + Alpha = 0.4f, + }, + }, + new Drawable[] + { + carousel = new BeatmapCarouselV2 + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Width = 500, + RelativeSizeAxes = Axes.Y, + }, + }, + new[] + { + new Box + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Colour = Color4.Cyan, + RelativeSizeAxes = Axes.Both, + Alpha = 0.4f, + }, + topBox.CreateProxy(), + } + } + }, + stats = new OsuTextFlowContainer(cp => cp.Font = FrameworkFont.Regular.With()) + { + Padding = new MarginPadding(10), + TextAnchor = Anchor.CentreLeft, + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + }, + }; + }); + } + + [Test] + public void TestBasic() + { + AddStep("add 10 beatmaps", () => + { + for (int i = 0; i < 10; i++) + beatmapSets.Add(TestResources.CreateTestBeatmapSetInfo(RNG.Next(1, 4))); + }); + + AddStep("add 1 beatmap", () => beatmapSets.Add(TestResources.CreateTestBeatmapSetInfo(RNG.Next(1, 4)))); + + AddStep("remove all beatmaps", () => beatmapSets.Clear()); + } + + [Test] + public void TestAddRemoveOneByOne() + { + AddRepeatStep("add beatmaps", () => beatmapSets.Add(TestResources.CreateTestBeatmapSetInfo(RNG.Next(1, 4))), 20); + + AddRepeatStep("remove beatmaps", () => beatmapSets.RemoveAt(RNG.Next(0, beatmapSets.Count)), 20); + } + + [Test] + [Explicit] + public void TestInsane() + { + const int count = 200000; + + List generated = new List(); + + AddStep($"populate {count} test beatmaps", () => + { + generated.Clear(); + Task.Run(() => + { + for (int j = 0; j < count; j++) + generated.Add(TestResources.CreateTestBeatmapSetInfo(RNG.Next(1, 4))); + }).ConfigureAwait(true); + }); + + AddUntilStep("wait for beatmaps populated", () => generated.Count, () => Is.GreaterThan(count / 3)); + AddUntilStep("this takes a while", () => generated.Count, () => Is.GreaterThan(count / 3 * 2)); + AddUntilStep("maybe they are done now", () => generated.Count, () => Is.EqualTo(count)); + + AddStep("add all beatmaps", () => beatmapSets.AddRange(generated)); + } + + private void updateStats() + { + if (carousel.IsNull()) + return; + + stats.Text = $""" + store + sets: {beatmapSets.Count} + beatmaps: {beatmapCount} + carousel: + sorting: {carousel.IsFiltering} + tracked: {carousel.ItemsTracked} + displayable: {carousel.DisplayableItems} + displayed: {carousel.VisibleItems} + """; + } + } +} diff --git a/osu.Game/Screens/SelectV2/BeatmapCarouselV2.cs b/osu.Game/Screens/SelectV2/BeatmapCarouselV2.cs new file mode 100644 index 0000000000..a54c2aceff --- /dev/null +++ b/osu.Game/Screens/SelectV2/BeatmapCarouselV2.cs @@ -0,0 +1,205 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using System.Collections.Generic; +using System.Collections.Specialized; +using System.Diagnostics; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using osu.Framework.Allocation; +using osu.Framework.Bindables; +using osu.Framework.Extensions.Color4Extensions; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Pooling; +using osu.Framework.Graphics.Shapes; +using osu.Game.Beatmaps; +using osu.Game.Database; +using osu.Game.Graphics.Sprites; +using osu.Game.Screens.Select; +using osuTK; +using osuTK.Graphics; + +namespace osu.Game.Screens.SelectV2 +{ + public partial class BeatmapCarouselV2 : Carousel + { + private IBindableList detachedBeatmaps = null!; + + private readonly DrawablePool carouselPanelPool = new DrawablePool(100); + + public BeatmapCarouselV2() + { + DebounceDelay = 100; + DistanceOffscreenToPreload = 100; + + Filters = new ICarouselFilter[] + { + new Sorter(), + new Grouper(), + }; + + AddInternal(carouselPanelPool); + } + + [BackgroundDependencyLoader] + private void load(BeatmapStore beatmapStore, CancellationToken? cancellationToken) + { + detachedBeatmaps = beatmapStore.GetBeatmapSets(cancellationToken); + detachedBeatmaps.BindCollectionChanged(beatmapSetsChanged, true); + } + + protected override Drawable GetDrawableForDisplay(CarouselItem item) + { + var drawable = carouselPanelPool.Get(); + drawable.FlashColour(Color4.Red, 2000); + + return drawable; + } + + private void beatmapSetsChanged(object? beatmaps, NotifyCollectionChangedEventArgs changed) + { + // TODO: moving management of BeatmapInfo tracking to BeatmapStore might be something we want to consider. + // right now we are managing this locally which is a bit of added overhead. + IEnumerable? newBeatmapSets = changed.NewItems?.Cast(); + IEnumerable? beatmapSetInfos = changed.OldItems?.Cast(); + + switch (changed.Action) + { + case NotifyCollectionChangedAction.Add: + Items.AddRange(newBeatmapSets!.SelectMany(s => s.Beatmaps).Select(b => new BeatmapCarouselItem(b))); + break; + + case NotifyCollectionChangedAction.Remove: + + foreach (var set in beatmapSetInfos!) + { + foreach (var beatmap in set.Beatmaps) + Items.RemoveAll(i => i.Model is BeatmapInfo bi && beatmap.Equals(bi)); + } + + break; + + case NotifyCollectionChangedAction.Move: + case NotifyCollectionChangedAction.Replace: + throw new NotImplementedException(); + + case NotifyCollectionChangedAction.Reset: + Items.Clear(); + break; + } + } + + public FilterCriteria Criteria { get; private set; } = new FilterCriteria(); + + public void Filter(FilterCriteria criteria) + { + Criteria = criteria; + QueueFilter(); + } + } + + public partial class BeatmapCarouselPanel : PoolableDrawable, ICarouselPanel + { + public CarouselItem? Item { get; set; } + + protected override void PrepareForUse() + { + base.PrepareForUse(); + + Debug.Assert(Item != null); + + Size = new Vector2(500, Item.DrawHeight); + + InternalChildren = new Drawable[] + { + new Box + { + Colour = (Item.Model is BeatmapInfo ? Color4.Aqua : Color4.Yellow).Darken(5), + RelativeSizeAxes = Axes.Both, + }, + new OsuSpriteText + { + Text = Item.ToString() ?? string.Empty, + Padding = new MarginPadding(5), + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + } + }; + } + } + + public class BeatmapCarouselItem : CarouselItem + { + public readonly Guid ID; + + public override float DrawHeight => Model is BeatmapInfo ? 40 : 80; + + public BeatmapCarouselItem(object model) + : base(model) + { + ID = (Model as IHasGuidPrimaryKey)?.ID ?? Guid.NewGuid(); + } + + public override string? ToString() + { + switch (Model) + { + case BeatmapInfo bi: + return $"Difficulty: {bi.DifficultyName} ({bi.StarRating:N1}*)"; + + case BeatmapSetInfo si: + return $"{si.Metadata}"; + } + + return Model.ToString(); + } + } + + public class Grouper : ICarouselFilter + { + public async Task> Run(IEnumerable items, CancellationToken cancellationToken) => await Task.Run(() => + { + // TODO: perform grouping based on FilterCriteria + + CarouselItem? lastItem = null; + + var newItems = new List(); + + foreach (var item in items) + { + cancellationToken.ThrowIfCancellationRequested(); + + if (item.Model is BeatmapInfo b1) + { + // Add set header + if (lastItem == null || (lastItem.Model is BeatmapInfo b2 && b2.BeatmapSet!.OnlineID != b1.BeatmapSet!.OnlineID)) + newItems.Add(new BeatmapCarouselItem(b1.BeatmapSet!)); + } + + newItems.Add(item); + lastItem = item; + } + + return newItems; + }, cancellationToken).ConfigureAwait(false); + } + + public class Sorter : ICarouselFilter + { + public async Task> Run(IEnumerable items, CancellationToken cancellationToken) => await Task.Run(() => + { + return items.OrderDescending(Comparer.Create((a, b) => + { + if (a.Model is BeatmapInfo ab && b.Model is BeatmapInfo bb) + return ab.OnlineID.CompareTo(bb.OnlineID); + + if (a is BeatmapCarouselItem aItem && b is BeatmapCarouselItem bItem) + return aItem.ID.CompareTo(bItem.ID); + + return 0; + })); + }, cancellationToken).ConfigureAwait(false); + } +} diff --git a/osu.Game/Screens/SelectV2/Carousel.cs b/osu.Game/Screens/SelectV2/Carousel.cs new file mode 100644 index 0000000000..2f3c47a0a3 --- /dev/null +++ b/osu.Game/Screens/SelectV2/Carousel.cs @@ -0,0 +1,371 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using osu.Framework.Bindables; +using osu.Framework.Extensions.TypeExtensions; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Pooling; +using osu.Framework.Graphics.Shapes; +using osu.Framework.Logging; +using osu.Game.Graphics.Containers; +using osuTK; +using osuTK.Graphics; + +namespace osu.Game.Screens.SelectV2 +{ + /// + /// A highly efficient vertical list display that is used primarily for the song select screen, + /// but flexible enough to be used for other use cases. + /// + public abstract partial class Carousel : CompositeDrawable + { + /// + /// A collection of filters which should be run each time a is executed. + /// + public IEnumerable Filters { get; init; } = Enumerable.Empty(); + + /// + /// Height of the area above the carousel that should be treated as visible due to transparency of elements in front of it. + /// + public float BleedTop { get; set; } = 0; + + /// + /// Height of the area below the carousel that should be treated as visible due to transparency of elements in front of it. + /// + public float BleedBottom { get; set; } = 0; + + /// + /// The number of pixels outside the carousel's vertical bounds to manifest drawables. + /// This allows preloading content before it scrolls into view. + /// + public float DistanceOffscreenToPreload { get; set; } = 0; + + /// + /// When a new request arrives to change filtering, the number of milliseconds to wait before performing the filter. + /// Regardless of any external debouncing, this is a safety measure to avoid triggering too many threaded operations. + /// + public int DebounceDelay { get; set; } = 0; + + /// + /// Whether an asynchronous filter / group operation is currently underway. + /// + public bool IsFiltering => !filterTask.IsCompleted; + + /// + /// The number of displayable items currently being tracked (before filtering). + /// + public int ItemsTracked => Items.Count; + + /// + /// The number of carousel items currently in rotation for display. + /// + public int DisplayableItems => displayedCarouselItems?.Count ?? 0; + + /// + /// The number of items currently actualised into drawables. + /// + public int VisibleItems => scroll.Panels.Count; + + /// + /// All items which are to be considered for display in this carousel. + /// Mutating this list will automatically queue a . + /// + protected readonly BindableList Items = new BindableList(); + + private List? displayedCarouselItems; + + private readonly DoublePrecisionScroll scroll; + + protected Carousel() + { + InternalChildren = new Drawable[] + { + new Box + { + Colour = Color4.Black, + RelativeSizeAxes = Axes.Both, + }, + scroll = new DoublePrecisionScroll + { + RelativeSizeAxes = Axes.Both, + Masking = false, + } + }; + + Items.BindCollectionChanged((_, _) => QueueFilter()); + } + + /// + /// Queue an asynchronous filter operation. + /// + public void QueueFilter() => Scheduler.AddOnce(() => filterTask = performFilter()); + + /// + /// Create a drawable for the given carousel item so it can be displayed. + /// + /// + /// For efficiency, it is recommended the drawables are retrieved from a . + /// + /// The item which should be represented by the returned drawable. + /// The manifested drawable. + protected abstract Drawable GetDrawableForDisplay(CarouselItem item); + + #region Filtering and display preparation + + private Task filterTask = Task.CompletedTask; + private CancellationTokenSource cancellationSource = new CancellationTokenSource(); + + private async Task performFilter() + { + Debug.Assert(SynchronizationContext.Current != null); + + var cts = new CancellationTokenSource(); + + lock (this) + { + cancellationSource.Cancel(); + cancellationSource = cts; + } + + Stopwatch stopwatch = Stopwatch.StartNew(); + IEnumerable items = new List(Items); + + await Task.Run(async () => + { + try + { + if (DebounceDelay > 0) + { + log($"Filter operation queued, waiting for {DebounceDelay} ms debounce"); + await Task.Delay(DebounceDelay, cts.Token).ConfigureAwait(false); + } + + foreach (var filter in Filters) + { + log($"Performing {filter.GetType().ReadableName()}"); + items = await filter.Run(items, cts.Token).ConfigureAwait(false); + } + + log("Updating Y positions"); + await updateYPositions(items, cts.Token).ConfigureAwait(false); + } + catch (OperationCanceledException) + { + log("Cancelled due to newer request arriving"); + } + }, cts.Token).ConfigureAwait(true); + + if (cts.Token.IsCancellationRequested) + return; + + log("Items ready for display"); + displayedCarouselItems = items.ToList(); + displayedRange = null; + + void log(string text) => Logger.Log($"Carousel[op {cts.GetHashCode().ToString().Substring(0, 5)}] {stopwatch.ElapsedMilliseconds} ms: {text}"); + } + + private async Task updateYPositions(IEnumerable carouselItems, CancellationToken cancellationToken) => await Task.Run(() => + { + const float spacing = 10; + float yPos = 0; + + foreach (var item in carouselItems) + { + item.CarouselYPosition = yPos; + yPos += item.DrawHeight + spacing; + } + }, cancellationToken).ConfigureAwait(false); + + #endregion + + #region Display handling + + private DisplayRange? displayedRange; + + private readonly CarouselItem carouselBoundsItem = new BoundsCarouselItem(); + + /// + /// The position of the lower visible bound with respect to the current scroll position. + /// + private float visibleBottomBound => (float)(scroll.Current + DrawHeight + BleedBottom); + + /// + /// The position of the upper visible bound with respect to the current scroll position. + /// + private float visibleUpperBound => (float)(scroll.Current - BleedTop); + + protected override void Update() + { + base.Update(); + + if (displayedCarouselItems == null) + return; + + var range = getDisplayRange(); + + if (range != displayedRange) + { + Logger.Log($"Updating displayed range of carousel from {displayedRange} to {range}"); + displayedRange = range; + + updateDisplayedRange(range); + } + } + + private DisplayRange getDisplayRange() + { + Debug.Assert(displayedCarouselItems != null); + + // Find index range of all items that should be on-screen + carouselBoundsItem.CarouselYPosition = visibleUpperBound - DistanceOffscreenToPreload; + int firstIndex = displayedCarouselItems.BinarySearch(carouselBoundsItem); + if (firstIndex < 0) firstIndex = ~firstIndex; + + carouselBoundsItem.CarouselYPosition = visibleBottomBound + DistanceOffscreenToPreload; + int lastIndex = displayedCarouselItems.BinarySearch(carouselBoundsItem); + if (lastIndex < 0) lastIndex = ~lastIndex; + + firstIndex = Math.Max(0, firstIndex - 1); + lastIndex = Math.Max(0, lastIndex - 1); + + return new DisplayRange(firstIndex, lastIndex); + } + + private void updateDisplayedRange(DisplayRange range) + { + Debug.Assert(displayedCarouselItems != null); + + List toDisplay = range.Last - range.First == 0 + ? new List() + : displayedCarouselItems.GetRange(range.First, range.Last - range.First + 1); + + // Iterate over all panels which are already displayed and figure which need to be displayed / removed. + foreach (var panel in scroll.Panels) + { + var carouselPanel = (ICarouselPanel)panel; + + // The case where we're intending to display this panel, but it's already displayed. + // Note that we **must compare the model here** as the CarouselItems may be fresh instances due to a filter operation. + var existing = toDisplay.FirstOrDefault(i => i.Model == carouselPanel.Item!.Model); + + if (existing != null) + { + carouselPanel.Item = existing; + toDisplay.Remove(existing); + continue; + } + + // If the new display range doesn't contain the panel, it's no longer required for display. + expirePanelImmediately(panel); + } + + // Add any new items which need to be displayed and haven't yet. + foreach (var item in toDisplay) + { + var drawable = GetDrawableForDisplay(item); + + if (drawable is not ICarouselPanel carouselPanel) + throw new InvalidOperationException($"Carousel panel drawables must implement {typeof(ICarouselPanel)}"); + + carouselPanel.Item = item; + scroll.Add(drawable); + } + + // Update the total height of all items (to make the scroll container scrollable through the full height even though + // most items are not displayed / loaded). + if (displayedCarouselItems.Count > 0) + { + var lastItem = displayedCarouselItems[^1]; + scroll.SetLayoutHeight((float)(lastItem.CarouselYPosition + lastItem.DrawHeight)); + } + else + scroll.SetLayoutHeight(0); + } + + private static void expirePanelImmediately(Drawable panel) + { + panel.FinishTransforms(); + panel.Expire(); + } + + #endregion + + #region Internal helper classes + + private record DisplayRange(int First, int Last); + + /// + /// Implementation of scroll container which handles very large vertical lists by internally using double precision + /// for pre-display Y values. + /// + private partial class DoublePrecisionScroll : OsuScrollContainer + { + public readonly Container Panels; + + public void SetLayoutHeight(float height) => Panels.Height = height; + + public DoublePrecisionScroll() + { + // Managing our own custom layout within ScrollContent causes feedback with public ScrollContainer calculations, + // so we must maintain one level of separation from ScrollContent. + base.Add(Panels = new Container + { + Name = "Layout content", + RelativeSizeAxes = Axes.X, + }); + } + + public override void Clear(bool disposeChildren) + { + Panels.Height = 0; + Panels.Clear(disposeChildren); + } + + public override void Add(Drawable drawable) + { + if (drawable is not ICarouselPanel) + throw new InvalidOperationException($"Carousel panel drawables must implement {typeof(ICarouselPanel)}"); + + Panels.Add(drawable); + } + + public override double GetChildPosInContent(Drawable d, Vector2 offset) + { + if (d is not ICarouselPanel panel) + return base.GetChildPosInContent(d, offset); + + return panel.YPosition + offset.X; + } + + protected override void ApplyCurrentToContent() + { + Debug.Assert(ScrollDirection == Direction.Vertical); + + double scrollableExtent = -Current + ScrollableExtent * ScrollContent.RelativeAnchorPosition.Y; + + foreach (var d in Panels) + d.Y = (float)(((ICarouselPanel)d).YPosition + scrollableExtent); + } + } + + private class BoundsCarouselItem : CarouselItem + { + public override float DrawHeight => 0; + + public BoundsCarouselItem() + : base(new object()) + { + } + } + + #endregion + } +} diff --git a/osu.Game/Screens/SelectV2/CarouselItem.cs b/osu.Game/Screens/SelectV2/CarouselItem.cs new file mode 100644 index 0000000000..69abe86205 --- /dev/null +++ b/osu.Game/Screens/SelectV2/CarouselItem.cs @@ -0,0 +1,41 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; + +namespace osu.Game.Screens.SelectV2 +{ + /// + /// Represents a single display item for display in a . + /// This is used to house information related to the attached model that helps with display and tracking. + /// + public abstract class CarouselItem : IComparable + { + /// + /// The model this item is representing. + /// + public readonly object Model; + + /// + /// The current Y position in the carousel. This is managed by and should not be set manually. + /// + public double CarouselYPosition { get; set; } + + /// + /// The height this item will take when displayed. + /// + public abstract float DrawHeight { get; } + + protected CarouselItem(object model) + { + Model = model; + } + + public int CompareTo(CarouselItem? other) + { + if (other == null) return 1; + + return CarouselYPosition.CompareTo(other.CarouselYPosition); + } + } +} diff --git a/osu.Game/Screens/SelectV2/ICarouselFilter.cs b/osu.Game/Screens/SelectV2/ICarouselFilter.cs new file mode 100644 index 0000000000..82aca18b85 --- /dev/null +++ b/osu.Game/Screens/SelectV2/ICarouselFilter.cs @@ -0,0 +1,23 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; + +namespace osu.Game.Screens.SelectV2 +{ + /// + /// An interface representing a filter operation which can be run on a . + /// + public interface ICarouselFilter + { + /// + /// Execute the filter operation. + /// + /// The items to be filtered. + /// A cancellation token. + /// The post-filtered items. + Task> Run(IEnumerable items, CancellationToken cancellationToken); + } +} diff --git a/osu.Game/Screens/SelectV2/ICarouselPanel.cs b/osu.Game/Screens/SelectV2/ICarouselPanel.cs new file mode 100644 index 0000000000..2f03bd8e26 --- /dev/null +++ b/osu.Game/Screens/SelectV2/ICarouselPanel.cs @@ -0,0 +1,23 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Graphics; + +namespace osu.Game.Screens.SelectV2 +{ + /// + /// An interface to be attached to any s which are used for display inside a . + /// + public interface ICarouselPanel + { + /// + /// The Y position which should be used for displaying this item within the carousel. + /// + double YPosition => Item!.CarouselYPosition; + + /// + /// The carousel item this drawable is representing. This is managed by and should not be set manually. + /// + CarouselItem? Item { get; set; } + } +} diff --git a/osu.Game/Tests/Beatmaps/TestBeatmapStore.cs b/osu.Game/Tests/Beatmaps/TestBeatmapStore.cs index 1734f1397f..eaef2af7c8 100644 --- a/osu.Game/Tests/Beatmaps/TestBeatmapStore.cs +++ b/osu.Game/Tests/Beatmaps/TestBeatmapStore.cs @@ -11,6 +11,6 @@ namespace osu.Game.Tests.Beatmaps internal partial class TestBeatmapStore : BeatmapStore { public readonly BindableList BeatmapSets = new BindableList(); - public override IBindableList GetBeatmapSets(CancellationToken? cancellationToken) => BeatmapSets; + public override IBindableList GetBeatmapSets(CancellationToken? cancellationToken) => BeatmapSets.GetBoundCopy(); } } From 288be46b17d3c87347e2e8ed1df8f7af3df379e7 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 10 Jan 2025 19:34:56 +0900 Subject: [PATCH 211/290] Add basic selection support --- .../Screens/SelectV2/BeatmapCarouselV2.cs | 54 ++++++++++++++++++- osu.Game/Screens/SelectV2/Carousel.cs | 40 ++++++++++++++ osu.Game/Screens/SelectV2/CarouselItem.cs | 7 ++- osu.Game/Screens/SelectV2/ICarouselFilter.cs | 2 +- osu.Game/Screens/SelectV2/ICarouselPanel.cs | 4 +- 5 files changed, 100 insertions(+), 7 deletions(-) diff --git a/osu.Game/Screens/SelectV2/BeatmapCarouselV2.cs b/osu.Game/Screens/SelectV2/BeatmapCarouselV2.cs index a54c2aceff..37c33446da 100644 --- a/osu.Game/Screens/SelectV2/BeatmapCarouselV2.cs +++ b/osu.Game/Screens/SelectV2/BeatmapCarouselV2.cs @@ -14,6 +14,7 @@ using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Pooling; using osu.Framework.Graphics.Shapes; +using osu.Framework.Input.Events; using osu.Game.Beatmaps; using osu.Game.Database; using osu.Game.Graphics.Sprites; @@ -23,6 +24,7 @@ using osuTK.Graphics; namespace osu.Game.Screens.SelectV2 { + [Cached] public partial class BeatmapCarouselV2 : Carousel { private IBindableList detachedBeatmaps = null!; @@ -102,7 +104,48 @@ namespace osu.Game.Screens.SelectV2 public partial class BeatmapCarouselPanel : PoolableDrawable, ICarouselPanel { - public CarouselItem? Item { get; set; } + [Resolved] + private BeatmapCarouselV2 carousel { get; set; } = null!; + + public CarouselItem? Item + { + get => item; + set + { + item = value; + + selected.UnbindBindings(); + + if (item != null) + selected.BindTo(item.Selected); + } + } + + private readonly BindableBool selected = new BindableBool(); + private CarouselItem? item; + + [BackgroundDependencyLoader] + private void load() + { + selected.BindValueChanged(value => + { + if (value.NewValue) + { + BorderThickness = 5; + BorderColour = Color4.Pink; + } + else + { + BorderThickness = 0; + } + }); + } + + protected override void FreeAfterUse() + { + base.FreeAfterUse(); + Item = null; + } protected override void PrepareForUse() { @@ -111,6 +154,7 @@ namespace osu.Game.Screens.SelectV2 Debug.Assert(Item != null); Size = new Vector2(500, Item.DrawHeight); + Masking = true; InternalChildren = new Drawable[] { @@ -128,6 +172,12 @@ namespace osu.Game.Screens.SelectV2 } }; } + + protected override bool OnClick(ClickEvent e) + { + carousel.CurrentSelection = Item!.Model; + return true; + } } public class BeatmapCarouselItem : CarouselItem @@ -165,7 +215,7 @@ namespace osu.Game.Screens.SelectV2 CarouselItem? lastItem = null; - var newItems = new List(); + var newItems = new List(items.Count()); foreach (var item in items) { diff --git a/osu.Game/Screens/SelectV2/Carousel.cs b/osu.Game/Screens/SelectV2/Carousel.cs index 2f3c47a0a3..45dadc3455 100644 --- a/osu.Game/Screens/SelectV2/Carousel.cs +++ b/osu.Game/Screens/SelectV2/Carousel.cs @@ -77,8 +77,28 @@ namespace osu.Game.Screens.SelectV2 /// All items which are to be considered for display in this carousel. /// Mutating this list will automatically queue a . /// + /// + /// Note that an may add new items which are displayed but not tracked in this list. + /// protected readonly BindableList Items = new BindableList(); + /// + /// The currently selected model. + /// + /// + /// Setting this will ensure is set to true only on the matching . + /// Of note, if no matching item exists all items will be deselected while waiting for potential new item which matches. + /// + public virtual object? CurrentSelection + { + get => currentSelection; + set + { + currentSelection = value; + updateSelection(); + } + } + private List? displayedCarouselItems; private readonly DoublePrecisionScroll scroll; @@ -169,6 +189,8 @@ namespace osu.Game.Screens.SelectV2 displayedCarouselItems = items.ToList(); displayedRange = null; + updateSelection(); + void log(string text) => Logger.Log($"Carousel[op {cts.GetHashCode().ToString().Substring(0, 5)}] {stopwatch.ElapsedMilliseconds} ms: {text}"); } @@ -186,6 +208,24 @@ namespace osu.Game.Screens.SelectV2 #endregion + #region Selection handling + + private object? currentSelection; + + private void updateSelection() + { + if (displayedCarouselItems == null) return; + + // TODO: this is ugly, we probably should stop exposing CarouselItem externally. + foreach (var item in Items) + item.Selected.Value = item.Model == currentSelection; + + foreach (var item in displayedCarouselItems) + item.Selected.Value = item.Model == currentSelection; + } + + #endregion + #region Display handling private DisplayRange? displayedRange; diff --git a/osu.Game/Screens/SelectV2/CarouselItem.cs b/osu.Game/Screens/SelectV2/CarouselItem.cs index 69abe86205..4636e8a32f 100644 --- a/osu.Game/Screens/SelectV2/CarouselItem.cs +++ b/osu.Game/Screens/SelectV2/CarouselItem.cs @@ -2,22 +2,25 @@ // See the LICENCE file in the repository root for full licence text. using System; +using osu.Framework.Bindables; namespace osu.Game.Screens.SelectV2 { /// - /// Represents a single display item for display in a . + /// Represents a single display item for display in a . /// This is used to house information related to the attached model that helps with display and tracking. /// public abstract class CarouselItem : IComparable { + public readonly BindableBool Selected = new BindableBool(); + /// /// The model this item is representing. /// public readonly object Model; /// - /// The current Y position in the carousel. This is managed by and should not be set manually. + /// The current Y position in the carousel. This is managed by and should not be set manually. /// public double CarouselYPosition { get; set; } diff --git a/osu.Game/Screens/SelectV2/ICarouselFilter.cs b/osu.Game/Screens/SelectV2/ICarouselFilter.cs index 82aca18b85..f510a7cd4b 100644 --- a/osu.Game/Screens/SelectV2/ICarouselFilter.cs +++ b/osu.Game/Screens/SelectV2/ICarouselFilter.cs @@ -8,7 +8,7 @@ using System.Threading.Tasks; namespace osu.Game.Screens.SelectV2 { /// - /// An interface representing a filter operation which can be run on a . + /// An interface representing a filter operation which can be run on a . /// public interface ICarouselFilter { diff --git a/osu.Game/Screens/SelectV2/ICarouselPanel.cs b/osu.Game/Screens/SelectV2/ICarouselPanel.cs index 2f03bd8e26..97c585492c 100644 --- a/osu.Game/Screens/SelectV2/ICarouselPanel.cs +++ b/osu.Game/Screens/SelectV2/ICarouselPanel.cs @@ -6,7 +6,7 @@ using osu.Framework.Graphics; namespace osu.Game.Screens.SelectV2 { /// - /// An interface to be attached to any s which are used for display inside a . + /// An interface to be attached to any s which are used for display inside a . /// public interface ICarouselPanel { @@ -16,7 +16,7 @@ namespace osu.Game.Screens.SelectV2 double YPosition => Item!.CarouselYPosition; /// - /// The carousel item this drawable is representing. This is managed by and should not be set manually. + /// The carousel item this drawable is representing. This is managed by and should not be set manually. /// CarouselItem? Item { get; set; } } From ad04681b2856d9e821a1e4a5f65a2b6b8ced0993 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 10 Jan 2025 20:24:14 +0900 Subject: [PATCH 212/290] Add scroll position maintaining --- .../SongSelect/TestSceneBeatmapCarouselV2.cs | 30 ++++++++++++++++ osu.Game/Screens/SelectV2/Carousel.cs | 36 ++++++++++++++++--- 2 files changed, 61 insertions(+), 5 deletions(-) diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2.cs index 75223adc2b..dde4ef88bd 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2.cs @@ -10,6 +10,7 @@ using osu.Framework.Bindables; using osu.Framework.Extensions.ObjectExtensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Primitives; using osu.Framework.Graphics.Shapes; using osu.Framework.Testing; using osu.Framework.Utils; @@ -34,6 +35,8 @@ namespace osu.Game.Tests.Visual.SongSelect private OsuTextFlowContainer stats = null!; private BeatmapCarouselV2 carousel = null!; + private OsuScrollContainer scroll => carousel.ChildrenOfType().Single(); + private int beatmapCount; public TestSceneBeatmapCarouselV2() @@ -136,6 +139,33 @@ namespace osu.Game.Tests.Visual.SongSelect AddStep("remove all beatmaps", () => beatmapSets.Clear()); } + [Test] + public void TestScrollPositionVelocityMaintained() + { + Quad positionBefore = default; + + AddStep("add 10 beatmaps", () => + { + for (int i = 0; i < 10; i++) + beatmapSets.Add(TestResources.CreateTestBeatmapSetInfo(RNG.Next(1, 4))); + }); + + AddUntilStep("visual item added", () => carousel.ChildrenOfType().Count(), () => Is.GreaterThan(0)); + + AddStep("scroll to last item", () => scroll.ScrollToEnd(false)); + + AddStep("select last beatmap", () => carousel.CurrentSelection = beatmapSets.First()); + + AddUntilStep("wait for scroll finished", () => scroll.Current, () => Is.EqualTo(scroll.Target)); + + AddStep("save selected screen position", () => positionBefore = carousel.ChildrenOfType().FirstOrDefault(p => p.Item!.Selected.Value)!.ScreenSpaceDrawQuad); + + AddStep("remove first beatmap", () => beatmapSets.Remove(beatmapSets.Last())); + AddUntilStep("sorting finished", () => carousel.IsFiltering, () => Is.False); + AddAssert("select screen position unchanged", () => carousel.ChildrenOfType().Single(p => p.Item!.Selected.Value).ScreenSpaceDrawQuad, + () => Is.EqualTo(positionBefore)); + } + [Test] public void TestAddRemoveOneByOne() { diff --git a/osu.Game/Screens/SelectV2/Carousel.cs b/osu.Game/Screens/SelectV2/Carousel.cs index 45dadc3455..54a671949f 100644 --- a/osu.Game/Screens/SelectV2/Carousel.cs +++ b/osu.Game/Screens/SelectV2/Carousel.cs @@ -94,7 +94,13 @@ namespace osu.Game.Screens.SelectV2 get => currentSelection; set { + if (currentSelectionCarouselItem != null) + currentSelectionCarouselItem.Selected.Value = false; + currentSelection = value; + + currentSelectionCarouselItem = null; + currentSelectionYPosition = null; updateSelection(); } } @@ -211,17 +217,37 @@ namespace osu.Game.Screens.SelectV2 #region Selection handling private object? currentSelection; + private CarouselItem? currentSelectionCarouselItem; + private double? currentSelectionYPosition; private void updateSelection() { + currentSelectionCarouselItem = null; + if (displayedCarouselItems == null) return; - // TODO: this is ugly, we probably should stop exposing CarouselItem externally. - foreach (var item in Items) - item.Selected.Value = item.Model == currentSelection; - foreach (var item in displayedCarouselItems) - item.Selected.Value = item.Model == currentSelection; + { + bool isSelected = item.Model == currentSelection; + + if (isSelected) + { + currentSelectionCarouselItem = item; + + if (currentSelectionYPosition != item.CarouselYPosition) + { + if (currentSelectionYPosition != null) + { + float adjustment = (float)(item.CarouselYPosition - currentSelectionYPosition.Value); + scroll.OffsetScrollPosition(adjustment); + } + + currentSelectionYPosition = item.CarouselYPosition; + } + } + + item.Selected.Value = isSelected; + } } #endregion From 6fbab1bbceb4d26838bb35a3c5cf824151320a37 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 10 Jan 2025 20:30:41 +0900 Subject: [PATCH 213/290] Stop exposing `CarouselItem` externally --- osu.Game/Screens/SelectV2/BeatmapCarouselV2.cs | 6 ++++-- osu.Game/Screens/SelectV2/Carousel.cs | 11 +++++++++-- 2 files changed, 13 insertions(+), 4 deletions(-) diff --git a/osu.Game/Screens/SelectV2/BeatmapCarouselV2.cs b/osu.Game/Screens/SelectV2/BeatmapCarouselV2.cs index 37c33446da..dd4aaadfbb 100644 --- a/osu.Game/Screens/SelectV2/BeatmapCarouselV2.cs +++ b/osu.Game/Screens/SelectV2/BeatmapCarouselV2.cs @@ -60,6 +60,8 @@ namespace osu.Game.Screens.SelectV2 return drawable; } + protected override CarouselItem CreateCarouselItemForModel(object model) => new BeatmapCarouselItem(model); + private void beatmapSetsChanged(object? beatmaps, NotifyCollectionChangedEventArgs changed) { // TODO: moving management of BeatmapInfo tracking to BeatmapStore might be something we want to consider. @@ -70,7 +72,7 @@ namespace osu.Game.Screens.SelectV2 switch (changed.Action) { case NotifyCollectionChangedAction.Add: - Items.AddRange(newBeatmapSets!.SelectMany(s => s.Beatmaps).Select(b => new BeatmapCarouselItem(b))); + Items.AddRange(newBeatmapSets!.SelectMany(s => s.Beatmaps)); break; case NotifyCollectionChangedAction.Remove: @@ -78,7 +80,7 @@ namespace osu.Game.Screens.SelectV2 foreach (var set in beatmapSetInfos!) { foreach (var beatmap in set.Beatmaps) - Items.RemoveAll(i => i.Model is BeatmapInfo bi && beatmap.Equals(bi)); + Items.RemoveAll(i => i is BeatmapInfo bi && beatmap.Equals(bi)); } break; diff --git a/osu.Game/Screens/SelectV2/Carousel.cs b/osu.Game/Screens/SelectV2/Carousel.cs index 54a671949f..9fab9d0bf6 100644 --- a/osu.Game/Screens/SelectV2/Carousel.cs +++ b/osu.Game/Screens/SelectV2/Carousel.cs @@ -80,7 +80,7 @@ namespace osu.Game.Screens.SelectV2 /// /// Note that an may add new items which are displayed but not tracked in this list. /// - protected readonly BindableList Items = new BindableList(); + protected readonly BindableList Items = new BindableList(); /// /// The currently selected model. @@ -143,6 +143,13 @@ namespace osu.Game.Screens.SelectV2 /// The manifested drawable. protected abstract Drawable GetDrawableForDisplay(CarouselItem item); + /// + /// Create an internal carousel representation for the provided model object. + /// + /// The model. + /// A representing the model. + protected abstract CarouselItem CreateCarouselItemForModel(object model); + #region Filtering and display preparation private Task filterTask = Task.CompletedTask; @@ -161,7 +168,7 @@ namespace osu.Game.Screens.SelectV2 } Stopwatch stopwatch = Stopwatch.StartNew(); - IEnumerable items = new List(Items); + IEnumerable items = new List(Items.Select(CreateCarouselItemForModel)); await Task.Run(async () => { From cf55fe16abbb08ce8815c14a1a38c01be44235ba Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 10 Jan 2025 20:32:07 +0900 Subject: [PATCH 214/290] Generic type instead of raw `object`? --- osu.Game/Screens/SelectV2/BeatmapCarouselV2.cs | 4 ++-- osu.Game/Screens/SelectV2/Carousel.cs | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/osu.Game/Screens/SelectV2/BeatmapCarouselV2.cs b/osu.Game/Screens/SelectV2/BeatmapCarouselV2.cs index dd4aaadfbb..23954da3a1 100644 --- a/osu.Game/Screens/SelectV2/BeatmapCarouselV2.cs +++ b/osu.Game/Screens/SelectV2/BeatmapCarouselV2.cs @@ -25,7 +25,7 @@ using osuTK.Graphics; namespace osu.Game.Screens.SelectV2 { [Cached] - public partial class BeatmapCarouselV2 : Carousel + public partial class BeatmapCarouselV2 : Carousel { private IBindableList detachedBeatmaps = null!; @@ -60,7 +60,7 @@ namespace osu.Game.Screens.SelectV2 return drawable; } - protected override CarouselItem CreateCarouselItemForModel(object model) => new BeatmapCarouselItem(model); + protected override CarouselItem CreateCarouselItemForModel(BeatmapInfo model) => new BeatmapCarouselItem(model); private void beatmapSetsChanged(object? beatmaps, NotifyCollectionChangedEventArgs changed) { diff --git a/osu.Game/Screens/SelectV2/Carousel.cs b/osu.Game/Screens/SelectV2/Carousel.cs index 9fab9d0bf6..02e87c7704 100644 --- a/osu.Game/Screens/SelectV2/Carousel.cs +++ b/osu.Game/Screens/SelectV2/Carousel.cs @@ -24,7 +24,7 @@ namespace osu.Game.Screens.SelectV2 /// A highly efficient vertical list display that is used primarily for the song select screen, /// but flexible enough to be used for other use cases. /// - public abstract partial class Carousel : CompositeDrawable + public abstract partial class Carousel : CompositeDrawable { /// /// A collection of filters which should be run each time a is executed. @@ -80,7 +80,7 @@ namespace osu.Game.Screens.SelectV2 /// /// Note that an may add new items which are displayed but not tracked in this list. /// - protected readonly BindableList Items = new BindableList(); + protected readonly BindableList Items = new BindableList(); /// /// The currently selected model. @@ -148,7 +148,7 @@ namespace osu.Game.Screens.SelectV2 /// /// The model. /// A representing the model. - protected abstract CarouselItem CreateCarouselItemForModel(object model); + protected abstract CarouselItem CreateCarouselItemForModel(T model); #region Filtering and display preparation From 83a2fe09c5cede3991615135c10e1853c8e22164 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sun, 12 Jan 2025 13:07:20 +0900 Subject: [PATCH 215/290] Update readme with updated mobile release information --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 6043497181..32c43995f4 100644 --- a/README.md +++ b/README.md @@ -37,7 +37,7 @@ You can also generally download a version for your current device from the [osu! If your platform is unsupported or not listed above, there is still a chance you can run the release or manually build it by following the instructions below. -**For iOS/iPadOS users**: The iOS testflight link fills up very fast (Apple has a hard limit of 10,000 users). We reset it occasionally. Please do not ask about this. Check back regularly for link resets or follow [peppy](https://twitter.com/ppy) on twitter for announcements. Our goal is to get the game on mobile app stores in early 2024. +**For iOS/iPadOS users**: The iOS testflight link fills up very fast (Apple has a hard limit of 10,000 users). We reset it occasionally. Please do not ask about this. Check back regularly for link resets or follow [peppy](https://twitter.com/ppy) on twitter for announcements. Our goal is to get the game on mobile app stores very soon to se don't have to live with this limitation. ## Developing a custom ruleset From dfbc93c3dc99653bb221bc07e3647402505bb676 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sun, 12 Jan 2025 19:16:53 +0900 Subject: [PATCH 216/290] Fix typo --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 32c43995f4..d87ca31f72 100644 --- a/README.md +++ b/README.md @@ -37,7 +37,7 @@ You can also generally download a version for your current device from the [osu! If your platform is unsupported or not listed above, there is still a chance you can run the release or manually build it by following the instructions below. -**For iOS/iPadOS users**: The iOS testflight link fills up very fast (Apple has a hard limit of 10,000 users). We reset it occasionally. Please do not ask about this. Check back regularly for link resets or follow [peppy](https://twitter.com/ppy) on twitter for announcements. Our goal is to get the game on mobile app stores very soon to se don't have to live with this limitation. +**For iOS/iPadOS users**: The iOS testflight link fills up very fast (Apple has a hard limit of 10,000 users). We reset it occasionally. Please do not ask about this. Check back regularly for link resets or follow [peppy](https://twitter.com/ppy) on twitter for announcements. Our goal is to get the game on mobile app stores very soon so we don't have to live with this limitation. ## Developing a custom ruleset From 76e09586fd3951b7659d67bf1aefaa5a8cfbecb2 Mon Sep 17 00:00:00 2001 From: Susko3 Date: Sun, 12 Jan 2025 23:33:04 +0000 Subject: [PATCH 217/290] Fix possible nullref in `handleIntent()` Could happen if we get a malformed intent without data --- osu.Android/OsuGameActivity.cs | 28 ++++++++++++++++++++-------- 1 file changed, 20 insertions(+), 8 deletions(-) diff --git a/osu.Android/OsuGameActivity.cs b/osu.Android/OsuGameActivity.cs index bbee491d90..fe11672767 100644 --- a/osu.Android/OsuGameActivity.cs +++ b/osu.Android/OsuGameActivity.cs @@ -13,7 +13,6 @@ using Android.Graphics; using Android.OS; using Android.Views; using osu.Framework.Android; -using osu.Framework.Extensions.ObjectExtensions; using osu.Game.Database; using Debug = System.Diagnostics.Debug; using Uri = Android.Net.Uri; @@ -95,25 +94,38 @@ namespace osu.Android private void handleIntent(Intent? intent) { - switch (intent?.Action) + if (intent == null) + return; + + switch (intent.Action) { case Intent.ActionDefault: if (intent.Scheme == ContentResolver.SchemeContent) - handleImportFromUris(intent.Data.AsNonNull()); + { + if (intent.Data != null) + handleImportFromUris(intent.Data); + } else if (osu_url_schemes.Contains(intent.Scheme)) - game.HandleLink(intent.DataString); + { + if (intent.DataString != null) + game.HandleLink(intent.DataString); + } + break; case Intent.ActionSend: case Intent.ActionSendMultiple: { + if (intent.ClipData == null) + break; + var uris = new List(); - for (int i = 0; i < intent.ClipData?.ItemCount; i++) + for (int i = 0; i < intent.ClipData.ItemCount; i++) { - var content = intent.ClipData?.GetItemAt(i); - if (content != null) - uris.Add(content.Uri.AsNonNull()); + var item = intent.ClipData.GetItemAt(i); + if (item?.Uri != null) + uris.Add(item.Uri); } handleImportFromUris(uris.ToArray()); From b0339a9d63252a56cea9a1ec1da187a530419183 Mon Sep 17 00:00:00 2001 From: Susko3 Date: Mon, 13 Jan 2025 00:47:52 +0000 Subject: [PATCH 218/290] Create game as soon as possible --- osu.Android/OsuGameActivity.cs | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/osu.Android/OsuGameActivity.cs b/osu.Android/OsuGameActivity.cs index fe11672767..42065e61fd 100644 --- a/osu.Android/OsuGameActivity.cs +++ b/osu.Android/OsuGameActivity.cs @@ -49,9 +49,23 @@ namespace osu.Android /// Adjusted on startup to match expected UX for the current device type (phone/tablet). public ScreenOrientation DefaultOrientation = ScreenOrientation.Unspecified; - private OsuGameAndroid game = null!; + private readonly OsuGameAndroid game; - protected override Framework.Game CreateGame() => game = new OsuGameAndroid(this); + private bool gameCreated; + + protected override Framework.Game CreateGame() + { + if (gameCreated) + throw new InvalidOperationException("Framework tried to create a game twice."); + + gameCreated = true; + return game; + } + + public OsuGameActivity() + { + game = new OsuGameAndroid(this); + } protected override void OnCreate(Bundle? savedInstanceState) { From fc069e060c69599285dcba82c657b2568c399674 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 13 Jan 2025 12:38:28 +0100 Subject: [PATCH 219/290] Only show colour on new combo selector button if overridden As proposed in https://discord.com/channels/188630481301012481/188630652340404224/1327309179911929936. --- .../Edit/Components/TernaryButtons/NewComboTernaryButton.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Game/Screens/Edit/Components/TernaryButtons/NewComboTernaryButton.cs b/osu.Game/Screens/Edit/Components/TernaryButtons/NewComboTernaryButton.cs index effe35c0c3..8c64480b43 100644 --- a/osu.Game/Screens/Edit/Components/TernaryButtons/NewComboTernaryButton.cs +++ b/osu.Game/Screens/Edit/Components/TernaryButtons/NewComboTernaryButton.cs @@ -147,19 +147,19 @@ namespace osu.Game.Screens.Edit.Components.TernaryButtons private void updateState() { - if (SelectedHitObject.Value == null) + Enabled.Value = SelectedHitObject.Value != null; + + if (SelectedHitObject.Value == null || SelectedHitObject.Value.ComboOffset == 0) { BackgroundColour = colourProvider.Background3; icon.Colour = BackgroundColour.Darken(0.5f); icon.Blending = BlendingParameters.Additive; - Enabled.Value = false; } else { BackgroundColour = ComboColours[comboIndexFor(SelectedHitObject.Value, ComboColours)]; icon.Colour = OsuColour.ForegroundTextColourFor(BackgroundColour); icon.Blending = BlendingParameters.Inherit; - Enabled.Value = true; } } From 39a69d64548de357b2c408da774783f463d727ee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 13 Jan 2025 13:04:17 +0100 Subject: [PATCH 220/290] Adjust test to pass What I think was happening here is that the dump of the accuracy counter's state was happening too early. The component is loaded synchronously into the `ISerialisableDrawableContainer` before its default position is set via the "apply defaults" `ArgonSkin` flow - so the test needs to wait for that to take place first. --- .../Visual/Navigation/TestSceneSkinEditorNavigation.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game.Tests/Visual/Navigation/TestSceneSkinEditorNavigation.cs b/osu.Game.Tests/Visual/Navigation/TestSceneSkinEditorNavigation.cs index b319c88fc2..622c85774a 100644 --- a/osu.Game.Tests/Visual/Navigation/TestSceneSkinEditorNavigation.cs +++ b/osu.Game.Tests/Visual/Navigation/TestSceneSkinEditorNavigation.cs @@ -120,7 +120,7 @@ namespace osu.Game.Tests.Visual.Navigation string state = string.Empty; - AddUntilStep("wait for accuracy counter", () => Game.ChildrenOfType().Any(counter => counter.IsLoaded)); + AddUntilStep("wait for accuracy counter", () => Game.ChildrenOfType().Any(counter => counter.Position != new Vector2())); AddStep("dump state of accuracy meter", () => state = JsonConvert.SerializeObject(Game.ChildrenOfType().First().CreateSerialisedInfo())); AddStep("add any component", () => Game.ChildrenOfType().First().TriggerClick()); AddStep("undo", () => @@ -157,7 +157,7 @@ namespace osu.Game.Tests.Visual.Navigation string state = string.Empty; - AddUntilStep("wait for accuracy counter", () => Game.ChildrenOfType().Any(counter => counter.IsLoaded)); + AddUntilStep("wait for accuracy counter", () => Game.ChildrenOfType().Any(counter => counter.Position != new Vector2())); AddStep("dump state of accuracy meter", () => state = JsonConvert.SerializeObject(Game.ChildrenOfType().First().CreateSerialisedInfo())); AddStep("add any component", () => Game.ChildrenOfType().First().TriggerClick()); AddStep("undo", () => From 7761a0c18a3080f49e6c7dda9bc467005af625a3 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 14 Jan 2025 16:15:43 +0900 Subject: [PATCH 221/290] Add failing test coverage showing storyboard not being updated when dimmed --- .../Background/TestSceneUserDimBackgrounds.cs | 61 ++++++++++++++++--- 1 file changed, 51 insertions(+), 10 deletions(-) diff --git a/osu.Game.Tests/Visual/Background/TestSceneUserDimBackgrounds.cs b/osu.Game.Tests/Visual/Background/TestSceneUserDimBackgrounds.cs index 693e1e48d4..96954f6984 100644 --- a/osu.Game.Tests/Visual/Background/TestSceneUserDimBackgrounds.cs +++ b/osu.Game.Tests/Visual/Background/TestSceneUserDimBackgrounds.cs @@ -3,6 +3,7 @@ #nullable disable +using System.Linq; using System.Threading; using NUnit.Framework; using osu.Framework.Allocation; @@ -15,6 +16,7 @@ using osu.Framework.Input.Events; using osu.Framework.Input.States; using osu.Framework.Platform; using osu.Framework.Screens; +using osu.Framework.Testing; using osu.Framework.Utils; using osu.Game.Beatmaps; using osu.Game.Configuration; @@ -31,6 +33,7 @@ using osu.Game.Screens.Play; using osu.Game.Screens.Play.PlayerSettings; using osu.Game.Screens.Ranking; using osu.Game.Screens.Select; +using osu.Game.Storyboards.Drawables; using osu.Game.Tests.Resources; using osuTK; using osuTK.Graphics; @@ -45,6 +48,7 @@ namespace osu.Game.Tests.Visual.Background private LoadBlockingTestPlayer player; private BeatmapManager manager; private RulesetStore rulesets; + private UpdateCounter storyboardUpdateCounter; [BackgroundDependencyLoader] private void load(GameHost host, AudioManager audio) @@ -194,6 +198,28 @@ namespace osu.Game.Tests.Visual.Background AddUntilStep("Storyboard is visible", () => player.IsStoryboardVisible); } + [Test] + public void TestStoryboardUpdatesWhenDimmed() + { + performFullSetup(); + createFakeStoryboard(); + + AddStep("Enable fully dimmed storyboard", () => + { + player.StoryboardReplacesBackground.Value = true; + player.StoryboardEnabled.Value = true; + player.DimmableStoryboard.IgnoreUserSettings.Value = false; + songSelect.DimLevel.Value = 1f; + }); + + AddUntilStep("Storyboard is invisible", () => !player.IsStoryboardVisible); + + AddWaitStep("wait some", 20); + + AddUntilStep("Storyboard is always present", () => player.ChildrenOfType().Single().AlwaysPresent, () => Is.True); + AddUntilStep("Dimmable storyboard content is being updated", () => storyboardUpdateCounter.StoryboardContentLastUpdated, () => Is.EqualTo(Time.Current).Within(100)); + } + [Test] public void TestStoryboardIgnoreUserSettings() { @@ -269,15 +295,19 @@ namespace osu.Game.Tests.Visual.Background { player.StoryboardEnabled.Value = false; player.StoryboardReplacesBackground.Value = false; - player.DimmableStoryboard.Add(new OsuSpriteText + player.DimmableStoryboard.AddRange(new Drawable[] { - Size = new Vector2(500, 50), - Alpha = 1, - Colour = Color4.White, - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Text = "THIS IS A STORYBOARD", - Font = new FontUsage(size: 50) + storyboardUpdateCounter = new UpdateCounter(), + new OsuSpriteText + { + Size = new Vector2(500, 50), + Alpha = 1, + Colour = Color4.White, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Text = "THIS IS A STORYBOARD", + Font = new FontUsage(size: 50) + } }); }); @@ -353,7 +383,7 @@ namespace osu.Game.Tests.Visual.Background /// /// Make sure every time a screen gets pushed, the background doesn't get replaced /// - /// Whether or not the original background (The one created in DummySongSelect) is still the current background + /// Whether the original background (The one created in DummySongSelect) is still the current background public bool IsBackgroundCurrent() => background?.IsCurrentScreen() == true; } @@ -384,7 +414,7 @@ namespace osu.Game.Tests.Visual.Background public new DimmableStoryboard DimmableStoryboard => base.DimmableStoryboard; - // Whether or not the player should be allowed to load. + // Whether the player should be allowed to load. public bool BlockLoad; public Bindable StoryboardEnabled; @@ -451,6 +481,17 @@ namespace osu.Game.Tests.Visual.Background } } + private class UpdateCounter : Drawable + { + public double StoryboardContentLastUpdated; + + protected override void Update() + { + base.Update(); + StoryboardContentLastUpdated = Time.Current; + } + } + private partial class TestDimmableBackground : BackgroundScreenBeatmap.DimmableBackground { public Color4 CurrentColour => Content.Colour; From 77db35580900896fa46fca26b45780c21727e3af Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 14 Jan 2025 15:55:29 +0900 Subject: [PATCH 222/290] Ensure storyboards are still updated even when dim is 100% This avoids piled-up overhead when entering break time. It's not great, but it is what we need for now to avoid weirdness. --- osu.Game/Screens/Play/DimmableStoryboard.cs | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/osu.Game/Screens/Play/DimmableStoryboard.cs b/osu.Game/Screens/Play/DimmableStoryboard.cs index 84d99ea863..a096400fe0 100644 --- a/osu.Game/Screens/Play/DimmableStoryboard.cs +++ b/osu.Game/Screens/Play/DimmableStoryboard.cs @@ -69,7 +69,22 @@ namespace osu.Game.Screens.Play protected override void LoadComplete() { - ShowStoryboard.BindValueChanged(_ => initializeStoryboard(true), true); + ShowStoryboard.BindValueChanged(show => + { + initializeStoryboard(true); + + if (drawableStoryboard != null) + { + // Regardless of user dim setting, for the time being we need to ensure storyboards are still updated in the background (even if not displayed). + // If we don't do this, an intensive storyboard will have a lot of catch-up work to do at the start of a break, causing a huge stutter. + // + // This can be reconsidered when https://github.com/ppy/osu-framework/issues/6491 is resolved. + bool alwaysPresent = show.NewValue; + + Content.AlwaysPresent = alwaysPresent; + drawableStoryboard.AlwaysPresent = alwaysPresent; + } + }, true); base.LoadComplete(); } From 2c57cd59a5cbbb4c9d95a70e25a7d64d0bd3d9cd Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 14 Jan 2025 16:26:56 +0900 Subject: [PATCH 223/290] Update framework --- osu.Android.props | 2 +- osu.Game/osu.Game.csproj | 2 +- osu.iOS.props | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Android.props b/osu.Android.props index 84827ce76b..dbb0a6d610 100644 --- a/osu.Android.props +++ b/osu.Android.props @@ -10,7 +10,7 @@ true - + diff --git a/osu.iOS.props b/osu.iOS.props index 349d6fa1d7..afbcf49d32 100644 --- a/osu.iOS.props +++ b/osu.iOS.props @@ -17,6 +17,6 @@ -all - + From 904a08af26b2c0ba9992365de56c6bb2f2a12a68 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 14 Jan 2025 16:29:56 +0900 Subject: [PATCH 224/290] Update textbox usage in line with framework changes --- osu.Game/Graphics/UserInterface/OsuNumberBox.cs | 6 ++++-- osu.Game/Graphics/UserInterface/OsuPasswordTextBox.cs | 6 +++--- osu.Game/Overlays/Settings/SettingsNumberBox.cs | 6 +++++- osu.Game/Screens/Edit/Setup/MetadataSection.cs | 6 +++++- 4 files changed, 17 insertions(+), 7 deletions(-) diff --git a/osu.Game/Graphics/UserInterface/OsuNumberBox.cs b/osu.Game/Graphics/UserInterface/OsuNumberBox.cs index db4b7b2ab3..1742cb6bdd 100644 --- a/osu.Game/Graphics/UserInterface/OsuNumberBox.cs +++ b/osu.Game/Graphics/UserInterface/OsuNumberBox.cs @@ -1,14 +1,16 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using osu.Framework.Input; + namespace osu.Game.Graphics.UserInterface { public partial class OsuNumberBox : OsuTextBox { - protected override bool AllowIme => false; - public OsuNumberBox() { + InputProperties = new TextInputProperties(TextInputType.Number, false); + SelectAllOnFocus = true; } diff --git a/osu.Game/Graphics/UserInterface/OsuPasswordTextBox.cs b/osu.Game/Graphics/UserInterface/OsuPasswordTextBox.cs index 0be7b4dc48..e2e273cfe1 100644 --- a/osu.Game/Graphics/UserInterface/OsuPasswordTextBox.cs +++ b/osu.Game/Graphics/UserInterface/OsuPasswordTextBox.cs @@ -18,7 +18,7 @@ using osu.Game.Localisation; namespace osu.Game.Graphics.UserInterface { - public partial class OsuPasswordTextBox : OsuTextBox, ISuppressKeyEventLogging + public partial class OsuPasswordTextBox : OsuTextBox { protected override Drawable GetDrawableCharacter(char c) => new FallingDownContainer { @@ -32,8 +32,6 @@ namespace osu.Game.Graphics.UserInterface protected override bool AllowWordNavigation => false; - protected override bool AllowIme => false; - private readonly CapsWarning warning; [Resolved] @@ -41,6 +39,8 @@ namespace osu.Game.Graphics.UserInterface public OsuPasswordTextBox() { + InputProperties = new TextInputProperties(TextInputType.Password, false); + Add(warning = new CapsWarning { Size = new Vector2(20), diff --git a/osu.Game/Overlays/Settings/SettingsNumberBox.cs b/osu.Game/Overlays/Settings/SettingsNumberBox.cs index fbcdb4a968..2548f3c87b 100644 --- a/osu.Game/Overlays/Settings/SettingsNumberBox.cs +++ b/osu.Game/Overlays/Settings/SettingsNumberBox.cs @@ -5,6 +5,7 @@ using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.UserInterface; +using osu.Framework.Input; namespace osu.Game.Overlays.Settings { @@ -66,7 +67,10 @@ namespace osu.Game.Overlays.Settings private partial class OutlinedNumberBox : OutlinedTextBox { - protected override bool AllowIme => false; + public OutlinedNumberBox() + { + InputProperties = new TextInputProperties(TextInputType.Number, false); + } protected override bool CanAddCharacter(char character) => char.IsAsciiDigit(character); diff --git a/osu.Game/Screens/Edit/Setup/MetadataSection.cs b/osu.Game/Screens/Edit/Setup/MetadataSection.cs index 7b74aa7642..85247bc15a 100644 --- a/osu.Game/Screens/Edit/Setup/MetadataSection.cs +++ b/osu.Game/Screens/Edit/Setup/MetadataSection.cs @@ -4,6 +4,7 @@ using System.Linq; using osu.Framework.Allocation; using osu.Framework.Graphics.UserInterface; +using osu.Framework.Input; using osu.Framework.Localisation; using osu.Game.Beatmaps; using osu.Game.Graphics.UserInterfaceV2; @@ -136,7 +137,10 @@ namespace osu.Game.Screens.Edit.Setup private partial class RomanisedTextBox : InnerTextBox { - protected override bool AllowIme => false; + public RomanisedTextBox() + { + InputProperties = new TextInputProperties(TextInputType.Text, false); + } protected override bool CanAddCharacter(char character) => MetadataUtils.IsRomanised(character); From 8ffd2547196d89123cb51566418f2aaa012f9793 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 14 Jan 2025 08:54:40 +0100 Subject: [PATCH 225/290] Adjust initialisation code to start with combo colour picker hidden --- .../Edit/Components/TernaryButtons/NewComboTernaryButton.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/Edit/Components/TernaryButtons/NewComboTernaryButton.cs b/osu.Game/Screens/Edit/Components/TernaryButtons/NewComboTernaryButton.cs index 8c64480b43..1f95d5f239 100644 --- a/osu.Game/Screens/Edit/Components/TernaryButtons/NewComboTernaryButton.cs +++ b/osu.Game/Screens/Edit/Components/TernaryButtons/NewComboTernaryButton.cs @@ -54,7 +54,6 @@ namespace osu.Game.Screens.Edit.Components.TernaryButtons { RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y, - Padding = new MarginPadding { Right = 30 }, Child = new DrawableTernaryButton { Current = Current, @@ -66,6 +65,7 @@ namespace osu.Game.Screens.Edit.Components.TernaryButtons { Anchor = Anchor.CentreRight, Origin = Anchor.CentreRight, + Alpha = 0, Width = 25, ComboColours = { BindTarget = comboColours } } From 058ff8af7769cbc50438d0d6078b51c5902564fc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 14 Jan 2025 09:22:56 +0100 Subject: [PATCH 226/290] Make test class partial --- osu.Game.Tests/Visual/Background/TestSceneUserDimBackgrounds.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Tests/Visual/Background/TestSceneUserDimBackgrounds.cs b/osu.Game.Tests/Visual/Background/TestSceneUserDimBackgrounds.cs index 96954f6984..eeaa68e2ee 100644 --- a/osu.Game.Tests/Visual/Background/TestSceneUserDimBackgrounds.cs +++ b/osu.Game.Tests/Visual/Background/TestSceneUserDimBackgrounds.cs @@ -481,7 +481,7 @@ namespace osu.Game.Tests.Visual.Background } } - private class UpdateCounter : Drawable + private partial class UpdateCounter : Drawable { public double StoryboardContentLastUpdated; From f6073d4ac09c499d7b828d01a7d04671fc252563 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 14 Jan 2025 17:43:29 +0900 Subject: [PATCH 227/290] Ensure API starts up with `LocalUser` in correct state I noticed in passing that in a very edge case scenario where the API's `run` thread doesn't run before it is loaded into the game, something could access it and get a guest `LocalUser` when the local user actually has a valid login. Put another way, the `protected HasLogin` could be `true` while `LocalUser` is `Guest`. I think we want to avoid this, so I've moved the initial set of the local user earlier in the initialisation process. If this is controversial in any way, the PR can be closed and we can assume no one is ever going to run into this scenario (or that it doesn't matter enough even if they did). --- osu.Game/Online/API/APIAccess.cs | 43 ++++++++++++++++++++------------ 1 file changed, 27 insertions(+), 16 deletions(-) diff --git a/osu.Game/Online/API/APIAccess.cs b/osu.Game/Online/API/APIAccess.cs index ec48fa2436..e0927dbc4e 100644 --- a/osu.Game/Online/API/APIAccess.cs +++ b/osu.Game/Online/API/APIAccess.cs @@ -13,6 +13,7 @@ using System.Threading; using System.Threading.Tasks; using Newtonsoft.Json.Linq; using osu.Framework.Bindables; +using osu.Framework.Development; using osu.Framework.Extensions.ExceptionExtensions; using osu.Framework.Extensions.ObjectExtensions; using osu.Framework.Graphics; @@ -110,6 +111,9 @@ namespace osu.Game.Online.API config.BindWith(OsuSetting.UserOnlineStatus, configStatus); + // Early call to ensure the local user / "logged in" state is correct immediately. + setPlaceholderLocalUser(); + localUser.BindValueChanged(u => { u.OldValue?.Activity.UnbindFrom(activity); @@ -193,7 +197,7 @@ namespace osu.Game.Online.API Debug.Assert(HasLogin); - // Ensure that we are in an online state. If not, attempt a connect. + // Ensure that we are in an online state. If not, attempt to connect. if (state.Value != APIState.Online) { attemptConnect(); @@ -247,17 +251,7 @@ namespace osu.Game.Online.API /// Whether the connection attempt was successful. private void attemptConnect() { - if (localUser.IsDefault) - { - // Show a placeholder user if saved credentials are available. - // This is useful for storing local scores and showing a placeholder username after starting the game, - // until a valid connection has been established. - setLocalUser(new APIUser - { - Username = ProvidedUsername, - Status = { Value = configStatus.Value ?? UserStatus.Online } - }); - } + Scheduler.Add(setPlaceholderLocalUser, false); // save the username at this point, if the user requested for it to be. config.SetValue(OsuSetting.Username, config.Get(OsuSetting.SaveUsername) ? ProvidedUsername : string.Empty); @@ -339,9 +333,11 @@ namespace osu.Game.Online.API userReq.Success += me => { + Debug.Assert(ThreadSafety.IsUpdateThread); + me.Status.Value = configStatus.Value ?? UserStatus.Online; - setLocalUser(me); + localUser.Value = me; state.Value = me.SessionVerified ? APIState.Online : APIState.RequiresSecondFactorAuth; failureCount = 0; @@ -366,6 +362,23 @@ namespace osu.Game.Online.API Thread.Sleep(500); } + /// + /// Show a placeholder user if saved credentials are available. + /// This is useful for storing local scores and showing a placeholder username after starting the game, + /// until a valid connection has been established. + /// + private void setPlaceholderLocalUser() + { + if (!localUser.IsDefault) + return; + + localUser.Value = new APIUser + { + Username = ProvidedUsername, + Status = { Value = configStatus.Value ?? UserStatus.Online } + }; + } + public void Perform(APIRequest request) { try @@ -593,7 +606,7 @@ namespace osu.Game.Online.API // Scheduled prior to state change such that the state changed event is invoked with the correct user and their friends present Schedule(() => { - setLocalUser(createGuestUser()); + localUser.Value = createGuestUser(); friends.Clear(); }); @@ -619,8 +632,6 @@ namespace osu.Game.Online.API private static APIUser createGuestUser() => new GuestUser(); - private void setLocalUser(APIUser user) => Scheduler.Add(() => localUser.Value = user, false); - protected override void Dispose(bool isDisposing) { base.Dispose(isDisposing); From 51c7c218bfc83c8b45c7b1853485877c6a7504dd Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Tue, 14 Jan 2025 17:51:04 +0900 Subject: [PATCH 228/290] Simplify operations on local list --- osu.Game/Online/API/APIAccess.cs | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/osu.Game/Online/API/APIAccess.cs b/osu.Game/Online/API/APIAccess.cs index 46476ab7f0..9d0ef06ebf 100644 --- a/osu.Game/Online/API/APIAccess.cs +++ b/osu.Game/Online/API/APIAccess.cs @@ -612,14 +612,14 @@ namespace osu.Game.Online.API friendsReq.Failure += _ => state.Value = APIState.Failing; friendsReq.Success += res => { - // Add new friends into local list. - HashSet friendsSet = friends.Select(f => f.TargetID).ToHashSet(); - friends.AddRange(res.Where(f => !friendsSet.Contains(f.TargetID))); + var existingFriends = friends.Select(f => f.TargetID).ToHashSet(); + var updatedFriends = res.Select(f => f.TargetID).ToHashSet(); - // Remove non-friends from local lists. - friendsSet.Clear(); - friendsSet.AddRange(res.Select(f => f.TargetID)); - friends.RemoveAll(f => !friendsSet.Contains(f.TargetID)); + // Add new friends into local list. + friends.AddRange(res.Where(r => !existingFriends.Contains(r.TargetID))); + + // Remove non-friends from local list. + friends.RemoveAll(f => !updatedFriends.Contains(f.TargetID)); }; Queue(friendsReq); From 156207d3472541422fe3b57fec0f05435b684e7f Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Tue, 14 Jan 2025 17:54:40 +0900 Subject: [PATCH 229/290] Remove unused using --- osu.Game/Online/API/APIAccess.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/osu.Game/Online/API/APIAccess.cs b/osu.Game/Online/API/APIAccess.cs index 9d0ef06ebf..d44ca90fa1 100644 --- a/osu.Game/Online/API/APIAccess.cs +++ b/osu.Game/Online/API/APIAccess.cs @@ -19,7 +19,6 @@ using osu.Framework.Extensions.ObjectExtensions; using osu.Framework.Graphics; using osu.Framework.Logging; using osu.Game.Configuration; -using osu.Game.Extensions; using osu.Game.Localisation; using osu.Game.Online.API.Requests; using osu.Game.Online.API.Requests.Responses; From 55ae0403d8ee2f4b37f78a4f9fcf185443d50832 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 14 Jan 2025 18:18:53 +0900 Subject: [PATCH 230/290] Ensure API state is `Connecting` immediately on startup when credentials are present Currently, there's a period where the API is `Offline` even though it is about to connect (as soon as the `run` thread starts up). This can cause any `Queue`d requests to fail if they arrive too early. To avoid this, let's ensure the `Connecting` state is set as early as possible. --- osu.Game/Online/API/APIAccess.cs | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/osu.Game/Online/API/APIAccess.cs b/osu.Game/Online/API/APIAccess.cs index e0927dbc4e..49ba99daa9 100644 --- a/osu.Game/Online/API/APIAccess.cs +++ b/osu.Game/Online/API/APIAccess.cs @@ -111,8 +111,14 @@ namespace osu.Game.Online.API config.BindWith(OsuSetting.UserOnlineStatus, configStatus); - // Early call to ensure the local user / "logged in" state is correct immediately. - setPlaceholderLocalUser(); + if (HasLogin) + { + // Early call to ensure the local user / "logged in" state is correct immediately. + prepareForConnect(); + + // This is required so that Queue() requests during startup sequence don't fail due to "not logged in". + state.Value = APIState.Connecting; + } localUser.BindValueChanged(u => { @@ -251,7 +257,7 @@ namespace osu.Game.Online.API /// Whether the connection attempt was successful. private void attemptConnect() { - Scheduler.Add(setPlaceholderLocalUser, false); + Scheduler.Add(prepareForConnect, false); // save the username at this point, if the user requested for it to be. config.SetValue(OsuSetting.Username, config.Get(OsuSetting.SaveUsername) ? ProvidedUsername : string.Empty); @@ -367,7 +373,7 @@ namespace osu.Game.Online.API /// This is useful for storing local scores and showing a placeholder username after starting the game, /// until a valid connection has been established. /// - private void setPlaceholderLocalUser() + private void prepareForConnect() { if (!localUser.IsDefault) return; From 3ddff1933738c17911514306734c2f266b618a28 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 14 Jan 2025 19:03:58 +0900 Subject: [PATCH 231/290] Fix potential nullref due to silly null handling and too much OOP --- osu.Game/Storyboards/Drawables/DrawableStoryboard.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Storyboards/Drawables/DrawableStoryboard.cs b/osu.Game/Storyboards/Drawables/DrawableStoryboard.cs index 095bd95314..5ef6b30a82 100644 --- a/osu.Game/Storyboards/Drawables/DrawableStoryboard.cs +++ b/osu.Game/Storyboards/Drawables/DrawableStoryboard.cs @@ -35,7 +35,7 @@ namespace osu.Game.Storyboards.Drawables protected override Container Content { get; } - protected override Vector2 DrawScale => new Vector2(Parent!.DrawHeight / 480); + protected override Vector2 DrawScale => new Vector2((Parent?.DrawHeight ?? 0) / 480); public override bool RemoveCompletedTransforms => false; From d97a3270a50154817c20d1f9f2b1e92016b868df Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 14 Jan 2025 19:18:02 +0900 Subject: [PATCH 232/290] Split out `BeatmapCarousel` classes and drop `V2` suffix --- .../SongSelect/TestSceneBeatmapCarouselV2.cs | 4 +- osu.Game/Screens/SelectV2/BeatmapCarousel.cs | 99 +++++++ .../SelectV2/BeatmapCarouselFilterGrouping.cs | 40 +++ .../SelectV2/BeatmapCarouselFilterSorting.cs | 28 ++ .../Screens/SelectV2/BeatmapCarouselItem.cs | 36 +++ .../Screens/SelectV2/BeatmapCarouselPanel.cs | 96 +++++++ .../Screens/SelectV2/BeatmapCarouselV2.cs | 257 ------------------ 7 files changed, 301 insertions(+), 259 deletions(-) create mode 100644 osu.Game/Screens/SelectV2/BeatmapCarousel.cs create mode 100644 osu.Game/Screens/SelectV2/BeatmapCarouselFilterGrouping.cs create mode 100644 osu.Game/Screens/SelectV2/BeatmapCarouselFilterSorting.cs create mode 100644 osu.Game/Screens/SelectV2/BeatmapCarouselItem.cs create mode 100644 osu.Game/Screens/SelectV2/BeatmapCarouselPanel.cs delete mode 100644 osu.Game/Screens/SelectV2/BeatmapCarouselV2.cs diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2.cs index dde4ef88bd..6d54e13b6f 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2.cs @@ -33,7 +33,7 @@ namespace osu.Game.Tests.Visual.SongSelect private BeatmapStore store; private OsuTextFlowContainer stats = null!; - private BeatmapCarouselV2 carousel = null!; + private BeatmapCarousel carousel = null!; private OsuScrollContainer scroll => carousel.ChildrenOfType().Single(); @@ -92,7 +92,7 @@ namespace osu.Game.Tests.Visual.SongSelect }, new Drawable[] { - carousel = new BeatmapCarouselV2 + carousel = new BeatmapCarousel { Anchor = Anchor.Centre, Origin = Anchor.Centre, diff --git a/osu.Game/Screens/SelectV2/BeatmapCarousel.cs b/osu.Game/Screens/SelectV2/BeatmapCarousel.cs new file mode 100644 index 0000000000..3c431a6003 --- /dev/null +++ b/osu.Game/Screens/SelectV2/BeatmapCarousel.cs @@ -0,0 +1,99 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using System.Collections.Generic; +using System.Collections.Specialized; +using System.Linq; +using System.Threading; +using osu.Framework.Allocation; +using osu.Framework.Bindables; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Pooling; +using osu.Game.Beatmaps; +using osu.Game.Database; +using osu.Game.Screens.Select; +using osuTK.Graphics; + +namespace osu.Game.Screens.SelectV2 +{ + [Cached] + public partial class BeatmapCarousel : Carousel + { + private IBindableList detachedBeatmaps = null!; + + private readonly DrawablePool carouselPanelPool = new DrawablePool(100); + + public BeatmapCarousel() + { + DebounceDelay = 100; + DistanceOffscreenToPreload = 100; + + Filters = new ICarouselFilter[] + { + new BeatmapCarouselFilterSorting(), + new BeatmapCarouselFilterGrouping(), + }; + + AddInternal(carouselPanelPool); + } + + [BackgroundDependencyLoader] + private void load(BeatmapStore beatmapStore, CancellationToken? cancellationToken) + { + detachedBeatmaps = beatmapStore.GetBeatmapSets(cancellationToken); + detachedBeatmaps.BindCollectionChanged(beatmapSetsChanged, true); + } + + protected override Drawable GetDrawableForDisplay(CarouselItem item) + { + var drawable = carouselPanelPool.Get(); + drawable.FlashColour(Color4.Red, 2000); + + return drawable; + } + + protected override CarouselItem CreateCarouselItemForModel(BeatmapInfo model) => new BeatmapCarouselItem(model); + + private void beatmapSetsChanged(object? beatmaps, NotifyCollectionChangedEventArgs changed) + { + // TODO: moving management of BeatmapInfo tracking to BeatmapStore might be something we want to consider. + // right now we are managing this locally which is a bit of added overhead. + IEnumerable? newBeatmapSets = changed.NewItems?.Cast(); + IEnumerable? beatmapSetInfos = changed.OldItems?.Cast(); + + switch (changed.Action) + { + case NotifyCollectionChangedAction.Add: + Items.AddRange(newBeatmapSets!.SelectMany(s => s.Beatmaps)); + break; + + case NotifyCollectionChangedAction.Remove: + + foreach (var set in beatmapSetInfos!) + { + foreach (var beatmap in set.Beatmaps) + Items.RemoveAll(i => i is BeatmapInfo bi && beatmap.Equals(bi)); + } + + break; + + case NotifyCollectionChangedAction.Move: + case NotifyCollectionChangedAction.Replace: + throw new NotImplementedException(); + + case NotifyCollectionChangedAction.Reset: + Items.Clear(); + break; + } + } + + public FilterCriteria Criteria { get; private set; } = new FilterCriteria(); + + public void Filter(FilterCriteria criteria) + { + Criteria = criteria; + QueueFilter(); + } + } +} diff --git a/osu.Game/Screens/SelectV2/BeatmapCarouselFilterGrouping.cs b/osu.Game/Screens/SelectV2/BeatmapCarouselFilterGrouping.cs new file mode 100644 index 0000000000..ee4b9ddb69 --- /dev/null +++ b/osu.Game/Screens/SelectV2/BeatmapCarouselFilterGrouping.cs @@ -0,0 +1,40 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using osu.Game.Beatmaps; + +namespace osu.Game.Screens.SelectV2 +{ + public class BeatmapCarouselFilterGrouping : ICarouselFilter + { + public async Task> Run(IEnumerable items, CancellationToken cancellationToken) => await Task.Run(() => + { + // TODO: perform grouping based on FilterCriteria + + CarouselItem? lastItem = null; + + var newItems = new List(items.Count()); + + foreach (var item in items) + { + cancellationToken.ThrowIfCancellationRequested(); + + if (item.Model is BeatmapInfo b1) + { + // Add set header + if (lastItem == null || (lastItem.Model is BeatmapInfo b2 && b2.BeatmapSet!.OnlineID != b1.BeatmapSet!.OnlineID)) + newItems.Add(new BeatmapCarouselItem(b1.BeatmapSet!)); + } + + newItems.Add(item); + lastItem = item; + } + + return newItems; + }, cancellationToken).ConfigureAwait(false); + } +} diff --git a/osu.Game/Screens/SelectV2/BeatmapCarouselFilterSorting.cs b/osu.Game/Screens/SelectV2/BeatmapCarouselFilterSorting.cs new file mode 100644 index 0000000000..a2fd774cf0 --- /dev/null +++ b/osu.Game/Screens/SelectV2/BeatmapCarouselFilterSorting.cs @@ -0,0 +1,28 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using osu.Game.Beatmaps; + +namespace osu.Game.Screens.SelectV2 +{ + public class BeatmapCarouselFilterSorting : ICarouselFilter + { + public async Task> Run(IEnumerable items, CancellationToken cancellationToken) => await Task.Run(() => + { + return items.OrderDescending(Comparer.Create((a, b) => + { + if (a.Model is BeatmapInfo ab && b.Model is BeatmapInfo bb) + return ab.OnlineID.CompareTo(bb.OnlineID); + + if (a is BeatmapCarouselItem aItem && b is BeatmapCarouselItem bItem) + return aItem.ID.CompareTo(bItem.ID); + + return 0; + })); + }, cancellationToken).ConfigureAwait(false); + } +} diff --git a/osu.Game/Screens/SelectV2/BeatmapCarouselItem.cs b/osu.Game/Screens/SelectV2/BeatmapCarouselItem.cs new file mode 100644 index 0000000000..adb5a19875 --- /dev/null +++ b/osu.Game/Screens/SelectV2/BeatmapCarouselItem.cs @@ -0,0 +1,36 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using osu.Game.Beatmaps; +using osu.Game.Database; + +namespace osu.Game.Screens.SelectV2 +{ + public class BeatmapCarouselItem : CarouselItem + { + public readonly Guid ID; + + public override float DrawHeight => Model is BeatmapInfo ? 40 : 80; + + public BeatmapCarouselItem(object model) + : base(model) + { + ID = (Model as IHasGuidPrimaryKey)?.ID ?? Guid.NewGuid(); + } + + public override string? ToString() + { + switch (Model) + { + case BeatmapInfo bi: + return $"Difficulty: {bi.DifficultyName} ({bi.StarRating:N1}*)"; + + case BeatmapSetInfo si: + return $"{si.Metadata}"; + } + + return Model.ToString(); + } + } +} diff --git a/osu.Game/Screens/SelectV2/BeatmapCarouselPanel.cs b/osu.Game/Screens/SelectV2/BeatmapCarouselPanel.cs new file mode 100644 index 0000000000..a64d16a984 --- /dev/null +++ b/osu.Game/Screens/SelectV2/BeatmapCarouselPanel.cs @@ -0,0 +1,96 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Diagnostics; +using osu.Framework.Allocation; +using osu.Framework.Bindables; +using osu.Framework.Extensions.Color4Extensions; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Pooling; +using osu.Framework.Graphics.Shapes; +using osu.Framework.Input.Events; +using osu.Game.Beatmaps; +using osu.Game.Graphics.Sprites; +using osuTK; +using osuTK.Graphics; + +namespace osu.Game.Screens.SelectV2 +{ + public partial class BeatmapCarouselPanel : PoolableDrawable, ICarouselPanel + { + [Resolved] + private BeatmapCarousel carousel { get; set; } = null!; + + public CarouselItem? Item + { + get => item; + set + { + item = value; + + selected.UnbindBindings(); + + if (item != null) + selected.BindTo(item.Selected); + } + } + + private readonly BindableBool selected = new BindableBool(); + private CarouselItem? item; + + [BackgroundDependencyLoader] + private void load() + { + selected.BindValueChanged(value => + { + if (value.NewValue) + { + BorderThickness = 5; + BorderColour = Color4.Pink; + } + else + { + BorderThickness = 0; + } + }); + } + + protected override void FreeAfterUse() + { + base.FreeAfterUse(); + Item = null; + } + + protected override void PrepareForUse() + { + base.PrepareForUse(); + + Debug.Assert(Item != null); + + Size = new Vector2(500, Item.DrawHeight); + Masking = true; + + InternalChildren = new Drawable[] + { + new Box + { + Colour = (Item.Model is BeatmapInfo ? Color4.Aqua : Color4.Yellow).Darken(5), + RelativeSizeAxes = Axes.Both, + }, + new OsuSpriteText + { + Text = Item.ToString() ?? string.Empty, + Padding = new MarginPadding(5), + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + } + }; + } + + protected override bool OnClick(ClickEvent e) + { + carousel.CurrentSelection = Item!.Model; + return true; + } + } +} diff --git a/osu.Game/Screens/SelectV2/BeatmapCarouselV2.cs b/osu.Game/Screens/SelectV2/BeatmapCarouselV2.cs deleted file mode 100644 index 23954da3a1..0000000000 --- a/osu.Game/Screens/SelectV2/BeatmapCarouselV2.cs +++ /dev/null @@ -1,257 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -using System; -using System.Collections.Generic; -using System.Collections.Specialized; -using System.Diagnostics; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using osu.Framework.Allocation; -using osu.Framework.Bindables; -using osu.Framework.Extensions.Color4Extensions; -using osu.Framework.Graphics; -using osu.Framework.Graphics.Pooling; -using osu.Framework.Graphics.Shapes; -using osu.Framework.Input.Events; -using osu.Game.Beatmaps; -using osu.Game.Database; -using osu.Game.Graphics.Sprites; -using osu.Game.Screens.Select; -using osuTK; -using osuTK.Graphics; - -namespace osu.Game.Screens.SelectV2 -{ - [Cached] - public partial class BeatmapCarouselV2 : Carousel - { - private IBindableList detachedBeatmaps = null!; - - private readonly DrawablePool carouselPanelPool = new DrawablePool(100); - - public BeatmapCarouselV2() - { - DebounceDelay = 100; - DistanceOffscreenToPreload = 100; - - Filters = new ICarouselFilter[] - { - new Sorter(), - new Grouper(), - }; - - AddInternal(carouselPanelPool); - } - - [BackgroundDependencyLoader] - private void load(BeatmapStore beatmapStore, CancellationToken? cancellationToken) - { - detachedBeatmaps = beatmapStore.GetBeatmapSets(cancellationToken); - detachedBeatmaps.BindCollectionChanged(beatmapSetsChanged, true); - } - - protected override Drawable GetDrawableForDisplay(CarouselItem item) - { - var drawable = carouselPanelPool.Get(); - drawable.FlashColour(Color4.Red, 2000); - - return drawable; - } - - protected override CarouselItem CreateCarouselItemForModel(BeatmapInfo model) => new BeatmapCarouselItem(model); - - private void beatmapSetsChanged(object? beatmaps, NotifyCollectionChangedEventArgs changed) - { - // TODO: moving management of BeatmapInfo tracking to BeatmapStore might be something we want to consider. - // right now we are managing this locally which is a bit of added overhead. - IEnumerable? newBeatmapSets = changed.NewItems?.Cast(); - IEnumerable? beatmapSetInfos = changed.OldItems?.Cast(); - - switch (changed.Action) - { - case NotifyCollectionChangedAction.Add: - Items.AddRange(newBeatmapSets!.SelectMany(s => s.Beatmaps)); - break; - - case NotifyCollectionChangedAction.Remove: - - foreach (var set in beatmapSetInfos!) - { - foreach (var beatmap in set.Beatmaps) - Items.RemoveAll(i => i is BeatmapInfo bi && beatmap.Equals(bi)); - } - - break; - - case NotifyCollectionChangedAction.Move: - case NotifyCollectionChangedAction.Replace: - throw new NotImplementedException(); - - case NotifyCollectionChangedAction.Reset: - Items.Clear(); - break; - } - } - - public FilterCriteria Criteria { get; private set; } = new FilterCriteria(); - - public void Filter(FilterCriteria criteria) - { - Criteria = criteria; - QueueFilter(); - } - } - - public partial class BeatmapCarouselPanel : PoolableDrawable, ICarouselPanel - { - [Resolved] - private BeatmapCarouselV2 carousel { get; set; } = null!; - - public CarouselItem? Item - { - get => item; - set - { - item = value; - - selected.UnbindBindings(); - - if (item != null) - selected.BindTo(item.Selected); - } - } - - private readonly BindableBool selected = new BindableBool(); - private CarouselItem? item; - - [BackgroundDependencyLoader] - private void load() - { - selected.BindValueChanged(value => - { - if (value.NewValue) - { - BorderThickness = 5; - BorderColour = Color4.Pink; - } - else - { - BorderThickness = 0; - } - }); - } - - protected override void FreeAfterUse() - { - base.FreeAfterUse(); - Item = null; - } - - protected override void PrepareForUse() - { - base.PrepareForUse(); - - Debug.Assert(Item != null); - - Size = new Vector2(500, Item.DrawHeight); - Masking = true; - - InternalChildren = new Drawable[] - { - new Box - { - Colour = (Item.Model is BeatmapInfo ? Color4.Aqua : Color4.Yellow).Darken(5), - RelativeSizeAxes = Axes.Both, - }, - new OsuSpriteText - { - Text = Item.ToString() ?? string.Empty, - Padding = new MarginPadding(5), - Anchor = Anchor.CentreLeft, - Origin = Anchor.CentreLeft, - } - }; - } - - protected override bool OnClick(ClickEvent e) - { - carousel.CurrentSelection = Item!.Model; - return true; - } - } - - public class BeatmapCarouselItem : CarouselItem - { - public readonly Guid ID; - - public override float DrawHeight => Model is BeatmapInfo ? 40 : 80; - - public BeatmapCarouselItem(object model) - : base(model) - { - ID = (Model as IHasGuidPrimaryKey)?.ID ?? Guid.NewGuid(); - } - - public override string? ToString() - { - switch (Model) - { - case BeatmapInfo bi: - return $"Difficulty: {bi.DifficultyName} ({bi.StarRating:N1}*)"; - - case BeatmapSetInfo si: - return $"{si.Metadata}"; - } - - return Model.ToString(); - } - } - - public class Grouper : ICarouselFilter - { - public async Task> Run(IEnumerable items, CancellationToken cancellationToken) => await Task.Run(() => - { - // TODO: perform grouping based on FilterCriteria - - CarouselItem? lastItem = null; - - var newItems = new List(items.Count()); - - foreach (var item in items) - { - cancellationToken.ThrowIfCancellationRequested(); - - if (item.Model is BeatmapInfo b1) - { - // Add set header - if (lastItem == null || (lastItem.Model is BeatmapInfo b2 && b2.BeatmapSet!.OnlineID != b1.BeatmapSet!.OnlineID)) - newItems.Add(new BeatmapCarouselItem(b1.BeatmapSet!)); - } - - newItems.Add(item); - lastItem = item; - } - - return newItems; - }, cancellationToken).ConfigureAwait(false); - } - - public class Sorter : ICarouselFilter - { - public async Task> Run(IEnumerable items, CancellationToken cancellationToken) => await Task.Run(() => - { - return items.OrderDescending(Comparer.Create((a, b) => - { - if (a.Model is BeatmapInfo ab && b.Model is BeatmapInfo bb) - return ab.OnlineID.CompareTo(bb.OnlineID); - - if (a is BeatmapCarouselItem aItem && b is BeatmapCarouselItem bItem) - return aItem.ID.CompareTo(bItem.ID); - - return 0; - })); - }, cancellationToken).ConfigureAwait(false); - } -} From 7e8a80a0e5e812a30df71687e91952def018aeeb Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 14 Jan 2025 19:37:28 +0900 Subject: [PATCH 233/290] Add difficulty, artist and title sort examples Also: - Adds hinting at grouping and header status of items - Passes through criteria and prepare for grouping tests. - Makes `Filters` list `protected` because naming clash with `Filter()` on `BeatmapCarousel`. --- .../SongSelect/TestSceneBeatmapCarouselV2.cs | 28 +++++++++++++ osu.Game/Screens/SelectV2/BeatmapCarousel.cs | 4 +- .../SelectV2/BeatmapCarouselFilterGrouping.cs | 28 +++++++++++-- .../SelectV2/BeatmapCarouselFilterSorting.cs | 39 ++++++++++++++++++- .../Screens/SelectV2/BeatmapCarouselItem.cs | 14 ++++++- osu.Game/Screens/SelectV2/Carousel.cs | 2 +- 6 files changed, 106 insertions(+), 9 deletions(-) diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2.cs index 6d54e13b6f..1d7d6041ae 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2.cs @@ -17,10 +17,13 @@ using osu.Framework.Utils; using osu.Game.Beatmaps; using osu.Game.Database; using osu.Game.Graphics.Containers; +using osu.Game.Screens.Select; +using osu.Game.Screens.Select.Filter; using osu.Game.Screens.SelectV2; using osu.Game.Tests.Beatmaps; using osu.Game.Tests.Resources; using osuTK.Graphics; +using BeatmapCarousel = osu.Game.Screens.SelectV2.BeatmapCarousel; namespace osu.Game.Tests.Visual.SongSelect { @@ -123,6 +126,11 @@ namespace osu.Game.Tests.Visual.SongSelect }, }; }); + + AddStep("sort by title", () => + { + carousel.Filter(new FilterCriteria { Sort = SortMode.Title }); + }); } [Test] @@ -139,6 +147,26 @@ namespace osu.Game.Tests.Visual.SongSelect AddStep("remove all beatmaps", () => beatmapSets.Clear()); } + [Test] + public void TestSorting() + { + AddStep("add 10 beatmaps", () => + { + for (int i = 0; i < 10; i++) + beatmapSets.Add(TestResources.CreateTestBeatmapSetInfo(RNG.Next(1, 4))); + }); + + AddStep("sort by difficulty", () => + { + carousel.Filter(new FilterCriteria { Sort = SortMode.Difficulty }); + }); + + AddStep("sort by artist", () => + { + carousel.Filter(new FilterCriteria { Sort = SortMode.Artist }); + }); + } + [Test] public void TestScrollPositionVelocityMaintained() { diff --git a/osu.Game/Screens/SelectV2/BeatmapCarousel.cs b/osu.Game/Screens/SelectV2/BeatmapCarousel.cs index 3c431a6003..582933bbaf 100644 --- a/osu.Game/Screens/SelectV2/BeatmapCarousel.cs +++ b/osu.Game/Screens/SelectV2/BeatmapCarousel.cs @@ -31,8 +31,8 @@ namespace osu.Game.Screens.SelectV2 Filters = new ICarouselFilter[] { - new BeatmapCarouselFilterSorting(), - new BeatmapCarouselFilterGrouping(), + new BeatmapCarouselFilterSorting(() => Criteria), + new BeatmapCarouselFilterGrouping(() => Criteria), }; AddInternal(carouselPanelPool); diff --git a/osu.Game/Screens/SelectV2/BeatmapCarouselFilterGrouping.cs b/osu.Game/Screens/SelectV2/BeatmapCarouselFilterGrouping.cs index ee4b9ddb69..6cdd15d301 100644 --- a/osu.Game/Screens/SelectV2/BeatmapCarouselFilterGrouping.cs +++ b/osu.Game/Screens/SelectV2/BeatmapCarouselFilterGrouping.cs @@ -1,19 +1,36 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System; using System.Collections.Generic; using System.Linq; using System.Threading; using System.Threading.Tasks; using osu.Game.Beatmaps; +using osu.Game.Screens.Select; namespace osu.Game.Screens.SelectV2 { public class BeatmapCarouselFilterGrouping : ICarouselFilter { + private readonly Func getCriteria; + + public BeatmapCarouselFilterGrouping(Func getCriteria) + { + this.getCriteria = getCriteria; + } + public async Task> Run(IEnumerable items, CancellationToken cancellationToken) => await Task.Run(() => { - // TODO: perform grouping based on FilterCriteria + var criteria = getCriteria(); + + if (criteria.SplitOutDifficulties) + { + foreach (var item in items) + ((BeatmapCarouselItem)item).HasGroupHeader = false; + + return items; + } CarouselItem? lastItem = null; @@ -23,15 +40,18 @@ namespace osu.Game.Screens.SelectV2 { cancellationToken.ThrowIfCancellationRequested(); - if (item.Model is BeatmapInfo b1) + if (item.Model is BeatmapInfo b) { // Add set header - if (lastItem == null || (lastItem.Model is BeatmapInfo b2 && b2.BeatmapSet!.OnlineID != b1.BeatmapSet!.OnlineID)) - newItems.Add(new BeatmapCarouselItem(b1.BeatmapSet!)); + if (lastItem == null || (lastItem.Model is BeatmapInfo b2 && b2.BeatmapSet!.OnlineID != b.BeatmapSet!.OnlineID)) + newItems.Add(new BeatmapCarouselItem(b.BeatmapSet!) { IsGroupHeader = true }); } newItems.Add(item); lastItem = item; + + var beatmapCarouselItem = (BeatmapCarouselItem)item; + beatmapCarouselItem.HasGroupHeader = true; } return newItems; diff --git a/osu.Game/Screens/SelectV2/BeatmapCarouselFilterSorting.cs b/osu.Game/Screens/SelectV2/BeatmapCarouselFilterSorting.cs index a2fd774cf0..df41aa3e86 100644 --- a/osu.Game/Screens/SelectV2/BeatmapCarouselFilterSorting.cs +++ b/osu.Game/Screens/SelectV2/BeatmapCarouselFilterSorting.cs @@ -1,22 +1,59 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System; using System.Collections.Generic; using System.Linq; using System.Threading; using System.Threading.Tasks; using osu.Game.Beatmaps; +using osu.Game.Screens.Select; +using osu.Game.Screens.Select.Filter; +using osu.Game.Utils; namespace osu.Game.Screens.SelectV2 { public class BeatmapCarouselFilterSorting : ICarouselFilter { + private readonly Func getCriteria; + + public BeatmapCarouselFilterSorting(Func getCriteria) + { + this.getCriteria = getCriteria; + } + public async Task> Run(IEnumerable items, CancellationToken cancellationToken) => await Task.Run(() => { + var criteria = getCriteria(); + return items.OrderDescending(Comparer.Create((a, b) => { + int comparison = 0; + if (a.Model is BeatmapInfo ab && b.Model is BeatmapInfo bb) - return ab.OnlineID.CompareTo(bb.OnlineID); + { + switch (criteria.Sort) + { + case SortMode.Artist: + comparison = OrdinalSortByCaseStringComparer.DEFAULT.Compare(ab.BeatmapSet!.Metadata.Artist, bb.BeatmapSet!.Metadata.Artist); + if (comparison == 0) + goto case SortMode.Title; + break; + + case SortMode.Difficulty: + comparison = ab.StarRating.CompareTo(bb.StarRating); + break; + + case SortMode.Title: + comparison = OrdinalSortByCaseStringComparer.DEFAULT.Compare(ab.BeatmapSet!.Metadata.Title, bb.BeatmapSet!.Metadata.Title); + break; + + default: + throw new ArgumentOutOfRangeException(); + } + } + + if (comparison != 0) return comparison; if (a is BeatmapCarouselItem aItem && b is BeatmapCarouselItem bItem) return aItem.ID.CompareTo(bItem.ID); diff --git a/osu.Game/Screens/SelectV2/BeatmapCarouselItem.cs b/osu.Game/Screens/SelectV2/BeatmapCarouselItem.cs index adb5a19875..dd7aae3db9 100644 --- a/osu.Game/Screens/SelectV2/BeatmapCarouselItem.cs +++ b/osu.Game/Screens/SelectV2/BeatmapCarouselItem.cs @@ -11,7 +11,19 @@ namespace osu.Game.Screens.SelectV2 { public readonly Guid ID; - public override float DrawHeight => Model is BeatmapInfo ? 40 : 80; + /// + /// Whether this item has a header providing extra information for it. + /// When displaying items which don't have header, we should make sure enough information is included inline. + /// + public bool HasGroupHeader { get; set; } + + /// + /// Whether this item is a group header. + /// Group headers are generally larger in display. Setting this will account for the size difference. + /// + public bool IsGroupHeader { get; set; } + + public override float DrawHeight => IsGroupHeader ? 80 : 40; public BeatmapCarouselItem(object model) : base(model) diff --git a/osu.Game/Screens/SelectV2/Carousel.cs b/osu.Game/Screens/SelectV2/Carousel.cs index 02e87c7704..f0289d634d 100644 --- a/osu.Game/Screens/SelectV2/Carousel.cs +++ b/osu.Game/Screens/SelectV2/Carousel.cs @@ -29,7 +29,7 @@ namespace osu.Game.Screens.SelectV2 /// /// A collection of filters which should be run each time a is executed. /// - public IEnumerable Filters { get; init; } = Enumerable.Empty(); + protected IEnumerable Filters { get; init; } = Enumerable.Empty(); /// /// Height of the area above the carousel that should be treated as visible due to transparency of elements in front of it. From cc8941a94a3522d3a4fc13d82b421bd7004d7ca3 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 14 Jan 2025 20:07:09 +0900 Subject: [PATCH 234/290] Add animation and depth control --- osu.Game/Screens/SelectV2/BeatmapCarousel.cs | 8 +------- .../Screens/SelectV2/BeatmapCarouselPanel.cs | 19 +++++++++++++++++++ osu.Game/Screens/SelectV2/Carousel.cs | 12 ++++++++++-- osu.Game/Screens/SelectV2/ICarouselPanel.cs | 2 +- 4 files changed, 31 insertions(+), 10 deletions(-) diff --git a/osu.Game/Screens/SelectV2/BeatmapCarousel.cs b/osu.Game/Screens/SelectV2/BeatmapCarousel.cs index 582933bbaf..a394cc894f 100644 --- a/osu.Game/Screens/SelectV2/BeatmapCarousel.cs +++ b/osu.Game/Screens/SelectV2/BeatmapCarousel.cs @@ -45,13 +45,7 @@ namespace osu.Game.Screens.SelectV2 detachedBeatmaps.BindCollectionChanged(beatmapSetsChanged, true); } - protected override Drawable GetDrawableForDisplay(CarouselItem item) - { - var drawable = carouselPanelPool.Get(); - drawable.FlashColour(Color4.Red, 2000); - - return drawable; - } + protected override Drawable GetDrawableForDisplay(CarouselItem item) => carouselPanelPool.Get(); protected override CarouselItem CreateCarouselItemForModel(BeatmapInfo model) => new BeatmapCarouselItem(model); diff --git a/osu.Game/Screens/SelectV2/BeatmapCarouselPanel.cs b/osu.Game/Screens/SelectV2/BeatmapCarouselPanel.cs index a64d16a984..5b8ae211d1 100644 --- a/osu.Game/Screens/SelectV2/BeatmapCarouselPanel.cs +++ b/osu.Game/Screens/SelectV2/BeatmapCarouselPanel.cs @@ -9,6 +9,7 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Pooling; using osu.Framework.Graphics.Shapes; using osu.Framework.Input.Events; +using osu.Framework.Utils; using osu.Game.Beatmaps; using osu.Game.Graphics.Sprites; using osuTK; @@ -67,6 +68,8 @@ namespace osu.Game.Screens.SelectV2 Debug.Assert(Item != null); + DrawYPosition = Item.CarouselYPosition; + Size = new Vector2(500, Item.DrawHeight); Masking = true; @@ -85,6 +88,8 @@ namespace osu.Game.Screens.SelectV2 Origin = Anchor.CentreLeft, } }; + + this.FadeInFromZero(500, Easing.OutQuint); } protected override bool OnClick(ClickEvent e) @@ -92,5 +97,19 @@ namespace osu.Game.Screens.SelectV2 carousel.CurrentSelection = Item!.Model; return true; } + + protected override void Update() + { + base.Update(); + + Debug.Assert(Item != null); + + if (DrawYPosition != Item.CarouselYPosition) + { + DrawYPosition = Interpolation.DampContinuously(DrawYPosition, Item.CarouselYPosition, 50, Time.Elapsed); + } + } + + public double DrawYPosition { get; private set; } } } diff --git a/osu.Game/Screens/SelectV2/Carousel.cs b/osu.Game/Screens/SelectV2/Carousel.cs index f0289d634d..f10ab1c1b0 100644 --- a/osu.Game/Screens/SelectV2/Carousel.cs +++ b/osu.Game/Screens/SelectV2/Carousel.cs @@ -291,6 +291,14 @@ namespace osu.Game.Screens.SelectV2 updateDisplayedRange(range); } + + foreach (var panel in scroll.Panels) + { + var carouselPanel = (ICarouselPanel)panel; + + if (panel.Depth != carouselPanel.DrawYPosition) + scroll.Panels.ChangeChildDepth(panel, (float)carouselPanel.DrawYPosition); + } } private DisplayRange getDisplayRange() @@ -415,7 +423,7 @@ namespace osu.Game.Screens.SelectV2 if (d is not ICarouselPanel panel) return base.GetChildPosInContent(d, offset); - return panel.YPosition + offset.X; + return panel.DrawYPosition + offset.X; } protected override void ApplyCurrentToContent() @@ -425,7 +433,7 @@ namespace osu.Game.Screens.SelectV2 double scrollableExtent = -Current + ScrollableExtent * ScrollContent.RelativeAnchorPosition.Y; foreach (var d in Panels) - d.Y = (float)(((ICarouselPanel)d).YPosition + scrollableExtent); + d.Y = (float)(((ICarouselPanel)d).DrawYPosition + scrollableExtent); } } diff --git a/osu.Game/Screens/SelectV2/ICarouselPanel.cs b/osu.Game/Screens/SelectV2/ICarouselPanel.cs index 97c585492c..d729df7876 100644 --- a/osu.Game/Screens/SelectV2/ICarouselPanel.cs +++ b/osu.Game/Screens/SelectV2/ICarouselPanel.cs @@ -13,7 +13,7 @@ namespace osu.Game.Screens.SelectV2 /// /// The Y position which should be used for displaying this item within the carousel. /// - double YPosition => Item!.CarouselYPosition; + double DrawYPosition { get; } /// /// The carousel item this drawable is representing. This is managed by and should not be set manually. From 900237c1ed7dbf06040fa1f24c2c2c7a09fe9132 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 14 Jan 2025 20:23:53 +0900 Subject: [PATCH 235/290] Add loading overlay and refine filter flow --- osu.Game/Screens/SelectV2/BeatmapCarousel.cs | 17 ++++++++++++-- osu.Game/Screens/SelectV2/Carousel.cs | 24 +++++++++++--------- 2 files changed, 28 insertions(+), 13 deletions(-) diff --git a/osu.Game/Screens/SelectV2/BeatmapCarousel.cs b/osu.Game/Screens/SelectV2/BeatmapCarousel.cs index a394cc894f..93d4c90be0 100644 --- a/osu.Game/Screens/SelectV2/BeatmapCarousel.cs +++ b/osu.Game/Screens/SelectV2/BeatmapCarousel.cs @@ -6,14 +6,16 @@ using System.Collections.Generic; using System.Collections.Specialized; using System.Linq; using System.Threading; +using System.Threading.Tasks; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Pooling; using osu.Game.Beatmaps; using osu.Game.Database; +using osu.Game.Graphics.UserInterface; +using osu.Game.Online.Multiplayer; using osu.Game.Screens.Select; -using osuTK.Graphics; namespace osu.Game.Screens.SelectV2 { @@ -24,6 +26,8 @@ namespace osu.Game.Screens.SelectV2 private readonly DrawablePool carouselPanelPool = new DrawablePool(100); + private readonly LoadingLayer loading; + public BeatmapCarousel() { DebounceDelay = 100; @@ -36,6 +40,8 @@ namespace osu.Game.Screens.SelectV2 }; AddInternal(carouselPanelPool); + + AddInternal(loading = new LoadingLayer(dimBackground: true)); } [BackgroundDependencyLoader] @@ -87,7 +93,14 @@ namespace osu.Game.Screens.SelectV2 public void Filter(FilterCriteria criteria) { Criteria = criteria; - QueueFilter(); + FilterAsync().FireAndForget(); + } + + protected override async Task FilterAsync() + { + loading.Show(); + await base.FilterAsync().ConfigureAwait(true); + loading.Hide(); } } } diff --git a/osu.Game/Screens/SelectV2/Carousel.cs b/osu.Game/Screens/SelectV2/Carousel.cs index f10ab1c1b0..dbecfc6601 100644 --- a/osu.Game/Screens/SelectV2/Carousel.cs +++ b/osu.Game/Screens/SelectV2/Carousel.cs @@ -27,7 +27,7 @@ namespace osu.Game.Screens.SelectV2 public abstract partial class Carousel : CompositeDrawable { /// - /// A collection of filters which should be run each time a is executed. + /// A collection of filters which should be run each time a is executed. /// protected IEnumerable Filters { get; init; } = Enumerable.Empty(); @@ -75,7 +75,7 @@ namespace osu.Game.Screens.SelectV2 /// /// All items which are to be considered for display in this carousel. - /// Mutating this list will automatically queue a . + /// Mutating this list will automatically queue a . /// /// /// Note that an may add new items which are displayed but not tracked in this list. @@ -125,13 +125,13 @@ namespace osu.Game.Screens.SelectV2 } }; - Items.BindCollectionChanged((_, _) => QueueFilter()); + Items.BindCollectionChanged((_, _) => FilterAsync()); } /// /// Queue an asynchronous filter operation. /// - public void QueueFilter() => Scheduler.AddOnce(() => filterTask = performFilter()); + protected virtual Task FilterAsync() => filterTask = performFilter(); /// /// Create a drawable for the given carousel item so it can be displayed. @@ -159,6 +159,7 @@ namespace osu.Game.Screens.SelectV2 { Debug.Assert(SynchronizationContext.Current != null); + Stopwatch stopwatch = Stopwatch.StartNew(); var cts = new CancellationTokenSource(); lock (this) @@ -167,19 +168,20 @@ namespace osu.Game.Screens.SelectV2 cancellationSource = cts; } - Stopwatch stopwatch = Stopwatch.StartNew(); + if (DebounceDelay > 0) + { + log($"Filter operation queued, waiting for {DebounceDelay} ms debounce"); + await Task.Delay(DebounceDelay, cts.Token).ConfigureAwait(true); + } + + // Copy must be performed on update thread for now (see ConfigureAwait above). + // Could potentially be optimised in the future if it becomes an issue. IEnumerable items = new List(Items.Select(CreateCarouselItemForModel)); await Task.Run(async () => { try { - if (DebounceDelay > 0) - { - log($"Filter operation queued, waiting for {DebounceDelay} ms debounce"); - await Task.Delay(DebounceDelay, cts.Token).ConfigureAwait(false); - } - foreach (var filter in Filters) { log($"Performing {filter.GetType().ReadableName()}"); From 91fa2e70d8e7d49d7143f62a393e68324f2fe7b0 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 14 Jan 2025 20:41:18 +0900 Subject: [PATCH 236/290] Revert name change --- osu.Game/Online/API/APIAccess.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Game/Online/API/APIAccess.cs b/osu.Game/Online/API/APIAccess.cs index 1f9dffc605..00fe3bb005 100644 --- a/osu.Game/Online/API/APIAccess.cs +++ b/osu.Game/Online/API/APIAccess.cs @@ -115,7 +115,7 @@ namespace osu.Game.Online.API if (HasLogin) { // Early call to ensure the local user / "logged in" state is correct immediately. - prepareForConnect(); + setPlaceholderLocalUser(); // This is required so that Queue() requests during startup sequence don't fail due to "not logged in". state.Value = APIState.Connecting; @@ -258,7 +258,7 @@ namespace osu.Game.Online.API /// Whether the connection attempt was successful. private void attemptConnect() { - Scheduler.Add(prepareForConnect, false); + Scheduler.Add(setPlaceholderLocalUser, false); // save the username at this point, if the user requested for it to be. config.SetValue(OsuSetting.Username, config.Get(OsuSetting.SaveUsername) ? ProvidedUsername : string.Empty); @@ -374,7 +374,7 @@ namespace osu.Game.Online.API /// This is useful for storing local scores and showing a placeholder username after starting the game, /// until a valid connection has been established. /// - private void prepareForConnect() + private void setPlaceholderLocalUser() { if (!localUser.IsDefault) return; From e871f0235020e294b7cfa35d82da0bdb25d403d9 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 14 Jan 2025 20:43:03 +0900 Subject: [PATCH 237/290] Fix inspections that don't show in rider --- osu.Game/Screens/SelectV2/Carousel.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/SelectV2/Carousel.cs b/osu.Game/Screens/SelectV2/Carousel.cs index dbecfc6601..12f520d6c4 100644 --- a/osu.Game/Screens/SelectV2/Carousel.cs +++ b/osu.Game/Screens/SelectV2/Carousel.cs @@ -45,13 +45,13 @@ namespace osu.Game.Screens.SelectV2 /// The number of pixels outside the carousel's vertical bounds to manifest drawables. /// This allows preloading content before it scrolls into view. /// - public float DistanceOffscreenToPreload { get; set; } = 0; + public float DistanceOffscreenToPreload { get; set; } /// /// When a new request arrives to change filtering, the number of milliseconds to wait before performing the filter. /// Regardless of any external debouncing, this is a safety measure to avoid triggering too many threaded operations. /// - public int DebounceDelay { get; set; } = 0; + public int DebounceDelay { get; set; } /// /// Whether an asynchronous filter / group operation is currently underway. From 20108e3b74084692b34643d4e61124b079c0aa44 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Tue, 14 Jan 2025 23:44:14 +0900 Subject: [PATCH 238/290] Remove Status and Activity bindables from APIUser As for the tests, I'm (ab)using the `IsOnline` state for the time being to restore functionality. --- osu.Desktop/DiscordRichPresence.cs | 14 ++++------- .../Visual/Menus/TestSceneLoginOverlay.cs | 2 +- .../Online/TestSceneUserClickableAvatar.cs | 5 +--- .../Visual/Online/TestSceneUserPanel.cs | 2 +- osu.Game/Online/API/APIAccess.cs | 21 ++++------------- osu.Game/Online/API/DummyAPIAccess.cs | 15 ++++-------- osu.Game/Online/API/IAPIProvider.cs | 7 +++++- .../Online/API/Requests/Responses/APIUser.cs | 5 ---- .../Online/Metadata/OnlineMetadataClient.cs | 17 +++++++------- .../Dashboard/CurrentlyOnlineDisplay.cs | 23 +++++++++---------- osu.Game/Overlays/Login/LoginPanel.cs | 19 ++++----------- 11 files changed, 46 insertions(+), 84 deletions(-) diff --git a/osu.Desktop/DiscordRichPresence.cs b/osu.Desktop/DiscordRichPresence.cs index 32a8ba51a3..94804ad1cc 100644 --- a/osu.Desktop/DiscordRichPresence.cs +++ b/osu.Desktop/DiscordRichPresence.cs @@ -54,8 +54,8 @@ namespace osu.Desktop [Resolved] private OsuConfigManager config { get; set; } = null!; - private readonly IBindable status = new Bindable(); - private readonly IBindable activity = new Bindable(); + private readonly IBindable status = new Bindable(); + private readonly IBindable activity = new Bindable(); private readonly Bindable privacyMode = new Bindable(); private readonly RichPresence presence = new RichPresence @@ -108,14 +108,8 @@ namespace osu.Desktop config.BindWith(OsuSetting.DiscordRichPresence, privacyMode); user = api.LocalUser.GetBoundCopy(); - user.BindValueChanged(u => - { - status.UnbindBindings(); - status.BindTo(u.NewValue.Status); - - activity.UnbindBindings(); - activity.BindTo(u.NewValue.Activity); - }, true); + status.BindTo(api.Status); + activity.BindTo(api.Activity); ruleset.BindValueChanged(_ => schedulePresenceUpdate()); status.BindValueChanged(_ => schedulePresenceUpdate()); diff --git a/osu.Game.Tests/Visual/Menus/TestSceneLoginOverlay.cs b/osu.Game.Tests/Visual/Menus/TestSceneLoginOverlay.cs index 609bc6e166..5c12e0c102 100644 --- a/osu.Game.Tests/Visual/Menus/TestSceneLoginOverlay.cs +++ b/osu.Game.Tests/Visual/Menus/TestSceneLoginOverlay.cs @@ -89,7 +89,7 @@ namespace osu.Game.Tests.Visual.Menus AddStep("clear handler", () => dummyAPI.HandleRequest = null); assertDropdownState(UserAction.Online); - AddStep("change user state", () => dummyAPI.LocalUser.Value.Status.Value = UserStatus.DoNotDisturb); + AddStep("change user state", () => dummyAPI.Status.Value = UserStatus.DoNotDisturb); assertDropdownState(UserAction.DoNotDisturb); } diff --git a/osu.Game.Tests/Visual/Online/TestSceneUserClickableAvatar.cs b/osu.Game.Tests/Visual/Online/TestSceneUserClickableAvatar.cs index 4539eae25f..fce888094d 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneUserClickableAvatar.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneUserClickableAvatar.cs @@ -62,10 +62,7 @@ namespace osu.Game.Tests.Visual.Online CountryCode = countryCode, CoverUrl = cover, Colour = color ?? "000000", - Status = - { - Value = UserStatus.Online - }, + IsOnline = true }; return new ClickableAvatar(user, showPanel) diff --git a/osu.Game.Tests/Visual/Online/TestSceneUserPanel.cs b/osu.Game.Tests/Visual/Online/TestSceneUserPanel.cs index 3f1d961588..4c2e47d336 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneUserPanel.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneUserPanel.cs @@ -76,7 +76,7 @@ namespace osu.Game.Tests.Visual.Online Id = 3103765, CountryCode = CountryCode.JP, CoverUrl = @"https://assets.ppy.sh/user-cover-presets/1/df28696b58541a9e67f6755918951d542d93bdf1da41720fcca2fd2c1ea8cf51.jpeg", - Status = { Value = UserStatus.Online } + IsOnline = true }) { Width = 300 }, boundPanel1 = new UserGridPanel(new APIUser { diff --git a/osu.Game/Online/API/APIAccess.cs b/osu.Game/Online/API/APIAccess.cs index 00fe3bb005..4f8c5dcb22 100644 --- a/osu.Game/Online/API/APIAccess.cs +++ b/osu.Game/Online/API/APIAccess.cs @@ -60,6 +60,7 @@ namespace osu.Game.Online.API public IBindable LocalUser => localUser; public IBindableList Friends => friends; + public Bindable Status { get; } = new Bindable(UserStatus.Online); public IBindable Activity => activity; public INotificationsClient NotificationsClient { get; } @@ -73,7 +74,6 @@ namespace osu.Game.Online.API private Bindable activity { get; } = new Bindable(); private Bindable configStatus { get; } = new Bindable(); - private Bindable localUserStatus { get; } = new Bindable(); protected bool HasLogin => authentication.Token.Value != null || (!string.IsNullOrEmpty(ProvidedUsername) && !string.IsNullOrEmpty(password)); @@ -121,17 +121,6 @@ namespace osu.Game.Online.API state.Value = APIState.Connecting; } - localUser.BindValueChanged(u => - { - u.OldValue?.Activity.UnbindFrom(activity); - u.NewValue.Activity.BindTo(activity); - - u.OldValue?.Status.UnbindFrom(localUserStatus); - u.NewValue.Status.BindTo(localUserStatus); - }, true); - - localUserStatus.BindTo(configStatus); - var thread = new Thread(run) { Name = "APIAccess", @@ -342,9 +331,8 @@ namespace osu.Game.Online.API { Debug.Assert(ThreadSafety.IsUpdateThread); - me.Status.Value = configStatus.Value ?? UserStatus.Online; - localUser.Value = me; + Status.Value = configStatus.Value ?? UserStatus.Online; state.Value = me.SessionVerified ? APIState.Online : APIState.RequiresSecondFactorAuth; failureCount = 0; @@ -381,9 +369,10 @@ namespace osu.Game.Online.API localUser.Value = new APIUser { - Username = ProvidedUsername, - Status = { Value = configStatus.Value ?? UserStatus.Online } + Username = ProvidedUsername }; + + Status.Value = configStatus.Value ?? UserStatus.Online; } public void Perform(APIRequest request) diff --git a/osu.Game/Online/API/DummyAPIAccess.cs b/osu.Game/Online/API/DummyAPIAccess.cs index 5d63c04925..b338f4e8cb 100644 --- a/osu.Game/Online/API/DummyAPIAccess.cs +++ b/osu.Game/Online/API/DummyAPIAccess.cs @@ -28,7 +28,9 @@ namespace osu.Game.Online.API public BindableList Friends { get; } = new BindableList(); - public Bindable Activity { get; } = new Bindable(); + public Bindable Status { get; } = new Bindable(UserStatus.Online); + + public Bindable Activity { get; } = new Bindable(); public DummyNotificationsClient NotificationsClient { get; } = new DummyNotificationsClient(); INotificationsClient IAPIProvider.NotificationsClient => NotificationsClient; @@ -69,15 +71,6 @@ namespace osu.Game.Online.API /// public IBindable State => state; - public DummyAPIAccess() - { - LocalUser.BindValueChanged(u => - { - u.OldValue?.Activity.UnbindFrom(Activity); - u.NewValue.Activity.BindTo(Activity); - }, true); - } - public virtual void Queue(APIRequest request) { request.AttachAPI(this); @@ -204,7 +197,7 @@ namespace osu.Game.Online.API IBindable IAPIProvider.LocalUser => LocalUser; IBindableList IAPIProvider.Friends => Friends; - IBindable IAPIProvider.Activity => Activity; + IBindable IAPIProvider.Activity => Activity; /// /// Skip 2FA requirement for next login. diff --git a/osu.Game/Online/API/IAPIProvider.cs b/osu.Game/Online/API/IAPIProvider.cs index 1c4b2da742..cc065a659a 100644 --- a/osu.Game/Online/API/IAPIProvider.cs +++ b/osu.Game/Online/API/IAPIProvider.cs @@ -24,10 +24,15 @@ namespace osu.Game.Online.API /// IBindableList Friends { get; } + /// + /// The current user's status. + /// + Bindable Status { get; } + /// /// The current user's activity. /// - IBindable Activity { get; } + IBindable Activity { get; } /// /// The language supplied by this provider to API requests. diff --git a/osu.Game/Online/API/Requests/Responses/APIUser.cs b/osu.Game/Online/API/Requests/Responses/APIUser.cs index a829484506..30fceab852 100644 --- a/osu.Game/Online/API/Requests/Responses/APIUser.cs +++ b/osu.Game/Online/API/Requests/Responses/APIUser.cs @@ -8,7 +8,6 @@ using System.Collections.Generic; using System.Linq; using JetBrains.Annotations; using Newtonsoft.Json; -using osu.Framework.Bindables; using osu.Game.Extensions; using osu.Game.Users; @@ -56,10 +55,6 @@ namespace osu.Game.Online.API.Requests.Responses set => countryCodeString = value.ToString(); } - public readonly Bindable Status = new Bindable(); - - public readonly Bindable Activity = new Bindable(); - [JsonProperty(@"profile_colour")] public string Colour; diff --git a/osu.Game/Online/Metadata/OnlineMetadataClient.cs b/osu.Game/Online/Metadata/OnlineMetadataClient.cs index a8a14b1c78..b3204a7cd1 100644 --- a/osu.Game/Online/Metadata/OnlineMetadataClient.cs +++ b/osu.Game/Online/Metadata/OnlineMetadataClient.cs @@ -37,8 +37,9 @@ namespace osu.Game.Online.Metadata private IHubClientConnector? connector; private Bindable lastQueueId = null!; private IBindable localUser = null!; + + private IBindable userStatus = null!; private IBindable userActivity = null!; - private IBindable? userStatus; private HubConnection? connection => connector?.CurrentConnection; @@ -75,22 +76,20 @@ namespace osu.Game.Online.Metadata lastQueueId = config.GetBindable(OsuSetting.LastProcessedMetadataId); localUser = api.LocalUser.GetBoundCopy(); + userStatus = api.Status.GetBoundCopy(); userActivity = api.Activity.GetBoundCopy()!; } protected override void LoadComplete() { base.LoadComplete(); - localUser.BindValueChanged(_ => + + userStatus.BindValueChanged(status => { if (localUser.Value is not GuestUser) - { - userStatus = localUser.Value.Status.GetBoundCopy(); - userStatus.BindValueChanged(status => UpdateStatus(status.NewValue), true); - } - else - userStatus = null; + UpdateStatus(status.NewValue); }, true); + userActivity.BindValueChanged(activity => { if (localUser.Value is not GuestUser) @@ -117,7 +116,7 @@ namespace osu.Game.Online.Metadata if (localUser.Value is not GuestUser) { UpdateActivity(userActivity.Value); - UpdateStatus(userStatus?.Value); + UpdateStatus(userStatus.Value); } if (lastQueueId.Value >= 0) diff --git a/osu.Game/Overlays/Dashboard/CurrentlyOnlineDisplay.cs b/osu.Game/Overlays/Dashboard/CurrentlyOnlineDisplay.cs index ee277ff538..2ca548fdf5 100644 --- a/osu.Game/Overlays/Dashboard/CurrentlyOnlineDisplay.cs +++ b/osu.Game/Overlays/Dashboard/CurrentlyOnlineDisplay.cs @@ -140,15 +140,11 @@ namespace osu.Game.Overlays.Dashboard Schedule(() => { - // explicitly refetch the user's status. - // things may have changed in between the time of scheduling and the time of actual execution. - if (onlineUsers.TryGetValue(userId, out var updatedStatus)) + userFlow.Add(userPanels[userId] = createUserPanel(user).With(p => { - user.Activity.Value = updatedStatus.Activity; - user.Status.Value = updatedStatus.Status; - } - - userFlow.Add(userPanels[userId] = createUserPanel(user)); + p.Status.Value = onlineUsers.GetValueOrDefault(userId).Status; + p.Activity.Value = onlineUsers.GetValueOrDefault(userId).Activity; + })); }); }); } @@ -162,8 +158,8 @@ namespace osu.Game.Overlays.Dashboard { if (userPanels.TryGetValue(kvp.Key, out var panel)) { - panel.User.Activity.Value = kvp.Value.Activity; - panel.User.Status.Value = kvp.Value.Status; + panel.Activity.Value = kvp.Value.Activity; + panel.Status.Value = kvp.Value.Status; } } @@ -223,6 +219,9 @@ namespace osu.Game.Overlays.Dashboard { public readonly APIUser User; + public readonly Bindable Status = new Bindable(); + public readonly Bindable Activity = new Bindable(); + public BindableBool CanSpectate { get; } = new BindableBool(); public IEnumerable FilterTerms { get; } @@ -271,8 +270,8 @@ namespace osu.Game.Overlays.Dashboard Anchor = Anchor.TopCentre, Origin = Anchor.TopCentre, // this is SHOCKING - Activity = { BindTarget = User.Activity }, - Status = { BindTarget = User.Status }, + Activity = { BindTarget = Activity }, + Status = { BindTarget = Status }, }, new PurpleRoundedButton { diff --git a/osu.Game/Overlays/Login/LoginPanel.cs b/osu.Game/Overlays/Login/LoginPanel.cs index 84bd0c36b9..b947731f8b 100644 --- a/osu.Game/Overlays/Login/LoginPanel.cs +++ b/osu.Game/Overlays/Login/LoginPanel.cs @@ -15,7 +15,6 @@ using osu.Game.Graphics.Sprites; using osu.Game.Graphics.UserInterface; using osu.Game.Localisation; using osu.Game.Online.API; -using osu.Game.Online.API.Requests.Responses; using osu.Game.Overlays.Settings; using osu.Game.Users; using osuTK; @@ -38,9 +37,7 @@ namespace osu.Game.Overlays.Login /// public Action? RequestHide; - private IBindable user = null!; - private readonly Bindable status = new Bindable(); - + private readonly Bindable status = new Bindable(); private readonly IBindable apiState = new Bindable(); [Resolved] @@ -71,13 +68,7 @@ namespace osu.Game.Overlays.Login apiState.BindTo(api.State); apiState.BindValueChanged(onlineStateChanged, true); - user = api.LocalUser.GetBoundCopy(); - user.BindValueChanged(u => - { - status.UnbindBindings(); - status.BindTo(u.NewValue.Status); - }, true); - + status.BindTo(api.Status); status.BindValueChanged(e => updateDropdownCurrent(e.NewValue), true); } @@ -163,17 +154,17 @@ namespace osu.Game.Overlays.Login switch (action.NewValue) { case UserAction.Online: - api.LocalUser.Value.Status.Value = UserStatus.Online; + status.Value = UserStatus.Online; dropdown.StatusColour = colours.Green; break; case UserAction.DoNotDisturb: - api.LocalUser.Value.Status.Value = UserStatus.DoNotDisturb; + status.Value = UserStatus.DoNotDisturb; dropdown.StatusColour = colours.Red; break; case UserAction.AppearOffline: - api.LocalUser.Value.Status.Value = UserStatus.Offline; + status.Value = UserStatus.Offline; dropdown.StatusColour = colours.Gray7; break; From b7a9b77efef2590a6f47e013165c95c71d837bb3 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Wed, 15 Jan 2025 00:01:19 +0900 Subject: [PATCH 239/290] Make config the definitive status value --- osu.Game/Configuration/OsuConfigManager.cs | 2 +- osu.Game/Online/API/APIAccess.cs | 10 ++-------- 2 files changed, 3 insertions(+), 9 deletions(-) diff --git a/osu.Game/Configuration/OsuConfigManager.cs b/osu.Game/Configuration/OsuConfigManager.cs index d4a75334a9..642da16d2d 100644 --- a/osu.Game/Configuration/OsuConfigManager.cs +++ b/osu.Game/Configuration/OsuConfigManager.cs @@ -211,7 +211,7 @@ namespace osu.Game.Configuration SetDefault(OsuSetting.LastProcessedMetadataId, -1); SetDefault(OsuSetting.ComboColourNormalisationAmount, 0.2f, 0f, 1f, 0.01f); - SetDefault(OsuSetting.UserOnlineStatus, null); + SetDefault(OsuSetting.UserOnlineStatus, UserStatus.Online); SetDefault(OsuSetting.EditorTimelineShowTimingChanges, true); SetDefault(OsuSetting.EditorTimelineShowBreaks, true); diff --git a/osu.Game/Online/API/APIAccess.cs b/osu.Game/Online/API/APIAccess.cs index 4f8c5dcb22..a4ac577a02 100644 --- a/osu.Game/Online/API/APIAccess.cs +++ b/osu.Game/Online/API/APIAccess.cs @@ -73,8 +73,6 @@ namespace osu.Game.Online.API private Bindable activity { get; } = new Bindable(); - private Bindable configStatus { get; } = new Bindable(); - protected bool HasLogin => authentication.Token.Value != null || (!string.IsNullOrEmpty(ProvidedUsername) && !string.IsNullOrEmpty(password)); private readonly CancellationTokenSource cancellationToken = new CancellationTokenSource(); @@ -110,7 +108,7 @@ namespace osu.Game.Online.API authentication.TokenString = config.Get(OsuSetting.Token); authentication.Token.ValueChanged += onTokenChanged; - config.BindWith(OsuSetting.UserOnlineStatus, configStatus); + config.BindWith(OsuSetting.UserOnlineStatus, Status); if (HasLogin) { @@ -332,8 +330,6 @@ namespace osu.Game.Online.API Debug.Assert(ThreadSafety.IsUpdateThread); localUser.Value = me; - Status.Value = configStatus.Value ?? UserStatus.Online; - state.Value = me.SessionVerified ? APIState.Online : APIState.RequiresSecondFactorAuth; failureCount = 0; }; @@ -371,8 +367,6 @@ namespace osu.Game.Online.API { Username = ProvidedUsername }; - - Status.Value = configStatus.Value ?? UserStatus.Online; } public void Perform(APIRequest request) @@ -597,7 +591,7 @@ namespace osu.Game.Online.API password = null; SecondFactorCode = null; authentication.Clear(); - configStatus.Value = UserStatus.Online; + Status.Value = UserStatus.Online; // Scheduled prior to state change such that the state changed event is invoked with the correct user and their friends present Schedule(() => From 208824e9f47de863860ac8a010cae9deabb0f20b Mon Sep 17 00:00:00 2001 From: Andrei Zavatski Date: Tue, 14 Jan 2025 21:40:14 +0300 Subject: [PATCH 240/290] Add ability for cursor trail to spin --- .../Skinning/Legacy/LegacyCursorTrail.cs | 1 + .../Skinning/OsuSkinConfiguration.cs | 1 + .../UI/Cursor/CursorTrail.cs | 22 +++++++++++++++---- 3 files changed, 20 insertions(+), 4 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyCursorTrail.cs b/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyCursorTrail.cs index ca0002d8c0..4c21b94326 100644 --- a/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyCursorTrail.cs +++ b/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyCursorTrail.cs @@ -34,6 +34,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy private void load(OsuConfigManager config, ISkinSource skinSource) { cursorSize = config.GetBindable(OsuSetting.GameplayCursorSize).GetBoundCopy(); + Spin = skin.GetConfig(OsuSkinConfiguration.CursorTrailRotate)?.Value ?? true; Texture = skin.GetTexture("cursortrail"); diff --git a/osu.Game.Rulesets.Osu/Skinning/OsuSkinConfiguration.cs b/osu.Game.Rulesets.Osu/Skinning/OsuSkinConfiguration.cs index 9685ab685d..81488ca1a3 100644 --- a/osu.Game.Rulesets.Osu/Skinning/OsuSkinConfiguration.cs +++ b/osu.Game.Rulesets.Osu/Skinning/OsuSkinConfiguration.cs @@ -9,6 +9,7 @@ namespace osu.Game.Rulesets.Osu.Skinning CursorCentre, CursorExpand, CursorRotate, + CursorTrailRotate, HitCircleOverlayAboveNumber, // ReSharper disable once IdentifierTypo diff --git a/osu.Game.Rulesets.Osu/UI/Cursor/CursorTrail.cs b/osu.Game.Rulesets.Osu/UI/Cursor/CursorTrail.cs index 5132dc2859..920a8c372f 100644 --- a/osu.Game.Rulesets.Osu/UI/Cursor/CursorTrail.cs +++ b/osu.Game.Rulesets.Osu/UI/Cursor/CursorTrail.cs @@ -39,6 +39,7 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor private IShader shader; private double timeOffset; private float time; + protected bool Spin { get; set; } /// /// The scale used on creation of a new trail part. @@ -220,6 +221,7 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor private float time; private float fadeExponent; + private float angle; private readonly TrailPart[] parts = new TrailPart[max_sprites]; private Vector2 originPosition; @@ -239,6 +241,7 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor texture = Source.texture; time = Source.time; fadeExponent = Source.FadeExponent; + angle = Source.Spin ? time / 10 : 0; originPosition = Vector2.Zero; @@ -279,6 +282,9 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor renderer.PushLocalMatrix(DrawInfo.Matrix); + float sin = MathF.Sin(angle); + float cos = MathF.Cos(angle); + foreach (var part in parts) { if (part.InvalidationID == -1) @@ -289,7 +295,7 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor vertexBatch.Add(new TexturedTrailVertex { - Position = new Vector2(part.Position.X - texture.DisplayWidth * originPosition.X * part.Scale.X, part.Position.Y + texture.DisplayHeight * (1 - originPosition.Y) * part.Scale.Y), + Position = rotateAround(new Vector2(part.Position.X - texture.DisplayWidth * originPosition.X * part.Scale.X, part.Position.Y + texture.DisplayHeight * (1 - originPosition.Y) * part.Scale.Y), part.Position, sin, cos), TexturePosition = textureRect.BottomLeft, TextureRect = new Vector4(0, 0, 1, 1), Colour = DrawColourInfo.Colour.BottomLeft.Linear, @@ -298,7 +304,7 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor vertexBatch.Add(new TexturedTrailVertex { - Position = new Vector2(part.Position.X + texture.DisplayWidth * (1 - originPosition.X) * part.Scale.X, part.Position.Y + texture.DisplayHeight * (1 - originPosition.Y) * part.Scale.Y), + Position = rotateAround(new Vector2(part.Position.X + texture.DisplayWidth * (1 - originPosition.X) * part.Scale.X, part.Position.Y + texture.DisplayHeight * (1 - originPosition.Y) * part.Scale.Y), part.Position, sin, cos), TexturePosition = textureRect.BottomRight, TextureRect = new Vector4(0, 0, 1, 1), Colour = DrawColourInfo.Colour.BottomRight.Linear, @@ -307,7 +313,7 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor vertexBatch.Add(new TexturedTrailVertex { - Position = new Vector2(part.Position.X + texture.DisplayWidth * (1 - originPosition.X) * part.Scale.X, part.Position.Y - texture.DisplayHeight * originPosition.Y * part.Scale.Y), + Position = rotateAround(new Vector2(part.Position.X + texture.DisplayWidth * (1 - originPosition.X) * part.Scale.X, part.Position.Y - texture.DisplayHeight * originPosition.Y * part.Scale.Y), part.Position, sin, cos), TexturePosition = textureRect.TopRight, TextureRect = new Vector4(0, 0, 1, 1), Colour = DrawColourInfo.Colour.TopRight.Linear, @@ -316,7 +322,7 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor vertexBatch.Add(new TexturedTrailVertex { - Position = new Vector2(part.Position.X - texture.DisplayWidth * originPosition.X * part.Scale.X, part.Position.Y - texture.DisplayHeight * originPosition.Y * part.Scale.Y), + Position = rotateAround(new Vector2(part.Position.X - texture.DisplayWidth * originPosition.X * part.Scale.X, part.Position.Y - texture.DisplayHeight * originPosition.Y * part.Scale.Y), part.Position, sin, cos), TexturePosition = textureRect.TopLeft, TextureRect = new Vector4(0, 0, 1, 1), Colour = DrawColourInfo.Colour.TopLeft.Linear, @@ -330,6 +336,14 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor shader.Unbind(); } + private static Vector2 rotateAround(Vector2 input, Vector2 origin, float sin, float cos) + { + float xTranslated = input.X - origin.X; + float yTranslated = input.Y - origin.Y; + + return new Vector2(xTranslated * cos - yTranslated * sin, xTranslated * sin + yTranslated * cos) + origin; + } + protected override void Dispose(bool isDisposing) { base.Dispose(isDisposing); From 7a6355d7cfe61abaaf4167ecda84755f4da9c9a4 Mon Sep 17 00:00:00 2001 From: Andrei Zavatski Date: Tue, 14 Jan 2025 22:51:17 +0300 Subject: [PATCH 241/290] Sync cursor trail rotation with the cursor --- osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyCursor.cs | 4 +++- osu.Game.Rulesets.Osu/UI/Cursor/CursorTrail.cs | 7 ++++++- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyCursor.cs b/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyCursor.cs index 375d81049d..e526c4f14c 100644 --- a/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyCursor.cs +++ b/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyCursor.cs @@ -11,6 +11,8 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy { public partial class LegacyCursor : SkinnableCursor { + public static readonly int REVOLUTION_DURATION = 10000; + private const float pressed_scale = 1.3f; private const float released_scale = 1f; @@ -52,7 +54,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy protected override void LoadComplete() { if (spin) - ExpandTarget.Spin(10000, RotationDirection.Clockwise); + ExpandTarget.Spin(REVOLUTION_DURATION, RotationDirection.Clockwise); } public override void Expand() diff --git a/osu.Game.Rulesets.Osu/UI/Cursor/CursorTrail.cs b/osu.Game.Rulesets.Osu/UI/Cursor/CursorTrail.cs index 920a8c372f..5b7d2d40d3 100644 --- a/osu.Game.Rulesets.Osu/UI/Cursor/CursorTrail.cs +++ b/osu.Game.Rulesets.Osu/UI/Cursor/CursorTrail.cs @@ -18,6 +18,7 @@ using osu.Framework.Graphics.Visualisation; using osu.Framework.Input; using osu.Framework.Input.Events; using osu.Framework.Timing; +using osu.Game.Rulesets.Osu.Skinning.Legacy; using osuTK; using osuTK.Graphics; using osuTK.Graphics.ES30; @@ -79,9 +80,12 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor shader = shaders.Load(@"CursorTrail", FragmentShaderDescriptor.TEXTURE); } + private double loadCompleteTime; + protected override void LoadComplete() { base.LoadComplete(); + loadCompleteTime = Parent!.Clock.CurrentTime; // using parent's clock since our is overridden resetTime(); } @@ -241,7 +245,8 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor texture = Source.texture; time = Source.time; fadeExponent = Source.FadeExponent; - angle = Source.Spin ? time / 10 : 0; + // The goal is to sync trail rotation with the cursor. Cursor uses spin transform which starts rotation at LoadComplete time. + angle = Source.Spin ? (float)((Source.Parent!.Clock.CurrentTime - Source.loadCompleteTime) * 2 * Math.PI / LegacyCursor.REVOLUTION_DURATION) : 0; originPosition = Vector2.Zero; From 57a9911b22e29979f1bd55c16e1e911c8ab748a5 Mon Sep 17 00:00:00 2001 From: Rudi Herouard Date: Wed, 15 Jan 2025 04:12:54 +0100 Subject: [PATCH 242/290] Apply beatmap offset on every beatmap set difficulty if they have the same audio --- osu.Game/Screens/Play/PlayerSettings/BeatmapOffsetControl.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/Play/PlayerSettings/BeatmapOffsetControl.cs b/osu.Game/Screens/Play/PlayerSettings/BeatmapOffsetControl.cs index f93fa1b3c5..ac224794ea 100644 --- a/osu.Game/Screens/Play/PlayerSettings/BeatmapOffsetControl.cs +++ b/osu.Game/Screens/Play/PlayerSettings/BeatmapOffsetControl.cs @@ -165,13 +165,14 @@ namespace osu.Game.Screens.Play.PlayerSettings if (setInfo == null) // only the case for tests. return; - // Apply to all difficulties in a beatmap set for now (they generally always share timing). + // Apply to all difficulties in a beatmap set if they have the same audio + // (they generally always share timing). foreach (var b in setInfo.Beatmaps) { BeatmapUserSettings userSettings = b.UserSettings; double val = Current.Value; - if (userSettings.Offset != val) + if (userSettings.Offset != val && b.AudioEquals(beatmap.Value.BeatmapInfo)) userSettings.Offset = val; } }); From 0b764e63720a03867f7fb1ab183410e84ba6bf29 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 15 Jan 2025 16:18:34 +0900 Subject: [PATCH 243/290] Fix substring of `GetHashCode` potentially failing --- osu.Game/Screens/SelectV2/Carousel.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/SelectV2/Carousel.cs b/osu.Game/Screens/SelectV2/Carousel.cs index 12f520d6c4..aeab6a96d0 100644 --- a/osu.Game/Screens/SelectV2/Carousel.cs +++ b/osu.Game/Screens/SelectV2/Carousel.cs @@ -206,7 +206,7 @@ namespace osu.Game.Screens.SelectV2 updateSelection(); - void log(string text) => Logger.Log($"Carousel[op {cts.GetHashCode().ToString().Substring(0, 5)}] {stopwatch.ElapsedMilliseconds} ms: {text}"); + void log(string text) => Logger.Log($"Carousel[op {cts.GetHashCode().ToString()}] {stopwatch.ElapsedMilliseconds} ms: {text}"); } private async Task updateYPositions(IEnumerable carouselItems, CancellationToken cancellationToken) => await Task.Run(() => From 60279476570a20b5a9bf40525c615078a83c5e6a Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 15 Jan 2025 17:01:07 +0900 Subject: [PATCH 244/290] Move animation handling to `Carousel` implementation to better handle add/removes With the animation logic being external, it was going to make it very hard to apply the scroll offset when a new panel is added or removed before the current selection. There's no real reason for the animations to be local to beatmap carousel. If there's a usage in the future where the animation is to change, we can add more customisation to `Carousel` itself. --- .../SongSelect/TestSceneBeatmapCarouselV2.cs | 28 ++++++++++++++- .../Screens/SelectV2/BeatmapCarouselPanel.cs | 15 +------- osu.Game/Screens/SelectV2/Carousel.cs | 36 ++++++++++++++++--- osu.Game/Screens/SelectV2/ICarouselPanel.cs | 4 +-- 4 files changed, 62 insertions(+), 21 deletions(-) diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2.cs index 1d7d6041ae..f99e0a418a 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2.cs @@ -168,7 +168,33 @@ namespace osu.Game.Tests.Visual.SongSelect } [Test] - public void TestScrollPositionVelocityMaintained() + public void TestScrollPositionMaintainedOnAddSecondSelected() + { + Quad positionBefore = default; + + AddStep("add 10 beatmaps", () => + { + for (int i = 0; i < 10; i++) + beatmapSets.Add(TestResources.CreateTestBeatmapSetInfo(RNG.Next(1, 4))); + }); + + AddUntilStep("visual item added", () => carousel.ChildrenOfType().Count(), () => Is.GreaterThan(0)); + + AddStep("select middle beatmap", () => carousel.CurrentSelection = beatmapSets.ElementAt(beatmapSets.Count - 2)); + AddStep("scroll to selected item", () => scroll.ScrollTo(scroll.ChildrenOfType().Single(p => p.Item!.Selected.Value))); + + AddUntilStep("wait for scroll finished", () => scroll.Current, () => Is.EqualTo(scroll.Target)); + + AddStep("save selected screen position", () => positionBefore = carousel.ChildrenOfType().FirstOrDefault(p => p.Item!.Selected.Value)!.ScreenSpaceDrawQuad); + + AddStep("remove first beatmap", () => beatmapSets.Remove(beatmapSets.Last())); + AddUntilStep("sorting finished", () => carousel.IsFiltering, () => Is.False); + AddAssert("select screen position unchanged", () => carousel.ChildrenOfType().Single(p => p.Item!.Selected.Value).ScreenSpaceDrawQuad, + () => Is.EqualTo(positionBefore)); + } + + [Test] + public void TestScrollPositionMaintainedOnAddLastSelected() { Quad positionBefore = default; diff --git a/osu.Game/Screens/SelectV2/BeatmapCarouselPanel.cs b/osu.Game/Screens/SelectV2/BeatmapCarouselPanel.cs index 5b8ae211d1..27023b50be 100644 --- a/osu.Game/Screens/SelectV2/BeatmapCarouselPanel.cs +++ b/osu.Game/Screens/SelectV2/BeatmapCarouselPanel.cs @@ -9,7 +9,6 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Pooling; using osu.Framework.Graphics.Shapes; using osu.Framework.Input.Events; -using osu.Framework.Utils; using osu.Game.Beatmaps; using osu.Game.Graphics.Sprites; using osuTK; @@ -98,18 +97,6 @@ namespace osu.Game.Screens.SelectV2 return true; } - protected override void Update() - { - base.Update(); - - Debug.Assert(Item != null); - - if (DrawYPosition != Item.CarouselYPosition) - { - DrawYPosition = Interpolation.DampContinuously(DrawYPosition, Item.CarouselYPosition, 50, Time.Elapsed); - } - } - - public double DrawYPosition { get; private set; } + public double DrawYPosition { get; set; } } } diff --git a/osu.Game/Screens/SelectV2/Carousel.cs b/osu.Game/Screens/SelectV2/Carousel.cs index aeab6a96d0..12a86be7b9 100644 --- a/osu.Game/Screens/SelectV2/Carousel.cs +++ b/osu.Game/Screens/SelectV2/Carousel.cs @@ -14,6 +14,7 @@ using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Pooling; using osu.Framework.Graphics.Shapes; using osu.Framework.Logging; +using osu.Framework.Utils; using osu.Game.Graphics.Containers; using osuTK; using osuTK.Graphics; @@ -107,7 +108,7 @@ namespace osu.Game.Screens.SelectV2 private List? displayedCarouselItems; - private readonly DoublePrecisionScroll scroll; + private readonly CarouselScrollContainer scroll; protected Carousel() { @@ -118,7 +119,7 @@ namespace osu.Game.Screens.SelectV2 Colour = Color4.Black, RelativeSizeAxes = Axes.Both, }, - scroll = new DoublePrecisionScroll + scroll = new CarouselScrollContainer { RelativeSizeAxes = Axes.Both, Masking = false, @@ -389,13 +390,13 @@ namespace osu.Game.Screens.SelectV2 /// Implementation of scroll container which handles very large vertical lists by internally using double precision /// for pre-display Y values. /// - private partial class DoublePrecisionScroll : OsuScrollContainer + private partial class CarouselScrollContainer : OsuScrollContainer { public readonly Container Panels; public void SetLayoutHeight(float height) => Panels.Height = height; - public DoublePrecisionScroll() + public CarouselScrollContainer() { // Managing our own custom layout within ScrollContent causes feedback with public ScrollContainer calculations, // so we must maintain one level of separation from ScrollContent. @@ -406,6 +407,33 @@ namespace osu.Game.Screens.SelectV2 }); } + public override void OffsetScrollPosition(double offset) + { + base.OffsetScrollPosition(offset); + + foreach (var panel in Panels) + { + var c = (ICarouselPanel)panel; + Debug.Assert(c.Item != null); + + c.DrawYPosition += offset; + } + } + + protected override void Update() + { + base.Update(); + + foreach (var panel in Panels) + { + var c = (ICarouselPanel)panel; + Debug.Assert(c.Item != null); + + if (c.DrawYPosition != c.Item.CarouselYPosition) + c.DrawYPosition = Interpolation.DampContinuously(c.DrawYPosition, c.Item.CarouselYPosition, 50, Time.Elapsed); + } + } + public override void Clear(bool disposeChildren) { Panels.Height = 0; diff --git a/osu.Game/Screens/SelectV2/ICarouselPanel.cs b/osu.Game/Screens/SelectV2/ICarouselPanel.cs index d729df7876..117feab621 100644 --- a/osu.Game/Screens/SelectV2/ICarouselPanel.cs +++ b/osu.Game/Screens/SelectV2/ICarouselPanel.cs @@ -11,9 +11,9 @@ namespace osu.Game.Screens.SelectV2 public interface ICarouselPanel { /// - /// The Y position which should be used for displaying this item within the carousel. + /// The Y position which should be used for displaying this item within the carousel. This is managed by and should not be set manually. /// - double DrawYPosition { get; } + double DrawYPosition { get; set; } /// /// The carousel item this drawable is representing. This is managed by and should not be set manually. From 7ca3a6fc26f78c639ddefb725c25f40442c94dc6 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Wed, 15 Jan 2025 17:48:22 +0900 Subject: [PATCH 245/290] Clear Discord presence when logged out --- osu.Desktop/DiscordRichPresence.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Desktop/DiscordRichPresence.cs b/osu.Desktop/DiscordRichPresence.cs index 94804ad1cc..6c7e7d393f 100644 --- a/osu.Desktop/DiscordRichPresence.cs +++ b/osu.Desktop/DiscordRichPresence.cs @@ -145,7 +145,7 @@ namespace osu.Desktop if (!client.IsInitialized) return; - if (status.Value == UserStatus.Offline || privacyMode.Value == DiscordRichPresenceMode.Off) + if (!api.IsLoggedIn || status.Value == UserStatus.Offline || privacyMode.Value == DiscordRichPresenceMode.Off) { client.ClearPresence(); return; From e22dc09149097555fe81b66e5ff8ef36fca9caaf Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 15 Jan 2025 20:42:46 +0900 Subject: [PATCH 246/290] Update framework --- osu.Android.props | 2 +- osu.Game/osu.Game.csproj | 2 +- osu.iOS.props | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Android.props b/osu.Android.props index dbb0a6d610..7ae16b8b70 100644 --- a/osu.Android.props +++ b/osu.Android.props @@ -10,7 +10,7 @@ true - + diff --git a/osu.iOS.props b/osu.iOS.props index afbcf49d32..ece42e87b4 100644 --- a/osu.iOS.props +++ b/osu.iOS.props @@ -17,6 +17,6 @@ -all - + From 582c5180b9830e01a34a0d68db1dec850059aa43 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 14 Jan 2025 13:24:31 +0100 Subject: [PATCH 247/290] Implement spectator list display - First step for https://github.com/ppy/osu/issues/22087 - Supersedes / closes https://github.com/ppy/osu/pull/22795 Roughly uses design shown in https://github.com/ppy/osu/pull/22795#issuecomment-1579936284 with some modifications to better fit everything else, and some customisation options so it can fit better on other skins. --- .../Visual/Gameplay/TestSceneSpectatorList.cs | 49 ++++ .../Localisation/HUD/SpectatorListStrings.cs | 19 ++ osu.Game/Online/Chat/DrawableLinkCompiler.cs | 16 +- osu.Game/Screens/Play/HUD/SpectatorList.cs | 219 ++++++++++++++++++ 4 files changed, 302 insertions(+), 1 deletion(-) create mode 100644 osu.Game.Tests/Visual/Gameplay/TestSceneSpectatorList.cs create mode 100644 osu.Game/Localisation/HUD/SpectatorListStrings.cs create mode 100644 osu.Game/Screens/Play/HUD/SpectatorList.cs diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSpectatorList.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSpectatorList.cs new file mode 100644 index 0000000000..3cd37baafd --- /dev/null +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSpectatorList.cs @@ -0,0 +1,49 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Threading; +using NUnit.Framework; +using osu.Framework.Bindables; +using osu.Framework.Graphics; +using osu.Framework.Utils; +using osu.Game.Graphics; +using osu.Game.Screens.Play; +using osu.Game.Screens.Play.HUD; + +namespace osu.Game.Tests.Visual.Gameplay +{ + [TestFixture] + public partial class TestSceneSpectatorList : OsuTestScene + { + private readonly BindableList spectators = new BindableList(); + private readonly Bindable localUserPlayingState = new Bindable(); + + private int counter; + + [Test] + public void TestBasics() + { + SpectatorList list = null!; + AddStep("create spectator list", () => Child = list = new SpectatorList + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Spectators = { BindTarget = spectators }, + UserPlayingState = { BindTarget = localUserPlayingState } + }); + + AddStep("start playing", () => localUserPlayingState.Value = LocalUserPlayingState.Playing); + AddStep("add a user", () => + { + int id = Interlocked.Increment(ref counter); + spectators.Add(new SpectatorList.Spectator(id, $"User {id}")); + }); + AddStep("remove random user", () => spectators.RemoveAt(RNG.Next(0, spectators.Count))); + AddStep("enter break", () => localUserPlayingState.Value = LocalUserPlayingState.Break); + AddStep("stop playing", () => localUserPlayingState.Value = LocalUserPlayingState.NotPlaying); + AddStep("change font to venera", () => list.Font.Value = Typeface.Venera); + AddStep("change font to torus", () => list.Font.Value = Typeface.Torus); + AddStep("change header colour", () => list.HeaderColour.Value = new Colour4(RNG.NextSingle(), RNG.NextSingle(), RNG.NextSingle(), 1)); + } + } +} diff --git a/osu.Game/Localisation/HUD/SpectatorListStrings.cs b/osu.Game/Localisation/HUD/SpectatorListStrings.cs new file mode 100644 index 0000000000..8d82250526 --- /dev/null +++ b/osu.Game/Localisation/HUD/SpectatorListStrings.cs @@ -0,0 +1,19 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Localisation; + +namespace osu.Game.Localisation.HUD +{ + public static class SpectatorListStrings + { + private const string prefix = @"osu.Game.Resources.Localisation.SpectatorList"; + + /// + /// "Spectators ({0})" + /// + public static LocalisableString SpectatorCount(int arg0) => new TranslatableString(getKey(@"spectator_count"), @"Spectators ({0})", arg0); + + private static string getKey(string key) => $@"{prefix}:{key}"; + } +} diff --git a/osu.Game/Online/Chat/DrawableLinkCompiler.cs b/osu.Game/Online/Chat/DrawableLinkCompiler.cs index fa107a0e43..f640a3dab5 100644 --- a/osu.Game/Online/Chat/DrawableLinkCompiler.cs +++ b/osu.Game/Online/Chat/DrawableLinkCompiler.cs @@ -14,6 +14,7 @@ using osu.Game.Graphics.Containers; using osu.Game.Graphics.UserInterface; using osu.Game.Overlays; using osuTK; +using osuTK.Graphics; namespace osu.Game.Online.Chat { @@ -27,6 +28,18 @@ namespace osu.Game.Online.Chat /// public readonly SlimReadOnlyListWrapper Parts; + public new Color4 IdleColour + { + get => base.IdleColour; + set => base.IdleColour = value; + } + + public new Color4 HoverColour + { + get => base.HoverColour; + set => base.HoverColour = value; + } + [Resolved] private OverlayColourProvider? overlayColourProvider { get; set; } @@ -56,7 +69,8 @@ namespace osu.Game.Online.Chat [BackgroundDependencyLoader] private void load(OsuColour colours) { - IdleColour = overlayColourProvider?.Light2 ?? colours.Blue; + if (IdleColour == default) + IdleColour = overlayColourProvider?.Light2 ?? colours.Blue; } protected override IEnumerable EffectTargets => Parts; diff --git a/osu.Game/Screens/Play/HUD/SpectatorList.cs b/osu.Game/Screens/Play/HUD/SpectatorList.cs new file mode 100644 index 0000000000..ad94b23cd7 --- /dev/null +++ b/osu.Game/Screens/Play/HUD/SpectatorList.cs @@ -0,0 +1,219 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using System.Collections.Specialized; +using osu.Framework.Allocation; +using osu.Framework.Bindables; +using osu.Framework.Extensions.LocalisationExtensions; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Pooling; +using osu.Game.Configuration; +using osu.Game.Graphics; +using osu.Game.Graphics.Sprites; +using osu.Game.Online.Chat; +using osu.Game.Users; +using osu.Game.Localisation.HUD; +using osu.Game.Localisation.SkinComponents; + +namespace osu.Game.Screens.Play.HUD +{ + public partial class SpectatorList : CompositeDrawable + { + private const int max_spectators_displayed = 10; + + public BindableList Spectators { get; } = new BindableList(); + public Bindable UserPlayingState { get; } = new Bindable(); + + [SettingSource(typeof(SkinnableComponentStrings), nameof(SkinnableComponentStrings.Font), nameof(SkinnableComponentStrings.FontDescription))] + public Bindable Font { get; } = new Bindable(Typeface.Torus); + + [SettingSource(typeof(SkinnableComponentStrings), nameof(SkinnableComponentStrings.TextColour), nameof(SkinnableComponentStrings.TextColourDescription))] + public BindableColour4 HeaderColour { get; } = new BindableColour4(Colour4.White); + + protected OsuSpriteText Header { get; private set; } = null!; + + private FillFlowContainer mainFlow = null!; + private FillFlowContainer spectatorsFlow = null!; + private DrawablePool pool = null!; + + [BackgroundDependencyLoader] + private void load(OsuColour colours) + { + AutoSizeAxes = Axes.Both; + + InternalChildren = new Drawable[] + { + mainFlow = new FillFlowContainer + { + AutoSizeAxes = Axes.Both, + AutoSizeDuration = 250, + AutoSizeEasing = Easing.OutQuint, + Direction = FillDirection.Vertical, + Children = new Drawable[] + { + Header = new OsuSpriteText + { + Colour = colours.Blue0, + Font = OsuFont.GetFont(size: 12, weight: FontWeight.Bold), + }, + spectatorsFlow = new FillFlowContainer + { + AutoSizeAxes = Axes.Both, + Direction = FillDirection.Vertical, + } + } + }, + pool = new DrawablePool(max_spectators_displayed), + }; + + HeaderColour.Value = Header.Colour; + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + Spectators.BindCollectionChanged(onSpectatorsChanged, true); + UserPlayingState.BindValueChanged(_ => updateVisibility()); + + Font.BindValueChanged(_ => updateAppearance()); + HeaderColour.BindValueChanged(_ => updateAppearance(), true); + FinishTransforms(true); + } + + private void onSpectatorsChanged(object? sender, NotifyCollectionChangedEventArgs e) + { + switch (e.Action) + { + case NotifyCollectionChangedAction.Add: + { + for (int i = 0; i < e.NewItems!.Count; i++) + { + var spectator = (Spectator)e.NewItems![i]!; + int index = e.NewStartingIndex + i; + + if (index >= max_spectators_displayed) + break; + + spectatorsFlow.Insert(e.NewStartingIndex + i, pool.Get(entry => + { + entry.Current.Value = spectator; + entry.UserPlayingState = UserPlayingState; + })); + } + + break; + } + + case NotifyCollectionChangedAction.Remove: + { + spectatorsFlow.RemoveAll(entry => e.OldItems!.Contains(entry.Current.Value), false); + + for (int i = 0; i < spectatorsFlow.Count; i++) + spectatorsFlow.SetLayoutPosition(spectatorsFlow[i], i); + + if (Spectators.Count >= max_spectators_displayed && spectatorsFlow.Count < max_spectators_displayed) + { + for (int i = spectatorsFlow.Count; i < max_spectators_displayed; i++) + { + var spectator = Spectators[i]; + spectatorsFlow.Insert(i, pool.Get(entry => + { + entry.Current.Value = spectator; + entry.UserPlayingState = UserPlayingState; + })); + } + } + + break; + } + + case NotifyCollectionChangedAction.Reset: + { + spectatorsFlow.Clear(false); + break; + } + + default: + throw new NotSupportedException(); + } + + Header.Text = SpectatorListStrings.SpectatorCount(Spectators.Count).ToUpper(); + updateVisibility(); + } + + private void updateVisibility() + { + mainFlow.FadeTo(Spectators.Count > 0 && UserPlayingState.Value != LocalUserPlayingState.NotPlaying ? 1 : 0, 250, Easing.OutQuint); + } + + private void updateAppearance() + { + Header.Font = OsuFont.GetFont(Font.Value, 12, FontWeight.Bold); + Header.Colour = HeaderColour.Value; + } + + private partial class SpectatorListEntry : PoolableDrawable + { + public Bindable Current { get; } = new Bindable(); + + private readonly BindableWithCurrent current = new BindableWithCurrent(); + + public Bindable UserPlayingState + { + get => current.Current; + set => current.Current = value; + } + + private OsuSpriteText username = null!; + private DrawableLinkCompiler? linkCompiler; + + [Resolved] + private OsuGame? game { get; set; } + + [BackgroundDependencyLoader] + private void load() + { + AutoSizeAxes = Axes.Both; + + InternalChildren = new Drawable[] + { + username = new OsuSpriteText(), + }; + } + + protected override void LoadComplete() + { + base.LoadComplete(); + UserPlayingState.BindValueChanged(_ => updateEnabledState()); + Current.BindValueChanged(_ => updateState(), true); + } + + private void updateState() + { + username.Text = Current.Value.Username; + linkCompiler?.Expire(); + AddInternal(linkCompiler = new DrawableLinkCompiler([username]) + { + IdleColour = Colour4.White, + Action = () => game?.HandleLink(new LinkDetails(LinkAction.OpenUserProfile, Current.Value)), + }); + updateEnabledState(); + } + + private void updateEnabledState() + { + if (linkCompiler != null) + linkCompiler.Enabled.Value = UserPlayingState.Value != LocalUserPlayingState.Playing; + } + } + + public record Spectator(int OnlineID, string Username) : IUser + { + public CountryCode CountryCode => CountryCode.Unknown; + public bool IsBot => false; + } + } +} From 2eb63e6fe045f7e2b6087897669add86cc8932cf Mon Sep 17 00:00:00 2001 From: Andrei Zavatski Date: Wed, 15 Jan 2025 20:38:51 +0300 Subject: [PATCH 248/290] Simplify rotation sync with no clocks involved --- osu.Game.Rulesets.Osu/UI/Cursor/CursorTrail.cs | 8 ++------ osu.Game.Rulesets.Osu/UI/Cursor/OsuCursor.cs | 5 +++++ osu.Game.Rulesets.Osu/UI/Cursor/OsuCursorContainer.cs | 3 +++ 3 files changed, 10 insertions(+), 6 deletions(-) diff --git a/osu.Game.Rulesets.Osu/UI/Cursor/CursorTrail.cs b/osu.Game.Rulesets.Osu/UI/Cursor/CursorTrail.cs index 5b7d2d40d3..7809a0bf05 100644 --- a/osu.Game.Rulesets.Osu/UI/Cursor/CursorTrail.cs +++ b/osu.Game.Rulesets.Osu/UI/Cursor/CursorTrail.cs @@ -18,7 +18,6 @@ using osu.Framework.Graphics.Visualisation; using osu.Framework.Input; using osu.Framework.Input.Events; using osu.Framework.Timing; -using osu.Game.Rulesets.Osu.Skinning.Legacy; using osuTK; using osuTK.Graphics; using osuTK.Graphics.ES30; @@ -41,6 +40,7 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor private double timeOffset; private float time; protected bool Spin { get; set; } + public float PartRotation { get; set; } /// /// The scale used on creation of a new trail part. @@ -80,12 +80,9 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor shader = shaders.Load(@"CursorTrail", FragmentShaderDescriptor.TEXTURE); } - private double loadCompleteTime; - protected override void LoadComplete() { base.LoadComplete(); - loadCompleteTime = Parent!.Clock.CurrentTime; // using parent's clock since our is overridden resetTime(); } @@ -245,8 +242,7 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor texture = Source.texture; time = Source.time; fadeExponent = Source.FadeExponent; - // The goal is to sync trail rotation with the cursor. Cursor uses spin transform which starts rotation at LoadComplete time. - angle = Source.Spin ? (float)((Source.Parent!.Clock.CurrentTime - Source.loadCompleteTime) * 2 * Math.PI / LegacyCursor.REVOLUTION_DURATION) : 0; + angle = Source.Spin ? float.DegreesToRadians(Source.PartRotation) : 0; originPosition = Vector2.Zero; diff --git a/osu.Game.Rulesets.Osu/UI/Cursor/OsuCursor.cs b/osu.Game.Rulesets.Osu/UI/Cursor/OsuCursor.cs index c2f7d84f5e..e84fb9e2d6 100644 --- a/osu.Game.Rulesets.Osu/UI/Cursor/OsuCursor.cs +++ b/osu.Game.Rulesets.Osu/UI/Cursor/OsuCursor.cs @@ -36,6 +36,11 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor /// public Vector2 CurrentExpandedScale => skinnableCursor.ExpandTarget?.Scale ?? Vector2.One; + /// + /// The current rotation of the cursor. + /// + public float CurrentRotation => skinnableCursor.ExpandTarget?.Rotation ?? 0; + public IBindable CursorScale => cursorScale; /// diff --git a/osu.Game.Rulesets.Osu/UI/Cursor/OsuCursorContainer.cs b/osu.Game.Rulesets.Osu/UI/Cursor/OsuCursorContainer.cs index 8c0871d54f..974d99d7c8 100644 --- a/osu.Game.Rulesets.Osu/UI/Cursor/OsuCursorContainer.cs +++ b/osu.Game.Rulesets.Osu/UI/Cursor/OsuCursorContainer.cs @@ -83,7 +83,10 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor base.Update(); if (cursorTrail.Drawable is CursorTrail trail) + { trail.NewPartScale = ActiveCursor.CurrentExpandedScale; + trail.PartRotation = ActiveCursor.CurrentRotation; + } } public bool OnPressed(KeyBindingPressEvent e) From 6008c3138ead169b6586dfaf481afa832cda3bc6 Mon Sep 17 00:00:00 2001 From: Shawn Presser Date: Wed, 15 Jan 2025 19:29:41 -0600 Subject: [PATCH 249/290] Typo fix --- osu.Game/Rulesets/Scoring/HitResult.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Rulesets/Scoring/HitResult.cs b/osu.Game/Rulesets/Scoring/HitResult.cs index b6cfca58db..46c0371d9f 100644 --- a/osu.Game/Rulesets/Scoring/HitResult.cs +++ b/osu.Game/Rulesets/Scoring/HitResult.cs @@ -28,7 +28,7 @@ namespace osu.Game.Rulesets.Scoring /// /// /// This miss window should determine how early a hit can be before it is considered for judgement (as opposed to being ignored as - /// "too far in the future). It should also define when a forced miss should be triggered (as a result of no user input in time). + /// "too far in the future"). It should also define when a forced miss should be triggered (as a result of no user input in time). /// [Description(@"Miss")] [EnumMember(Value = "miss")] From 920648c267484c4e57386bbc39bd3a83c6f9ac35 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Thu, 16 Jan 2025 14:00:27 +0900 Subject: [PATCH 250/290] Minor refactorings and xmldoc additions --- .../Skinning/Legacy/LegacyCursorTrail.cs | 2 +- .../UI/Cursor/CursorTrail.cs | 48 +++++++++++++------ 2 files changed, 34 insertions(+), 16 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyCursorTrail.cs b/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyCursorTrail.cs index 4c21b94326..375bef721d 100644 --- a/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyCursorTrail.cs +++ b/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyCursorTrail.cs @@ -34,7 +34,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy private void load(OsuConfigManager config, ISkinSource skinSource) { cursorSize = config.GetBindable(OsuSetting.GameplayCursorSize).GetBoundCopy(); - Spin = skin.GetConfig(OsuSkinConfiguration.CursorTrailRotate)?.Value ?? true; + AllowPartRotation = skin.GetConfig(OsuSkinConfiguration.CursorTrailRotate)?.Value ?? true; Texture = skin.GetTexture("cursortrail"); diff --git a/osu.Game.Rulesets.Osu/UI/Cursor/CursorTrail.cs b/osu.Game.Rulesets.Osu/UI/Cursor/CursorTrail.cs index 7809a0bf05..1c2d69fa00 100644 --- a/osu.Game.Rulesets.Osu/UI/Cursor/CursorTrail.cs +++ b/osu.Game.Rulesets.Osu/UI/Cursor/CursorTrail.cs @@ -34,21 +34,24 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor /// protected virtual float FadeExponent => 1.7f; - private readonly TrailPart[] parts = new TrailPart[max_sprites]; - private int currentIndex; - private IShader shader; - private double timeOffset; - private float time; - protected bool Spin { get; set; } - public float PartRotation { get; set; } - /// /// The scale used on creation of a new trail part. /// - public Vector2 NewPartScale = Vector2.One; + public Vector2 NewPartScale { get; set; } = Vector2.One; - private Anchor trailOrigin = Anchor.Centre; + /// + /// The rotation (in degrees) to apply to trail parts when is true. + /// + public float PartRotation { get; set; } + /// + /// Whether to rotate trail parts based on the value of . + /// + protected bool AllowPartRotation { get; set; } + + /// + /// The trail part texture origin. + /// protected Anchor TrailOrigin { get => trailOrigin; @@ -59,6 +62,13 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor } } + private readonly TrailPart[] parts = new TrailPart[max_sprites]; + private Anchor trailOrigin = Anchor.Centre; + private int currentIndex; + private IShader shader; + private double timeOffset; + private float time; + public CursorTrail() { // as we are currently very dependent on having a running clock, let's make our own clock for the time being. @@ -242,7 +252,7 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor texture = Source.texture; time = Source.time; fadeExponent = Source.FadeExponent; - angle = Source.Spin ? float.DegreesToRadians(Source.PartRotation) : 0; + angle = Source.AllowPartRotation ? float.DegreesToRadians(Source.PartRotation) : 0; originPosition = Vector2.Zero; @@ -296,7 +306,9 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor vertexBatch.Add(new TexturedTrailVertex { - Position = rotateAround(new Vector2(part.Position.X - texture.DisplayWidth * originPosition.X * part.Scale.X, part.Position.Y + texture.DisplayHeight * (1 - originPosition.Y) * part.Scale.Y), part.Position, sin, cos), + Position = rotateAround( + new Vector2(part.Position.X - texture.DisplayWidth * originPosition.X * part.Scale.X, part.Position.Y + texture.DisplayHeight * (1 - originPosition.Y) * part.Scale.Y), + part.Position, sin, cos), TexturePosition = textureRect.BottomLeft, TextureRect = new Vector4(0, 0, 1, 1), Colour = DrawColourInfo.Colour.BottomLeft.Linear, @@ -305,7 +317,9 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor vertexBatch.Add(new TexturedTrailVertex { - Position = rotateAround(new Vector2(part.Position.X + texture.DisplayWidth * (1 - originPosition.X) * part.Scale.X, part.Position.Y + texture.DisplayHeight * (1 - originPosition.Y) * part.Scale.Y), part.Position, sin, cos), + Position = rotateAround( + new Vector2(part.Position.X + texture.DisplayWidth * (1 - originPosition.X) * part.Scale.X, + part.Position.Y + texture.DisplayHeight * (1 - originPosition.Y) * part.Scale.Y), part.Position, sin, cos), TexturePosition = textureRect.BottomRight, TextureRect = new Vector4(0, 0, 1, 1), Colour = DrawColourInfo.Colour.BottomRight.Linear, @@ -314,7 +328,9 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor vertexBatch.Add(new TexturedTrailVertex { - Position = rotateAround(new Vector2(part.Position.X + texture.DisplayWidth * (1 - originPosition.X) * part.Scale.X, part.Position.Y - texture.DisplayHeight * originPosition.Y * part.Scale.Y), part.Position, sin, cos), + Position = rotateAround( + new Vector2(part.Position.X + texture.DisplayWidth * (1 - originPosition.X) * part.Scale.X, part.Position.Y - texture.DisplayHeight * originPosition.Y * part.Scale.Y), + part.Position, sin, cos), TexturePosition = textureRect.TopRight, TextureRect = new Vector4(0, 0, 1, 1), Colour = DrawColourInfo.Colour.TopRight.Linear, @@ -323,7 +339,9 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor vertexBatch.Add(new TexturedTrailVertex { - Position = rotateAround(new Vector2(part.Position.X - texture.DisplayWidth * originPosition.X * part.Scale.X, part.Position.Y - texture.DisplayHeight * originPosition.Y * part.Scale.Y), part.Position, sin, cos), + Position = rotateAround( + new Vector2(part.Position.X - texture.DisplayWidth * originPosition.X * part.Scale.X, part.Position.Y - texture.DisplayHeight * originPosition.Y * part.Scale.Y), + part.Position, sin, cos), TexturePosition = textureRect.TopLeft, TextureRect = new Vector4(0, 0, 1, 1), Colour = DrawColourInfo.Colour.TopLeft.Linear, From fe8389bc2b0a65c39351275f3db4e79b6afc514c Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Thu, 16 Jan 2025 14:11:21 +0900 Subject: [PATCH 251/290] Add test --- .../TestSceneCursorTrail.cs | 51 ++++++++++++++++++- 1 file changed, 49 insertions(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneCursorTrail.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneCursorTrail.cs index 17f365f820..a8a65f7edb 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneCursorTrail.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneCursorTrail.cs @@ -6,6 +6,7 @@ using System; using System.Collections.Generic; using System.Linq; +using JetBrains.Annotations; using NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Audio.Sample; @@ -17,6 +18,7 @@ using osu.Framework.Graphics.Textures; using osu.Framework.Testing; using osu.Framework.Testing.Input; using osu.Game.Audio; +using osu.Game.Rulesets.Osu.Skinning; using osu.Game.Rulesets.Osu.Skinning.Legacy; using osu.Game.Rulesets.Osu.UI.Cursor; using osu.Game.Skinning; @@ -103,6 +105,23 @@ namespace osu.Game.Rulesets.Osu.Tests AddStep("contract", () => this.ChildrenOfType().Single().NewPartScale = Vector2.One); } + [Test] + public void TestRotation() + { + createTest(() => + { + var skinContainer = new LegacySkinContainer(renderer, provideMiddle: true, enableRotation: true); + var legacyCursorTrail = new LegacyRotatingCursorTrail(skinContainer) + { + NewPartScale = new Vector2(10) + }; + + skinContainer.Child = legacyCursorTrail; + + return skinContainer; + }); + } + private void createTest(Func createContent) => AddStep("create trail", () => { Clear(); @@ -121,12 +140,14 @@ namespace osu.Game.Rulesets.Osu.Tests private readonly IRenderer renderer; private readonly bool provideMiddle; private readonly bool provideCursor; + private readonly bool enableRotation; - public LegacySkinContainer(IRenderer renderer, bool provideMiddle, bool provideCursor = true) + public LegacySkinContainer(IRenderer renderer, bool provideMiddle, bool provideCursor = true, bool enableRotation = false) { this.renderer = renderer; this.provideMiddle = provideMiddle; this.provideCursor = provideCursor; + this.enableRotation = enableRotation; RelativeSizeAxes = Axes.Both; } @@ -152,7 +173,19 @@ namespace osu.Game.Rulesets.Osu.Tests public ISample GetSample(ISampleInfo sampleInfo) => null; - public IBindable GetConfig(TLookup lookup) => null; + public IBindable GetConfig(TLookup lookup) + { + switch (lookup) + { + case OsuSkinConfiguration osuLookup: + if (osuLookup == OsuSkinConfiguration.CursorTrailRotate) + return SkinUtils.As(new BindableBool(enableRotation)); + + break; + } + + return null; + } public ISkin FindProvider(Func lookupFunction) => lookupFunction(this) ? this : null; @@ -185,5 +218,19 @@ namespace osu.Game.Rulesets.Osu.Tests MoveMouseTo(ToScreenSpace(DrawSize / 2 + DrawSize / 3 * rPos)); } } + + private partial class LegacyRotatingCursorTrail : LegacyCursorTrail + { + public LegacyRotatingCursorTrail([NotNull] ISkin skin) + : base(skin) + { + } + + protected override void Update() + { + base.Update(); + PartRotation += (float)(Time.Elapsed * 0.1); + } + } } } From b54d95926329c0af71df64458196ec4339b66147 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Thu, 16 Jan 2025 17:05:18 +0900 Subject: [PATCH 252/290] Expose as IBindable from IAPIProvider, writes via config --- .../Visual/Menus/TestSceneLoginOverlay.cs | 27 ++++++++++--------- osu.Game/Configuration/OsuConfigManager.cs | 5 ++++ osu.Game/Online/API/APIAccess.cs | 10 ++++--- osu.Game/Online/API/DummyAPIAccess.cs | 2 +- osu.Game/Online/API/IAPIProvider.cs | 6 ++--- .../Online/Metadata/OnlineMetadataClient.cs | 1 - osu.Game/Overlays/Login/LoginPanel.cs | 20 ++++++++------ 7 files changed, 41 insertions(+), 30 deletions(-) diff --git a/osu.Game.Tests/Visual/Menus/TestSceneLoginOverlay.cs b/osu.Game.Tests/Visual/Menus/TestSceneLoginOverlay.cs index 5c12e0c102..3c97b291ee 100644 --- a/osu.Game.Tests/Visual/Menus/TestSceneLoginOverlay.cs +++ b/osu.Game.Tests/Visual/Menus/TestSceneLoginOverlay.cs @@ -29,9 +29,7 @@ namespace osu.Game.Tests.Visual.Menus private DummyAPIAccess dummyAPI => (DummyAPIAccess)API; private LoginOverlay loginOverlay = null!; - - [Resolved] - private OsuConfigManager configManager { get; set; } = null!; + private OsuConfigManager localConfig = null!; [Cached(typeof(LocalUserStatisticsProvider))] private readonly TestSceneUserPanel.TestUserStatisticsProvider statisticsProvider = new TestSceneUserPanel.TestUserStatisticsProvider(); @@ -39,6 +37,8 @@ namespace osu.Game.Tests.Visual.Menus [BackgroundDependencyLoader] private void load() { + Dependencies.Cache(localConfig = new OsuConfigManager(LocalStorage)); + Child = loginOverlay = new LoginOverlay { Anchor = Anchor.Centre, @@ -49,6 +49,7 @@ namespace osu.Game.Tests.Visual.Menus [SetUpSteps] public void SetUpSteps() { + AddStep("reset online state", () => localConfig.SetValue(OsuSetting.UserOnlineStatus, UserStatus.Online)); AddStep("show login overlay", () => loginOverlay.Show()); } @@ -89,7 +90,7 @@ namespace osu.Game.Tests.Visual.Menus AddStep("clear handler", () => dummyAPI.HandleRequest = null); assertDropdownState(UserAction.Online); - AddStep("change user state", () => dummyAPI.Status.Value = UserStatus.DoNotDisturb); + AddStep("change user state", () => localConfig.SetValue(OsuSetting.UserOnlineStatus, UserStatus.DoNotDisturb)); assertDropdownState(UserAction.DoNotDisturb); } @@ -188,31 +189,31 @@ namespace osu.Game.Tests.Visual.Menus public void TestUncheckingRememberUsernameClearsIt() { AddStep("logout", () => API.Logout()); - AddStep("set username", () => configManager.SetValue(OsuSetting.Username, "test_user")); - AddStep("set remember password", () => configManager.SetValue(OsuSetting.SavePassword, true)); + AddStep("set username", () => localConfig.SetValue(OsuSetting.Username, "test_user")); + AddStep("set remember password", () => localConfig.SetValue(OsuSetting.SavePassword, true)); AddStep("uncheck remember username", () => { InputManager.MoveMouseTo(loginOverlay.ChildrenOfType().First()); InputManager.Click(MouseButton.Left); }); - AddAssert("remember username off", () => configManager.Get(OsuSetting.SaveUsername), () => Is.False); - AddAssert("remember password off", () => configManager.Get(OsuSetting.SavePassword), () => Is.False); - AddAssert("username cleared", () => configManager.Get(OsuSetting.Username), () => Is.Empty); + AddAssert("remember username off", () => localConfig.Get(OsuSetting.SaveUsername), () => Is.False); + AddAssert("remember password off", () => localConfig.Get(OsuSetting.SavePassword), () => Is.False); + AddAssert("username cleared", () => localConfig.Get(OsuSetting.Username), () => Is.Empty); } [Test] public void TestUncheckingRememberPasswordClearsToken() { AddStep("logout", () => API.Logout()); - AddStep("set token", () => configManager.SetValue(OsuSetting.Token, "test_token")); - AddStep("set remember password", () => configManager.SetValue(OsuSetting.SavePassword, true)); + AddStep("set token", () => localConfig.SetValue(OsuSetting.Token, "test_token")); + AddStep("set remember password", () => localConfig.SetValue(OsuSetting.SavePassword, true)); AddStep("uncheck remember token", () => { InputManager.MoveMouseTo(loginOverlay.ChildrenOfType().Last()); InputManager.Click(MouseButton.Left); }); - AddAssert("remember password off", () => configManager.Get(OsuSetting.SavePassword), () => Is.False); - AddAssert("token cleared", () => configManager.Get(OsuSetting.Token), () => Is.Empty); + AddAssert("remember password off", () => localConfig.Get(OsuSetting.SavePassword), () => Is.False); + AddAssert("token cleared", () => localConfig.Get(OsuSetting.Token), () => Is.Empty); } } } diff --git a/osu.Game/Configuration/OsuConfigManager.cs b/osu.Game/Configuration/OsuConfigManager.cs index 642da16d2d..d4f5b2af76 100644 --- a/osu.Game/Configuration/OsuConfigManager.cs +++ b/osu.Game/Configuration/OsuConfigManager.cs @@ -443,7 +443,12 @@ namespace osu.Game.Configuration EditorShowSpeedChanges, TouchDisableGameplayTaps, ModSelectTextSearchStartsActive, + + /// + /// The status for the current user to broadcast to other players. + /// UserOnlineStatus, + MultiplayerRoomFilter, HideCountryFlags, EditorTimelineShowTimingChanges, diff --git a/osu.Game/Online/API/APIAccess.cs b/osu.Game/Online/API/APIAccess.cs index a4ac577a02..dcb8a193bc 100644 --- a/osu.Game/Online/API/APIAccess.cs +++ b/osu.Game/Online/API/APIAccess.cs @@ -60,7 +60,7 @@ namespace osu.Game.Online.API public IBindable LocalUser => localUser; public IBindableList Friends => friends; - public Bindable Status { get; } = new Bindable(UserStatus.Online); + public IBindable Status => configStatus; public IBindable Activity => activity; public INotificationsClient NotificationsClient { get; } @@ -75,8 +75,8 @@ namespace osu.Game.Online.API protected bool HasLogin => authentication.Token.Value != null || (!string.IsNullOrEmpty(ProvidedUsername) && !string.IsNullOrEmpty(password)); + private readonly Bindable configStatus = new Bindable(); private readonly CancellationTokenSource cancellationToken = new CancellationTokenSource(); - private readonly Logger log; public APIAccess(OsuGameBase game, OsuConfigManager config, EndpointConfiguration endpointConfiguration, string versionHash) @@ -108,7 +108,7 @@ namespace osu.Game.Online.API authentication.TokenString = config.Get(OsuSetting.Token); authentication.Token.ValueChanged += onTokenChanged; - config.BindWith(OsuSetting.UserOnlineStatus, Status); + config.BindWith(OsuSetting.UserOnlineStatus, configStatus); if (HasLogin) { @@ -591,7 +591,9 @@ namespace osu.Game.Online.API password = null; SecondFactorCode = null; authentication.Clear(); - Status.Value = UserStatus.Online; + + // Reset the status to be broadcast on the next login, in case multiple players share the same system. + configStatus.Value = UserStatus.Online; // Scheduled prior to state change such that the state changed event is invoked with the correct user and their friends present Schedule(() => diff --git a/osu.Game/Online/API/DummyAPIAccess.cs b/osu.Game/Online/API/DummyAPIAccess.cs index b338f4e8cb..4cd3c02414 100644 --- a/osu.Game/Online/API/DummyAPIAccess.cs +++ b/osu.Game/Online/API/DummyAPIAccess.cs @@ -28,7 +28,7 @@ namespace osu.Game.Online.API public BindableList Friends { get; } = new BindableList(); - public Bindable Status { get; } = new Bindable(UserStatus.Online); + public IBindable Status { get; } = new Bindable(UserStatus.Online); public Bindable Activity { get; } = new Bindable(); diff --git a/osu.Game/Online/API/IAPIProvider.cs b/osu.Game/Online/API/IAPIProvider.cs index cc065a659a..9ac7343885 100644 --- a/osu.Game/Online/API/IAPIProvider.cs +++ b/osu.Game/Online/API/IAPIProvider.cs @@ -25,12 +25,12 @@ namespace osu.Game.Online.API IBindableList Friends { get; } /// - /// The current user's status. + /// The status for the current user that's broadcast to other players. /// - Bindable Status { get; } + IBindable Status { get; } /// - /// The current user's activity. + /// The activity for the current user that's broadcast to other players. /// IBindable Activity { get; } diff --git a/osu.Game/Online/Metadata/OnlineMetadataClient.cs b/osu.Game/Online/Metadata/OnlineMetadataClient.cs index b3204a7cd1..101307636a 100644 --- a/osu.Game/Online/Metadata/OnlineMetadataClient.cs +++ b/osu.Game/Online/Metadata/OnlineMetadataClient.cs @@ -37,7 +37,6 @@ namespace osu.Game.Online.Metadata private IHubClientConnector? connector; private Bindable lastQueueId = null!; private IBindable localUser = null!; - private IBindable userStatus = null!; private IBindable userActivity = null!; diff --git a/osu.Game/Overlays/Login/LoginPanel.cs b/osu.Game/Overlays/Login/LoginPanel.cs index b947731f8b..6d74fc442e 100644 --- a/osu.Game/Overlays/Login/LoginPanel.cs +++ b/osu.Game/Overlays/Login/LoginPanel.cs @@ -9,6 +9,7 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Primitives; using osu.Framework.Input.Events; +using osu.Game.Configuration; using osu.Game.Graphics; using osu.Game.Graphics.Containers; using osu.Game.Graphics.Sprites; @@ -37,12 +38,15 @@ namespace osu.Game.Overlays.Login /// public Action? RequestHide; - private readonly Bindable status = new Bindable(); private readonly IBindable apiState = new Bindable(); + private readonly Bindable configUserStatus = new Bindable(); [Resolved] private IAPIProvider api { get; set; } = null!; + [Resolved] + private OsuConfigManager config { get; set; } = null!; + public override RectangleF BoundingBox => bounding ? base.BoundingBox : RectangleF.Empty; public bool Bounding @@ -65,11 +69,11 @@ namespace osu.Game.Overlays.Login { base.LoadComplete(); + config.BindWith(OsuSetting.UserOnlineStatus, configUserStatus); + configUserStatus.BindValueChanged(e => updateDropdownCurrent(e.NewValue), true); + apiState.BindTo(api.State); apiState.BindValueChanged(onlineStateChanged, true); - - status.BindTo(api.Status); - status.BindValueChanged(e => updateDropdownCurrent(e.NewValue), true); } private void onlineStateChanged(ValueChangedEvent state) => Schedule(() => @@ -148,23 +152,23 @@ namespace osu.Game.Overlays.Login }, }; - updateDropdownCurrent(status.Value); + updateDropdownCurrent(configUserStatus.Value); dropdown.Current.BindValueChanged(action => { switch (action.NewValue) { case UserAction.Online: - status.Value = UserStatus.Online; + configUserStatus.Value = UserStatus.Online; dropdown.StatusColour = colours.Green; break; case UserAction.DoNotDisturb: - status.Value = UserStatus.DoNotDisturb; + configUserStatus.Value = UserStatus.DoNotDisturb; dropdown.StatusColour = colours.Red; break; case UserAction.AppearOffline: - status.Value = UserStatus.Offline; + configUserStatus.Value = UserStatus.Offline; dropdown.StatusColour = colours.Gray7; break; From c1f0c47586a3816936a5148732ccd4545eaf0a9b Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Thu, 16 Jan 2025 17:06:54 +0900 Subject: [PATCH 253/290] Allow setting of DummyAPIAccess status --- osu.Game/Online/API/DummyAPIAccess.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/osu.Game/Online/API/DummyAPIAccess.cs b/osu.Game/Online/API/DummyAPIAccess.cs index 4cd3c02414..3fef2b59cf 100644 --- a/osu.Game/Online/API/DummyAPIAccess.cs +++ b/osu.Game/Online/API/DummyAPIAccess.cs @@ -28,7 +28,7 @@ namespace osu.Game.Online.API public BindableList Friends { get; } = new BindableList(); - public IBindable Status { get; } = new Bindable(UserStatus.Online); + public Bindable Status { get; } = new Bindable(UserStatus.Online); public Bindable Activity { get; } = new Bindable(); @@ -197,6 +197,7 @@ namespace osu.Game.Online.API IBindable IAPIProvider.LocalUser => LocalUser; IBindableList IAPIProvider.Friends => Friends; + IBindable IAPIProvider.Status => Status; IBindable IAPIProvider.Activity => Activity; /// From a4174a36447fddeeb13c83fa6724520486271c62 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 16 Jan 2025 17:39:34 +0900 Subject: [PATCH 254/290] Add failing test coverage showing offset adjust is not limited correctly --- .../Navigation/TestSceneScreenNavigation.cs | 76 +++++++++++++++++++ 1 file changed, 76 insertions(+) diff --git a/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs b/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs index 58e780cf16..326f21ff13 100644 --- a/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs +++ b/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs @@ -317,6 +317,82 @@ namespace osu.Game.Tests.Visual.Navigation AddUntilStep("wait for song select", () => songSelect.IsCurrentScreen()); } + [Test] + public void TestOffsetAdjustDuringPause() + { + Player player = null; + + Screens.Select.SongSelect songSelect = null; + PushAndConfirm(() => songSelect = new TestPlaySongSelect()); + AddUntilStep("wait for song select", () => songSelect.BeatmapSetsLoaded); + + AddStep("import beatmap", () => BeatmapImportHelper.LoadQuickOszIntoOsu(Game).WaitSafely()); + + AddUntilStep("wait for selected", () => !Game.Beatmap.IsDefault); + + AddStep("set mods", () => Game.SelectedMods.Value = new Mod[] { new OsuModNoFail() }); + AddStep("press enter", () => InputManager.Key(Key.Enter)); + + AddUntilStep("wait for player", () => + { + DismissAnyNotifications(); + player = Game.ScreenStack.CurrentScreen as Player; + return player?.IsLoaded == true; + }); + + AddUntilStep("wait for track playing", () => Game.Beatmap.Value.Track.IsRunning); + checkOffset(0); + + AddStep("adjust offset via keyboard", () => InputManager.Key(Key.Minus)); + checkOffset(-1); + + AddStep("pause", () => player.ChildrenOfType().First().Stop()); + AddUntilStep("wait for pause", () => player.ChildrenOfType().First().IsPaused.Value, () => Is.True); + AddStep("attempt adjust offset via keyboard", () => InputManager.Key(Key.Minus)); + checkOffset(-1); + + void checkOffset(double offset) => AddUntilStep($"offset is {offset}", () => Game.BeatmapManager.QueryBeatmap(b => b.ID == Game.Beatmap.Value.BeatmapInfo.ID)!.UserSettings.Offset, + () => Is.EqualTo(offset)); + } + + [Test] + public void TestOffsetAdjustDuringGameplay() + { + Player player = null; + + Screens.Select.SongSelect songSelect = null; + PushAndConfirm(() => songSelect = new TestPlaySongSelect()); + AddUntilStep("wait for song select", () => songSelect.BeatmapSetsLoaded); + + AddStep("import beatmap", () => BeatmapImportHelper.LoadOszIntoOsu(Game).WaitSafely()); + + AddUntilStep("wait for selected", () => !Game.Beatmap.IsDefault); + + AddStep("set mods", () => Game.SelectedMods.Value = new Mod[] { new OsuModNoFail() }); + AddStep("press enter", () => InputManager.Key(Key.Enter)); + + AddUntilStep("wait for player", () => + { + DismissAnyNotifications(); + player = Game.ScreenStack.CurrentScreen as Player; + return player?.IsLoaded == true; + }); + + AddUntilStep("wait for track playing", () => Game.Beatmap.Value.Track.IsRunning); + checkOffset(0); + + AddStep("adjust offset via keyboard", () => InputManager.Key(Key.Minus)); + checkOffset(-1); + + AddStep("seek beyond 10 seconds", () => player.ChildrenOfType().First().Seek(10500)); + AddUntilStep("wait for seek", () => player.ChildrenOfType().First().CurrentTime, () => Is.GreaterThan(10600)); + AddStep("attempt adjust offset via keyboard", () => InputManager.Key(Key.Minus)); + checkOffset(-1); + + void checkOffset(double offset) => AddUntilStep($"offset is {offset}", () => Game.BeatmapManager.QueryBeatmap(b => b.ID == Game.Beatmap.Value.BeatmapInfo.ID)!.UserSettings.Offset, + () => Is.EqualTo(offset)); + } + [Test] public void TestRetryCountIncrements() { From 1d240eb4050d1c195e17cb36c0e511a1e834b6c2 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 16 Jan 2025 17:23:02 +0900 Subject: [PATCH 255/290] Fix gameplay limitations for adjusting offset not actually being applied --- osu.Game/Screens/Play/Player.cs | 1 + .../PlayerSettings/BeatmapOffsetControl.cs | 46 +++++++++++++------ 2 files changed, 33 insertions(+), 14 deletions(-) diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs index 228b77b780..513f4854ad 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -322,6 +322,7 @@ namespace osu.Game.Screens.Play } dependencies.CacheAs(DrawableRuleset.FrameStableClock); + dependencies.CacheAs(DrawableRuleset.FrameStableClock); // add the overlay components as a separate step as they proxy some elements from the above underlay/gameplay components. // also give the overlays the ruleset skin provider to allow rulesets to potentially override HUD elements (used to disable combo counters etc.) diff --git a/osu.Game/Screens/Play/PlayerSettings/BeatmapOffsetControl.cs b/osu.Game/Screens/Play/PlayerSettings/BeatmapOffsetControl.cs index ac224794ea..e988760834 100644 --- a/osu.Game/Screens/Play/PlayerSettings/BeatmapOffsetControl.cs +++ b/osu.Game/Screens/Play/PlayerSettings/BeatmapOffsetControl.cs @@ -274,20 +274,36 @@ namespace osu.Game.Screens.Play.PlayerSettings beatmapOffsetSubscription?.Dispose(); } + protected override void Update() + { + base.Update(); + Current.Disabled = !allowOffsetAdjust; + } + + private bool allowOffsetAdjust + { + get + { + // General limitations to ensure players don't do anything too weird. + // These match stable for now. + if (player is SubmittingPlayer) + { + Debug.Assert(gameplayClock != null); + + // TODO: the blocking conditions should probably display a message. + if (!player.IsBreakTime.Value && gameplayClock.CurrentTime - gameplayClock.StartTime > 10000) + return false; + + if (gameplayClock.IsPaused.Value) + return false; + } + + return true; + } + } + public bool OnPressed(KeyBindingPressEvent e) { - // General limitations to ensure players don't do anything too weird. - // These match stable for now. - if (player is SubmittingPlayer) - { - // TODO: the blocking conditions should probably display a message. - if (player?.IsBreakTime.Value == false && gameplayClock?.CurrentTime - gameplayClock?.StartTime > 10000) - return false; - - if (gameplayClock?.IsPaused.Value == true) - return false; - } - // To match stable, this should adjust by 5 ms, or 1 ms when holding alt. // But that is hard to make work with global actions due to the operating mode. // Let's use the more precise as a default for now. @@ -296,11 +312,13 @@ namespace osu.Game.Screens.Play.PlayerSettings switch (e.Action) { case GlobalAction.IncreaseOffset: - Current.Value += amount; + if (!Current.Disabled) + Current.Value += amount; return true; case GlobalAction.DecreaseOffset: - Current.Value -= amount; + if (!Current.Disabled) + Current.Value -= amount; return true; } From cde8e7b82e204010fad79177f9fa3aa3a7f35b84 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 16 Jan 2025 18:54:51 +0900 Subject: [PATCH 256/290] Fix idle/hover colour handling weirdness in `OsuHoverContainer` --- .../Graphics/Containers/OsuHoverContainer.cs | 16 +++++++++------- osu.Game/Online/Chat/DrawableLinkCompiler.cs | 16 +--------------- .../Profile/Header/Components/FollowersButton.cs | 10 +++++++--- 3 files changed, 17 insertions(+), 25 deletions(-) diff --git a/osu.Game/Graphics/Containers/OsuHoverContainer.cs b/osu.Game/Graphics/Containers/OsuHoverContainer.cs index 3b5e48d23e..e396eb6ec9 100644 --- a/osu.Game/Graphics/Containers/OsuHoverContainer.cs +++ b/osu.Game/Graphics/Containers/OsuHoverContainer.cs @@ -15,9 +15,11 @@ namespace osu.Game.Graphics.Containers { protected const float FADE_DURATION = 500; - protected Color4 HoverColour; + public Color4? HoverColour { get; set; } + private Color4 fallbackHoverColour; - protected Color4 IdleColour = Color4.White; + public Color4? IdleColour { get; set; } + private Color4 fallbackIdleColour; protected virtual IEnumerable EffectTargets => new[] { Content }; @@ -67,18 +69,18 @@ namespace osu.Game.Graphics.Containers [BackgroundDependencyLoader] private void load(OsuColour colours) { - if (HoverColour == default) - HoverColour = colours.Yellow; + fallbackHoverColour = colours.Yellow; + fallbackIdleColour = Color4.White; } protected override void LoadComplete() { base.LoadComplete(); - EffectTargets.ForEach(d => d.FadeColour(IdleColour)); + EffectTargets.ForEach(d => d.FadeColour(IdleColour ?? fallbackIdleColour)); } - private void fadeIn() => EffectTargets.ForEach(d => d.FadeColour(HoverColour, FADE_DURATION, Easing.OutQuint)); + private void fadeIn() => EffectTargets.ForEach(d => d.FadeColour(HoverColour ?? fallbackHoverColour, FADE_DURATION, Easing.OutQuint)); - private void fadeOut() => EffectTargets.ForEach(d => d.FadeColour(IdleColour, FADE_DURATION, Easing.OutQuint)); + private void fadeOut() => EffectTargets.ForEach(d => d.FadeColour(IdleColour ?? fallbackIdleColour, FADE_DURATION, Easing.OutQuint)); } } diff --git a/osu.Game/Online/Chat/DrawableLinkCompiler.cs b/osu.Game/Online/Chat/DrawableLinkCompiler.cs index f640a3dab5..e4baeb4838 100644 --- a/osu.Game/Online/Chat/DrawableLinkCompiler.cs +++ b/osu.Game/Online/Chat/DrawableLinkCompiler.cs @@ -14,7 +14,6 @@ using osu.Game.Graphics.Containers; using osu.Game.Graphics.UserInterface; using osu.Game.Overlays; using osuTK; -using osuTK.Graphics; namespace osu.Game.Online.Chat { @@ -28,18 +27,6 @@ namespace osu.Game.Online.Chat /// public readonly SlimReadOnlyListWrapper Parts; - public new Color4 IdleColour - { - get => base.IdleColour; - set => base.IdleColour = value; - } - - public new Color4 HoverColour - { - get => base.HoverColour; - set => base.HoverColour = value; - } - [Resolved] private OverlayColourProvider? overlayColourProvider { get; set; } @@ -69,8 +56,7 @@ namespace osu.Game.Online.Chat [BackgroundDependencyLoader] private void load(OsuColour colours) { - if (IdleColour == default) - IdleColour = overlayColourProvider?.Light2 ?? colours.Blue; + IdleColour ??= overlayColourProvider?.Light2 ?? colours.Blue; } protected override IEnumerable EffectTargets => Parts; diff --git a/osu.Game/Overlays/Profile/Header/Components/FollowersButton.cs b/osu.Game/Overlays/Profile/Header/Components/FollowersButton.cs index af78d62789..c4425643fd 100644 --- a/osu.Game/Overlays/Profile/Header/Components/FollowersButton.cs +++ b/osu.Game/Overlays/Profile/Header/Components/FollowersButton.cs @@ -1,6 +1,7 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System; using System.Linq; using osu.Framework.Allocation; using osu.Framework.Bindables; @@ -200,16 +201,19 @@ namespace osu.Game.Overlays.Profile.Header.Components case FriendStatus.NotMutual: IdleColour = colour.Green.Opacity(0.7f); - HoverColour = IdleColour.Lighten(0.1f); + HoverColour = IdleColour.Value.Lighten(0.1f); break; case FriendStatus.Mutual: IdleColour = colour.Pink.Opacity(0.7f); - HoverColour = IdleColour.Lighten(0.1f); + HoverColour = IdleColour.Value.Lighten(0.1f); break; + + default: + throw new ArgumentOutOfRangeException(); } - EffectTargets.ForEach(d => d.FadeColour(IsHovered ? HoverColour : IdleColour, FADE_DURATION, Easing.OutQuint)); + EffectTargets.ForEach(d => d.FadeColour(IsHovered ? HoverColour.Value : IdleColour.Value, FADE_DURATION, Easing.OutQuint)); } private enum FriendStatus From 56dfe4a2314853b1e995cef65a3da7529b58cdf6 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 16 Jan 2025 18:56:21 +0900 Subject: [PATCH 257/290] Adjust test to work better when running in sequence --- .../Visual/Gameplay/TestSceneSpectatorList.cs | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSpectatorList.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSpectatorList.cs index 3cd37baafd..9a54de1459 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneSpectatorList.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSpectatorList.cs @@ -33,17 +33,21 @@ namespace osu.Game.Tests.Visual.Gameplay }); AddStep("start playing", () => localUserPlayingState.Value = LocalUserPlayingState.Playing); - AddStep("add a user", () => + + AddRepeatStep("add a user", () => { int id = Interlocked.Increment(ref counter); spectators.Add(new SpectatorList.Spectator(id, $"User {id}")); - }); - AddStep("remove random user", () => spectators.RemoveAt(RNG.Next(0, spectators.Count))); - AddStep("enter break", () => localUserPlayingState.Value = LocalUserPlayingState.Break); - AddStep("stop playing", () => localUserPlayingState.Value = LocalUserPlayingState.NotPlaying); + }, 10); + + AddRepeatStep("remove random user", () => spectators.RemoveAt(RNG.Next(0, spectators.Count)), 5); + AddStep("change font to venera", () => list.Font.Value = Typeface.Venera); AddStep("change font to torus", () => list.Font.Value = Typeface.Torus); AddStep("change header colour", () => list.HeaderColour.Value = new Colour4(RNG.NextSingle(), RNG.NextSingle(), RNG.NextSingle(), 1)); + + AddStep("enter break", () => localUserPlayingState.Value = LocalUserPlayingState.Break); + AddStep("stop playing", () => localUserPlayingState.Value = LocalUserPlayingState.NotPlaying); } } } From 996798d2df27003aa03aeb19585763fbe1afd340 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 16 Jan 2025 19:02:14 +0900 Subject: [PATCH 258/290] Avoid list width changing when spectator count changes --- osu.Game/Screens/Play/HUD/SpectatorList.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/osu.Game/Screens/Play/HUD/SpectatorList.cs b/osu.Game/Screens/Play/HUD/SpectatorList.cs index ad94b23cd7..19d7f2c490 100644 --- a/osu.Game/Screens/Play/HUD/SpectatorList.cs +++ b/osu.Game/Screens/Play/HUD/SpectatorList.cs @@ -41,7 +41,7 @@ namespace osu.Game.Screens.Play.HUD [BackgroundDependencyLoader] private void load(OsuColour colours) { - AutoSizeAxes = Axes.Both; + AutoSizeAxes = Axes.Y; InternalChildren = new Drawable[] { @@ -153,6 +153,8 @@ namespace osu.Game.Screens.Play.HUD { Header.Font = OsuFont.GetFont(Font.Value, 12, FontWeight.Bold); Header.Colour = HeaderColour.Value; + + Width = Header.DrawWidth; } private partial class SpectatorListEntry : PoolableDrawable From 32906aefde0543dbce565ecfb7f0b674f91cdd2a Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 16 Jan 2025 19:05:19 +0900 Subject: [PATCH 259/290] Add gradient on final spectator if more than list capacity are displayed --- osu.Game/Screens/Play/HUD/SpectatorList.cs | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/osu.Game/Screens/Play/HUD/SpectatorList.cs b/osu.Game/Screens/Play/HUD/SpectatorList.cs index 19d7f2c490..7e928e1861 100644 --- a/osu.Game/Screens/Play/HUD/SpectatorList.cs +++ b/osu.Game/Screens/Play/HUD/SpectatorList.cs @@ -5,8 +5,10 @@ using System; using System.Collections.Specialized; using osu.Framework.Allocation; using osu.Framework.Bindables; +using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Extensions.LocalisationExtensions; using osu.Framework.Graphics; +using osu.Framework.Graphics.Colour; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Pooling; using osu.Game.Configuration; @@ -16,6 +18,7 @@ using osu.Game.Online.Chat; using osu.Game.Users; using osu.Game.Localisation.HUD; using osu.Game.Localisation.SkinComponents; +using osuTK.Graphics; namespace osu.Game.Screens.Play.HUD { @@ -142,6 +145,13 @@ namespace osu.Game.Screens.Play.HUD Header.Text = SpectatorListStrings.SpectatorCount(Spectators.Count).ToUpper(); updateVisibility(); + + for (int i = 0; i < spectatorsFlow.Count; i++) + { + spectatorsFlow[i].Colour = i < max_spectators_displayed - 1 + ? Color4.White + : ColourInfo.GradientVertical(Color4.White, Color4.White.Opacity(0)); + } } private void updateVisibility() From e47244989a230a845b4ea928dcec2a9a6e9faab0 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 16 Jan 2025 19:23:54 +0900 Subject: [PATCH 260/290] Adjust animations a bit Removed autosize duration stuff because it looks weird when the list is shown from scratch where users are already fully populated in it. --- osu.Game/Screens/Play/HUD/SpectatorList.cs | 41 ++++++++++++++-------- 1 file changed, 26 insertions(+), 15 deletions(-) diff --git a/osu.Game/Screens/Play/HUD/SpectatorList.cs b/osu.Game/Screens/Play/HUD/SpectatorList.cs index 7e928e1861..04bd03f153 100644 --- a/osu.Game/Screens/Play/HUD/SpectatorList.cs +++ b/osu.Game/Screens/Play/HUD/SpectatorList.cs @@ -51,8 +51,6 @@ namespace osu.Game.Screens.Play.HUD mainFlow = new FillFlowContainer { AutoSizeAxes = Axes.Both, - AutoSizeDuration = 250, - AutoSizeEasing = Easing.OutQuint, Direction = FillDirection.Vertical, Children = new Drawable[] { @@ -84,6 +82,8 @@ namespace osu.Game.Screens.Play.HUD Font.BindValueChanged(_ => updateAppearance()); HeaderColour.BindValueChanged(_ => updateAppearance(), true); FinishTransforms(true); + + this.FadeInFromZero(200, Easing.OutQuint); } private void onSpectatorsChanged(object? sender, NotifyCollectionChangedEventArgs e) @@ -100,11 +100,7 @@ namespace osu.Game.Screens.Play.HUD if (index >= max_spectators_displayed) break; - spectatorsFlow.Insert(e.NewStartingIndex + i, pool.Get(entry => - { - entry.Current.Value = spectator; - entry.UserPlayingState = UserPlayingState; - })); + addNewSpectatorToList(index, spectator); } break; @@ -120,14 +116,7 @@ namespace osu.Game.Screens.Play.HUD if (Spectators.Count >= max_spectators_displayed && spectatorsFlow.Count < max_spectators_displayed) { for (int i = spectatorsFlow.Count; i < max_spectators_displayed; i++) - { - var spectator = Spectators[i]; - spectatorsFlow.Insert(i, pool.Get(entry => - { - entry.Current.Value = spectator; - entry.UserPlayingState = UserPlayingState; - })); - } + addNewSpectatorToList(i, Spectators[i]); } break; @@ -154,6 +143,17 @@ namespace osu.Game.Screens.Play.HUD } } + private void addNewSpectatorToList(int i, Spectator spectator) + { + var entry = pool.Get(entry => + { + entry.Current.Value = spectator; + entry.UserPlayingState = UserPlayingState; + }); + + spectatorsFlow.Insert(i, entry); + } + private void updateVisibility() { mainFlow.FadeTo(Spectators.Count > 0 && UserPlayingState.Value != LocalUserPlayingState.NotPlaying ? 1 : 0, 250, Easing.OutQuint); @@ -203,6 +203,17 @@ namespace osu.Game.Screens.Play.HUD Current.BindValueChanged(_ => updateState(), true); } + protected override void PrepareForUse() + { + base.PrepareForUse(); + + username.MoveToX(10) + .Then() + .MoveToX(0, 400, Easing.OutQuint); + + this.FadeInFromZero(400, Easing.OutQuint); + } + private void updateState() { username.Text = Current.Value.Username; From 840072688749f6c24f3aab3926d9eeed22b36861 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Thu, 16 Jan 2025 19:33:38 +0900 Subject: [PATCH 261/290] Move bindables to OsuConfigManager & SessionStatics --- osu.Desktop/DiscordRichPresence.cs | 35 +++++++++---------- .../Online/TestSceneNowPlayingCommand.cs | 20 +++++++---- osu.Game/Configuration/SessionStatics.cs | 4 +++ osu.Game/Online/API/APIAccess.cs | 4 --- osu.Game/Online/API/DummyAPIAccess.cs | 7 ---- osu.Game/Online/API/IAPIProvider.cs | 11 ------ osu.Game/Online/Chat/NowPlayingCommand.cs | 14 ++++++-- .../Online/Metadata/OnlineMetadataClient.cs | 21 +++++++---- osu.Game/OsuGame.cs | 8 +++-- osu.Game/Screens/IOsuScreen.cs | 2 +- osu.Game/Screens/OsuScreen.cs | 2 +- 11 files changed, 67 insertions(+), 61 deletions(-) diff --git a/osu.Desktop/DiscordRichPresence.cs b/osu.Desktop/DiscordRichPresence.cs index 6c7e7d393f..7dd9250ab6 100644 --- a/osu.Desktop/DiscordRichPresence.cs +++ b/osu.Desktop/DiscordRichPresence.cs @@ -51,12 +51,9 @@ namespace osu.Desktop [Resolved] private LocalUserStatisticsProvider statisticsProvider { get; set; } = null!; - [Resolved] - private OsuConfigManager config { get; set; } = null!; - - private readonly IBindable status = new Bindable(); - private readonly IBindable activity = new Bindable(); - private readonly Bindable privacyMode = new Bindable(); + private IBindable privacyMode = null!; + private IBindable userStatus = null!; + private IBindable userActivity = null!; private readonly RichPresence presence = new RichPresence { @@ -71,8 +68,12 @@ namespace osu.Desktop private IBindable? user; [BackgroundDependencyLoader] - private void load() + private void load(OsuConfigManager config, SessionStatics session) { + privacyMode = config.GetBindable(OsuSetting.DiscordRichPresence); + userStatus = config.GetBindable(OsuSetting.UserOnlineStatus); + userActivity = session.GetBindable(Static.UserOnlineActivity); + client = new DiscordRpcClient(client_id) { // SkipIdenticalPresence allows us to fire SetPresence at any point and leave it to the underlying implementation @@ -105,15 +106,11 @@ namespace osu.Desktop { base.LoadComplete(); - config.BindWith(OsuSetting.DiscordRichPresence, privacyMode); - user = api.LocalUser.GetBoundCopy(); - status.BindTo(api.Status); - activity.BindTo(api.Activity); ruleset.BindValueChanged(_ => schedulePresenceUpdate()); - status.BindValueChanged(_ => schedulePresenceUpdate()); - activity.BindValueChanged(_ => schedulePresenceUpdate()); + userStatus.BindValueChanged(_ => schedulePresenceUpdate()); + userActivity.BindValueChanged(_ => schedulePresenceUpdate()); privacyMode.BindValueChanged(_ => schedulePresenceUpdate()); multiplayerClient.RoomUpdated += onRoomUpdated; @@ -145,13 +142,13 @@ namespace osu.Desktop if (!client.IsInitialized) return; - if (!api.IsLoggedIn || status.Value == UserStatus.Offline || privacyMode.Value == DiscordRichPresenceMode.Off) + if (!api.IsLoggedIn || userStatus.Value == UserStatus.Offline || privacyMode.Value == DiscordRichPresenceMode.Off) { client.ClearPresence(); return; } - bool hideIdentifiableInformation = privacyMode.Value == DiscordRichPresenceMode.Limited || status.Value == UserStatus.DoNotDisturb; + bool hideIdentifiableInformation = privacyMode.Value == DiscordRichPresenceMode.Limited || userStatus.Value == UserStatus.DoNotDisturb; updatePresence(hideIdentifiableInformation); client.SetPresence(presence); @@ -164,12 +161,12 @@ namespace osu.Desktop return; // user activity - if (activity.Value != null) + if (userActivity.Value != null) { - presence.State = clampLength(activity.Value.GetStatus(hideIdentifiableInformation)); - presence.Details = clampLength(activity.Value.GetDetails(hideIdentifiableInformation) ?? string.Empty); + presence.State = clampLength(userActivity.Value.GetStatus(hideIdentifiableInformation)); + presence.Details = clampLength(userActivity.Value.GetDetails(hideIdentifiableInformation) ?? string.Empty); - if (activity.Value.GetBeatmapID(hideIdentifiableInformation) is int beatmapId && beatmapId > 0) + if (userActivity.Value.GetBeatmapID(hideIdentifiableInformation) is int beatmapId && beatmapId > 0) { presence.Buttons = new[] { diff --git a/osu.Game.Tests/Visual/Online/TestSceneNowPlayingCommand.cs b/osu.Game.Tests/Visual/Online/TestSceneNowPlayingCommand.cs index 1e9b0317fb..428554f761 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneNowPlayingCommand.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneNowPlayingCommand.cs @@ -8,7 +8,7 @@ using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Testing; using osu.Game.Beatmaps; -using osu.Game.Online.API; +using osu.Game.Configuration; using osu.Game.Online.Chat; using osu.Game.Online.Rooms; using osu.Game.Rulesets.Mods; @@ -23,17 +23,23 @@ namespace osu.Game.Tests.Visual.Online [Cached(typeof(IChannelPostTarget))] private PostTarget postTarget { get; set; } - private DummyAPIAccess api => (DummyAPIAccess)API; + private SessionStatics session = null!; public TestSceneNowPlayingCommand() { Add(postTarget = new PostTarget()); } + [BackgroundDependencyLoader] + private void load() + { + Dependencies.Cache(session = new SessionStatics()); + } + [Test] public void TestGenericActivity() { - AddStep("Set activity", () => api.Activity.Value = new UserActivity.InLobby(new Room())); + AddStep("Set activity", () => session.SetValue(Static.UserOnlineActivity, new UserActivity.InLobby(new Room()))); AddStep("Run command", () => Add(new NowPlayingCommand(new Channel()))); @@ -43,7 +49,7 @@ namespace osu.Game.Tests.Visual.Online [Test] public void TestEditActivity() { - AddStep("Set activity", () => api.Activity.Value = new UserActivity.EditingBeatmap(new BeatmapInfo())); + AddStep("Set activity", () => session.SetValue(Static.UserOnlineActivity, new UserActivity.EditingBeatmap(new BeatmapInfo()))); AddStep("Run command", () => Add(new NowPlayingCommand(new Channel()))); @@ -53,7 +59,7 @@ namespace osu.Game.Tests.Visual.Online [Test] public void TestPlayActivity() { - AddStep("Set activity", () => api.Activity.Value = new UserActivity.InSoloGame(new BeatmapInfo(), new OsuRuleset().RulesetInfo)); + AddStep("Set activity", () => session.SetValue(Static.UserOnlineActivity, new UserActivity.InSoloGame(new BeatmapInfo(), new OsuRuleset().RulesetInfo))); AddStep("Run command", () => Add(new NowPlayingCommand(new Channel()))); @@ -64,7 +70,7 @@ namespace osu.Game.Tests.Visual.Online [TestCase(false)] public void TestLinkPresence(bool hasOnlineId) { - AddStep("Set activity", () => api.Activity.Value = new UserActivity.InLobby(new Room())); + AddStep("Set activity", () => session.SetValue(Static.UserOnlineActivity, new UserActivity.InLobby(new Room()))); AddStep("Set beatmap", () => Beatmap.Value = new DummyWorkingBeatmap(Audio, null) { @@ -82,7 +88,7 @@ namespace osu.Game.Tests.Visual.Online [Test] public void TestModPresence() { - AddStep("Set activity", () => api.Activity.Value = new UserActivity.InSoloGame(new BeatmapInfo(), new OsuRuleset().RulesetInfo)); + AddStep("Set activity", () => session.SetValue(Static.UserOnlineActivity, new UserActivity.InSoloGame(new BeatmapInfo(), new OsuRuleset().RulesetInfo))); AddStep("Add Hidden mod", () => SelectedMods.Value = new[] { Ruleset.Value.CreateInstance().CreateMod() }); diff --git a/osu.Game/Configuration/SessionStatics.cs b/osu.Game/Configuration/SessionStatics.cs index c55a597c32..bdfb0217ad 100644 --- a/osu.Game/Configuration/SessionStatics.cs +++ b/osu.Game/Configuration/SessionStatics.cs @@ -10,6 +10,7 @@ using osu.Game.Online.API.Requests.Responses; using osu.Game.Overlays; using osu.Game.Overlays.Mods; using osu.Game.Scoring; +using osu.Game.Users; namespace osu.Game.Configuration { @@ -30,6 +31,7 @@ namespace osu.Game.Configuration SetDefault(Static.TouchInputActive, RuntimeInfo.IsMobile); SetDefault(Static.LastLocalUserScore, null); SetDefault(Static.LastAppliedOffsetScore, null); + SetDefault(Static.UserOnlineActivity, null); } /// @@ -92,5 +94,7 @@ namespace osu.Game.Configuration /// This is reset when a new challenge is up. /// DailyChallengeIntroPlayed, + + UserOnlineActivity, } } diff --git a/osu.Game/Online/API/APIAccess.cs b/osu.Game/Online/API/APIAccess.cs index dcb8a193bc..f7fbacf76c 100644 --- a/osu.Game/Online/API/APIAccess.cs +++ b/osu.Game/Online/API/APIAccess.cs @@ -60,8 +60,6 @@ namespace osu.Game.Online.API public IBindable LocalUser => localUser; public IBindableList Friends => friends; - public IBindable Status => configStatus; - public IBindable Activity => activity; public INotificationsClient NotificationsClient { get; } @@ -71,8 +69,6 @@ namespace osu.Game.Online.API private BindableList friends { get; } = new BindableList(); - private Bindable activity { get; } = new Bindable(); - protected bool HasLogin => authentication.Token.Value != null || (!string.IsNullOrEmpty(ProvidedUsername) && !string.IsNullOrEmpty(password)); private readonly Bindable configStatus = new Bindable(); diff --git a/osu.Game/Online/API/DummyAPIAccess.cs b/osu.Game/Online/API/DummyAPIAccess.cs index 3fef2b59cf..48c08afb8c 100644 --- a/osu.Game/Online/API/DummyAPIAccess.cs +++ b/osu.Game/Online/API/DummyAPIAccess.cs @@ -12,7 +12,6 @@ using osu.Game.Online.API.Requests.Responses; using osu.Game.Online.Chat; using osu.Game.Online.Notifications.WebSocket; using osu.Game.Tests; -using osu.Game.Users; namespace osu.Game.Online.API { @@ -28,10 +27,6 @@ namespace osu.Game.Online.API public BindableList Friends { get; } = new BindableList(); - public Bindable Status { get; } = new Bindable(UserStatus.Online); - - public Bindable Activity { get; } = new Bindable(); - public DummyNotificationsClient NotificationsClient { get; } = new DummyNotificationsClient(); INotificationsClient IAPIProvider.NotificationsClient => NotificationsClient; @@ -197,8 +192,6 @@ namespace osu.Game.Online.API IBindable IAPIProvider.LocalUser => LocalUser; IBindableList IAPIProvider.Friends => Friends; - IBindable IAPIProvider.Status => Status; - IBindable IAPIProvider.Activity => Activity; /// /// Skip 2FA requirement for next login. diff --git a/osu.Game/Online/API/IAPIProvider.cs b/osu.Game/Online/API/IAPIProvider.cs index 9ac7343885..3b6763d736 100644 --- a/osu.Game/Online/API/IAPIProvider.cs +++ b/osu.Game/Online/API/IAPIProvider.cs @@ -8,7 +8,6 @@ using osu.Game.Localisation; using osu.Game.Online.API.Requests.Responses; using osu.Game.Online.Chat; using osu.Game.Online.Notifications.WebSocket; -using osu.Game.Users; namespace osu.Game.Online.API { @@ -24,16 +23,6 @@ namespace osu.Game.Online.API /// IBindableList Friends { get; } - /// - /// The status for the current user that's broadcast to other players. - /// - IBindable Status { get; } - - /// - /// The activity for the current user that's broadcast to other players. - /// - IBindable Activity { get; } - /// /// The language supplied by this provider to API requests. /// diff --git a/osu.Game/Online/Chat/NowPlayingCommand.cs b/osu.Game/Online/Chat/NowPlayingCommand.cs index 0e6f6f0bf6..db44017a1b 100644 --- a/osu.Game/Online/Chat/NowPlayingCommand.cs +++ b/osu.Game/Online/Chat/NowPlayingCommand.cs @@ -8,6 +8,7 @@ using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Game.Beatmaps; +using osu.Game.Configuration; using osu.Game.Online.API; using osu.Game.Rulesets; using osu.Game.Rulesets.Mods; @@ -33,6 +34,7 @@ namespace osu.Game.Online.Chat private IBindable currentRuleset { get; set; } = null!; private readonly Channel? target; + private IBindable userActivity = null!; /// /// Creates a new to post the currently-playing beatmap to a parenting . @@ -43,6 +45,12 @@ namespace osu.Game.Online.Chat this.target = target; } + [BackgroundDependencyLoader] + private void load(SessionStatics session) + { + userActivity = session.GetBindable(Static.UserOnlineActivity); + } + protected override void LoadComplete() { base.LoadComplete(); @@ -52,7 +60,7 @@ namespace osu.Game.Online.Chat int beatmapOnlineID; string beatmapDisplayTitle; - switch (api.Activity.Value) + switch (userActivity.Value) { case UserActivity.InGame game: verb = "playing"; @@ -92,14 +100,14 @@ namespace osu.Game.Online.Chat string getRulesetPart() { - if (api.Activity.Value is not UserActivity.InGame) return string.Empty; + if (userActivity.Value is not UserActivity.InGame) return string.Empty; return $"<{currentRuleset.Value.Name}>"; } string getModPart() { - if (api.Activity.Value is not UserActivity.InGame) return string.Empty; + if (userActivity.Value is not UserActivity.InGame) return string.Empty; if (selectedMods.Value.Count == 0) { diff --git a/osu.Game/Online/Metadata/OnlineMetadataClient.cs b/osu.Game/Online/Metadata/OnlineMetadataClient.cs index 101307636a..01d7a564fa 100644 --- a/osu.Game/Online/Metadata/OnlineMetadataClient.cs +++ b/osu.Game/Online/Metadata/OnlineMetadataClient.cs @@ -34,6 +34,9 @@ namespace osu.Game.Online.Metadata private readonly string endpoint; + [Resolved] + private IAPIProvider api { get; set; } = null!; + private IHubClientConnector? connector; private Bindable lastQueueId = null!; private IBindable localUser = null!; @@ -48,7 +51,7 @@ namespace osu.Game.Online.Metadata } [BackgroundDependencyLoader] - private void load(IAPIProvider api, OsuConfigManager config) + private void load(OsuConfigManager config, SessionStatics session) { // Importantly, we are intentionally not using MessagePack here to correctly support derived class serialization. // More information on the limitations / reasoning can be found in osu-server-spectator's initialisation code. @@ -72,11 +75,10 @@ namespace osu.Game.Online.Metadata IsConnected.BindValueChanged(isConnectedChanged, true); } - lastQueueId = config.GetBindable(OsuSetting.LastProcessedMetadataId); - localUser = api.LocalUser.GetBoundCopy(); - userStatus = api.Status.GetBoundCopy(); - userActivity = api.Activity.GetBoundCopy()!; + lastQueueId = config.GetBindable(OsuSetting.LastProcessedMetadataId); + userStatus = config.GetBindable(OsuSetting.UserOnlineStatus); + userActivity = session.GetBindable(Static.UserOnlineActivity); } protected override void LoadComplete() @@ -240,7 +242,14 @@ namespace osu.Game.Online.Metadata throw new OperationCanceledException(); // must be scheduled before any remote calls to avoid mis-ordering. - Schedule(() => userStates.Clear()); + Schedule(() => + { + bool hadLocalUserState = userStates.TryGetValue(api.LocalUser.Value.OnlineID, out var presence); + userStates.Clear(); + if (hadLocalUserState) + userStates[api.LocalUser.Value.OnlineID] = presence; + }); + Debug.Assert(connection != null); await connection.InvokeAsync(nameof(IMetadataServer.EndWatchingUserPresence)).ConfigureAwait(false); Logger.Log($@"{nameof(OnlineMetadataClient)} stopped watching user presence", LoggingTarget.Network); diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs index 859991496d..40d13ae0b7 100644 --- a/osu.Game/OsuGame.cs +++ b/osu.Game/OsuGame.cs @@ -211,6 +211,8 @@ namespace osu.Game private Bindable uiScale; + private Bindable configUserActivity; + private Bindable configSkin; private readonly string[] args; @@ -391,6 +393,8 @@ namespace osu.Game Ruleset.ValueChanged += r => configRuleset.Value = r.NewValue.ShortName; + configUserActivity = SessionStatics.GetBindable(Static.UserOnlineActivity); + configSkin = LocalConfig.GetBindable(OsuSetting.Skin); // Transfer skin from config to realm instance once on startup. @@ -1588,14 +1592,14 @@ namespace osu.Game { backButtonVisibility.UnbindFrom(currentOsuScreen.BackButtonVisibility); OverlayActivationMode.UnbindFrom(currentOsuScreen.OverlayActivationMode); - API.Activity.UnbindFrom(currentOsuScreen.Activity); + configUserActivity.UnbindFrom(currentOsuScreen.Activity); } if (newScreen is IOsuScreen newOsuScreen) { backButtonVisibility.BindTo(newOsuScreen.BackButtonVisibility); OverlayActivationMode.BindTo(newOsuScreen.OverlayActivationMode); - API.Activity.BindTo(newOsuScreen.Activity); + configUserActivity.BindTo(newOsuScreen.Activity); GlobalCursorDisplay.MenuCursor.HideCursorOnNonMouseInput = newOsuScreen.HideMenuCursorOnNonMouseInput; diff --git a/osu.Game/Screens/IOsuScreen.cs b/osu.Game/Screens/IOsuScreen.cs index 9e474ed0c6..69bde877c7 100644 --- a/osu.Game/Screens/IOsuScreen.cs +++ b/osu.Game/Screens/IOsuScreen.cs @@ -74,7 +74,7 @@ namespace osu.Game.Screens /// /// The current for this screen. /// - IBindable Activity { get; } + Bindable Activity { get; } /// /// The amount of parallax to be applied while this screen is displayed. diff --git a/osu.Game/Screens/OsuScreen.cs b/osu.Game/Screens/OsuScreen.cs index ab66241a77..f5325b3928 100644 --- a/osu.Game/Screens/OsuScreen.cs +++ b/osu.Game/Screens/OsuScreen.cs @@ -81,7 +81,7 @@ namespace osu.Game.Screens /// protected readonly Bindable Activity = new Bindable(); - IBindable IOsuScreen.Activity => Activity; + Bindable IOsuScreen.Activity => Activity; /// /// Whether to disallow changes to game-wise Beatmap/Ruleset bindables for this screen (and all children). From 56b450c4a639b7c73a7e642570cce81fb4d2bcf6 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 16 Jan 2025 19:35:49 +0900 Subject: [PATCH 262/290] Remove setting for right-mouse scroll (make it always applicable) --- osu.Game/Configuration/OsuConfigManager.cs | 3 --- .../Settings/Sections/UserInterface/SongSelectSettings.cs | 6 ------ osu.Game/Screens/Select/BeatmapCarousel.cs | 6 +----- 3 files changed, 1 insertion(+), 14 deletions(-) diff --git a/osu.Game/Configuration/OsuConfigManager.cs b/osu.Game/Configuration/OsuConfigManager.cs index d4a75334a9..dea7931ed5 100644 --- a/osu.Game/Configuration/OsuConfigManager.cs +++ b/osu.Game/Configuration/OsuConfigManager.cs @@ -170,8 +170,6 @@ namespace osu.Game.Configuration SetDefault(OsuSetting.ScreenshotFormat, ScreenshotFormat.Jpg); SetDefault(OsuSetting.ScreenshotCaptureMenuCursor, false); - SetDefault(OsuSetting.SongSelectRightMouseScroll, false); - SetDefault(OsuSetting.Scaling, ScalingMode.Off); SetDefault(OsuSetting.SafeAreaConsiderations, true); SetDefault(OsuSetting.ScalingBackgroundDim, 0.9f, 0.5f, 1f, 0.01f); @@ -401,7 +399,6 @@ namespace osu.Game.Configuration Skin, ScreenshotFormat, ScreenshotCaptureMenuCursor, - SongSelectRightMouseScroll, BeatmapSkins, BeatmapColours, BeatmapHitsounds, diff --git a/osu.Game/Overlays/Settings/Sections/UserInterface/SongSelectSettings.cs b/osu.Game/Overlays/Settings/Sections/UserInterface/SongSelectSettings.cs index 49bd17dfde..cb0d738a2c 100644 --- a/osu.Game/Overlays/Settings/Sections/UserInterface/SongSelectSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/UserInterface/SongSelectSettings.cs @@ -19,12 +19,6 @@ namespace osu.Game.Overlays.Settings.Sections.UserInterface { Children = new Drawable[] { - new SettingsCheckbox - { - ClassicDefault = true, - LabelText = UserInterfaceStrings.RightMouseScroll, - Current = config.GetBindable(OsuSetting.SongSelectRightMouseScroll), - }, new SettingsCheckbox { LabelText = UserInterfaceStrings.ShowConvertedBeatmaps, diff --git a/osu.Game/Screens/Select/BeatmapCarousel.cs b/osu.Game/Screens/Select/BeatmapCarousel.cs index de12b36b17..37876eeca6 100644 --- a/osu.Game/Screens/Select/BeatmapCarousel.cs +++ b/osu.Game/Screens/Select/BeatmapCarousel.cs @@ -184,8 +184,6 @@ namespace osu.Game.Screens.Select private readonly Cached itemsCache = new Cached(); private PendingScrollOperation pendingScrollOperation = PendingScrollOperation.None; - public Bindable RightClickScrollingEnabled = new Bindable(); - public Bindable RandomAlgorithm = new Bindable(); private readonly List previouslyVisitedRandomSets = new List(); private readonly List randomSelectedBeatmaps = new List(); @@ -210,6 +208,7 @@ namespace osu.Game.Screens.Select setPool, Scroll = new CarouselScrollContainer { + RightMouseScrollbar = true, RelativeSizeAxes = Axes.Both, }, noResultsPlaceholder = new NoResultsPlaceholder() @@ -226,9 +225,6 @@ namespace osu.Game.Screens.Select randomSelectSample = audio.Samples.Get(@"SongSelect/select-random"); config.BindWith(OsuSetting.RandomSelectAlgorithm, RandomAlgorithm); - config.BindWith(OsuSetting.SongSelectRightMouseScroll, RightClickScrollingEnabled); - - RightClickScrollingEnabled.BindValueChanged(enabled => Scroll.RightMouseScrollbar = enabled.NewValue, true); detachedBeatmapSets = beatmaps.GetBeatmapSets(cancellationToken); detachedBeatmapSets.BindCollectionChanged(beatmapSetsChanged); From 1c2621d88e8c86954c949ef538df86c05cc78285 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 16 Jan 2025 19:42:10 +0900 Subject: [PATCH 263/290] Add support to CarouselV2 for right mouse button scrolling --- osu.Game/Screens/SelectV2/Carousel.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/osu.Game/Screens/SelectV2/Carousel.cs b/osu.Game/Screens/SelectV2/Carousel.cs index 12a86be7b9..84b90c8fe0 100644 --- a/osu.Game/Screens/SelectV2/Carousel.cs +++ b/osu.Game/Screens/SelectV2/Carousel.cs @@ -121,6 +121,7 @@ namespace osu.Game.Screens.SelectV2 }, scroll = new CarouselScrollContainer { + RightMouseScrollbar = true, RelativeSizeAxes = Axes.Both, Masking = false, } @@ -390,7 +391,7 @@ namespace osu.Game.Screens.SelectV2 /// Implementation of scroll container which handles very large vertical lists by internally using double precision /// for pre-display Y values. /// - private partial class CarouselScrollContainer : OsuScrollContainer + private partial class CarouselScrollContainer : UserTrackingScrollContainer { public readonly Container Panels; From 48609d44e2f24a3733e114807ce095b6b23335ff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 16 Jan 2025 12:30:27 +0100 Subject: [PATCH 264/290] Bump NVika tool to 4.0.0 Code quality CI runs have suddenly started failing out of nowhere: - Passing run: https://github.com/ppy/osu/actions/runs/12806242929/job/35704267944#step:10:1 - Failing run: https://github.com/ppy/osu/actions/runs/12807108792/job/35707131634#step:10:1 In classic github fashion, they began rolling out another runner change wherein `ubuntu-latest` has started meaning `ubuntu-24.04` rather than `ubuntu-22.04`. `ubuntu-24.04` no longer has .NET 6 bundled. Therefore, upgrade NVika to 4.0.0 because that version is compatible with .NET 8. --- .config/dotnet-tools.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.config/dotnet-tools.json b/.config/dotnet-tools.json index c4ba6e5143..6ec071be2f 100644 --- a/.config/dotnet-tools.json +++ b/.config/dotnet-tools.json @@ -9,7 +9,7 @@ ] }, "nvika": { - "version": "3.0.0", + "version": "4.0.0", "commands": [ "nvika" ] From a6057a9f54e186557694861f292a132c5c881d0f Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 16 Jan 2025 20:25:16 +0900 Subject: [PATCH 265/290] Move absolute scroll support local to carousel and allow custom bindings --- .../SongSelect/TestSceneBeatmapCarouselV2.cs | 2 +- .../Graphics/Containers/OsuScrollContainer.cs | 77 ++++--------------- .../Containers/UserTrackingScrollContainer.cs | 6 +- .../Input/Bindings/GlobalActionContainer.cs | 4 + .../GlobalActionKeyBindingStrings.cs | 5 ++ osu.Game/Screens/Select/BeatmapCarousel.cs | 61 ++++++++++----- osu.Game/Screens/SelectV2/Carousel.cs | 57 +++++++++++++- 7 files changed, 122 insertions(+), 90 deletions(-) diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2.cs index f99e0a418a..b13d450c32 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2.cs @@ -38,7 +38,7 @@ namespace osu.Game.Tests.Visual.SongSelect private OsuTextFlowContainer stats = null!; private BeatmapCarousel carousel = null!; - private OsuScrollContainer scroll => carousel.ChildrenOfType().Single(); + private OsuScrollContainer scroll => carousel.ChildrenOfType>().Single(); private int beatmapCount; diff --git a/osu.Game/Graphics/Containers/OsuScrollContainer.cs b/osu.Game/Graphics/Containers/OsuScrollContainer.cs index f40c91e27e..43a42eae57 100644 --- a/osu.Game/Graphics/Containers/OsuScrollContainer.cs +++ b/osu.Game/Graphics/Containers/OsuScrollContainer.cs @@ -26,26 +26,12 @@ namespace osu.Game.Graphics.Containers } } - public partial class OsuScrollContainer : ScrollContainer where T : Drawable + public partial class OsuScrollContainer : ScrollContainer + where T : Drawable { public const float SCROLL_BAR_WIDTH = 10; public const float SCROLL_BAR_PADDING = 3; - /// - /// Allows controlling the scroll bar from any position in the container using the right mouse button. - /// Uses the value of to smoothly scroll to the dragged location. - /// - public bool RightMouseScrollbar; - - /// - /// Controls the rate with which the target position is approached when performing a relative drag. Default is 0.02. - /// - public double DistanceDecayOnRightMouseScrollbar = 0.02; - - private bool rightMouseDragging; - - protected override bool IsDragging => base.IsDragging || rightMouseDragging; - public OsuScrollContainer(Direction scrollDirection = Direction.Vertical) : base(scrollDirection) { @@ -71,50 +57,6 @@ namespace osu.Game.Graphics.Containers ScrollTo(maxPos - DisplayableContent + extraScroll, animated); } - protected override bool OnMouseDown(MouseDownEvent e) - { - if (shouldPerformRightMouseScroll(e)) - { - ScrollFromMouseEvent(e); - return true; - } - - return base.OnMouseDown(e); - } - - protected override void OnDrag(DragEvent e) - { - if (rightMouseDragging) - { - ScrollFromMouseEvent(e); - return; - } - - base.OnDrag(e); - } - - protected override bool OnDragStart(DragStartEvent e) - { - if (shouldPerformRightMouseScroll(e)) - { - rightMouseDragging = true; - return true; - } - - return base.OnDragStart(e); - } - - protected override void OnDragEnd(DragEndEvent e) - { - if (rightMouseDragging) - { - rightMouseDragging = false; - return; - } - - base.OnDragEnd(e); - } - protected override bool OnScroll(ScrollEvent e) { // allow for controlling volume when alt is held. @@ -124,15 +66,22 @@ namespace osu.Game.Graphics.Containers return base.OnScroll(e); } - protected virtual void ScrollFromMouseEvent(MouseEvent e) + #region Absolute scrolling + + /// + /// Controls the rate with which the target position is approached when performing a relative drag. Default is 0.02. + /// + public double DistanceDecayOnAbsoluteScroll = 0.02; + + protected virtual void ScrollToAbsolutePosition(Vector2 screenSpacePosition) { - float fromScrollbarPosition = FromScrollbarPosition(ToLocalSpace(e.ScreenSpaceMousePosition)[ScrollDim]); + float fromScrollbarPosition = FromScrollbarPosition(ToLocalSpace(screenSpacePosition)[ScrollDim]); float scrollbarCentreOffset = FromScrollbarPosition(Scrollbar.DrawHeight) * 0.5f; - ScrollTo(Clamp(fromScrollbarPosition - scrollbarCentreOffset), true, DistanceDecayOnRightMouseScrollbar); + ScrollTo(Clamp(fromScrollbarPosition - scrollbarCentreOffset), true, DistanceDecayOnAbsoluteScroll); } - private bool shouldPerformRightMouseScroll(MouseButtonEvent e) => RightMouseScrollbar && e.Button == MouseButton.Right; + #endregion protected override ScrollbarContainer CreateScrollbar(Direction direction) => new OsuScrollbar(direction); diff --git a/osu.Game/Graphics/Containers/UserTrackingScrollContainer.cs b/osu.Game/Graphics/Containers/UserTrackingScrollContainer.cs index 30b9eeb74c..ab17c3f9e3 100644 --- a/osu.Game/Graphics/Containers/UserTrackingScrollContainer.cs +++ b/osu.Game/Graphics/Containers/UserTrackingScrollContainer.cs @@ -2,7 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using osu.Framework.Graphics; -using osu.Framework.Input.Events; +using osuTK; namespace osu.Game.Graphics.Containers { @@ -47,10 +47,10 @@ namespace osu.Game.Graphics.Containers base.ScrollIntoView(target, animated); } - protected override void ScrollFromMouseEvent(MouseEvent e) + protected override void ScrollToAbsolutePosition(Vector2 screenSpacePosition) { UserScrolling = true; - base.ScrollFromMouseEvent(e); + base.ScrollToAbsolutePosition(screenSpacePosition); } public new void ScrollTo(double value, bool animated = true, double? distanceDecay = null) diff --git a/osu.Game/Input/Bindings/GlobalActionContainer.cs b/osu.Game/Input/Bindings/GlobalActionContainer.cs index 2666b24be9..5e509d2035 100644 --- a/osu.Game/Input/Bindings/GlobalActionContainer.cs +++ b/osu.Game/Input/Bindings/GlobalActionContainer.cs @@ -204,6 +204,7 @@ namespace osu.Game.Input.Bindings new KeyBinding(InputKey.BackSpace, GlobalAction.DeselectAllMods), new KeyBinding(new[] { InputKey.Control, InputKey.Up }, GlobalAction.IncreaseModSpeed), new KeyBinding(new[] { InputKey.Control, InputKey.Down }, GlobalAction.DecreaseModSpeed), + new KeyBinding(new[] { InputKey.MouseRight }, GlobalAction.AbsoluteScrollSongList), }; private static IEnumerable audioControlKeyBindings => new[] @@ -490,6 +491,9 @@ namespace osu.Game.Input.Bindings [LocalisableDescription(typeof(GlobalActionKeyBindingStrings), nameof(GlobalActionKeyBindingStrings.EditorSeekToNextBookmark))] EditorSeekToNextBookmark, + + [LocalisableDescription(typeof(GlobalActionKeyBindingStrings), nameof(GlobalActionKeyBindingStrings.AbsoluteScrollSongList))] + AbsoluteScrollSongList } public enum GlobalActionCategory diff --git a/osu.Game/Localisation/GlobalActionKeyBindingStrings.cs b/osu.Game/Localisation/GlobalActionKeyBindingStrings.cs index f9db0461ce..436a2be648 100644 --- a/osu.Game/Localisation/GlobalActionKeyBindingStrings.cs +++ b/osu.Game/Localisation/GlobalActionKeyBindingStrings.cs @@ -449,6 +449,11 @@ namespace osu.Game.Localisation /// public static LocalisableString EditorSeekToNextBookmark => new TranslatableString(getKey(@"editor_seek_to_next_bookmark"), @"Seek to next bookmark"); + /// + /// "Absolute scroll song list" + /// + public static LocalisableString AbsoluteScrollSongList => new TranslatableString(getKey(@"absolute_scroll_song_list"), @"Absolute scroll song list"); + private static string getKey(string key) => $@"{prefix}:{key}"; } } diff --git a/osu.Game/Screens/Select/BeatmapCarousel.cs b/osu.Game/Screens/Select/BeatmapCarousel.cs index 37876eeca6..7e3c26a1ba 100644 --- a/osu.Game/Screens/Select/BeatmapCarousel.cs +++ b/osu.Game/Screens/Select/BeatmapCarousel.cs @@ -14,6 +14,7 @@ using osu.Framework.Bindables; using osu.Framework.Caching; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Cursor; using osu.Framework.Graphics.Pooling; using osu.Framework.Input.Bindings; using osu.Framework.Input.Events; @@ -208,7 +209,6 @@ namespace osu.Game.Screens.Select setPool, Scroll = new CarouselScrollContainer { - RightMouseScrollbar = true, RelativeSizeAxes = Axes.Both, }, noResultsPlaceholder = new NoResultsPlaceholder() @@ -1157,10 +1157,8 @@ namespace osu.Game.Screens.Select } } - public partial class CarouselScrollContainer : UserTrackingScrollContainer + public partial class CarouselScrollContainer : UserTrackingScrollContainer, IKeyBindingHandler { - private bool rightMouseScrollBlocked; - public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => true; public CarouselScrollContainer() @@ -1172,31 +1170,54 @@ namespace osu.Game.Screens.Select Masking = false; } - protected override bool OnMouseDown(MouseDownEvent e) + #region Absolute scrolling + + private bool absoluteScrolling; + + protected override bool IsDragging => base.IsDragging || absoluteScrolling; + + public bool OnPressed(KeyBindingPressEvent e) { - if (e.Button == MouseButton.Right) + switch (e.Action) { - // we need to block right click absolute scrolling when hovering a carousel item so context menus can display. - // this can be reconsidered when we have an alternative to right click scrolling. - if (GetContainingInputManager()!.HoveredDrawables.OfType().Any()) - { - rightMouseScrollBlocked = true; - return false; - } + case GlobalAction.AbsoluteScrollSongList: + // The default binding for absolute scroll is right mouse button. + // To avoid conflicts with context menus, disallow absolute scroll completely if it looks like things will fall over. + if (e.CurrentState.Mouse.Buttons.Contains(MouseButton.Right) + && GetContainingInputManager()!.HoveredDrawables.OfType().Any()) + return false; + + ScrollToAbsolutePosition(e.CurrentState.Mouse.Position); + absoluteScrolling = true; + return true; } - rightMouseScrollBlocked = false; - return base.OnMouseDown(e); + return false; } - protected override bool OnDragStart(DragStartEvent e) + public void OnReleased(KeyBindingReleaseEvent e) { - if (rightMouseScrollBlocked) - return false; - - return base.OnDragStart(e); + switch (e.Action) + { + case GlobalAction.AbsoluteScrollSongList: + absoluteScrolling = false; + break; + } } + protected override bool OnMouseMove(MouseMoveEvent e) + { + if (absoluteScrolling) + { + ScrollToAbsolutePosition(e.CurrentState.Mouse.Position); + return true; + } + + return base.OnMouseMove(e); + } + + #endregion + protected override ScrollbarContainer CreateScrollbar(Direction direction) { return new PaddedScrollbar(); diff --git a/osu.Game/Screens/SelectV2/Carousel.cs b/osu.Game/Screens/SelectV2/Carousel.cs index 84b90c8fe0..c8a54d4cd5 100644 --- a/osu.Game/Screens/SelectV2/Carousel.cs +++ b/osu.Game/Screens/SelectV2/Carousel.cs @@ -11,13 +11,18 @@ using osu.Framework.Bindables; using osu.Framework.Extensions.TypeExtensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Cursor; using osu.Framework.Graphics.Pooling; using osu.Framework.Graphics.Shapes; +using osu.Framework.Input.Bindings; +using osu.Framework.Input.Events; using osu.Framework.Logging; using osu.Framework.Utils; using osu.Game.Graphics.Containers; +using osu.Game.Input.Bindings; using osuTK; using osuTK.Graphics; +using osuTK.Input; namespace osu.Game.Screens.SelectV2 { @@ -121,7 +126,6 @@ namespace osu.Game.Screens.SelectV2 }, scroll = new CarouselScrollContainer { - RightMouseScrollbar = true, RelativeSizeAxes = Axes.Both, Masking = false, } @@ -391,7 +395,7 @@ namespace osu.Game.Screens.SelectV2 /// Implementation of scroll container which handles very large vertical lists by internally using double precision /// for pre-display Y values. /// - private partial class CarouselScrollContainer : UserTrackingScrollContainer + private partial class CarouselScrollContainer : UserTrackingScrollContainer, IKeyBindingHandler { public readonly Container Panels; @@ -466,6 +470,55 @@ namespace osu.Game.Screens.SelectV2 foreach (var d in Panels) d.Y = (float)(((ICarouselPanel)d).DrawYPosition + scrollableExtent); } + + #region Absolute scrolling + + private bool absoluteScrolling; + + protected override bool IsDragging => base.IsDragging || absoluteScrolling; + + public bool OnPressed(KeyBindingPressEvent e) + { + switch (e.Action) + { + case GlobalAction.AbsoluteScrollSongList: + + // The default binding for absolute scroll is right mouse button. + // To avoid conflicts with context menus, disallow absolute scroll completely if it looks like things will fall over. + if (e.CurrentState.Mouse.Buttons.Contains(MouseButton.Right) + && GetContainingInputManager()!.HoveredDrawables.OfType().Any()) + return false; + + ScrollToAbsolutePosition(e.CurrentState.Mouse.Position); + absoluteScrolling = true; + return true; + } + + return false; + } + + public void OnReleased(KeyBindingReleaseEvent e) + { + switch (e.Action) + { + case GlobalAction.AbsoluteScrollSongList: + absoluteScrolling = false; + break; + } + } + + protected override bool OnMouseMove(MouseMoveEvent e) + { + if (absoluteScrolling) + { + ScrollToAbsolutePosition(e.CurrentState.Mouse.Position); + return true; + } + + return base.OnMouseMove(e); + } + + #endregion } private class BoundsCarouselItem : CarouselItem From 81f54507ddb0cbabbd7d02d80838ff160b52f9eb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 16 Jan 2025 14:29:41 +0100 Subject: [PATCH 266/290] Fix potential index accounting mistake when creating spectator list with spectators already present Noticed by accident, but if the `BindCollectionChanged()` callback fires immediately in `LoadComplete()` when set up and there are spectators present already, then `NewStartingIndex` in the related event is -1: https://github.com/dotnet/runtime/blob/b03f83de362f7168c94daa2f4b192959abefe366/src/libraries/System.ObjectModel/src/System/Collections/Specialized/NotifyCollectionChangedEventArgs.cs#L84-L92 which kinda breaks the math introducing off-by-ones and in result causes 11 items to be displayed together rather than 10. --- osu.Game/Screens/Play/HUD/SpectatorList.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/Play/HUD/SpectatorList.cs b/osu.Game/Screens/Play/HUD/SpectatorList.cs index 04bd03f153..438aa61d9d 100644 --- a/osu.Game/Screens/Play/HUD/SpectatorList.cs +++ b/osu.Game/Screens/Play/HUD/SpectatorList.cs @@ -95,7 +95,7 @@ namespace osu.Game.Screens.Play.HUD for (int i = 0; i < e.NewItems!.Count; i++) { var spectator = (Spectator)e.NewItems![i]!; - int index = e.NewStartingIndex + i; + int index = Math.Max(e.NewStartingIndex, 0) + i; if (index >= max_spectators_displayed) break; From 5fc277aa7f88677ab68291ef592a1fdc9cb8d1be Mon Sep 17 00:00:00 2001 From: Rudi Herouard Date: Thu, 16 Jan 2025 21:53:56 +0100 Subject: [PATCH 267/290] Seek in replay scaled by replay speed --- osu.Game/Screens/Play/ReplayPlayer.cs | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/osu.Game/Screens/Play/ReplayPlayer.cs b/osu.Game/Screens/Play/ReplayPlayer.cs index c1b5397e61..ba572f6014 100644 --- a/osu.Game/Screens/Play/ReplayPlayer.cs +++ b/osu.Game/Screens/Play/ReplayPlayer.cs @@ -32,6 +32,8 @@ namespace osu.Game.Screens.Play private readonly bool replayIsFailedScore; + private PlaybackSettings playbackSettings; + protected override UserActivity InitialActivity => new UserActivity.WatchingReplay(Score.ScoreInfo); private bool isAutoplayPlayback => GameplayState.Mods.OfType().Any(); @@ -73,7 +75,7 @@ namespace osu.Game.Screens.Play if (!LoadedBeatmapSuccessfully) return; - var playbackSettings = new PlaybackSettings + playbackSettings = new PlaybackSettings { Depth = float.MaxValue, Expanded = { BindTarget = config.GetBindable(OsuSetting.ReplayPlaybackControlsExpanded) } @@ -124,11 +126,11 @@ namespace osu.Game.Screens.Play return true; case GlobalAction.SeekReplayBackward: - SeekInDirection(-5); + SeekInDirection(-5 * (float)playbackSettings.UserPlaybackRate.Value); return true; case GlobalAction.SeekReplayForward: - SeekInDirection(5); + SeekInDirection(5 * (float)playbackSettings.UserPlaybackRate.Value); return true; case GlobalAction.TogglePauseReplay: From 224f39825f5f452ec6e7341666b2cae6ac700334 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 17 Jan 2025 14:16:38 +0900 Subject: [PATCH 268/290] Fix test potentially false-negative due to realm write delays --- .../Navigation/TestSceneScreenNavigation.cs | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs b/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs index 326f21ff13..521d097fb9 100644 --- a/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs +++ b/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs @@ -41,6 +41,7 @@ using osu.Game.Screens.OnlinePlay.Match.Components; using osu.Game.Screens.OnlinePlay.Playlists; using osu.Game.Screens.Play; using osu.Game.Screens.Play.HUD; +using osu.Game.Screens.Play.PlayerSettings; using osu.Game.Screens.Ranking; using osu.Game.Screens.Select; using osu.Game.Screens.Select.Carousel; @@ -351,8 +352,13 @@ namespace osu.Game.Tests.Visual.Navigation AddStep("attempt adjust offset via keyboard", () => InputManager.Key(Key.Minus)); checkOffset(-1); - void checkOffset(double offset) => AddUntilStep($"offset is {offset}", () => Game.BeatmapManager.QueryBeatmap(b => b.ID == Game.Beatmap.Value.BeatmapInfo.ID)!.UserSettings.Offset, - () => Is.EqualTo(offset)); + void checkOffset(double offset) + { + AddUntilStep($"control offset is {offset}", () => this.ChildrenOfType().Single().ChildrenOfType().Single().Current.Value, + () => Is.EqualTo(offset)); + AddUntilStep($"database offset is {offset}", () => Game.BeatmapManager.QueryBeatmap(b => b.ID == Game.Beatmap.Value.BeatmapInfo.ID)!.UserSettings.Offset, + () => Is.EqualTo(offset)); + } } [Test] @@ -389,8 +395,13 @@ namespace osu.Game.Tests.Visual.Navigation AddStep("attempt adjust offset via keyboard", () => InputManager.Key(Key.Minus)); checkOffset(-1); - void checkOffset(double offset) => AddUntilStep($"offset is {offset}", () => Game.BeatmapManager.QueryBeatmap(b => b.ID == Game.Beatmap.Value.BeatmapInfo.ID)!.UserSettings.Offset, - () => Is.EqualTo(offset)); + void checkOffset(double offset) + { + AddUntilStep($"control offset is {offset}", () => this.ChildrenOfType().Single().ChildrenOfType().Single().Current.Value, + () => Is.EqualTo(offset)); + AddUntilStep($"database offset is {offset}", () => Game.BeatmapManager.QueryBeatmap(b => b.ID == Game.Beatmap.Value.BeatmapInfo.ID)!.UserSettings.Offset, + () => Is.EqualTo(offset)); + } } [Test] From ae7e4bef86d68dfb6e3db8f406f97c152e314cff Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Fri, 17 Jan 2025 15:42:19 +0900 Subject: [PATCH 269/290] Fix tests --- .../Visual/Online/TestSceneNowPlayingCommand.cs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/osu.Game.Tests/Visual/Online/TestSceneNowPlayingCommand.cs b/osu.Game.Tests/Visual/Online/TestSceneNowPlayingCommand.cs index 428554f761..56d03d4c7f 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneNowPlayingCommand.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneNowPlayingCommand.cs @@ -39,7 +39,7 @@ namespace osu.Game.Tests.Visual.Online [Test] public void TestGenericActivity() { - AddStep("Set activity", () => session.SetValue(Static.UserOnlineActivity, new UserActivity.InLobby(new Room()))); + AddStep("Set activity", () => session.SetValue(Static.UserOnlineActivity, new UserActivity.InLobby(new Room()))); AddStep("Run command", () => Add(new NowPlayingCommand(new Channel()))); @@ -49,7 +49,7 @@ namespace osu.Game.Tests.Visual.Online [Test] public void TestEditActivity() { - AddStep("Set activity", () => session.SetValue(Static.UserOnlineActivity, new UserActivity.EditingBeatmap(new BeatmapInfo()))); + AddStep("Set activity", () => session.SetValue(Static.UserOnlineActivity, new UserActivity.EditingBeatmap(new BeatmapInfo()))); AddStep("Run command", () => Add(new NowPlayingCommand(new Channel()))); @@ -59,7 +59,7 @@ namespace osu.Game.Tests.Visual.Online [Test] public void TestPlayActivity() { - AddStep("Set activity", () => session.SetValue(Static.UserOnlineActivity, new UserActivity.InSoloGame(new BeatmapInfo(), new OsuRuleset().RulesetInfo))); + AddStep("Set activity", () => session.SetValue(Static.UserOnlineActivity, new UserActivity.InSoloGame(new BeatmapInfo(), new OsuRuleset().RulesetInfo))); AddStep("Run command", () => Add(new NowPlayingCommand(new Channel()))); @@ -70,7 +70,7 @@ namespace osu.Game.Tests.Visual.Online [TestCase(false)] public void TestLinkPresence(bool hasOnlineId) { - AddStep("Set activity", () => session.SetValue(Static.UserOnlineActivity, new UserActivity.InLobby(new Room()))); + AddStep("Set activity", () => session.SetValue(Static.UserOnlineActivity, new UserActivity.InLobby(new Room()))); AddStep("Set beatmap", () => Beatmap.Value = new DummyWorkingBeatmap(Audio, null) { @@ -88,7 +88,7 @@ namespace osu.Game.Tests.Visual.Online [Test] public void TestModPresence() { - AddStep("Set activity", () => session.SetValue(Static.UserOnlineActivity, new UserActivity.InSoloGame(new BeatmapInfo(), new OsuRuleset().RulesetInfo))); + AddStep("Set activity", () => session.SetValue(Static.UserOnlineActivity, new UserActivity.InSoloGame(new BeatmapInfo(), new OsuRuleset().RulesetInfo))); AddStep("Add Hidden mod", () => SelectedMods.Value = new[] { Ruleset.Value.CreateInstance().CreateMod() }); From a51938f4e97c3d09673dc677bc368d17b351dfaf Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Fri, 17 Jan 2025 15:59:25 +0900 Subject: [PATCH 270/290] Separate the local user state --- osu.Game/Online/Metadata/MetadataClient.cs | 5 ++++ .../Online/Metadata/OnlineMetadataClient.cs | 27 ++++++++++++------- .../Visual/Metadata/TestMetadataClient.cs | 19 ++++++++++--- 3 files changed, 38 insertions(+), 13 deletions(-) diff --git a/osu.Game/Online/Metadata/MetadataClient.cs b/osu.Game/Online/Metadata/MetadataClient.cs index 6578f70f74..507f43467c 100644 --- a/osu.Game/Online/Metadata/MetadataClient.cs +++ b/osu.Game/Online/Metadata/MetadataClient.cs @@ -37,6 +37,11 @@ namespace osu.Game.Online.Metadata /// public abstract IBindable IsWatchingUserPresence { get; } + /// + /// The information about the current user. + /// + public abstract UserPresence LocalUserState { get; } + /// /// Dictionary keyed by user ID containing all of the information about currently online users received from the server. /// diff --git a/osu.Game/Online/Metadata/OnlineMetadataClient.cs b/osu.Game/Online/Metadata/OnlineMetadataClient.cs index 01d7a564fa..04abca1e9b 100644 --- a/osu.Game/Online/Metadata/OnlineMetadataClient.cs +++ b/osu.Game/Online/Metadata/OnlineMetadataClient.cs @@ -23,6 +23,9 @@ namespace osu.Game.Online.Metadata public override IBindable IsWatchingUserPresence => isWatchingUserPresence; private readonly BindableBool isWatchingUserPresence = new BindableBool(); + public override UserPresence LocalUserState => localUserState; + private UserPresence localUserState; + public override IBindableDictionary UserStates => userStates; private readonly BindableDictionary userStates = new BindableDictionary(); @@ -110,6 +113,7 @@ namespace osu.Game.Online.Metadata userStates.Clear(); friendStates.Clear(); dailyChallengeInfo.Value = null; + localUserState = default; }); return; } @@ -202,9 +206,19 @@ namespace osu.Game.Online.Metadata Schedule(() => { if (presence?.Status != null) - userStates[userId] = presence.Value; + { + if (userId == api.LocalUser.Value.OnlineID) + localUserState = presence.Value; + else + userStates[userId] = presence.Value; + } else - userStates.Remove(userId); + { + if (userId == api.LocalUser.Value.OnlineID) + localUserState = default; + else + userStates.Remove(userId); + } }); return Task.CompletedTask; @@ -242,14 +256,7 @@ namespace osu.Game.Online.Metadata throw new OperationCanceledException(); // must be scheduled before any remote calls to avoid mis-ordering. - Schedule(() => - { - bool hadLocalUserState = userStates.TryGetValue(api.LocalUser.Value.OnlineID, out var presence); - userStates.Clear(); - if (hadLocalUserState) - userStates[api.LocalUser.Value.OnlineID] = presence; - }); - + Schedule(() => userStates.Clear()); Debug.Assert(connection != null); await connection.InvokeAsync(nameof(IMetadataServer.EndWatchingUserPresence)).ConfigureAwait(false); Logger.Log($@"{nameof(OnlineMetadataClient)} stopped watching user presence", LoggingTarget.Network); diff --git a/osu.Game/Tests/Visual/Metadata/TestMetadataClient.cs b/osu.Game/Tests/Visual/Metadata/TestMetadataClient.cs index 36f79a5adc..d32d49b55e 100644 --- a/osu.Game/Tests/Visual/Metadata/TestMetadataClient.cs +++ b/osu.Game/Tests/Visual/Metadata/TestMetadataClient.cs @@ -19,6 +19,9 @@ namespace osu.Game.Tests.Visual.Metadata public override IBindable IsWatchingUserPresence => isWatchingUserPresence; private readonly BindableBool isWatchingUserPresence = new BindableBool(); + public override UserPresence LocalUserState => localUserState; + private UserPresence localUserState; + public override IBindableDictionary UserStates => userStates; private readonly BindableDictionary userStates = new BindableDictionary(); @@ -71,10 +74,20 @@ namespace osu.Game.Tests.Visual.Metadata { if (isWatchingUserPresence.Value) { - if (presence.HasValue) - userStates[userId] = presence.Value; + if (presence?.Status != null) + { + if (userId == api.LocalUser.Value.OnlineID) + localUserState = presence.Value; + else + userStates[userId] = presence.Value; + } else - userStates.Remove(userId); + { + if (userId == api.LocalUser.Value.OnlineID) + localUserState = default; + else + userStates.Remove(userId); + } } return Task.CompletedTask; From 3bb4b0c2b8a84c5bf3330a84422e6f3c077b346f Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 17 Jan 2025 16:25:48 +0900 Subject: [PATCH 271/290] Rename fields from `State` to `Presence` when presence is involved --- osu.Game/Online/FriendPresenceNotifier.cs | 10 +++--- osu.Game/Online/Metadata/MetadataClient.cs | 6 ++-- .../Online/Metadata/OnlineMetadataClient.cs | 32 +++++++++---------- .../Dashboard/CurrentlyOnlineDisplay.cs | 2 +- .../Visual/Metadata/TestMetadataClient.cs | 32 +++++++++---------- 5 files changed, 41 insertions(+), 41 deletions(-) diff --git a/osu.Game/Online/FriendPresenceNotifier.cs b/osu.Game/Online/FriendPresenceNotifier.cs index 330e0a908f..dd141b756b 100644 --- a/osu.Game/Online/FriendPresenceNotifier.cs +++ b/osu.Game/Online/FriendPresenceNotifier.cs @@ -46,7 +46,7 @@ namespace osu.Game.Online private readonly Bindable notifyOnFriendPresenceChange = new BindableBool(); private readonly IBindableList friends = new BindableList(); - private readonly IBindableDictionary friendStates = new BindableDictionary(); + private readonly IBindableDictionary friendPresences = new BindableDictionary(); private readonly HashSet onlineAlertQueue = new HashSet(); private readonly HashSet offlineAlertQueue = new HashSet(); @@ -63,8 +63,8 @@ namespace osu.Game.Online friends.BindTo(api.Friends); friends.BindCollectionChanged(onFriendsChanged, true); - friendStates.BindTo(metadataClient.FriendStates); - friendStates.BindCollectionChanged(onFriendStatesChanged, true); + friendPresences.BindTo(metadataClient.FriendPresences); + friendPresences.BindCollectionChanged(onFriendPresenceChanged, true); } protected override void Update() @@ -85,7 +85,7 @@ namespace osu.Game.Online if (friend.TargetUser is not APIUser user) continue; - if (friendStates.TryGetValue(friend.TargetID, out _)) + if (friendPresences.TryGetValue(friend.TargetID, out _)) markUserOnline(user); } @@ -105,7 +105,7 @@ namespace osu.Game.Online } } - private void onFriendStatesChanged(object? sender, NotifyDictionaryChangedEventArgs e) + private void onFriendPresenceChanged(object? sender, NotifyDictionaryChangedEventArgs e) { switch (e.Action) { diff --git a/osu.Game/Online/Metadata/MetadataClient.cs b/osu.Game/Online/Metadata/MetadataClient.cs index 507f43467c..3c0b47ad3d 100644 --- a/osu.Game/Online/Metadata/MetadataClient.cs +++ b/osu.Game/Online/Metadata/MetadataClient.cs @@ -40,17 +40,17 @@ namespace osu.Game.Online.Metadata /// /// The information about the current user. /// - public abstract UserPresence LocalUserState { get; } + public abstract UserPresence LocalUserPresence { get; } /// /// Dictionary keyed by user ID containing all of the information about currently online users received from the server. /// - public abstract IBindableDictionary UserStates { get; } + public abstract IBindableDictionary UserPresences { get; } /// /// Dictionary keyed by user ID containing all of the information about currently online friends received from the server. /// - public abstract IBindableDictionary FriendStates { get; } + public abstract IBindableDictionary FriendPresences { get; } /// public abstract Task UpdateActivity(UserActivity? activity); diff --git a/osu.Game/Online/Metadata/OnlineMetadataClient.cs b/osu.Game/Online/Metadata/OnlineMetadataClient.cs index 04abca1e9b..5aeeb04d11 100644 --- a/osu.Game/Online/Metadata/OnlineMetadataClient.cs +++ b/osu.Game/Online/Metadata/OnlineMetadataClient.cs @@ -23,14 +23,14 @@ namespace osu.Game.Online.Metadata public override IBindable IsWatchingUserPresence => isWatchingUserPresence; private readonly BindableBool isWatchingUserPresence = new BindableBool(); - public override UserPresence LocalUserState => localUserState; - private UserPresence localUserState; + public override UserPresence LocalUserPresence => localUserPresence; + private UserPresence localUserPresence; - public override IBindableDictionary UserStates => userStates; - private readonly BindableDictionary userStates = new BindableDictionary(); + public override IBindableDictionary UserPresences => userPresences; + private readonly BindableDictionary userPresences = new BindableDictionary(); - public override IBindableDictionary FriendStates => friendStates; - private readonly BindableDictionary friendStates = new BindableDictionary(); + public override IBindableDictionary FriendPresences => friendPresences; + private readonly BindableDictionary friendPresences = new BindableDictionary(); public override IBindable DailyChallengeInfo => dailyChallengeInfo; private readonly Bindable dailyChallengeInfo = new Bindable(); @@ -110,10 +110,10 @@ namespace osu.Game.Online.Metadata Schedule(() => { isWatchingUserPresence.Value = false; - userStates.Clear(); - friendStates.Clear(); + userPresences.Clear(); + friendPresences.Clear(); dailyChallengeInfo.Value = null; - localUserState = default; + localUserPresence = default; }); return; } @@ -208,16 +208,16 @@ namespace osu.Game.Online.Metadata if (presence?.Status != null) { if (userId == api.LocalUser.Value.OnlineID) - localUserState = presence.Value; + localUserPresence = presence.Value; else - userStates[userId] = presence.Value; + userPresences[userId] = presence.Value; } else { if (userId == api.LocalUser.Value.OnlineID) - localUserState = default; + localUserPresence = default; else - userStates.Remove(userId); + userPresences.Remove(userId); } }); @@ -229,9 +229,9 @@ namespace osu.Game.Online.Metadata Schedule(() => { if (presence?.Status != null) - friendStates[userId] = presence.Value; + friendPresences[userId] = presence.Value; else - friendStates.Remove(userId); + friendPresences.Remove(userId); }); return Task.CompletedTask; @@ -256,7 +256,7 @@ namespace osu.Game.Online.Metadata throw new OperationCanceledException(); // must be scheduled before any remote calls to avoid mis-ordering. - Schedule(() => userStates.Clear()); + Schedule(() => userPresences.Clear()); Debug.Assert(connection != null); await connection.InvokeAsync(nameof(IMetadataServer.EndWatchingUserPresence)).ConfigureAwait(false); Logger.Log($@"{nameof(OnlineMetadataClient)} stopped watching user presence", LoggingTarget.Network); diff --git a/osu.Game/Overlays/Dashboard/CurrentlyOnlineDisplay.cs b/osu.Game/Overlays/Dashboard/CurrentlyOnlineDisplay.cs index 2ca548fdf5..39023c16f6 100644 --- a/osu.Game/Overlays/Dashboard/CurrentlyOnlineDisplay.cs +++ b/osu.Game/Overlays/Dashboard/CurrentlyOnlineDisplay.cs @@ -106,7 +106,7 @@ namespace osu.Game.Overlays.Dashboard { base.LoadComplete(); - onlineUsers.BindTo(metadataClient.UserStates); + onlineUsers.BindTo(metadataClient.UserPresences); onlineUsers.BindCollectionChanged(onUserUpdated, true); playingUsers.BindTo(spectatorClient.PlayingUsers); diff --git a/osu.Game/Tests/Visual/Metadata/TestMetadataClient.cs b/osu.Game/Tests/Visual/Metadata/TestMetadataClient.cs index d32d49b55e..7b08108194 100644 --- a/osu.Game/Tests/Visual/Metadata/TestMetadataClient.cs +++ b/osu.Game/Tests/Visual/Metadata/TestMetadataClient.cs @@ -19,14 +19,14 @@ namespace osu.Game.Tests.Visual.Metadata public override IBindable IsWatchingUserPresence => isWatchingUserPresence; private readonly BindableBool isWatchingUserPresence = new BindableBool(); - public override UserPresence LocalUserState => localUserState; - private UserPresence localUserState; + public override UserPresence LocalUserPresence => localUserPresence; + private UserPresence localUserPresence; - public override IBindableDictionary UserStates => userStates; - private readonly BindableDictionary userStates = new BindableDictionary(); + public override IBindableDictionary UserPresences => userPresences; + private readonly BindableDictionary userPresences = new BindableDictionary(); - public override IBindableDictionary FriendStates => friendStates; - private readonly BindableDictionary friendStates = new BindableDictionary(); + public override IBindableDictionary FriendPresences => friendPresences; + private readonly BindableDictionary friendPresences = new BindableDictionary(); public override Bindable DailyChallengeInfo => dailyChallengeInfo; private readonly Bindable dailyChallengeInfo = new Bindable(); @@ -50,9 +50,9 @@ namespace osu.Game.Tests.Visual.Metadata { if (isWatchingUserPresence.Value) { - userStates.TryGetValue(api.LocalUser.Value.Id, out var localUserPresence); + userPresences.TryGetValue(api.LocalUser.Value.Id, out var localUserPresence); localUserPresence = localUserPresence with { Activity = activity }; - userStates[api.LocalUser.Value.Id] = localUserPresence; + userPresences[api.LocalUser.Value.Id] = localUserPresence; } return Task.CompletedTask; @@ -62,9 +62,9 @@ namespace osu.Game.Tests.Visual.Metadata { if (isWatchingUserPresence.Value) { - userStates.TryGetValue(api.LocalUser.Value.Id, out var localUserPresence); + userPresences.TryGetValue(api.LocalUser.Value.Id, out var localUserPresence); localUserPresence = localUserPresence with { Status = status }; - userStates[api.LocalUser.Value.Id] = localUserPresence; + userPresences[api.LocalUser.Value.Id] = localUserPresence; } return Task.CompletedTask; @@ -77,16 +77,16 @@ namespace osu.Game.Tests.Visual.Metadata if (presence?.Status != null) { if (userId == api.LocalUser.Value.OnlineID) - localUserState = presence.Value; + localUserPresence = presence.Value; else - userStates[userId] = presence.Value; + userPresences[userId] = presence.Value; } else { if (userId == api.LocalUser.Value.OnlineID) - localUserState = default; + localUserPresence = default; else - userStates.Remove(userId); + userPresences.Remove(userId); } } @@ -96,9 +96,9 @@ namespace osu.Game.Tests.Visual.Metadata public override Task FriendPresenceUpdated(int userId, UserPresence? presence) { if (presence.HasValue) - friendStates[userId] = presence.Value; + friendPresences[userId] = presence.Value; else - friendStates.Remove(userId); + friendPresences.Remove(userId); return Task.CompletedTask; } From 311f08b962a3ca2d99bc42f82459a231bbf41fa8 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 17 Jan 2025 16:29:02 +0900 Subject: [PATCH 272/290] Update `TestMetadataClient` to correctly set local user state in line with changes --- .../Tests/Visual/Metadata/TestMetadataClient.cs | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/osu.Game/Tests/Visual/Metadata/TestMetadataClient.cs b/osu.Game/Tests/Visual/Metadata/TestMetadataClient.cs index 7b08108194..d14cbd7743 100644 --- a/osu.Game/Tests/Visual/Metadata/TestMetadataClient.cs +++ b/osu.Game/Tests/Visual/Metadata/TestMetadataClient.cs @@ -48,11 +48,12 @@ namespace osu.Game.Tests.Visual.Metadata public override Task UpdateActivity(UserActivity? activity) { + localUserPresence = localUserPresence with { Activity = activity }; + if (isWatchingUserPresence.Value) { - userPresences.TryGetValue(api.LocalUser.Value.Id, out var localUserPresence); - localUserPresence = localUserPresence with { Activity = activity }; - userPresences[api.LocalUser.Value.Id] = localUserPresence; + if (userPresences.ContainsKey(api.LocalUser.Value.Id)) + userPresences[api.LocalUser.Value.Id] = localUserPresence; } return Task.CompletedTask; @@ -60,11 +61,12 @@ namespace osu.Game.Tests.Visual.Metadata public override Task UpdateStatus(UserStatus? status) { + localUserPresence = localUserPresence with { Status = status }; + if (isWatchingUserPresence.Value) { - userPresences.TryGetValue(api.LocalUser.Value.Id, out var localUserPresence); - localUserPresence = localUserPresence with { Status = status }; - userPresences[api.LocalUser.Value.Id] = localUserPresence; + if (userPresences.ContainsKey(api.LocalUser.Value.Id)) + userPresences[api.LocalUser.Value.Id] = localUserPresence; } return Task.CompletedTask; From 41c603b56f0b9d0fce6b2fe03954d88c82644cba Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 17 Jan 2025 16:41:02 +0900 Subject: [PATCH 273/290] Fix double-retrieval of user presence from dictionary in online display --- .../Overlays/Dashboard/CurrentlyOnlineDisplay.cs | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/osu.Game/Overlays/Dashboard/CurrentlyOnlineDisplay.cs b/osu.Game/Overlays/Dashboard/CurrentlyOnlineDisplay.cs index 39023c16f6..bb4c9d96c8 100644 --- a/osu.Game/Overlays/Dashboard/CurrentlyOnlineDisplay.cs +++ b/osu.Game/Overlays/Dashboard/CurrentlyOnlineDisplay.cs @@ -37,7 +37,7 @@ namespace osu.Game.Overlays.Dashboard private const float padding = 10; private readonly IBindableList playingUsers = new BindableList(); - private readonly IBindableDictionary onlineUsers = new BindableDictionary(); + private readonly IBindableDictionary onlineUserPresences = new BindableDictionary(); private readonly Dictionary userPanels = new Dictionary(); private SearchContainer userFlow; @@ -106,8 +106,8 @@ namespace osu.Game.Overlays.Dashboard { base.LoadComplete(); - onlineUsers.BindTo(metadataClient.UserPresences); - onlineUsers.BindCollectionChanged(onUserUpdated, true); + onlineUserPresences.BindTo(metadataClient.UserPresences); + onlineUserPresences.BindCollectionChanged(onUserPresenceUpdated, true); playingUsers.BindTo(spectatorClient.PlayingUsers); playingUsers.BindCollectionChanged(onPlayingUsersChanged, true); @@ -120,7 +120,7 @@ namespace osu.Game.Overlays.Dashboard searchTextBox.TakeFocus(); } - private void onUserUpdated(object sender, NotifyDictionaryChangedEventArgs e) => Schedule(() => + private void onUserPresenceUpdated(object sender, NotifyDictionaryChangedEventArgs e) => Schedule(() => { switch (e.Action) { @@ -142,8 +142,10 @@ namespace osu.Game.Overlays.Dashboard { userFlow.Add(userPanels[userId] = createUserPanel(user).With(p => { - p.Status.Value = onlineUsers.GetValueOrDefault(userId).Status; - p.Activity.Value = onlineUsers.GetValueOrDefault(userId).Activity; + var presence = onlineUserPresences.GetValueOrDefault(userId); + + p.Status.Value = presence.Status; + p.Activity.Value = presence.Activity; })); }); }); From a1c5fad6d45c24318028c9f00b0750ad2fb77b88 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 15 Jan 2025 20:02:46 +0900 Subject: [PATCH 274/290] Add curvature to new carousel implementation --- osu.Game/Screens/SelectV2/Carousel.cs | 67 +++++++++++++++------------ 1 file changed, 37 insertions(+), 30 deletions(-) diff --git a/osu.Game/Screens/SelectV2/Carousel.cs b/osu.Game/Screens/SelectV2/Carousel.cs index c8a54d4cd5..a19c86d90b 100644 --- a/osu.Game/Screens/SelectV2/Carousel.cs +++ b/osu.Game/Screens/SelectV2/Carousel.cs @@ -13,7 +13,6 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Cursor; using osu.Framework.Graphics.Pooling; -using osu.Framework.Graphics.Shapes; using osu.Framework.Input.Bindings; using osu.Framework.Input.Events; using osu.Framework.Logging; @@ -21,7 +20,6 @@ using osu.Framework.Utils; using osu.Game.Graphics.Containers; using osu.Game.Input.Bindings; using osuTK; -using osuTK.Graphics; using osuTK.Input; namespace osu.Game.Screens.SelectV2 @@ -117,18 +115,10 @@ namespace osu.Game.Screens.SelectV2 protected Carousel() { - InternalChildren = new Drawable[] + InternalChild = scroll = new CarouselScrollContainer { - new Box - { - Colour = Color4.Black, - RelativeSizeAxes = Axes.Both, - }, - scroll = new CarouselScrollContainer - { - RelativeSizeAxes = Axes.Both, - Masking = false, - } + RelativeSizeAxes = Axes.Both, + Masking = false, }; Items.BindCollectionChanged((_, _) => FilterAsync()); @@ -283,6 +273,11 @@ namespace osu.Game.Screens.SelectV2 /// private float visibleUpperBound => (float)(scroll.Current - BleedTop); + /// + /// Half the height of the visible content. + /// + private float visibleHalfHeight => (DrawHeight + BleedBottom + BleedTop) / 2; + protected override void Update() { base.Update(); @@ -302,13 +297,39 @@ namespace osu.Game.Screens.SelectV2 foreach (var panel in scroll.Panels) { - var carouselPanel = (ICarouselPanel)panel; + var c = (ICarouselPanel)panel; - if (panel.Depth != carouselPanel.DrawYPosition) - scroll.Panels.ChangeChildDepth(panel, (float)carouselPanel.DrawYPosition); + if (panel.Depth != c.DrawYPosition) + scroll.Panels.ChangeChildDepth(panel, (float)c.DrawYPosition); + + Debug.Assert(c.Item != null); + + if (c.DrawYPosition != c.Item.CarouselYPosition) + c.DrawYPosition = Interpolation.DampContinuously(c.DrawYPosition, c.Item.CarouselYPosition, 50, Time.Elapsed); + + Vector2 posInScroll = scroll.ToLocalSpace(panel.ScreenSpaceDrawQuad.Centre); + float dist = Math.Abs(1f - posInScroll.Y / visibleHalfHeight); + + panel.X = offsetX(dist, visibleHalfHeight); } } + /// + /// Computes the x-offset of currently visible items. Makes the carousel appear round. + /// + /// + /// Vertical distance from the center of the carousel container + /// ranging from -1 to 1. + /// + /// Half the height of the carousel container. + private static float offsetX(float dist, float halfHeight) + { + // The radius of the circle the carousel moves on. + const float circle_radius = 3; + float discriminant = MathF.Max(0, circle_radius * circle_radius - dist * dist); + return (circle_radius - MathF.Sqrt(discriminant)) * halfHeight; + } + private DisplayRange getDisplayRange() { Debug.Assert(displayedCarouselItems != null); @@ -425,20 +446,6 @@ namespace osu.Game.Screens.SelectV2 } } - protected override void Update() - { - base.Update(); - - foreach (var panel in Panels) - { - var c = (ICarouselPanel)panel; - Debug.Assert(c.Item != null); - - if (c.DrawYPosition != c.Item.CarouselYPosition) - c.DrawYPosition = Interpolation.DampContinuously(c.DrawYPosition, c.Item.CarouselYPosition, 50, Time.Elapsed); - } - } - public override void Clear(bool disposeChildren) { Panels.Height = 0; From 54f9cb7f6817341d992b7bbda62d5a31db4aae1e Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 15 Jan 2025 19:02:27 +0900 Subject: [PATCH 275/290] Add overlapping spacing support --- osu.Game/Screens/SelectV2/Carousel.cs | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/SelectV2/Carousel.cs b/osu.Game/Screens/SelectV2/Carousel.cs index a19c86d90b..42c272401a 100644 --- a/osu.Game/Screens/SelectV2/Carousel.cs +++ b/osu.Game/Screens/SelectV2/Carousel.cs @@ -51,6 +51,11 @@ namespace osu.Game.Screens.SelectV2 /// public float DistanceOffscreenToPreload { get; set; } + /// + /// Vertical space between panel layout. Negative value can be used to create an overlapping effect. + /// + protected float SpacingBetweenPanels { get; set; } = -5; + /// /// When a new request arrives to change filtering, the number of milliseconds to wait before performing the filter. /// Regardless of any external debouncing, this is a safety measure to avoid triggering too many threaded operations. @@ -207,13 +212,12 @@ namespace osu.Game.Screens.SelectV2 private async Task updateYPositions(IEnumerable carouselItems, CancellationToken cancellationToken) => await Task.Run(() => { - const float spacing = 10; float yPos = 0; foreach (var item in carouselItems) { item.CarouselYPosition = yPos; - yPos += item.DrawHeight + spacing; + yPos += item.DrawHeight + SpacingBetweenPanels; } }, cancellationToken).ConfigureAwait(false); From 43b54623d9ac8a02125d896cfb59d341b5eccc95 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 15 Jan 2025 20:24:41 +0900 Subject: [PATCH 276/290] Add required padding on either side of panels so selection can remain centered --- osu.Game/Screens/SelectV2/Carousel.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/SelectV2/Carousel.cs b/osu.Game/Screens/SelectV2/Carousel.cs index 42c272401a..a07022b32f 100644 --- a/osu.Game/Screens/SelectV2/Carousel.cs +++ b/osu.Game/Screens/SelectV2/Carousel.cs @@ -212,7 +212,7 @@ namespace osu.Game.Screens.SelectV2 private async Task updateYPositions(IEnumerable carouselItems, CancellationToken cancellationToken) => await Task.Run(() => { - float yPos = 0; + float yPos = visibleHalfHeight; foreach (var item in carouselItems) { @@ -398,7 +398,7 @@ namespace osu.Game.Screens.SelectV2 if (displayedCarouselItems.Count > 0) { var lastItem = displayedCarouselItems[^1]; - scroll.SetLayoutHeight((float)(lastItem.CarouselYPosition + lastItem.DrawHeight)); + scroll.SetLayoutHeight((float)(lastItem.CarouselYPosition + lastItem.DrawHeight + visibleHalfHeight)); } else scroll.SetLayoutHeight(0); From b2150739573b3e3f8ca27577b19b724c66722661 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 17 Jan 2025 10:26:59 +0100 Subject: [PATCH 277/290] Add completion marker to daily challenge profile counter --- .../TestSceneUserProfileDailyChallenge.cs | 4 + .../Components/DailyChallengeStatsDisplay.cs | 120 +++++++++++++----- 2 files changed, 92 insertions(+), 32 deletions(-) diff --git a/osu.Game.Tests/Visual/Online/TestSceneUserProfileDailyChallenge.cs b/osu.Game.Tests/Visual/Online/TestSceneUserProfileDailyChallenge.cs index 0477d39193..ce62a3255d 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneUserProfileDailyChallenge.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneUserProfileDailyChallenge.cs @@ -38,6 +38,10 @@ namespace osu.Game.Tests.Visual.Online AddSliderStep("top 10%", 0, 999, 0, v => update(s => s.Top10PercentPlacements = v)); AddSliderStep("top 50%", 0, 999, 0, v => update(s => s.Top50PercentPlacements = v)); AddSliderStep("playcount", 0, 1500, 1, v => update(s => s.PlayCount = v)); + AddStep("user played today", () => update(s => s.LastUpdate = DateTimeOffset.UtcNow.Date)); + AddStep("user played yesterday", () => update(s => s.LastUpdate = DateTimeOffset.UtcNow.Date.AddDays(-1))); + AddStep("user is local user", () => update(s => s.UserID = API.LocalUser.Value.Id)); + AddStep("user is not local user", () => update(s => s.UserID = API.LocalUser.Value.Id + 1000)); AddStep("create", () => { Clear(); diff --git a/osu.Game/Overlays/Profile/Header/Components/DailyChallengeStatsDisplay.cs b/osu.Game/Overlays/Profile/Header/Components/DailyChallengeStatsDisplay.cs index 3e86b2268f..ad64f7d7ac 100644 --- a/osu.Game/Overlays/Profile/Header/Components/DailyChallengeStatsDisplay.cs +++ b/osu.Game/Overlays/Profile/Header/Components/DailyChallengeStatsDisplay.cs @@ -1,6 +1,7 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Extensions.LocalisationExtensions; @@ -8,11 +9,14 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Cursor; using osu.Framework.Graphics.Shapes; +using osu.Framework.Graphics.Sprites; using osu.Game.Graphics; using osu.Game.Graphics.Containers; using osu.Game.Graphics.Sprites; using osu.Game.Localisation; +using osu.Game.Online.API; using osu.Game.Online.API.Requests.Responses; +using osuTK; namespace osu.Game.Overlays.Profile.Header.Components { @@ -23,6 +27,11 @@ namespace osu.Game.Overlays.Profile.Header.Components public DailyChallengeTooltipData? TooltipContent { get; private set; } private OsuSpriteText dailyPlayCount = null!; + private Container content = null!; + private CircularContainer completionMark = null!; + + [Resolved] + private IAPIProvider api { get; set; } [Resolved] private OsuColour colours { get; set; } = null!; @@ -34,58 +43,91 @@ namespace osu.Game.Overlays.Profile.Header.Components private void load() { AutoSizeAxes = Axes.Both; - CornerRadius = 5; - Masking = true; InternalChildren = new Drawable[] { - new Box + content = new Container { - RelativeSizeAxes = Axes.Both, - Colour = colourProvider.Background4, - }, - new FillFlowContainer - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Padding = new MarginPadding(5f), AutoSizeAxes = Axes.Both, - Direction = FillDirection.Horizontal, + CornerRadius = 6, + BorderThickness = 2, + BorderColour = colourProvider.Background4, + Masking = true, Children = new Drawable[] { - new OsuTextFlowContainer(s => s.Font = OsuFont.GetFont(size: 12)) + new Box { - AutoSizeAxes = Axes.Both, - // can't use this because osu-web does weird stuff with \\n. - // Text = UsersStrings.ShowDailyChallengeTitle., - Text = "Daily\nChallenge", - Margin = new MarginPadding { Horizontal = 5f, Bottom = 2f }, + RelativeSizeAxes = Axes.Both, + Colour = colourProvider.Background4, }, - new Container + new FillFlowContainer { - AutoSizeAxes = Axes.X, - RelativeSizeAxes = Axes.Y, - CornerRadius = 5f, - Masking = true, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Padding = new MarginPadding(3f), + AutoSizeAxes = Axes.Both, + Direction = FillDirection.Horizontal, Children = new Drawable[] { - new Box + new OsuTextFlowContainer(s => s.Font = OsuFont.GetFont(size: 12)) { - RelativeSizeAxes = Axes.Both, - Colour = colourProvider.Background6, + AutoSizeAxes = Axes.Both, + // can't use this because osu-web does weird stuff with \\n. + // Text = UsersStrings.ShowDailyChallengeTitle., + Text = "Daily\nChallenge", + Margin = new MarginPadding { Horizontal = 5f, Bottom = 2f }, }, - dailyPlayCount = new OsuSpriteText + new Container { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - UseFullGlyphHeight = false, - Colour = colourProvider.Content2, - Margin = new MarginPadding { Horizontal = 10f, Vertical = 5f }, + AutoSizeAxes = Axes.X, + RelativeSizeAxes = Axes.Y, + CornerRadius = 3, + Masking = true, + Children = new Drawable[] + { + new Box + { + RelativeSizeAxes = Axes.Both, + Colour = colourProvider.Background6, + }, + dailyPlayCount = new OsuSpriteText + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + UseFullGlyphHeight = false, + Colour = colourProvider.Content2, + Margin = new MarginPadding { Horizontal = 10f, Vertical = 5f }, + }, + } }, } }, } }, + completionMark = new CircularContainer + { + Alpha = 0, + Size = new Vector2(16), + Anchor = Anchor.TopRight, + Origin = Anchor.Centre, + Masking = true, + Children = new Drawable[] + { + new Box + { + RelativeSizeAxes = Axes.Both, + Colour = colours.Lime1, + }, + new SpriteIcon + { + Size = new Vector2(8), + Colour = colourProvider.Background6, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Icon = FontAwesome.Solid.Check, + } + } + }, }; } @@ -114,6 +156,20 @@ namespace osu.Game.Overlays.Profile.Header.Components dailyPlayCount.Text = DailyChallengeStatsDisplayStrings.UnitDay(stats.PlayCount.ToLocalisableString("N0")); dailyPlayCount.Colour = colours.ForRankingTier(DailyChallengeStatsTooltip.TierForPlayCount(stats.PlayCount)); + bool playedToday = stats.LastUpdate?.Date == DateTimeOffset.UtcNow.Date; + bool userIsOnOwnProfile = stats.UserID == api.LocalUser.Value.Id; + + if (playedToday && userIsOnOwnProfile) + { + completionMark.Alpha = 1; + content.BorderColour = colours.Lime1; + } + else + { + completionMark.Alpha = 0; + content.BorderColour = colourProvider.Background4; + } + TooltipContent = new DailyChallengeTooltipData(colourProvider, stats); Show(); From a67a68c5969e61349a8d5866dd9c946bbf39c823 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 17 Jan 2025 10:40:26 +0100 Subject: [PATCH 278/290] Remove unnecessary masking spec It was clipping the daily challenge completion checkmark, and it originates in some veeeeery old code where the profile overlay looked and behaved very differently (0fa02718786a0eefa063cce18e9e5351f509ab59). --- osu.Game/Overlays/Profile/Header/Components/MainDetails.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/osu.Game/Overlays/Profile/Header/Components/MainDetails.cs b/osu.Game/Overlays/Profile/Header/Components/MainDetails.cs index 4bdd5425c0..10bb69f0f5 100644 --- a/osu.Game/Overlays/Profile/Header/Components/MainDetails.cs +++ b/osu.Game/Overlays/Profile/Header/Components/MainDetails.cs @@ -41,7 +41,6 @@ namespace osu.Game.Overlays.Profile.Header.Components AutoSizeAxes = Axes.Y, AutoSizeDuration = 200, AutoSizeEasing = Easing.OutQuint, - Masking = true, Direction = FillDirection.Vertical, Spacing = new Vector2(0, 15), Children = new Drawable[] From b79e937d2dbf0d9363833c7d725a5ab4c5d9f28d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 17 Jan 2025 13:34:16 +0100 Subject: [PATCH 279/290] Fix code quality --- .../Profile/Header/Components/DailyChallengeStatsDisplay.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Overlays/Profile/Header/Components/DailyChallengeStatsDisplay.cs b/osu.Game/Overlays/Profile/Header/Components/DailyChallengeStatsDisplay.cs index ad64f7d7ac..a9d982e17f 100644 --- a/osu.Game/Overlays/Profile/Header/Components/DailyChallengeStatsDisplay.cs +++ b/osu.Game/Overlays/Profile/Header/Components/DailyChallengeStatsDisplay.cs @@ -31,7 +31,7 @@ namespace osu.Game.Overlays.Profile.Header.Components private CircularContainer completionMark = null!; [Resolved] - private IAPIProvider api { get; set; } + private IAPIProvider api { get; set; } = null!; [Resolved] private OsuColour colours { get; set; } = null!; From ebca2e4b4ffc2bee95016e4fac4063dc5bc78405 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 17 Jan 2025 13:33:59 +0100 Subject: [PATCH 280/290] Implement precise movement tool As mentioned in one of the points in https://github.com/ppy/osu/discussions/31263. --- .../Edit/PreciseMovementPopover.cs | 190 ++++++++++++++++++ .../Edit/TransformToolboxGroup.cs | 25 ++- .../UserInterfaceV2/SliderWithTextBoxInput.cs | 5 + .../Input/Bindings/GlobalActionContainer.cs | 6 +- .../GlobalActionKeyBindingStrings.cs | 5 + 5 files changed, 228 insertions(+), 3 deletions(-) create mode 100644 osu.Game.Rulesets.Osu/Edit/PreciseMovementPopover.cs diff --git a/osu.Game.Rulesets.Osu/Edit/PreciseMovementPopover.cs b/osu.Game.Rulesets.Osu/Edit/PreciseMovementPopover.cs new file mode 100644 index 0000000000..151ca31ac0 --- /dev/null +++ b/osu.Game.Rulesets.Osu/Edit/PreciseMovementPopover.cs @@ -0,0 +1,190 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using osu.Framework.Allocation; +using osu.Framework.Bindables; +using osu.Framework.Extensions; +using osu.Game.Graphics.UserInterfaceV2; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Primitives; +using osu.Framework.Input.Events; +using osu.Game.Extensions; +using osu.Game.Graphics.UserInterface; +using osu.Game.Input.Bindings; +using osu.Game.Rulesets.Objects; +using osu.Game.Rulesets.Objects.Types; +using osu.Game.Rulesets.Osu.Objects; +using osu.Game.Rulesets.Osu.UI; +using osu.Game.Screens.Edit; +using osu.Game.Utils; +using osuTK; + +namespace osu.Game.Rulesets.Osu.Edit +{ + public partial class PreciseMovementPopover : OsuPopover + { + [Resolved] + private EditorBeatmap editorBeatmap { get; set; } = null!; + + private readonly Dictionary initialPositions = new Dictionary(); + private RectangleF initialSurroundingQuad; + + private BindableNumber xBindable = null!; + private BindableNumber yBindable = null!; + + private SliderWithTextBoxInput xInput = null!; + private OsuCheckbox relativeCheckbox = null!; + + public PreciseMovementPopover() + { + AllowableAnchors = new[] { Anchor.CentreLeft, Anchor.CentreRight }; + } + + [BackgroundDependencyLoader] + private void load() + { + Child = new FillFlowContainer + { + Width = 220, + AutoSizeAxes = Axes.Y, + Spacing = new Vector2(20), + Children = new Drawable[] + { + xInput = new SliderWithTextBoxInput("X:") + { + Current = xBindable = new BindableNumber + { + Precision = 1, + }, + Instantaneous = true, + TabbableContentContainer = this, + }, + new SliderWithTextBoxInput("Y:") + { + Current = yBindable = new BindableNumber + { + Precision = 1, + }, + Instantaneous = true, + TabbableContentContainer = this, + }, + relativeCheckbox = new OsuCheckbox(false) + { + RelativeSizeAxes = Axes.X, + LabelText = "Relative movement", + } + } + }; + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + ScheduleAfterChildren(() => + { + xInput.TakeFocus(); + xInput.SelectAll(); + }); + } + + protected override void PopIn() + { + base.PopIn(); + editorBeatmap.BeginChange(); + initialPositions.AddRange(editorBeatmap.SelectedHitObjects.Where(ho => ho is not Spinner).Select(ho => new KeyValuePair(ho, ((IHasPosition)ho).Position))); + initialSurroundingQuad = GeometryUtils.GetSurroundingQuad(initialPositions.Keys.Cast()).AABBFloat; + + Debug.Assert(initialPositions.Count > 0); + + if (initialPositions.Count > 1) + { + relativeCheckbox.Current.Value = true; + relativeCheckbox.Current.Disabled = true; + } + + relativeCheckbox.Current.BindValueChanged(_ => relativeChanged(), true); + xBindable.BindValueChanged(_ => applyPosition()); + yBindable.BindValueChanged(_ => applyPosition()); + } + + protected override void PopOut() + { + base.PopOut(); + if (IsLoaded) editorBeatmap.EndChange(); + } + + private void relativeChanged() + { + // reset bindable bounds to something that is guaranteed to be larger than any previous value. + // this prevents crashes that can happen in the middle of changing the bounds, as updating both bound ends at the same is not atomic - + // if the old and new bounds are disjoint, assigning X first can produce a situation where MinValue > MaxValue. + (xBindable.MinValue, xBindable.MaxValue) = (float.MinValue, float.MaxValue); + (yBindable.MinValue, yBindable.MaxValue) = (float.MinValue, float.MaxValue); + + float previousX = xBindable.Value; + float previousY = yBindable.Value; + + if (relativeCheckbox.Current.Value) + { + (xBindable.MinValue, xBindable.MaxValue) = (0 - initialSurroundingQuad.TopLeft.X, OsuPlayfield.BASE_SIZE.X - initialSurroundingQuad.BottomRight.X); + (yBindable.MinValue, yBindable.MaxValue) = (0 - initialSurroundingQuad.TopLeft.Y, OsuPlayfield.BASE_SIZE.Y - initialSurroundingQuad.BottomRight.Y); + + xBindable.Default = yBindable.Default = 0; + + if (initialPositions.Count == 1) + { + var initialPosition = initialPositions.Single().Value; + xBindable.Value = previousX - initialPosition.X; + yBindable.Value = previousY - initialPosition.Y; + } + } + else + { + Debug.Assert(initialPositions.Count == 1); + var initialPosition = initialPositions.Single().Value; + + var quadRelativeToPosition = new RectangleF(initialSurroundingQuad.Location - initialPosition, initialSurroundingQuad.Size); + + (xBindable.MinValue, xBindable.MaxValue) = (0 - quadRelativeToPosition.TopLeft.X, OsuPlayfield.BASE_SIZE.X - quadRelativeToPosition.BottomRight.X); + (yBindable.MinValue, yBindable.MaxValue) = (0 - quadRelativeToPosition.TopLeft.Y, OsuPlayfield.BASE_SIZE.Y - quadRelativeToPosition.BottomRight.Y); + + xBindable.Default = initialPosition.X; + yBindable.Default = initialPosition.Y; + + xBindable.Value = xBindable.Default + previousX; + yBindable.Value = yBindable.Default + previousY; + } + } + + private void applyPosition() + { + editorBeatmap.PerformOnSelection(ho => + { + if (!initialPositions.TryGetValue(ho, out var initialPosition)) + return; + + var pos = new Vector2(xBindable.Value, yBindable.Value); + if (relativeCheckbox.Current.Value) + ((IHasPosition)ho).Position = initialPosition + pos; + else + ((IHasPosition)ho).Position = pos; + }); + } + + public override bool OnPressed(KeyBindingPressEvent e) + { + if (e.Action == GlobalAction.Select && !e.Repeat) + { + this.HidePopover(); + return true; + } + + return base.OnPressed(e); + } + } +} diff --git a/osu.Game.Rulesets.Osu/Edit/TransformToolboxGroup.cs b/osu.Game.Rulesets.Osu/Edit/TransformToolboxGroup.cs index a41412cbe3..440e06598d 100644 --- a/osu.Game.Rulesets.Osu/Edit/TransformToolboxGroup.cs +++ b/osu.Game.Rulesets.Osu/Edit/TransformToolboxGroup.cs @@ -1,6 +1,7 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System.Linq; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; @@ -10,6 +11,9 @@ using osu.Framework.Input.Bindings; using osu.Framework.Input.Events; using osu.Game.Input.Bindings; using osu.Game.Rulesets.Edit; +using osu.Game.Rulesets.Objects; +using osu.Game.Rulesets.Osu.Objects; +using osu.Game.Screens.Edit; using osu.Game.Screens.Edit.Components; using osu.Game.Screens.Edit.Compose.Components; using osuTK; @@ -18,9 +22,12 @@ namespace osu.Game.Rulesets.Osu.Edit { public partial class TransformToolboxGroup : EditorToolboxGroup, IKeyBindingHandler { + private readonly BindableList selectedHitObjects = new BindableList(); + private readonly BindableBool canMove = new BindableBool(); private readonly AggregateBindable canRotate = new AggregateBindable((x, y) => x || y); private readonly AggregateBindable canScale = new AggregateBindable((x, y) => x || y); + private EditorToolButton moveButton = null!; private EditorToolButton rotateButton = null!; private EditorToolButton scaleButton = null!; @@ -35,7 +42,7 @@ namespace osu.Game.Rulesets.Osu.Edit } [BackgroundDependencyLoader] - private void load() + private void load(EditorBeatmap editorBeatmap) { Child = new FillFlowContainer { @@ -44,20 +51,27 @@ namespace osu.Game.Rulesets.Osu.Edit Spacing = new Vector2(5), Children = new Drawable[] { + moveButton = new EditorToolButton("Move", + () => new SpriteIcon { Icon = FontAwesome.Solid.ArrowsAlt }, + () => new PreciseMovementPopover()), rotateButton = new EditorToolButton("Rotate", () => new SpriteIcon { Icon = FontAwesome.Solid.Undo }, () => new PreciseRotationPopover(RotationHandler, GridToolbox)), scaleButton = new EditorToolButton("Scale", - () => new SpriteIcon { Icon = FontAwesome.Solid.ArrowsAlt }, + () => new SpriteIcon { Icon = FontAwesome.Solid.ExpandArrowsAlt }, () => new PreciseScalePopover(ScaleHandler, GridToolbox)) } }; + + selectedHitObjects.BindTo(editorBeatmap.SelectedHitObjects); } protected override void LoadComplete() { base.LoadComplete(); + selectedHitObjects.BindCollectionChanged((_, _) => canMove.Value = selectedHitObjects.Any(ho => ho is not Spinner), true); + canRotate.AddSource(RotationHandler.CanRotateAroundPlayfieldOrigin); canRotate.AddSource(RotationHandler.CanRotateAroundSelectionOrigin); @@ -67,6 +81,7 @@ namespace osu.Game.Rulesets.Osu.Edit // bindings to `Enabled` on the buttons are decoupled on purpose // due to the weird `OsuButton` behaviour of resetting `Enabled` to `false` when `Action` is set. + canMove.BindValueChanged(move => moveButton.Enabled.Value = move.NewValue, true); canRotate.Result.BindValueChanged(rotate => rotateButton.Enabled.Value = rotate.NewValue, true); canScale.Result.BindValueChanged(scale => scaleButton.Enabled.Value = scale.NewValue, true); } @@ -77,6 +92,12 @@ namespace osu.Game.Rulesets.Osu.Edit switch (e.Action) { + case GlobalAction.EditorToggleMoveControl: + { + moveButton.TriggerClick(); + return true; + } + case GlobalAction.EditorToggleRotateControl: { if (!RotationHandler.OperationInProgress.Value || rotateButton.Selected.Value) diff --git a/osu.Game/Graphics/UserInterfaceV2/SliderWithTextBoxInput.cs b/osu.Game/Graphics/UserInterfaceV2/SliderWithTextBoxInput.cs index 50d8d763e1..c16a6c612d 100644 --- a/osu.Game/Graphics/UserInterfaceV2/SliderWithTextBoxInput.cs +++ b/osu.Game/Graphics/UserInterfaceV2/SliderWithTextBoxInput.cs @@ -32,6 +32,11 @@ namespace osu.Game.Graphics.UserInterfaceV2 set => slider.Current = value; } + public CompositeDrawable TabbableContentContainer + { + set => textBox.TabbableContentContainer = value; + } + private bool instantaneous; /// diff --git a/osu.Game/Input/Bindings/GlobalActionContainer.cs b/osu.Game/Input/Bindings/GlobalActionContainer.cs index 5e509d2035..6c130ff309 100644 --- a/osu.Game/Input/Bindings/GlobalActionContainer.cs +++ b/osu.Game/Input/Bindings/GlobalActionContainer.cs @@ -144,6 +144,7 @@ namespace osu.Game.Input.Bindings new KeyBinding(new[] { InputKey.Control, InputKey.Alt, InputKey.MouseWheelUp }, GlobalAction.EditorIncreaseDistanceSpacing), new KeyBinding(new[] { InputKey.Control, InputKey.MouseWheelDown }, GlobalAction.EditorCyclePreviousBeatSnapDivisor), new KeyBinding(new[] { InputKey.Control, InputKey.MouseWheelUp }, GlobalAction.EditorCycleNextBeatSnapDivisor), + new KeyBinding(InputKey.None, GlobalAction.EditorToggleMoveControl), new KeyBinding(new[] { InputKey.Control, InputKey.R }, GlobalAction.EditorToggleRotateControl), new KeyBinding(new[] { InputKey.Control, InputKey.E }, GlobalAction.EditorToggleScaleControl), new KeyBinding(new[] { InputKey.Control, InputKey.Left }, GlobalAction.EditorSeekToPreviousHitObject), @@ -493,7 +494,10 @@ namespace osu.Game.Input.Bindings EditorSeekToNextBookmark, [LocalisableDescription(typeof(GlobalActionKeyBindingStrings), nameof(GlobalActionKeyBindingStrings.AbsoluteScrollSongList))] - AbsoluteScrollSongList + AbsoluteScrollSongList, + + [LocalisableDescription(typeof(GlobalActionKeyBindingStrings), nameof(GlobalActionKeyBindingStrings.EditorToggleMoveControl))] + EditorToggleMoveControl, } public enum GlobalActionCategory diff --git a/osu.Game/Localisation/GlobalActionKeyBindingStrings.cs b/osu.Game/Localisation/GlobalActionKeyBindingStrings.cs index 436a2be648..5713df57c9 100644 --- a/osu.Game/Localisation/GlobalActionKeyBindingStrings.cs +++ b/osu.Game/Localisation/GlobalActionKeyBindingStrings.cs @@ -454,6 +454,11 @@ namespace osu.Game.Localisation /// public static LocalisableString AbsoluteScrollSongList => new TranslatableString(getKey(@"absolute_scroll_song_list"), @"Absolute scroll song list"); + /// + /// "Toggle movement control" + /// + public static LocalisableString EditorToggleMoveControl => new TranslatableString(getKey(@"editor_toggle_move_control"), @"Toggle movement control"); + private static string getKey(string key) => $@"{prefix}:{key}"; } } From e753e3ee2feea2bac8d698d910fa741695e5af05 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sat, 18 Jan 2025 00:25:06 +0900 Subject: [PATCH 281/290] Update framework (except android) --- osu.Game/osu.Game.csproj | 2 +- osu.iOS.props | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj index e1bc971034..bfb6e51f93 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -35,7 +35,7 @@ runtime; build; native; contentfiles; analyzers; buildtransitive - + diff --git a/osu.iOS.props b/osu.iOS.props index ece42e87b4..7b0a027d39 100644 --- a/osu.iOS.props +++ b/osu.iOS.props @@ -17,6 +17,6 @@ -all - + From cbbcf54d742f0b74d3c122d8487254862a662df6 Mon Sep 17 00:00:00 2001 From: ILW8 Date: Sat, 18 Jan 2025 02:41:15 +0000 Subject: [PATCH 282/290] add warning text on acronym conflict --- .../Screens/Editors/TeamEditorScreen.cs | 26 ++++++++++++++++++- 1 file changed, 25 insertions(+), 1 deletion(-) diff --git a/osu.Game.Tournament/Screens/Editors/TeamEditorScreen.cs b/osu.Game.Tournament/Screens/Editors/TeamEditorScreen.cs index 250d5acaae..4008f9d140 100644 --- a/osu.Game.Tournament/Screens/Editors/TeamEditorScreen.cs +++ b/osu.Game.Tournament/Screens/Editors/TeamEditorScreen.cs @@ -71,6 +71,8 @@ namespace osu.Game.Tournament.Screens.Editors [Resolved] private LadderInfo ladderInfo { get; set; } = null!; + private readonly SettingsTextBox acronymTextBox; + public TeamRow(TournamentTeam team, TournamentScreen parent) { Model = team; @@ -112,7 +114,7 @@ namespace osu.Game.Tournament.Screens.Editors Width = 0.2f, Current = Model.FullName }, - new SettingsTextBox + acronymTextBox = new SettingsTextBox { LabelText = "Acronym", Width = 0.2f, @@ -177,6 +179,28 @@ namespace osu.Game.Tournament.Screens.Editors }; } + protected override void LoadComplete() + { + base.LoadComplete(); + + Model.Acronym.BindValueChanged(acronym => + { + var matchingTeams = ladderInfo.Teams + .Where(t => t.Acronym.Value == acronym.NewValue && t != Model) + .ToList(); + + if (matchingTeams.Count > 0) + { + acronymTextBox.SetNoticeText( + $"Acronym '{acronym.NewValue}' is already in use by team{(matchingTeams.Count > 1 ? "s" : "")}:\n" + + $"{string.Join(",\n", matchingTeams)}", true); + return; + } + + acronymTextBox.ClearNoticeText(); + }, true); + } + private partial class LastYearPlacementSlider : RoundedSliderBar { public override LocalisableString TooltipText => Current.Value == 0 ? "N/A" : base.TooltipText; From 72e1b2954c57087d58a9cd5c6fd540c234ca7f66 Mon Sep 17 00:00:00 2001 From: CloneWith Date: Mon, 20 Jan 2025 00:21:10 +0800 Subject: [PATCH 283/290] Don't highlight friends' scores under beatmap's friend score leaderboard --- osu.Game/Online/Leaderboards/LeaderboardScore.cs | 6 ++++-- osu.Game/Screens/Select/Leaderboards/BeatmapLeaderboard.cs | 4 ++-- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/osu.Game/Online/Leaderboards/LeaderboardScore.cs b/osu.Game/Online/Leaderboards/LeaderboardScore.cs index 32b25a866d..6acf236bf3 100644 --- a/osu.Game/Online/Leaderboards/LeaderboardScore.cs +++ b/osu.Game/Online/Leaderboards/LeaderboardScore.cs @@ -54,6 +54,7 @@ namespace osu.Game.Online.Leaderboards private readonly int? rank; private readonly bool isOnlineScope; + private readonly bool highlightFriend; private Box background; private Container content; @@ -86,12 +87,13 @@ namespace osu.Game.Online.Leaderboards [Resolved] private ScoreManager scoreManager { get; set; } = null!; - public LeaderboardScore(ScoreInfo score, int? rank, bool isOnlineScope = true) + public LeaderboardScore(ScoreInfo score, int? rank, bool isOnlineScope = true, bool highlightFriend = true) { Score = score; this.rank = rank; this.isOnlineScope = isOnlineScope; + this.highlightFriend = highlightFriend; RelativeSizeAxes = Axes.X; Height = HEIGHT; @@ -130,7 +132,7 @@ namespace osu.Game.Online.Leaderboards background = new Box { RelativeSizeAxes = Axes.Both, - Colour = isUserFriend ? colour.Yellow : (user.OnlineID == api.LocalUser.Value.Id && isOnlineScope ? colour.Green : Color4.Black), + Colour = (highlightFriend && isUserFriend) ? colour.Yellow : (user.OnlineID == api.LocalUser.Value.Id && isOnlineScope ? colour.Green : Color4.Black), Alpha = background_alpha, }, }, diff --git a/osu.Game/Screens/Select/Leaderboards/BeatmapLeaderboard.cs b/osu.Game/Screens/Select/Leaderboards/BeatmapLeaderboard.cs index 58c14b15b9..57fe22aa59 100644 --- a/osu.Game/Screens/Select/Leaderboards/BeatmapLeaderboard.cs +++ b/osu.Game/Screens/Select/Leaderboards/BeatmapLeaderboard.cs @@ -169,12 +169,12 @@ namespace osu.Game.Screens.Select.Leaderboards return scoreRetrievalRequest = newRequest; } - protected override LeaderboardScore CreateDrawableScore(ScoreInfo model, int index) => new LeaderboardScore(model, index, IsOnlineScope) + protected override LeaderboardScore CreateDrawableScore(ScoreInfo model, int index) => new LeaderboardScore(model, index, IsOnlineScope, Scope != BeatmapLeaderboardScope.Friend) { Action = () => ScoreSelected?.Invoke(model) }; - protected override LeaderboardScore CreateDrawableTopScore(ScoreInfo model) => new LeaderboardScore(model, model.Position, false) + protected override LeaderboardScore CreateDrawableTopScore(ScoreInfo model) => new LeaderboardScore(model, model.Position, false, Scope != BeatmapLeaderboardScope.Friend) { Action = () => ScoreSelected?.Invoke(model) }; From b6ce72b6d92d28c6f95cf28255535a16ad6a1ef0 Mon Sep 17 00:00:00 2001 From: Berkan Diler Date: Sun, 19 Jan 2025 23:27:44 +0100 Subject: [PATCH 284/290] Remove redundant ToArray() calls in Osu/ManiaHitObjectComposer --- osu.Game.Rulesets.Mania/Edit/ManiaHitObjectComposer.cs | 4 ++-- osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Game.Rulesets.Mania/Edit/ManiaHitObjectComposer.cs b/osu.Game.Rulesets.Mania/Edit/ManiaHitObjectComposer.cs index 926a4b2736..9062c32b7b 100644 --- a/osu.Game.Rulesets.Mania/Edit/ManiaHitObjectComposer.cs +++ b/osu.Game.Rulesets.Mania/Edit/ManiaHitObjectComposer.cs @@ -64,11 +64,11 @@ namespace osu.Game.Rulesets.Mania.Edit return; List remainingHitObjects = EditorBeatmap.HitObjects.Cast().Where(h => h.StartTime >= timestamp).ToList(); - string[] objectDescriptions = objectDescription.Split(',').ToArray(); + string[] objectDescriptions = objectDescription.Split(','); for (int i = 0; i < objectDescriptions.Length; i++) { - string[] split = objectDescriptions[i].Split('|').ToArray(); + string[] split = objectDescriptions[i].Split('|'); if (split.Length != 2) continue; diff --git a/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs b/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs index f5e7ff6004..aad3d0c93b 100644 --- a/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs +++ b/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs @@ -178,7 +178,7 @@ namespace osu.Game.Rulesets.Osu.Edit return; List remainingHitObjects = EditorBeatmap.HitObjects.Cast().Where(h => h.StartTime >= timestamp).ToList(); - string[] splitDescription = objectDescription.Split(',').ToArray(); + string[] splitDescription = objectDescription.Split(','); for (int i = 0; i < splitDescription.Length; i++) { From 525e16ad1d8442a01b81ba501b49204ba9705c77 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 20 Jan 2025 15:00:35 +0900 Subject: [PATCH 285/290] Fix one more new inspection in EAP 2025 --- osu.Game/Skinning/ResourceStoreBackedSkin.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Skinning/ResourceStoreBackedSkin.cs b/osu.Game/Skinning/ResourceStoreBackedSkin.cs index 206c400a88..450794c4a8 100644 --- a/osu.Game/Skinning/ResourceStoreBackedSkin.cs +++ b/osu.Game/Skinning/ResourceStoreBackedSkin.cs @@ -33,7 +33,7 @@ namespace osu.Game.Skinning public ISample? GetSample(ISampleInfo sampleInfo) { - foreach (string? lookup in sampleInfo.LookupNames) + foreach (string lookup in sampleInfo.LookupNames) { ISample? sample = samples.Get(lookup); if (sample != null) From e3195e23160b8655ca542e9372959ca93e8c5fde Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 20 Jan 2025 15:02:31 +0900 Subject: [PATCH 286/290] Adjust new line break warning to hint --- osu.sln.DotSettings | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.sln.DotSettings b/osu.sln.DotSettings index 8f5e642f94..5cac0024b7 100644 --- a/osu.sln.DotSettings +++ b/osu.sln.DotSettings @@ -170,7 +170,7 @@ WARNING HINT WARNING - WARNING + HINT WARNING ERROR WARNING From b5b407fe7ca888ae1a9a8297767646e3bb60b2c0 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 20 Jan 2025 15:40:38 +0900 Subject: [PATCH 287/290] Knock some sense into daily challenge profile test scene --- .../TestSceneUserProfileDailyChallenge.cs | 53 ++++++++++++------- 1 file changed, 33 insertions(+), 20 deletions(-) diff --git a/osu.Game.Tests/Visual/Online/TestSceneUserProfileDailyChallenge.cs b/osu.Game.Tests/Visual/Online/TestSceneUserProfileDailyChallenge.cs index ce62a3255d..2be9c1ab14 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneUserProfileDailyChallenge.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneUserProfileDailyChallenge.cs @@ -7,6 +7,7 @@ using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Shapes; +using osu.Framework.Testing; using osu.Game.Online.API.Requests.Responses; using osu.Game.Overlays; using osu.Game.Overlays.Profile; @@ -20,28 +21,16 @@ namespace osu.Game.Tests.Visual.Online public partial class TestSceneUserProfileDailyChallenge : OsuManualInputManagerTestScene { [Cached] - public readonly Bindable User = new Bindable(new UserProfileData(new APIUser(), new OsuRuleset().RulesetInfo)); + private readonly Bindable userProfileData = new Bindable(new UserProfileData(new APIUser(), new OsuRuleset().RulesetInfo)); [Cached] private OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Pink); - protected override void LoadComplete() + private DailyChallengeStatsDisplay display = null!; + + [SetUpSteps] + public void SetUpSteps() { - base.LoadComplete(); - - DailyChallengeStatsDisplay display = null!; - - AddSliderStep("daily", 0, 999, 2, v => update(s => s.DailyStreakCurrent = v)); - AddSliderStep("daily best", 0, 999, 2, v => update(s => s.DailyStreakBest = v)); - AddSliderStep("weekly", 0, 250, 1, v => update(s => s.WeeklyStreakCurrent = v)); - AddSliderStep("weekly best", 0, 250, 1, v => update(s => s.WeeklyStreakBest = v)); - AddSliderStep("top 10%", 0, 999, 0, v => update(s => s.Top10PercentPlacements = v)); - AddSliderStep("top 50%", 0, 999, 0, v => update(s => s.Top50PercentPlacements = v)); - AddSliderStep("playcount", 0, 1500, 1, v => update(s => s.PlayCount = v)); - AddStep("user played today", () => update(s => s.LastUpdate = DateTimeOffset.UtcNow.Date)); - AddStep("user played yesterday", () => update(s => s.LastUpdate = DateTimeOffset.UtcNow.Date.AddDays(-1))); - AddStep("user is local user", () => update(s => s.UserID = API.LocalUser.Value.Id)); - AddStep("user is not local user", () => update(s => s.UserID = API.LocalUser.Value.Id + 1000)); AddStep("create", () => { Clear(); @@ -55,16 +44,40 @@ namespace osu.Game.Tests.Visual.Online Anchor = Anchor.Centre, Origin = Anchor.Centre, Scale = new Vector2(1f), - User = { BindTarget = User }, + User = { BindTarget = userProfileData }, }); }); + + AddStep("set local user", () => update(s => s.UserID = API.LocalUser.Value.Id)); + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + AddSliderStep("daily", 0, 999, 2, v => update(s => s.DailyStreakCurrent = v)); + AddSliderStep("daily best", 0, 999, 2, v => update(s => s.DailyStreakBest = v)); + AddSliderStep("weekly", 0, 250, 1, v => update(s => s.WeeklyStreakCurrent = v)); + AddSliderStep("weekly best", 0, 250, 1, v => update(s => s.WeeklyStreakBest = v)); + AddSliderStep("top 10%", 0, 999, 0, v => update(s => s.Top10PercentPlacements = v)); + AddSliderStep("top 50%", 0, 999, 0, v => update(s => s.Top50PercentPlacements = v)); + AddSliderStep("playcount", 0, 1500, 1, v => update(s => s.PlayCount = v)); + } + + [Test] + public void TestStates() + { + AddStep("played today", () => update(s => s.LastUpdate = DateTimeOffset.UtcNow.Date)); + AddStep("played yesterday", () => update(s => s.LastUpdate = DateTimeOffset.UtcNow.Date.AddDays(-1))); + AddStep("change to non-local user", () => update(s => s.UserID = API.LocalUser.Value.Id + 1000)); + AddStep("hover", () => InputManager.MoveMouseTo(display)); } private void update(Action change) { - change.Invoke(User.Value!.User.DailyChallengeStatistics); - User.Value = new UserProfileData(User.Value.User, User.Value.Ruleset); + change.Invoke(userProfileData.Value!.User.DailyChallengeStatistics); + userProfileData.Value = new UserProfileData(userProfileData.Value.User, userProfileData.Value.Ruleset); } [Test] From 04ba686be5f3abbe93ddfc7e59395f1a0b2d9f11 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 20 Jan 2025 15:47:47 +0900 Subject: [PATCH 288/290] Add basic animation --- .../Header/Components/DailyChallengeStatsDisplay.cs | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/osu.Game/Overlays/Profile/Header/Components/DailyChallengeStatsDisplay.cs b/osu.Game/Overlays/Profile/Header/Components/DailyChallengeStatsDisplay.cs index a9d982e17f..a3dce89ad4 100644 --- a/osu.Game/Overlays/Profile/Header/Components/DailyChallengeStatsDisplay.cs +++ b/osu.Game/Overlays/Profile/Header/Components/DailyChallengeStatsDisplay.cs @@ -161,12 +161,21 @@ namespace osu.Game.Overlays.Profile.Header.Components if (playedToday && userIsOnOwnProfile) { - completionMark.Alpha = 1; + if (completionMark.Alpha > 0.8f) + { + completionMark.ScaleTo(1.2f).ScaleTo(1, 800, Easing.OutElastic); + } + else + { + completionMark.FadeIn(500, Easing.OutExpo); + completionMark.ScaleTo(1.6f).ScaleTo(1, 500, Easing.OutExpo); + } + content.BorderColour = colours.Lime1; } else { - completionMark.Alpha = 0; + completionMark.FadeOut(50); content.BorderColour = colourProvider.Background4; } From a1bcdb091df348f8c0ccad760ef67215def1d7a0 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 20 Jan 2025 15:55:13 +0900 Subject: [PATCH 289/290] Adjust code slightly --- .../Screens/Editors/TeamEditorScreen.cs | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/osu.Game.Tournament/Screens/Editors/TeamEditorScreen.cs b/osu.Game.Tournament/Screens/Editors/TeamEditorScreen.cs index 4008f9d140..162379f4aa 100644 --- a/osu.Game.Tournament/Screens/Editors/TeamEditorScreen.cs +++ b/osu.Game.Tournament/Screens/Editors/TeamEditorScreen.cs @@ -185,19 +185,18 @@ namespace osu.Game.Tournament.Screens.Editors Model.Acronym.BindValueChanged(acronym => { - var matchingTeams = ladderInfo.Teams - .Where(t => t.Acronym.Value == acronym.NewValue && t != Model) - .ToList(); + var teamsWithSameAcronym = ladderInfo.Teams + .Where(t => t.Acronym.Value == acronym.NewValue && t != Model) + .ToList(); - if (matchingTeams.Count > 0) + if (teamsWithSameAcronym.Count > 0) { acronymTextBox.SetNoticeText( - $"Acronym '{acronym.NewValue}' is already in use by team{(matchingTeams.Count > 1 ? "s" : "")}:\n" - + $"{string.Join(",\n", matchingTeams)}", true); - return; + $"Acronym '{acronym.NewValue}' is already in use by team{(teamsWithSameAcronym.Count > 1 ? "s" : "")}:\n" + + $"{string.Join(",\n", teamsWithSameAcronym)}", true); } - - acronymTextBox.ClearNoticeText(); + else + acronymTextBox.ClearNoticeText(); }, true); } From dcdb8d13a998b049a377b93a2deed8d92e42562c Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 20 Jan 2025 16:17:39 +0900 Subject: [PATCH 290/290] Always select text when an editor slider-textbox is focused --- osu.Game.Rulesets.Osu/Edit/PreciseMovementPopover.cs | 6 +----- osu.Game.Rulesets.Osu/Edit/PreciseRotationPopover.cs | 6 +----- osu.Game.Rulesets.Osu/Edit/PreciseScalePopover.cs | 6 +----- osu.Game/Graphics/UserInterfaceV2/SliderWithTextBoxInput.cs | 3 +-- 4 files changed, 4 insertions(+), 17 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Edit/PreciseMovementPopover.cs b/osu.Game.Rulesets.Osu/Edit/PreciseMovementPopover.cs index 151ca31ac0..f2cb8794b5 100644 --- a/osu.Game.Rulesets.Osu/Edit/PreciseMovementPopover.cs +++ b/osu.Game.Rulesets.Osu/Edit/PreciseMovementPopover.cs @@ -85,11 +85,7 @@ namespace osu.Game.Rulesets.Osu.Edit { base.LoadComplete(); - ScheduleAfterChildren(() => - { - xInput.TakeFocus(); - xInput.SelectAll(); - }); + ScheduleAfterChildren(() => xInput.TakeFocus()); } protected override void PopIn() diff --git a/osu.Game.Rulesets.Osu/Edit/PreciseRotationPopover.cs b/osu.Game.Rulesets.Osu/Edit/PreciseRotationPopover.cs index 477d3b4e57..ae8ad2c01b 100644 --- a/osu.Game.Rulesets.Osu/Edit/PreciseRotationPopover.cs +++ b/osu.Game.Rulesets.Osu/Edit/PreciseRotationPopover.cs @@ -96,11 +96,7 @@ namespace osu.Game.Rulesets.Osu.Edit { base.LoadComplete(); - ScheduleAfterChildren(() => - { - angleInput.TakeFocus(); - angleInput.SelectAll(); - }); + ScheduleAfterChildren(() => angleInput.TakeFocus()); angleInput.Current.BindValueChanged(angle => rotationInfo.Value = rotationInfo.Value with { Degrees = angle.NewValue }); rotationHandler.CanRotateAroundSelectionOrigin.BindValueChanged(e => diff --git a/osu.Game.Rulesets.Osu/Edit/PreciseScalePopover.cs b/osu.Game.Rulesets.Osu/Edit/PreciseScalePopover.cs index e728290289..ac6d9fbb19 100644 --- a/osu.Game.Rulesets.Osu/Edit/PreciseScalePopover.cs +++ b/osu.Game.Rulesets.Osu/Edit/PreciseScalePopover.cs @@ -139,11 +139,7 @@ namespace osu.Game.Rulesets.Osu.Edit { base.LoadComplete(); - ScheduleAfterChildren(() => - { - scaleInput.TakeFocus(); - scaleInput.SelectAll(); - }); + ScheduleAfterChildren(() => scaleInput.TakeFocus()); scaleInput.Current.BindValueChanged(scale => scaleInfo.Value = scaleInfo.Value with { Scale = scale.NewValue }); xCheckBox.Current.BindValueChanged(_ => diff --git a/osu.Game/Graphics/UserInterfaceV2/SliderWithTextBoxInput.cs b/osu.Game/Graphics/UserInterfaceV2/SliderWithTextBoxInput.cs index c16a6c612d..2fbe3ae89b 100644 --- a/osu.Game/Graphics/UserInterfaceV2/SliderWithTextBoxInput.cs +++ b/osu.Game/Graphics/UserInterfaceV2/SliderWithTextBoxInput.cs @@ -74,6 +74,7 @@ namespace osu.Game.Graphics.UserInterfaceV2 textBox = new LabelledTextBox { Label = labelText, + SelectAllOnFocus = true, }, slider = new SettingsSlider { @@ -92,8 +93,6 @@ namespace osu.Game.Graphics.UserInterfaceV2 public bool TakeFocus() => GetContainingFocusManager()?.ChangeFocus(textBox) == true; - public bool SelectAll() => textBox.SelectAll(); - private bool updatingFromTextBox; private void textChanged(ValueChangedEvent change)