From 63e0768207752f674a44ada84acfc61be63f508c Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 22 Mar 2023 14:44:52 +0900 Subject: [PATCH 0001/1275] Show count of beatmaps in collections in manage dialog --- .../Collections/DrawableCollectionListItem.cs | 65 +++++++++++++++++-- 1 file changed, 59 insertions(+), 6 deletions(-) diff --git a/osu.Game/Collections/DrawableCollectionListItem.cs b/osu.Game/Collections/DrawableCollectionListItem.cs index 23156b1ad5..efeb066869 100644 --- a/osu.Game/Collections/DrawableCollectionListItem.cs +++ b/osu.Game/Collections/DrawableCollectionListItem.cs @@ -2,6 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using System; +using System.Collections.Generic; using osu.Framework.Allocation; using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; @@ -13,6 +14,7 @@ using osu.Framework.Input.Events; using osu.Game.Database; using osu.Game.Graphics; using osu.Game.Graphics.Containers; +using osu.Game.Graphics.Sprites; using osu.Game.Graphics.UserInterface; using osu.Game.Overlays; using osuTK; @@ -25,7 +27,7 @@ namespace osu.Game.Collections /// public partial class DrawableCollectionListItem : OsuRearrangeableListItem> { - private const float item_height = 35; + private const float item_height = 45; private const float button_width = item_height * 0.75f; /// @@ -81,12 +83,10 @@ namespace osu.Game.Collections Padding = new MarginPadding { Right = collection.IsManaged ? button_width : 0 }, Children = new Drawable[] { - textBox = new ItemTextBox + textBox = new ItemTextBox(collection) { - RelativeSizeAxes = Axes.Both, - Size = Vector2.One, - CornerRadius = item_height / 2, - PlaceholderText = collection.IsManaged ? string.Empty : "Create a new collection" + RelativeSizeAxes = Axes.X, + Height = item_height }, } }, @@ -117,11 +117,64 @@ namespace osu.Game.Collections { protected override float LeftRightPadding => item_height / 2; + private const float count_text_size = 12; + + [Resolved] + private RealmAccess realm { get; set; } = null!; + + private readonly Live collection; + + private OsuSpriteText countText = null!; + + private IDisposable? itemCountSubscription; + + public ItemTextBox(Live collection) + { + this.collection = collection; + + CornerRadius = item_height / 2; + } + [BackgroundDependencyLoader] private void load(OsuColour colours) { BackgroundUnfocused = colours.GreySeaFoamDarker.Darken(0.5f); BackgroundFocused = colours.GreySeaFoam; + + if (collection.IsManaged) + { + TextContainer.Height *= (Height - count_text_size) / Height; + TextContainer.Margin = new MarginPadding { Bottom = count_text_size }; + + TextContainer.Add(countText = new OsuSpriteText + { + Anchor = Anchor.BottomLeft, + Origin = Anchor.TopLeft, + Depth = float.MinValue, + Font = OsuFont.Default.With(size: count_text_size, weight: FontWeight.SemiBold), + Margin = new MarginPadding { Top = 2, Left = 2 }, + Colour = colours.Yellow + }); + + itemCountSubscription = realm.SubscribeToPropertyChanged>(r => r.Find(collection.ID), c => c.BeatmapMD5Hashes, items => + { + countText.Text = items.Count == 1 + // Intentionally not localised until we have proper support for this (see https://github.com/ppy/osu-framework/pull/4918 + // but also in this case we want support for formatting a number within a string). + ? $"{items.Count:#,0} beatmap" + : $"{items.Count:#,0} beatmaps"; + }); + } + else + { + PlaceholderText = "Create a new collection"; + } + } + + protected override void Dispose(bool isDisposing) + { + base.Dispose(isDisposing); + itemCountSubscription?.Dispose(); } } From 954be126922a63458dff577917ebf46e3ac72b75 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 22 Mar 2023 14:46:13 +0900 Subject: [PATCH 0002/1275] Debounce updates to ensure event isn't fired too often after much collection management --- .../Collections/DrawableCollectionListItem.cs | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/osu.Game/Collections/DrawableCollectionListItem.cs b/osu.Game/Collections/DrawableCollectionListItem.cs index efeb066869..87cc14ecb9 100644 --- a/osu.Game/Collections/DrawableCollectionListItem.cs +++ b/osu.Game/Collections/DrawableCollectionListItem.cs @@ -156,14 +156,17 @@ namespace osu.Game.Collections Colour = colours.Yellow }); - itemCountSubscription = realm.SubscribeToPropertyChanged>(r => r.Find(collection.ID), c => c.BeatmapMD5Hashes, items => - { - countText.Text = items.Count == 1 - // Intentionally not localised until we have proper support for this (see https://github.com/ppy/osu-framework/pull/4918 - // but also in this case we want support for formatting a number within a string). - ? $"{items.Count:#,0} beatmap" - : $"{items.Count:#,0} beatmaps"; - }); + itemCountSubscription = realm.SubscribeToPropertyChanged>(r => r.Find(collection.ID), c => c.BeatmapMD5Hashes, _ => + Scheduler.AddOnce(() => + { + int count = collection.PerformRead(c => c.BeatmapMD5Hashes.Count); + + countText.Text = count == 1 + // Intentionally not localised until we have proper support for this (see https://github.com/ppy/osu-framework/pull/4918 + // but also in this case we want support for formatting a number within a string). + ? $"{count:#,0} beatmap" + : $"{count:#,0} beatmaps"; + })); } else { From 256789193f7d99d6e1dd5ca00f23a82830d195a2 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 3 Apr 2023 15:28:01 +0900 Subject: [PATCH 0003/1275] Remove redundant type specification --- osu.Game/Collections/DrawableCollectionListItem.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/osu.Game/Collections/DrawableCollectionListItem.cs b/osu.Game/Collections/DrawableCollectionListItem.cs index 87cc14ecb9..31b127ef2a 100644 --- a/osu.Game/Collections/DrawableCollectionListItem.cs +++ b/osu.Game/Collections/DrawableCollectionListItem.cs @@ -2,7 +2,6 @@ // See the LICENCE file in the repository root for full licence text. using System; -using System.Collections.Generic; using osu.Framework.Allocation; using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; @@ -156,7 +155,7 @@ namespace osu.Game.Collections Colour = colours.Yellow }); - itemCountSubscription = realm.SubscribeToPropertyChanged>(r => r.Find(collection.ID), c => c.BeatmapMD5Hashes, _ => + itemCountSubscription = realm.SubscribeToPropertyChanged(r => r.Find(collection.ID), c => c.BeatmapMD5Hashes, _ => Scheduler.AddOnce(() => { int count = collection.PerformRead(c => c.BeatmapMD5Hashes.Count); From 91fb59ee15caf75b08a43bd6508a4edf3f49e8f5 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Sun, 11 Feb 2024 07:37:13 +0300 Subject: [PATCH 0004/1275] Introduce `LocalUserStatisticsProvider` component --- .../TestSceneLocalUserStatisticsProvider.cs | 141 ++++++++++++++++++ .../Online/LocalUserStatisticsProvider.cs | 94 ++++++++++++ osu.Game/OsuGameBase.cs | 6 +- 3 files changed, 240 insertions(+), 1 deletion(-) create mode 100644 osu.Game.Tests/Visual/Online/TestSceneLocalUserStatisticsProvider.cs create mode 100644 osu.Game/Online/LocalUserStatisticsProvider.cs diff --git a/osu.Game.Tests/Visual/Online/TestSceneLocalUserStatisticsProvider.cs b/osu.Game.Tests/Visual/Online/TestSceneLocalUserStatisticsProvider.cs new file mode 100644 index 0000000000..1a27fd1de5 --- /dev/null +++ b/osu.Game.Tests/Visual/Online/TestSceneLocalUserStatisticsProvider.cs @@ -0,0 +1,141 @@ +// 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.Framework.Extensions.ObjectExtensions; +using osu.Framework.Graphics; +using osu.Framework.Testing; +using osu.Game.Graphics.Sprites; +using osu.Game.Online; +using osu.Game.Online.API; +using osu.Game.Online.API.Requests; +using osu.Game.Online.API.Requests.Responses; +using osu.Game.Rulesets.Catch; +using osu.Game.Rulesets.Mania; +using osu.Game.Rulesets.Osu; +using osu.Game.Rulesets.Taiko; +using osu.Game.Users; + +namespace osu.Game.Tests.Visual.Online +{ + public partial class TestSceneLocalUserStatisticsProvider : OsuTestScene + { + private LocalUserStatisticsProvider statisticsProvider = null!; + + private readonly Dictionary<(int userId, string rulesetName), UserStatistics> serverSideStatistics = new Dictionary<(int userId, string rulesetName), UserStatistics>(); + + [SetUpSteps] + public void SetUpSteps() + { + AddStep("clear statistics", () => serverSideStatistics.Clear()); + + setUser(1000); + + AddStep("setup provider", () => + { + OsuSpriteText text; + + ((DummyAPIAccess)API).HandleRequest = r => + { + switch (r) + { + case GetUserRequest userRequest: + int userId = int.Parse(userRequest.Lookup); + string rulesetName = userRequest.Ruleset!.ShortName; + var response = new APIUser + { + Id = userId, + Statistics = tryGetStatistics(userId, rulesetName) + }; + + userRequest.TriggerSuccess(response); + return true; + + default: + return false; + } + }; + + Clear(); + Add(statisticsProvider = new LocalUserStatisticsProvider()); + Add(text = new OsuSpriteText + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + }); + + statisticsProvider.Statistics.BindValueChanged(s => + { + text.Text = s.NewValue == null + ? "Statistics: (null)" + : $"Statistics: (total score: {s.NewValue.TotalScore:N0})"; + }); + + Ruleset.Value = new OsuRuleset().RulesetInfo; + }); + } + + [Test] + public void TestInitialStatistics() + { + AddAssert("initial statistics populated", () => statisticsProvider.Statistics.Value.AsNonNull().TotalScore, () => Is.EqualTo(4_000_000)); + } + + [Test] + public void TestRulesetChanges() + { + AddAssert("statistics from osu", () => statisticsProvider.Statistics.Value.AsNonNull().TotalScore, () => Is.EqualTo(4_000_000)); + AddStep("change ruleset to taiko", () => Ruleset.Value = new TaikoRuleset().RulesetInfo); + AddAssert("statistics from taiko", () => statisticsProvider.Statistics.Value.AsNonNull().TotalScore, () => Is.EqualTo(3_000_000)); + AddStep("change ruleset to catch", () => Ruleset.Value = new CatchRuleset().RulesetInfo); + AddAssert("statistics from catch", () => statisticsProvider.Statistics.Value.AsNonNull().TotalScore, () => Is.EqualTo(2_000_000)); + AddStep("change ruleset to mania", () => Ruleset.Value = new ManiaRuleset().RulesetInfo); + AddAssert("statistics from mania", () => statisticsProvider.Statistics.Value.AsNonNull().TotalScore, () => Is.EqualTo(1_000_000)); + } + + [Test] + public void TestUserChanges() + { + setUser(1001); + + AddStep("update statistics for user 1000", () => + { + serverSideStatistics[(1000, "osu")] = new UserStatistics { TotalScore = 5_000_000 }; + serverSideStatistics[(1000, "taiko")] = new UserStatistics { TotalScore = 6_000_000 }; + }); + + AddAssert("statistics matches user 1001 from osu", () => statisticsProvider.Statistics.Value.AsNonNull().TotalScore, () => Is.EqualTo(4_000_000)); + + AddStep("change ruleset to taiko", () => Ruleset.Value = new TaikoRuleset().RulesetInfo); + AddAssert("statistics matches user 1001 from taiko", () => statisticsProvider.Statistics.Value.AsNonNull().TotalScore, () => Is.EqualTo(3_000_000)); + + AddStep("change ruleset to osu", () => Ruleset.Value = new OsuRuleset().RulesetInfo); + setUser(1000, false); + + AddAssert("statistics matches user 1000 from osu", () => statisticsProvider.Statistics.Value.AsNonNull().TotalScore, () => Is.EqualTo(5_000_000)); + + AddStep("change ruleset to osu", () => Ruleset.Value = new TaikoRuleset().RulesetInfo); + AddAssert("statistics matches user 1000 from taiko", () => statisticsProvider.Statistics.Value.AsNonNull().TotalScore, () => Is.EqualTo(6_000_000)); + } + + private UserStatistics tryGetStatistics(int userId, string rulesetName) + => serverSideStatistics.TryGetValue((userId, rulesetName), out var stats) ? stats : new UserStatistics(); + + private void setUser(int userId, bool generateStatistics = true) + { + AddStep($"set local user to {userId}", () => + { + if (generateStatistics) + { + serverSideStatistics[(userId, "osu")] = new UserStatistics { TotalScore = 4_000_000 }; + serverSideStatistics[(userId, "taiko")] = new UserStatistics { TotalScore = 3_000_000 }; + serverSideStatistics[(userId, "fruits")] = new UserStatistics { TotalScore = 2_000_000 }; + serverSideStatistics[(userId, "mania")] = new UserStatistics { TotalScore = 1_000_000 }; + } + + ((DummyAPIAccess)API).LocalUser.Value = new APIUser { Id = userId }; + }); + } + } +} diff --git a/osu.Game/Online/LocalUserStatisticsProvider.cs b/osu.Game/Online/LocalUserStatisticsProvider.cs new file mode 100644 index 0000000000..e2f016b336 --- /dev/null +++ b/osu.Game/Online/LocalUserStatisticsProvider.cs @@ -0,0 +1,94 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Collections.Generic; +using osu.Framework.Allocation; +using osu.Framework.Bindables; +using osu.Framework.Graphics; +using osu.Game.Extensions; +using osu.Game.Online.API; +using osu.Game.Online.API.Requests; +using osu.Game.Rulesets; +using osu.Game.Users; + +namespace osu.Game.Online +{ + /// + /// A component that is responsible for providing the latest statistics of the logged-in user for the game-wide selected ruleset. + /// + public partial class LocalUserStatisticsProvider : Component + { + /// + /// The statistics of the logged-in user for the game-wide selected ruleset. + /// + public IBindable Statistics => statistics; + + private readonly Bindable statistics = new Bindable(); + + [Resolved] + private IBindable ruleset { get; set; } = null!; + + [Resolved] + private IAPIProvider api { get; set; } = null!; + + private readonly Dictionary allStatistics = new Dictionary(); + + protected override void LoadComplete() + { + base.LoadComplete(); + + statistics.BindValueChanged(v => + { + if (api.LocalUser.Value != null && v.NewValue != null) + api.LocalUser.Value.Statistics = v.NewValue; + }); + + ruleset.BindValueChanged(_ => updateStatisticsBindable()); + + api.LocalUser.BindValueChanged(_ => + { + allStatistics.Clear(); + updateStatisticsBindable(); + }, true); + } + + private GetUserRequest? currentRequest; + + private void updateStatisticsBindable() => Schedule(() => + { + statistics.Value = null; + + if (api.LocalUser.Value == null || api.LocalUser.Value.OnlineID <= 1 || !ruleset.Value.IsLegacyRuleset()) + { + statistics.Value = new UserStatistics(); + return; + } + + if (currentRequest?.CompletionState == APIRequestCompletionState.Waiting) + { + currentRequest.Cancel(); + currentRequest = null; + } + + if (allStatistics.TryGetValue(ruleset.Value.ShortName, out var existing)) + statistics.Value = existing; + else + requestStatistics(ruleset.Value); + }); + + private void requestStatistics(RulesetInfo ruleset) + { + currentRequest = new GetUserRequest(api.LocalUser.Value.OnlineID, ruleset); + currentRequest.Success += u => statistics.Value = allStatistics[ruleset.ShortName] = u.Statistics; + api.Queue(currentRequest); + } + + internal void UpdateStatistics(UserStatistics statistics, RulesetInfo statisticsRuleset) + { + allStatistics[statisticsRuleset.ShortName] = statistics; + + if (statisticsRuleset.ShortName == ruleset.Value.ShortName) + updateStatisticsBindable(); + } + } +} diff --git a/osu.Game/OsuGameBase.cs b/osu.Game/OsuGameBase.cs index a2a6322665..f574885757 100644 --- a/osu.Game/OsuGameBase.cs +++ b/osu.Game/OsuGameBase.cs @@ -208,6 +208,7 @@ namespace osu.Game private MetadataClient metadataClient; private SoloStatisticsWatcher soloStatisticsWatcher; + private LocalUserStatisticsProvider localUserStatisticsProvider; private RealmAccess realm; @@ -328,7 +329,9 @@ namespace osu.Game dependencies.CacheAs(SpectatorClient = new OnlineSpectatorClient(endpoints)); dependencies.CacheAs(MultiplayerClient = new OnlineMultiplayerClient(endpoints)); dependencies.CacheAs(metadataClient = new OnlineMetadataClient(endpoints)); - dependencies.CacheAs(soloStatisticsWatcher = new SoloStatisticsWatcher()); + + dependencies.CacheAs(localUserStatisticsProvider = new LocalUserStatisticsProvider()); + dependencies.CacheAs(soloStatisticsWatcher = new SoloStatisticsWatcher(localUserStatisticsProvider)); base.Content.Add(new BeatmapOnlineChangeIngest(beatmapUpdater, realm, metadataClient)); @@ -371,6 +374,7 @@ namespace osu.Game base.Content.Add(SpectatorClient); base.Content.Add(MultiplayerClient); base.Content.Add(metadataClient); + base.Content.Add(localUserStatisticsProvider); base.Content.Add(soloStatisticsWatcher); base.Content.Add(rulesetConfigCache); From 3ab60b76df0ea520f53a0c2bd68d7821e11e45f7 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Sun, 11 Feb 2024 07:38:33 +0300 Subject: [PATCH 0005/1275] Remove `IAPIProvider.Statistics` in favour of the new component --- .../Online/TestSceneSoloStatisticsWatcher.cs | 8 ++++++-- osu.Game/Online/API/APIAccess.cs | 17 +---------------- osu.Game/Online/API/DummyAPIAccess.cs | 16 ---------------- osu.Game/Online/API/IAPIProvider.cs | 10 ---------- osu.Game/Online/Solo/SoloStatisticsWatcher.cs | 9 ++++++++- 5 files changed, 15 insertions(+), 45 deletions(-) diff --git a/osu.Game.Tests/Visual/Online/TestSceneSoloStatisticsWatcher.cs b/osu.Game.Tests/Visual/Online/TestSceneSoloStatisticsWatcher.cs index 3607b37c7e..0e762966d6 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneSoloStatisticsWatcher.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneSoloStatisticsWatcher.cs @@ -8,6 +8,7 @@ using NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Testing; using osu.Game.Models; +using osu.Game.Online; using osu.Game.Online.API; using osu.Game.Online.API.Requests; using osu.Game.Online.API.Requests.Responses; @@ -25,6 +26,7 @@ namespace osu.Game.Tests.Visual.Online { protected override bool UseOnlineAPI => false; + private LocalUserStatisticsProvider statisticsProvider = null!; private SoloStatisticsWatcher watcher = null!; [Resolved] @@ -109,7 +111,9 @@ namespace osu.Game.Tests.Visual.Online AddStep("create watcher", () => { - Child = watcher = new SoloStatisticsWatcher(); + Clear(); + Add(statisticsProvider = new LocalUserStatisticsProvider()); + Add(watcher = new SoloStatisticsWatcher(statisticsProvider)); }); } @@ -289,7 +293,7 @@ namespace osu.Game.Tests.Visual.Online AddStep("signal score processed", () => ((ISpectatorClient)spectatorClient).UserScoreProcessed(userId, scoreId)); AddUntilStep("update received", () => update != null); AddAssert("local user values are correct", () => dummyAPI.LocalUser.Value.Statistics.TotalScore, () => Is.EqualTo(5_000_000)); - AddAssert("statistics values are correct", () => dummyAPI.Statistics.Value!.TotalScore, () => Is.EqualTo(5_000_000)); + AddAssert("statistics values are correct", () => statisticsProvider.Statistics.Value!.TotalScore, () => Is.EqualTo(5_000_000)); } private int nextUserId = 2000; diff --git a/osu.Game/Online/API/APIAccess.cs b/osu.Game/Online/API/APIAccess.cs index d3707fe74d..5c3a8e7e92 100644 --- a/osu.Game/Online/API/APIAccess.cs +++ b/osu.Game/Online/API/APIAccess.cs @@ -55,7 +55,6 @@ namespace osu.Game.Online.API public IBindable LocalUser => localUser; public IBindableList Friends => friends; public IBindable Activity => activity; - public IBindable Statistics => statistics; public INotificationsClient NotificationsClient { get; } @@ -70,8 +69,6 @@ namespace osu.Game.Online.API private Bindable configStatus { get; } = new Bindable(); private Bindable localUserStatus { get; } = new Bindable(); - private Bindable statistics { get; } = new Bindable(); - protected bool HasLogin => authentication.Token.Value != null || (!string.IsNullOrEmpty(ProvidedUsername) && !string.IsNullOrEmpty(password)); private readonly CancellationTokenSource cancellationToken = new CancellationTokenSource(); @@ -595,21 +592,9 @@ namespace osu.Game.Online.API flushQueue(); } - public void UpdateStatistics(UserStatistics newStatistics) - { - statistics.Value = newStatistics; - - if (IsLoggedIn) - localUser.Value.Statistics = newStatistics; - } - private static APIUser createGuestUser() => new GuestUser(); - private void setLocalUser(APIUser user) => Scheduler.Add(() => - { - localUser.Value = user; - statistics.Value = user.Statistics; - }, false); + private void setLocalUser(APIUser user) => Scheduler.Add(() => localUser.Value = user, false); protected override void Dispose(bool isDisposing) { diff --git a/osu.Game/Online/API/DummyAPIAccess.cs b/osu.Game/Online/API/DummyAPIAccess.cs index 4962838bd9..ca21b15b1f 100644 --- a/osu.Game/Online/API/DummyAPIAccess.cs +++ b/osu.Game/Online/API/DummyAPIAccess.cs @@ -30,8 +30,6 @@ namespace osu.Game.Online.API public Bindable Activity { get; } = new Bindable(); - public Bindable Statistics { get; } = new Bindable(); - public DummyNotificationsClient NotificationsClient { get; } = new DummyNotificationsClient(); INotificationsClient IAPIProvider.NotificationsClient => NotificationsClient; @@ -158,11 +156,6 @@ namespace osu.Game.Online.API private void onSuccessfulLogin() { state.Value = APIState.Online; - Statistics.Value = new UserStatistics - { - GlobalRank = 1, - CountryRank = 1 - }; } public void Logout() @@ -173,14 +166,6 @@ namespace osu.Game.Online.API LocalUser.Value = new GuestUser(); } - public void UpdateStatistics(UserStatistics newStatistics) - { - Statistics.Value = newStatistics; - - if (IsLoggedIn) - LocalUser.Value.Statistics = newStatistics; - } - public IHubClientConnector? GetHubConnector(string clientName, string endpoint, bool preferMessagePack) => null; public IChatClient GetChatClient() => new TestChatClientConnector(this); @@ -196,7 +181,6 @@ namespace osu.Game.Online.API IBindable IAPIProvider.LocalUser => LocalUser; IBindableList IAPIProvider.Friends => Friends; IBindable IAPIProvider.Activity => Activity; - IBindable IAPIProvider.Statistics => Statistics; /// /// Skip 2FA requirement for next login. diff --git a/osu.Game/Online/API/IAPIProvider.cs b/osu.Game/Online/API/IAPIProvider.cs index 66f124f7c3..c1f2a52d24 100644 --- a/osu.Game/Online/API/IAPIProvider.cs +++ b/osu.Game/Online/API/IAPIProvider.cs @@ -29,11 +29,6 @@ namespace osu.Game.Online.API /// IBindable Activity { get; } - /// - /// The current user's online statistics. - /// - IBindable Statistics { get; } - /// /// The language supplied by this provider to API requests. /// @@ -123,11 +118,6 @@ namespace osu.Game.Online.API /// void Logout(); - /// - /// Sets Statistics bindable. - /// - void UpdateStatistics(UserStatistics newStatistics); - /// /// Constructs a new . May be null if not supported. /// diff --git a/osu.Game/Online/Solo/SoloStatisticsWatcher.cs b/osu.Game/Online/Solo/SoloStatisticsWatcher.cs index 55b27fb364..eb7c385fed 100644 --- a/osu.Game/Online/Solo/SoloStatisticsWatcher.cs +++ b/osu.Game/Online/Solo/SoloStatisticsWatcher.cs @@ -22,6 +22,8 @@ namespace osu.Game.Online.Solo /// public partial class SoloStatisticsWatcher : Component { + private readonly LocalUserStatisticsProvider? statisticsProvider; + [Resolved] private SpectatorClient spectatorClient { get; set; } = null!; @@ -33,6 +35,11 @@ namespace osu.Game.Online.Solo private Dictionary? latestStatistics; + public SoloStatisticsWatcher(LocalUserStatisticsProvider? statisticsProvider = null) + { + this.statisticsProvider = statisticsProvider; + } + protected override void LoadComplete() { base.LoadComplete(); @@ -127,7 +134,7 @@ namespace osu.Game.Online.Solo { string rulesetName = callback.Score.Ruleset.ShortName; - api.UpdateStatistics(updatedStatistics); + statisticsProvider?.UpdateStatistics(updatedStatistics, callback.Score.Ruleset); if (latestStatistics == null) return; From 633d85431bb0b01cce1d58ad4ac461eb5a9a51fc Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Sun, 11 Feb 2024 08:22:20 +0300 Subject: [PATCH 0006/1275] Update `UserRankPanel` implementation to use new component --- .../Visual/Online/TestSceneUserPanel.cs | 15 ++++++------- osu.Game/Users/UserRankPanel.cs | 21 +++++++++++++------ 2 files changed, 23 insertions(+), 13 deletions(-) diff --git a/osu.Game.Tests/Visual/Online/TestSceneUserPanel.cs b/osu.Game.Tests/Visual/Online/TestSceneUserPanel.cs index 4df34e6244..bb7b83cb97 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneUserPanel.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneUserPanel.cs @@ -11,6 +11,7 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Utils; using osu.Game.Beatmaps; +using osu.Game.Online; using osu.Game.Online.API.Requests.Responses; using osu.Game.Overlays; using osu.Game.Rulesets; @@ -32,7 +33,10 @@ namespace osu.Game.Tests.Visual.Online private TestUserListPanel boundPanel2; [Cached] - private OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Purple); + private readonly OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Purple); + + [Cached] + private readonly LocalUserStatisticsProvider statisticsProvider = new LocalUserStatisticsProvider(); [Resolved] private IRulesetStore rulesetStore { get; set; } @@ -163,16 +167,13 @@ namespace osu.Game.Tests.Visual.Online { AddStep("update statistics", () => { - API.UpdateStatistics(new UserStatistics + statisticsProvider.UpdateStatistics(new UserStatistics { GlobalRank = RNG.Next(100000), CountryRank = RNG.Next(100000) - }); - }); - AddStep("set statistics to empty", () => - { - API.UpdateStatistics(new UserStatistics()); + }, Ruleset.Value); }); + AddStep("set statistics to empty", () => statisticsProvider.UpdateStatistics(new UserStatistics(), Ruleset.Value)); } private UserActivity soloGameStatusForRuleset(int rulesetId) => new UserActivity.InSoloGame(new BeatmapInfo(), rulesetStore.GetRuleset(rulesetId)!); diff --git a/osu.Game/Users/UserRankPanel.cs b/osu.Game/Users/UserRankPanel.cs index 84ff3114fc..167c34e4b8 100644 --- a/osu.Game/Users/UserRankPanel.cs +++ b/osu.Game/Users/UserRankPanel.cs @@ -7,7 +7,8 @@ using osu.Framework.Extensions.LocalisationExtensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Input.Events; -using osu.Game.Online.API; +using osu.Game.Graphics.UserInterface; +using osu.Game.Online; using osu.Game.Online.API.Requests.Responses; using osu.Game.Overlays.Profile.Header.Components; using osu.Game.Resources.Localisation.Web; @@ -24,11 +25,9 @@ namespace osu.Game.Users private const int padding = 10; private const int main_content_height = 80; - [Resolved] - private IAPIProvider api { get; set; } = null!; - private ProfileValueDisplay globalRankDisplay = null!; private ProfileValueDisplay countryRankDisplay = null!; + private LoadingLayer loadingLayer = null!; private readonly IBindable statistics = new Bindable(); @@ -43,10 +42,19 @@ namespace osu.Game.Users private void load() { BorderColour = ColourProvider?.Light1 ?? Colours.GreyVioletLighter; + } - statistics.BindTo(api.Statistics); + [Resolved] + private LocalUserStatisticsProvider statisticsProvider { get; set; } = null!; + + protected override void LoadComplete() + { + base.LoadComplete(); + + statistics.BindTo(statisticsProvider.Statistics); statistics.BindValueChanged(stats => { + loadingLayer.State.Value = stats.NewValue == null ? Visibility.Visible : Visibility.Hidden; globalRankDisplay.Content = stats.NewValue?.GlobalRank?.ToLocalisableString("\\##,##0") ?? "-"; countryRankDisplay.Content = stats.NewValue?.CountryRank?.ToLocalisableString("\\##,##0") ?? "-"; }, true); @@ -173,7 +181,8 @@ namespace osu.Game.Users } } } - } + }, + loadingLayer = new LoadingLayer(true), } }; From bc2b7050635a62524ca37a3463a9097739a649c2 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Sun, 11 Feb 2024 11:16:54 +0300 Subject: [PATCH 0007/1275] Fix `ImportTest.TestOsuGameBase` having null ruleset --- osu.Game.Tests/ImportTest.cs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/osu.Game.Tests/ImportTest.cs b/osu.Game.Tests/ImportTest.cs index 27b8d3f21e..b1e2730703 100644 --- a/osu.Game.Tests/ImportTest.cs +++ b/osu.Game.Tests/ImportTest.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 NUnit.Framework; @@ -64,6 +65,10 @@ namespace osu.Game.Tests // Beatmap must be imported before the collection manager is loaded. if (withBeatmap) BeatmapManager.Import(TestResources.GetTestBeatmapForImport()).WaitSafely(); + + // the logic for setting the initial ruleset exists in OsuGame rather than OsuGameBase. + // the ruleset bindable is not meant to be nullable, so assign any ruleset in here. + Ruleset.Value = RulesetStore.AvailableRulesets.First(); } } } From 11b3fa8691d289d83cd8fcbd2940e7968c9ee2a4 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Sun, 11 Feb 2024 11:39:12 +0300 Subject: [PATCH 0008/1275] Fix `TestSceneUserPanel` tests failing --- osu.Game.Tests/Visual/Online/TestSceneUserPanel.cs | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/osu.Game.Tests/Visual/Online/TestSceneUserPanel.cs b/osu.Game.Tests/Visual/Online/TestSceneUserPanel.cs index bb7b83cb97..00072d52c1 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneUserPanel.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneUserPanel.cs @@ -47,7 +47,11 @@ namespace osu.Game.Tests.Visual.Online activity.Value = null; status.Value = null; - Child = new FillFlowContainer + Remove(statisticsProvider, false); + Clear(); + Add(statisticsProvider); + + Add(new FillFlowContainer { Anchor = Anchor.Centre, Origin = Anchor.Centre, @@ -113,7 +117,7 @@ namespace osu.Game.Tests.Visual.Online Statistics = new UserStatistics { GlobalRank = null, CountryRank = null } }) { Width = 300 } } - }; + }); boundPanel1.Status.BindTo(status); boundPanel1.Activity.BindTo(activity); From 42b76294db48e604b48ade266444190a29bf1424 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 18 Apr 2024 09:48:57 +0800 Subject: [PATCH 0009/1275] 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 0010/1275] 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 0011/1275] 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 0012/1275] 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 0013/1275] 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 4339e2dc4afb5035221398a88cc04f6718d8d523 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 12 Jun 2024 12:41:07 +0200 Subject: [PATCH 0014/1275] Move `AudioLeadIn` out of `BeatmapInfo` --- osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapDecoderTest.cs | 4 ++-- osu.Game.Tests/Beatmaps/Formats/OsuJsonDecoderTest.cs | 2 +- osu.Game.Tests/Visual/Gameplay/TestSceneLeadIn.cs | 4 ++-- osu.Game.Tests/Visual/Gameplay/TestScenePlayerLoader.cs | 2 +- osu.Game.Tests/Visual/Gameplay/TestSceneReplayPlayer.cs | 2 +- .../Visual/Multiplayer/TestSceneMultiSpectatorScreen.cs | 4 ++-- osu.Game/Beatmaps/Beatmap.cs | 2 ++ osu.Game/Beatmaps/BeatmapConverter.cs | 1 + osu.Game/Beatmaps/BeatmapImporter.cs | 1 - osu.Game/Beatmaps/BeatmapInfo.cs | 2 -- osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs | 2 +- osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs | 2 +- osu.Game/Beatmaps/IBeatmap.cs | 2 ++ osu.Game/Rulesets/Difficulty/DifficultyCalculator.cs | 6 ++++++ osu.Game/Screens/Edit/EditorBeatmap.cs | 6 ++++++ osu.Game/Screens/Play/MasterGameplayClockContainer.cs | 4 ++-- 16 files changed, 30 insertions(+), 16 deletions(-) diff --git a/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapDecoderTest.cs b/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapDecoderTest.cs index a4cd888823..9ffb3327b9 100644 --- a/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapDecoderTest.cs +++ b/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapDecoderTest.cs @@ -80,7 +80,7 @@ namespace osu.Game.Tests.Beatmaps.Formats var metadata = beatmap.Metadata; Assert.AreEqual("03. Renatus - Soleily 192kbps.mp3", metadata.AudioFile); - Assert.AreEqual(0, beatmapInfo.AudioLeadIn); + Assert.AreEqual(0, beatmap.AudioLeadIn); Assert.AreEqual(164471, metadata.PreviewTime); Assert.AreEqual(0.7f, beatmapInfo.StackLeniency); Assert.IsTrue(beatmapInfo.Ruleset.OnlineID == 0); @@ -950,7 +950,7 @@ namespace osu.Game.Tests.Beatmaps.Formats Assert.Multiple(() => { - Assert.That(decoded.BeatmapInfo.AudioLeadIn, Is.EqualTo(0)); + Assert.That(decoded.AudioLeadIn, Is.EqualTo(0)); Assert.That(decoded.BeatmapInfo.StackLeniency, Is.EqualTo(0.7f)); Assert.That(decoded.BeatmapInfo.SpecialStyle, Is.False); Assert.That(decoded.BeatmapInfo.LetterboxInBreaks, Is.False); diff --git a/osu.Game.Tests/Beatmaps/Formats/OsuJsonDecoderTest.cs b/osu.Game.Tests/Beatmaps/Formats/OsuJsonDecoderTest.cs index 3764467047..3fd05b692d 100644 --- a/osu.Game.Tests/Beatmaps/Formats/OsuJsonDecoderTest.cs +++ b/osu.Game.Tests/Beatmaps/Formats/OsuJsonDecoderTest.cs @@ -51,7 +51,7 @@ namespace osu.Game.Tests.Beatmaps.Formats { var beatmap = decodeAsJson(normal); var beatmapInfo = beatmap.BeatmapInfo; - Assert.AreEqual(0, beatmapInfo.AudioLeadIn); + Assert.AreEqual(0, beatmap.AudioLeadIn); Assert.AreEqual(0.7f, beatmapInfo.StackLeniency); Assert.AreEqual(false, beatmapInfo.SpecialStyle); Assert.IsTrue(beatmapInfo.Ruleset.OnlineID == 0); diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneLeadIn.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneLeadIn.cs index 5a71369976..a6b8e679b9 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneLeadIn.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneLeadIn.cs @@ -31,7 +31,7 @@ namespace osu.Game.Tests.Visual.Gameplay { loadPlayerWithBeatmap(new TestBeatmap(new OsuRuleset().RulesetInfo) { - BeatmapInfo = { AudioLeadIn = leadIn } + AudioLeadIn = leadIn }); checkFirstFrameTime(expectedStartTime); @@ -130,7 +130,7 @@ namespace osu.Game.Tests.Visual.Gameplay { Text = $"GameplayStartTime: {DrawableRuleset.GameplayStartTime} " + $"FirstHitObjectTime: {FirstHitObjectTime} " - + $"LeadInTime: {Beatmap.Value.BeatmapInfo.AudioLeadIn} " + + $"LeadInTime: {Beatmap.Value.Beatmap.AudioLeadIn} " + $"FirstFrameClockTime: {FirstFrameClockTime}" }); } diff --git a/osu.Game.Tests/Visual/Gameplay/TestScenePlayerLoader.cs b/osu.Game.Tests/Visual/Gameplay/TestScenePlayerLoader.cs index 1949808dfe..c17405c2ec 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestScenePlayerLoader.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestScenePlayerLoader.cs @@ -135,7 +135,7 @@ namespace osu.Game.Tests.Visual.Gameplay var workingBeatmap = CreateWorkingBeatmap(new OsuRuleset().RulesetInfo); // Add intro time to test quick retry skipping (TestQuickRetry). - workingBeatmap.BeatmapInfo.AudioLeadIn = 60000; + workingBeatmap.Beatmap.AudioLeadIn = 60000; // Set up data for testing disclaimer display. workingBeatmap.BeatmapInfo.EpilepsyWarning = epilepsyWarning ?? false; diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneReplayPlayer.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneReplayPlayer.cs index ae10207de0..81dd23661c 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneReplayPlayer.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneReplayPlayer.cs @@ -63,7 +63,7 @@ namespace osu.Game.Tests.Visual.Gameplay { loadPlayerWithBeatmap(new TestBeatmap(new OsuRuleset().RulesetInfo) { - BeatmapInfo = { AudioLeadIn = 60000 } + AudioLeadIn = 60000 }); AddUntilStep("wait for skip overlay", () => Player.ChildrenOfType().First().IsButtonVisible); diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiSpectatorScreen.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiSpectatorScreen.cs index 2b17f91e68..6108260481 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiSpectatorScreen.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiSpectatorScreen.cs @@ -406,13 +406,13 @@ namespace osu.Game.Tests.Visual.Multiplayer } /// - /// Tests spectating with a beatmap that has a high value. + /// Tests spectating with a beatmap that has a high value. /// /// This test is not intended not to check the correct initial time value, but only to guard against /// gameplay potentially getting stuck in a stopped state due to lead in time being present. /// [Test] - public void TestAudioLeadIn() => testLeadIn(b => b.BeatmapInfo.AudioLeadIn = 2000); + public void TestAudioLeadIn() => testLeadIn(b => b.Beatmap.AudioLeadIn = 2000); /// /// Tests spectating with a beatmap that has a storyboard element with a negative start time (i.e. intro storyboard element). diff --git a/osu.Game/Beatmaps/Beatmap.cs b/osu.Game/Beatmaps/Beatmap.cs index ae77e4adcf..f3ad02558a 100644 --- a/osu.Game/Beatmaps/Beatmap.cs +++ b/osu.Game/Beatmaps/Beatmap.cs @@ -114,6 +114,8 @@ namespace osu.Game.Beatmaps return mostCommon.beatLength; } + public double AudioLeadIn { get; set; } + IBeatmap IBeatmap.Clone() => Clone(); public Beatmap Clone() => (Beatmap)MemberwiseClone(); diff --git a/osu.Game/Beatmaps/BeatmapConverter.cs b/osu.Game/Beatmaps/BeatmapConverter.cs index b68c80d4b3..f33cdaf81f 100644 --- a/osu.Game/Beatmaps/BeatmapConverter.cs +++ b/osu.Game/Beatmaps/BeatmapConverter.cs @@ -67,6 +67,7 @@ namespace osu.Game.Beatmaps beatmap.HitObjects = convertHitObjects(original.HitObjects, original, cancellationToken).OrderBy(s => s.StartTime).ToList(); beatmap.Breaks = original.Breaks; beatmap.UnhandledEventLines = original.UnhandledEventLines; + beatmap.AudioLeadIn = original.AudioLeadIn; return beatmap; } diff --git a/osu.Game/Beatmaps/BeatmapImporter.cs b/osu.Game/Beatmaps/BeatmapImporter.cs index 2137f33e77..b8e253527b 100644 --- a/osu.Game/Beatmaps/BeatmapImporter.cs +++ b/osu.Game/Beatmaps/BeatmapImporter.cs @@ -414,7 +414,6 @@ namespace osu.Game.Beatmaps Hash = hash, DifficultyName = decodedInfo.DifficultyName, OnlineID = decodedInfo.OnlineID, - AudioLeadIn = decodedInfo.AudioLeadIn, StackLeniency = decodedInfo.StackLeniency, SpecialStyle = decodedInfo.SpecialStyle, LetterboxInBreaks = decodedInfo.LetterboxInBreaks, diff --git a/osu.Game/Beatmaps/BeatmapInfo.cs b/osu.Game/Beatmaps/BeatmapInfo.cs index 425fd98d27..e1580dc74e 100644 --- a/osu.Game/Beatmaps/BeatmapInfo.cs +++ b/osu.Game/Beatmaps/BeatmapInfo.cs @@ -138,8 +138,6 @@ namespace osu.Game.Beatmaps #region Properties we may not want persisted (but also maybe no harm?) - public double AudioLeadIn { get; set; } - public float StackLeniency { get; set; } = 0.7f; public bool SpecialStyle { get; set; } diff --git a/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs b/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs index c2f4097889..5966658c93 100644 --- a/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs +++ b/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs @@ -238,7 +238,7 @@ namespace osu.Game.Beatmaps.Formats break; case @"AudioLeadIn": - beatmap.BeatmapInfo.AudioLeadIn = Parsing.ParseInt(pair.Value); + beatmap.AudioLeadIn = Parsing.ParseInt(pair.Value); break; case @"PreviewTime": diff --git a/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs b/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs index 186b565c39..072223c8fb 100644 --- a/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs +++ b/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs @@ -79,7 +79,7 @@ namespace osu.Game.Beatmaps.Formats writer.WriteLine("[General]"); if (!string.IsNullOrEmpty(beatmap.Metadata.AudioFile)) writer.WriteLine(FormattableString.Invariant($"AudioFilename: {Path.GetFileName(beatmap.Metadata.AudioFile)}")); - writer.WriteLine(FormattableString.Invariant($"AudioLeadIn: {beatmap.BeatmapInfo.AudioLeadIn}")); + writer.WriteLine(FormattableString.Invariant($"AudioLeadIn: {beatmap.AudioLeadIn}")); writer.WriteLine(FormattableString.Invariant($"PreviewTime: {beatmap.Metadata.PreviewTime}")); writer.WriteLine(FormattableString.Invariant($"Countdown: {(int)beatmap.BeatmapInfo.Countdown}")); writer.WriteLine(FormattableString.Invariant( diff --git a/osu.Game/Beatmaps/IBeatmap.cs b/osu.Game/Beatmaps/IBeatmap.cs index 5cc38e5b84..993155a32e 100644 --- a/osu.Game/Beatmaps/IBeatmap.cs +++ b/osu.Game/Beatmaps/IBeatmap.cs @@ -68,6 +68,8 @@ namespace osu.Game.Beatmaps /// double GetMostCommonBeatLength(); + double AudioLeadIn { get; internal set; } + /// /// Creates a shallow-clone of this beatmap and returns it. /// diff --git a/osu.Game/Rulesets/Difficulty/DifficultyCalculator.cs b/osu.Game/Rulesets/Difficulty/DifficultyCalculator.cs index d37cfc28b9..5557051f05 100644 --- a/osu.Game/Rulesets/Difficulty/DifficultyCalculator.cs +++ b/osu.Game/Rulesets/Difficulty/DifficultyCalculator.cs @@ -337,6 +337,12 @@ namespace osu.Game.Rulesets.Difficulty public double GetMostCommonBeatLength() => baseBeatmap.GetMostCommonBeatLength(); public IBeatmap Clone() => new ProgressiveCalculationBeatmap(baseBeatmap.Clone()); + public double AudioLeadIn + { + get => baseBeatmap.AudioLeadIn; + set => baseBeatmap.AudioLeadIn = value; + } + #endregion } } diff --git a/osu.Game/Screens/Edit/EditorBeatmap.cs b/osu.Game/Screens/Edit/EditorBeatmap.cs index 5be1d27805..7392c66a26 100644 --- a/osu.Game/Screens/Edit/EditorBeatmap.cs +++ b/osu.Game/Screens/Edit/EditorBeatmap.cs @@ -184,6 +184,12 @@ namespace osu.Game.Screens.Edit public double GetMostCommonBeatLength() => PlayableBeatmap.GetMostCommonBeatLength(); + public double AudioLeadIn + { + get => PlayableBeatmap.AudioLeadIn; + set => PlayableBeatmap.AudioLeadIn = value; + } + public IBeatmap Clone() => (EditorBeatmap)MemberwiseClone(); private IList mutableHitObjects => (IList)PlayableBeatmap.HitObjects; diff --git a/osu.Game/Screens/Play/MasterGameplayClockContainer.cs b/osu.Game/Screens/Play/MasterGameplayClockContainer.cs index b2f0ae5561..3851806788 100644 --- a/osu.Game/Screens/Play/MasterGameplayClockContainer.cs +++ b/osu.Game/Screens/Play/MasterGameplayClockContainer.cs @@ -95,8 +95,8 @@ namespace osu.Game.Screens.Play // some beatmaps specify a current lead-in time which should be used instead of the ruleset-provided value when available. // this is not available as an option in the live editor but can still be applied via .osu editing. double firstHitObjectTime = beatmap.Beatmap.HitObjects.First().StartTime; - if (beatmap.BeatmapInfo.AudioLeadIn > 0) - time = Math.Min(time, firstHitObjectTime - beatmap.BeatmapInfo.AudioLeadIn); + if (beatmap.Beatmap.AudioLeadIn > 0) + time = Math.Min(time, firstHitObjectTime - beatmap.Beatmap.AudioLeadIn); return time; } From 0a4560a03e0c5dfccecc6be661586d378d3b88aa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 12 Jun 2024 12:50:41 +0200 Subject: [PATCH 0015/1275] Move `StackLeniency` out of `BeatmapInfo` --- .../Mods/TestSceneOsuModFlashlight.cs | 5 +---- .../Mods/TestSceneOsuModRandom.cs | 2 +- .../TestSceneSliderLateHitJudgement.cs | 2 +- .../Beatmaps/OsuBeatmapProcessor.cs | 14 +++++++------- .../Edit/Setup/OsuSetupSection.cs | 4 ++-- .../Beatmaps/Formats/LegacyBeatmapDecoderTest.cs | 4 ++-- .../Beatmaps/Formats/OsuJsonDecoderTest.cs | 2 +- osu.Game/Beatmaps/Beatmap.cs | 2 ++ osu.Game/Beatmaps/BeatmapConverter.cs | 1 + osu.Game/Beatmaps/BeatmapImporter.cs | 1 - osu.Game/Beatmaps/BeatmapInfo.cs | 2 -- osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs | 2 +- osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs | 2 +- osu.Game/Beatmaps/IBeatmap.cs | 2 ++ .../Rulesets/Difficulty/DifficultyCalculator.cs | 6 ++++++ osu.Game/Screens/Edit/EditorBeatmap.cs | 6 ++++++ 16 files changed, 34 insertions(+), 23 deletions(-) diff --git a/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModFlashlight.cs b/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModFlashlight.cs index 075fdd88ca..1a3b0310f7 100644 --- a/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModFlashlight.cs +++ b/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModFlashlight.cs @@ -83,10 +83,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Mods }) } }, - BeatmapInfo = - { - StackLeniency = 0, - } + StackLeniency = 0, }, ReplayFrames = new List { diff --git a/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModRandom.cs b/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModRandom.cs index 060a845137..75a5d36f32 100644 --- a/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModRandom.cs +++ b/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModRandom.cs @@ -74,12 +74,12 @@ namespace osu.Game.Rulesets.Osu.Tests.Mods { BeatmapInfo = new BeatmapInfo { - StackLeniency = 0, Difficulty = new BeatmapDifficulty { ApproachRate = 8.5f } }, + StackLeniency = 0, ControlPointInfo = controlPointInfo }; diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneSliderLateHitJudgement.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneSliderLateHitJudgement.cs index 1ba4a60b75..d089e924ca 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneSliderLateHitJudgement.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneSliderLateHitJudgement.cs @@ -465,7 +465,7 @@ namespace osu.Game.Rulesets.Osu.Tests private void performTest(List frames, Beatmap beatmap) { beatmap.BeatmapInfo.Ruleset = new OsuRuleset().RulesetInfo; - beatmap.BeatmapInfo.StackLeniency = 0; + beatmap.StackLeniency = 0; beatmap.BeatmapInfo.Difficulty = new BeatmapDifficulty { SliderMultiplier = 4, diff --git a/osu.Game.Rulesets.Osu/Beatmaps/OsuBeatmapProcessor.cs b/osu.Game.Rulesets.Osu/Beatmaps/OsuBeatmapProcessor.cs index d335913586..9cc22b764f 100644 --- a/osu.Game.Rulesets.Osu/Beatmaps/OsuBeatmapProcessor.cs +++ b/osu.Game.Rulesets.Osu/Beatmaps/OsuBeatmapProcessor.cs @@ -51,13 +51,13 @@ namespace osu.Game.Rulesets.Osu.Beatmaps h.StackHeight = 0; if (Beatmap.BeatmapInfo.BeatmapVersion >= 6) - applyStacking(Beatmap.BeatmapInfo, hitObjects, 0, hitObjects.Count - 1); + applyStacking(Beatmap, hitObjects, 0, hitObjects.Count - 1); else - applyStackingOld(Beatmap.BeatmapInfo, hitObjects); + applyStackingOld(Beatmap, hitObjects); } } - private void applyStacking(BeatmapInfo beatmapInfo, List hitObjects, int startIndex, int endIndex) + private void applyStacking(IBeatmap beatmap, List hitObjects, int startIndex, int endIndex) { ArgumentOutOfRangeException.ThrowIfGreaterThan(startIndex, endIndex); ArgumentOutOfRangeException.ThrowIfNegative(startIndex); @@ -82,7 +82,7 @@ namespace osu.Game.Rulesets.Osu.Beatmaps continue; double endTime = stackBaseObject.GetEndTime(); - double stackThreshold = objectN.TimePreempt * beatmapInfo.StackLeniency; + double stackThreshold = objectN.TimePreempt * beatmap.StackLeniency; if (objectN.StartTime - endTime > stackThreshold) // We are no longer within stacking range of the next object. @@ -127,7 +127,7 @@ namespace osu.Game.Rulesets.Osu.Beatmaps OsuHitObject objectI = hitObjects[i]; if (objectI.StackHeight != 0 || objectI is Spinner) continue; - double stackThreshold = objectI.TimePreempt * beatmapInfo.StackLeniency; + double stackThreshold = objectI.TimePreempt * beatmap.StackLeniency; /* If this object is a hitcircle, then we enter this "special" case. * It either ends with a stack of hitcircles only, or a stack of hitcircles that are underneath a slider. @@ -209,7 +209,7 @@ namespace osu.Game.Rulesets.Osu.Beatmaps } } - private void applyStackingOld(BeatmapInfo beatmapInfo, List hitObjects) + private void applyStackingOld(IBeatmap beatmap, List hitObjects) { for (int i = 0; i < hitObjects.Count; i++) { @@ -223,7 +223,7 @@ namespace osu.Game.Rulesets.Osu.Beatmaps for (int j = i + 1; j < hitObjects.Count; j++) { - double stackThreshold = hitObjects[i].TimePreempt * beatmapInfo.StackLeniency; + double stackThreshold = hitObjects[i].TimePreempt * beatmap.StackLeniency; if (hitObjects[j].StartTime - stackThreshold > startTime) break; diff --git a/osu.Game.Rulesets.Osu/Edit/Setup/OsuSetupSection.cs b/osu.Game.Rulesets.Osu/Edit/Setup/OsuSetupSection.cs index 552b887081..e1a588a32a 100644 --- a/osu.Game.Rulesets.Osu/Edit/Setup/OsuSetupSection.cs +++ b/osu.Game.Rulesets.Osu/Edit/Setup/OsuSetupSection.cs @@ -28,7 +28,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Setup { Label = "Stack Leniency", Description = "In play mode, osu! automatically stacks notes which occur at the same location. Increasing this value means it is more likely to snap notes of further time-distance.", - Current = new BindableFloat(Beatmap.BeatmapInfo.StackLeniency) + Current = new BindableFloat(Beatmap.StackLeniency) { Default = 0.7f, MinValue = 0, @@ -48,7 +48,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Setup private void updateBeatmap() { - Beatmap.BeatmapInfo.StackLeniency = stackLeniency.Current.Value; + Beatmap.StackLeniency = stackLeniency.Current.Value; Beatmap.UpdateAllHitObjects(); Beatmap.SaveState(); } diff --git a/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapDecoderTest.cs b/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapDecoderTest.cs index 9ffb3327b9..565c481920 100644 --- a/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapDecoderTest.cs +++ b/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapDecoderTest.cs @@ -82,7 +82,7 @@ namespace osu.Game.Tests.Beatmaps.Formats Assert.AreEqual("03. Renatus - Soleily 192kbps.mp3", metadata.AudioFile); Assert.AreEqual(0, beatmap.AudioLeadIn); Assert.AreEqual(164471, metadata.PreviewTime); - Assert.AreEqual(0.7f, beatmapInfo.StackLeniency); + Assert.AreEqual(0.7f, beatmap.StackLeniency); Assert.IsTrue(beatmapInfo.Ruleset.OnlineID == 0); Assert.IsFalse(beatmapInfo.LetterboxInBreaks); Assert.IsFalse(beatmapInfo.SpecialStyle); @@ -951,7 +951,7 @@ namespace osu.Game.Tests.Beatmaps.Formats Assert.Multiple(() => { Assert.That(decoded.AudioLeadIn, Is.EqualTo(0)); - Assert.That(decoded.BeatmapInfo.StackLeniency, Is.EqualTo(0.7f)); + Assert.That(decoded.StackLeniency, Is.EqualTo(0.7f)); Assert.That(decoded.BeatmapInfo.SpecialStyle, Is.False); Assert.That(decoded.BeatmapInfo.LetterboxInBreaks, Is.False); Assert.That(decoded.BeatmapInfo.WidescreenStoryboard, Is.False); diff --git a/osu.Game.Tests/Beatmaps/Formats/OsuJsonDecoderTest.cs b/osu.Game.Tests/Beatmaps/Formats/OsuJsonDecoderTest.cs index 3fd05b692d..5d2d9e006e 100644 --- a/osu.Game.Tests/Beatmaps/Formats/OsuJsonDecoderTest.cs +++ b/osu.Game.Tests/Beatmaps/Formats/OsuJsonDecoderTest.cs @@ -52,7 +52,7 @@ namespace osu.Game.Tests.Beatmaps.Formats var beatmap = decodeAsJson(normal); var beatmapInfo = beatmap.BeatmapInfo; Assert.AreEqual(0, beatmap.AudioLeadIn); - Assert.AreEqual(0.7f, beatmapInfo.StackLeniency); + Assert.AreEqual(0.7f, beatmap.StackLeniency); Assert.AreEqual(false, beatmapInfo.SpecialStyle); Assert.IsTrue(beatmapInfo.Ruleset.OnlineID == 0); Assert.AreEqual(false, beatmapInfo.LetterboxInBreaks); diff --git a/osu.Game/Beatmaps/Beatmap.cs b/osu.Game/Beatmaps/Beatmap.cs index f3ad02558a..ecee6e3416 100644 --- a/osu.Game/Beatmaps/Beatmap.cs +++ b/osu.Game/Beatmaps/Beatmap.cs @@ -116,6 +116,8 @@ namespace osu.Game.Beatmaps public double AudioLeadIn { get; set; } + public float StackLeniency { get; set; } = 0.7f; + IBeatmap IBeatmap.Clone() => Clone(); public Beatmap Clone() => (Beatmap)MemberwiseClone(); diff --git a/osu.Game/Beatmaps/BeatmapConverter.cs b/osu.Game/Beatmaps/BeatmapConverter.cs index f33cdaf81f..a5e9025404 100644 --- a/osu.Game/Beatmaps/BeatmapConverter.cs +++ b/osu.Game/Beatmaps/BeatmapConverter.cs @@ -68,6 +68,7 @@ namespace osu.Game.Beatmaps beatmap.Breaks = original.Breaks; beatmap.UnhandledEventLines = original.UnhandledEventLines; beatmap.AudioLeadIn = original.AudioLeadIn; + beatmap.StackLeniency = original.StackLeniency; return beatmap; } diff --git a/osu.Game/Beatmaps/BeatmapImporter.cs b/osu.Game/Beatmaps/BeatmapImporter.cs index b8e253527b..e589b0a754 100644 --- a/osu.Game/Beatmaps/BeatmapImporter.cs +++ b/osu.Game/Beatmaps/BeatmapImporter.cs @@ -414,7 +414,6 @@ namespace osu.Game.Beatmaps Hash = hash, DifficultyName = decodedInfo.DifficultyName, OnlineID = decodedInfo.OnlineID, - StackLeniency = decodedInfo.StackLeniency, SpecialStyle = decodedInfo.SpecialStyle, LetterboxInBreaks = decodedInfo.LetterboxInBreaks, WidescreenStoryboard = decodedInfo.WidescreenStoryboard, diff --git a/osu.Game/Beatmaps/BeatmapInfo.cs b/osu.Game/Beatmaps/BeatmapInfo.cs index e1580dc74e..fa2911438b 100644 --- a/osu.Game/Beatmaps/BeatmapInfo.cs +++ b/osu.Game/Beatmaps/BeatmapInfo.cs @@ -138,8 +138,6 @@ namespace osu.Game.Beatmaps #region Properties we may not want persisted (but also maybe no harm?) - public float StackLeniency { get; set; } = 0.7f; - public bool SpecialStyle { get; set; } public bool LetterboxInBreaks { get; set; } diff --git a/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs b/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs index 5966658c93..86552b21dd 100644 --- a/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs +++ b/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs @@ -255,7 +255,7 @@ namespace osu.Game.Beatmaps.Formats break; case @"StackLeniency": - beatmap.BeatmapInfo.StackLeniency = Parsing.ParseFloat(pair.Value); + beatmap.StackLeniency = Parsing.ParseFloat(pair.Value); break; case @"Mode": diff --git a/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs b/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs index 072223c8fb..8c371026ff 100644 --- a/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs +++ b/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs @@ -84,7 +84,7 @@ namespace osu.Game.Beatmaps.Formats writer.WriteLine(FormattableString.Invariant($"Countdown: {(int)beatmap.BeatmapInfo.Countdown}")); writer.WriteLine(FormattableString.Invariant( $"SampleSet: {toLegacySampleBank(((beatmap.ControlPointInfo as LegacyControlPointInfo)?.SamplePoints.FirstOrDefault() ?? SampleControlPoint.DEFAULT).SampleBank)}")); - writer.WriteLine(FormattableString.Invariant($"StackLeniency: {beatmap.BeatmapInfo.StackLeniency}")); + writer.WriteLine(FormattableString.Invariant($"StackLeniency: {beatmap.StackLeniency}")); writer.WriteLine(FormattableString.Invariant($"Mode: {onlineRulesetID}")); writer.WriteLine(FormattableString.Invariant($"LetterboxInBreaks: {(beatmap.BeatmapInfo.LetterboxInBreaks ? '1' : '0')}")); // if (beatmap.BeatmapInfo.UseSkinSprites) diff --git a/osu.Game/Beatmaps/IBeatmap.cs b/osu.Game/Beatmaps/IBeatmap.cs index 993155a32e..28d601620a 100644 --- a/osu.Game/Beatmaps/IBeatmap.cs +++ b/osu.Game/Beatmaps/IBeatmap.cs @@ -70,6 +70,8 @@ namespace osu.Game.Beatmaps double AudioLeadIn { get; internal set; } + float StackLeniency { get; internal set; } + /// /// Creates a shallow-clone of this beatmap and returns it. /// diff --git a/osu.Game/Rulesets/Difficulty/DifficultyCalculator.cs b/osu.Game/Rulesets/Difficulty/DifficultyCalculator.cs index 5557051f05..616d6d0848 100644 --- a/osu.Game/Rulesets/Difficulty/DifficultyCalculator.cs +++ b/osu.Game/Rulesets/Difficulty/DifficultyCalculator.cs @@ -343,6 +343,12 @@ namespace osu.Game.Rulesets.Difficulty set => baseBeatmap.AudioLeadIn = value; } + public float StackLeniency + { + get => baseBeatmap.StackLeniency; + set => baseBeatmap.StackLeniency = value; + } + #endregion } } diff --git a/osu.Game/Screens/Edit/EditorBeatmap.cs b/osu.Game/Screens/Edit/EditorBeatmap.cs index 7392c66a26..c02a22ae03 100644 --- a/osu.Game/Screens/Edit/EditorBeatmap.cs +++ b/osu.Game/Screens/Edit/EditorBeatmap.cs @@ -190,6 +190,12 @@ namespace osu.Game.Screens.Edit set => PlayableBeatmap.AudioLeadIn = value; } + public float StackLeniency + { + get => PlayableBeatmap.StackLeniency; + set => PlayableBeatmap.StackLeniency = value; + } + public IBeatmap Clone() => (EditorBeatmap)MemberwiseClone(); private IList mutableHitObjects => (IList)PlayableBeatmap.HitObjects; From 011c2e3651fe1485eca8663697630777a848a440 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 12 Jun 2024 13:12:30 +0200 Subject: [PATCH 0016/1275] Move `SpecialStyle` out of `BeatmapInfo` --- osu.Game.Rulesets.Mania/Edit/Setup/ManiaSetupSection.cs | 4 ++-- osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapDecoderTest.cs | 4 ++-- osu.Game.Tests/Beatmaps/Formats/OsuJsonDecoderTest.cs | 2 +- osu.Game/Beatmaps/Beatmap.cs | 2 ++ osu.Game/Beatmaps/BeatmapConverter.cs | 1 + osu.Game/Beatmaps/BeatmapImporter.cs | 1 - osu.Game/Beatmaps/BeatmapInfo.cs | 2 -- osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs | 2 +- osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs | 2 +- osu.Game/Beatmaps/IBeatmap.cs | 2 ++ osu.Game/Rulesets/Difficulty/DifficultyCalculator.cs | 6 ++++++ osu.Game/Screens/Edit/EditorBeatmap.cs | 6 ++++++ 12 files changed, 24 insertions(+), 10 deletions(-) diff --git a/osu.Game.Rulesets.Mania/Edit/Setup/ManiaSetupSection.cs b/osu.Game.Rulesets.Mania/Edit/Setup/ManiaSetupSection.cs index d5a9a311bc..8778c18c38 100644 --- a/osu.Game.Rulesets.Mania/Edit/Setup/ManiaSetupSection.cs +++ b/osu.Game.Rulesets.Mania/Edit/Setup/ManiaSetupSection.cs @@ -28,7 +28,7 @@ namespace osu.Game.Rulesets.Mania.Edit.Setup { Label = "Use special (N+1) style", Description = "Changes one column to act as a classic \"scratch\" or \"special\" column, which can be moved around by the user's skin (to the left/right/centre). Generally used in 6K (5+1) or 8K (7+1) configurations.", - Current = { Value = Beatmap.BeatmapInfo.SpecialStyle } + Current = { Value = Beatmap.SpecialStyle } } }; } @@ -42,7 +42,7 @@ namespace osu.Game.Rulesets.Mania.Edit.Setup private void updateBeatmap() { - Beatmap.BeatmapInfo.SpecialStyle = specialStyle.Current.Value; + Beatmap.SpecialStyle = specialStyle.Current.Value; Beatmap.SaveState(); } } diff --git a/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapDecoderTest.cs b/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapDecoderTest.cs index 565c481920..ad3721220a 100644 --- a/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapDecoderTest.cs +++ b/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapDecoderTest.cs @@ -85,7 +85,7 @@ namespace osu.Game.Tests.Beatmaps.Formats Assert.AreEqual(0.7f, beatmap.StackLeniency); Assert.IsTrue(beatmapInfo.Ruleset.OnlineID == 0); Assert.IsFalse(beatmapInfo.LetterboxInBreaks); - Assert.IsFalse(beatmapInfo.SpecialStyle); + Assert.IsFalse(beatmap.SpecialStyle); Assert.IsFalse(beatmapInfo.WidescreenStoryboard); Assert.IsFalse(beatmapInfo.SamplesMatchPlaybackRate); Assert.AreEqual(CountdownType.None, beatmapInfo.Countdown); @@ -952,7 +952,7 @@ namespace osu.Game.Tests.Beatmaps.Formats { Assert.That(decoded.AudioLeadIn, Is.EqualTo(0)); Assert.That(decoded.StackLeniency, Is.EqualTo(0.7f)); - Assert.That(decoded.BeatmapInfo.SpecialStyle, Is.False); + Assert.That(decoded.SpecialStyle, Is.False); Assert.That(decoded.BeatmapInfo.LetterboxInBreaks, Is.False); Assert.That(decoded.BeatmapInfo.WidescreenStoryboard, Is.False); Assert.That(decoded.BeatmapInfo.EpilepsyWarning, Is.False); diff --git a/osu.Game.Tests/Beatmaps/Formats/OsuJsonDecoderTest.cs b/osu.Game.Tests/Beatmaps/Formats/OsuJsonDecoderTest.cs index 5d2d9e006e..18f4651e94 100644 --- a/osu.Game.Tests/Beatmaps/Formats/OsuJsonDecoderTest.cs +++ b/osu.Game.Tests/Beatmaps/Formats/OsuJsonDecoderTest.cs @@ -53,7 +53,7 @@ namespace osu.Game.Tests.Beatmaps.Formats var beatmapInfo = beatmap.BeatmapInfo; Assert.AreEqual(0, beatmap.AudioLeadIn); Assert.AreEqual(0.7f, beatmap.StackLeniency); - Assert.AreEqual(false, beatmapInfo.SpecialStyle); + Assert.AreEqual(false, beatmap.SpecialStyle); Assert.IsTrue(beatmapInfo.Ruleset.OnlineID == 0); Assert.AreEqual(false, beatmapInfo.LetterboxInBreaks); Assert.AreEqual(false, beatmapInfo.WidescreenStoryboard); diff --git a/osu.Game/Beatmaps/Beatmap.cs b/osu.Game/Beatmaps/Beatmap.cs index ecee6e3416..f06d884bd1 100644 --- a/osu.Game/Beatmaps/Beatmap.cs +++ b/osu.Game/Beatmaps/Beatmap.cs @@ -118,6 +118,8 @@ namespace osu.Game.Beatmaps public float StackLeniency { get; set; } = 0.7f; + public bool SpecialStyle { get; set; } + IBeatmap IBeatmap.Clone() => Clone(); public Beatmap Clone() => (Beatmap)MemberwiseClone(); diff --git a/osu.Game/Beatmaps/BeatmapConverter.cs b/osu.Game/Beatmaps/BeatmapConverter.cs index a5e9025404..3b3b68de0a 100644 --- a/osu.Game/Beatmaps/BeatmapConverter.cs +++ b/osu.Game/Beatmaps/BeatmapConverter.cs @@ -69,6 +69,7 @@ namespace osu.Game.Beatmaps beatmap.UnhandledEventLines = original.UnhandledEventLines; beatmap.AudioLeadIn = original.AudioLeadIn; beatmap.StackLeniency = original.StackLeniency; + beatmap.SpecialStyle = original.SpecialStyle; return beatmap; } diff --git a/osu.Game/Beatmaps/BeatmapImporter.cs b/osu.Game/Beatmaps/BeatmapImporter.cs index e589b0a754..435a282b52 100644 --- a/osu.Game/Beatmaps/BeatmapImporter.cs +++ b/osu.Game/Beatmaps/BeatmapImporter.cs @@ -414,7 +414,6 @@ namespace osu.Game.Beatmaps Hash = hash, DifficultyName = decodedInfo.DifficultyName, OnlineID = decodedInfo.OnlineID, - SpecialStyle = decodedInfo.SpecialStyle, LetterboxInBreaks = decodedInfo.LetterboxInBreaks, WidescreenStoryboard = decodedInfo.WidescreenStoryboard, EpilepsyWarning = decodedInfo.EpilepsyWarning, diff --git a/osu.Game/Beatmaps/BeatmapInfo.cs b/osu.Game/Beatmaps/BeatmapInfo.cs index fa2911438b..4a1fa9b0b4 100644 --- a/osu.Game/Beatmaps/BeatmapInfo.cs +++ b/osu.Game/Beatmaps/BeatmapInfo.cs @@ -138,8 +138,6 @@ namespace osu.Game.Beatmaps #region Properties we may not want persisted (but also maybe no harm?) - public bool SpecialStyle { get; set; } - public bool LetterboxInBreaks { get; set; } public bool WidescreenStoryboard { get; set; } = true; diff --git a/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs b/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs index 86552b21dd..ea34c7d924 100644 --- a/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs +++ b/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs @@ -289,7 +289,7 @@ namespace osu.Game.Beatmaps.Formats break; case @"SpecialStyle": - beatmap.BeatmapInfo.SpecialStyle = Parsing.ParseInt(pair.Value) == 1; + beatmap.SpecialStyle = Parsing.ParseInt(pair.Value) == 1; break; case @"WidescreenStoryboard": diff --git a/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs b/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs index 8c371026ff..805ce49ca3 100644 --- a/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs +++ b/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs @@ -100,7 +100,7 @@ namespace osu.Game.Beatmaps.Formats if (beatmap.BeatmapInfo.CountdownOffset > 0) writer.WriteLine(FormattableString.Invariant($@"CountdownOffset: {beatmap.BeatmapInfo.CountdownOffset}")); if (onlineRulesetID == 3) - writer.WriteLine(FormattableString.Invariant($"SpecialStyle: {(beatmap.BeatmapInfo.SpecialStyle ? '1' : '0')}")); + writer.WriteLine(FormattableString.Invariant($"SpecialStyle: {(beatmap.SpecialStyle ? '1' : '0')}")); writer.WriteLine(FormattableString.Invariant($"WidescreenStoryboard: {(beatmap.BeatmapInfo.WidescreenStoryboard ? '1' : '0')}")); if (beatmap.BeatmapInfo.SamplesMatchPlaybackRate) writer.WriteLine(@"SamplesMatchPlaybackRate: 1"); diff --git a/osu.Game/Beatmaps/IBeatmap.cs b/osu.Game/Beatmaps/IBeatmap.cs index 28d601620a..3ac48c09b4 100644 --- a/osu.Game/Beatmaps/IBeatmap.cs +++ b/osu.Game/Beatmaps/IBeatmap.cs @@ -72,6 +72,8 @@ namespace osu.Game.Beatmaps float StackLeniency { get; internal set; } + bool SpecialStyle { get; internal set; } + /// /// Creates a shallow-clone of this beatmap and returns it. /// diff --git a/osu.Game/Rulesets/Difficulty/DifficultyCalculator.cs b/osu.Game/Rulesets/Difficulty/DifficultyCalculator.cs index 616d6d0848..87a20eec0b 100644 --- a/osu.Game/Rulesets/Difficulty/DifficultyCalculator.cs +++ b/osu.Game/Rulesets/Difficulty/DifficultyCalculator.cs @@ -349,6 +349,12 @@ namespace osu.Game.Rulesets.Difficulty set => baseBeatmap.StackLeniency = value; } + public bool SpecialStyle + { + get => baseBeatmap.SpecialStyle; + set => baseBeatmap.SpecialStyle = value; + } + #endregion } } diff --git a/osu.Game/Screens/Edit/EditorBeatmap.cs b/osu.Game/Screens/Edit/EditorBeatmap.cs index c02a22ae03..96216c6b1a 100644 --- a/osu.Game/Screens/Edit/EditorBeatmap.cs +++ b/osu.Game/Screens/Edit/EditorBeatmap.cs @@ -196,6 +196,12 @@ namespace osu.Game.Screens.Edit set => PlayableBeatmap.StackLeniency = value; } + public bool SpecialStyle + { + get => PlayableBeatmap.SpecialStyle; + set => PlayableBeatmap.SpecialStyle = value; + } + public IBeatmap Clone() => (EditorBeatmap)MemberwiseClone(); private IList mutableHitObjects => (IList)PlayableBeatmap.HitObjects; From a6b7600bf2b85f154624d8893fd9e658b098ab3f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 12 Jun 2024 13:15:41 +0200 Subject: [PATCH 0017/1275] Move `LetterboxInBreaks` out of `BeatmapInfo` --- osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapDecoderTest.cs | 4 ++-- osu.Game.Tests/Beatmaps/Formats/OsuJsonDecoderTest.cs | 2 +- osu.Game/Beatmaps/Beatmap.cs | 2 ++ osu.Game/Beatmaps/BeatmapConverter.cs | 1 + osu.Game/Beatmaps/BeatmapImporter.cs | 1 - osu.Game/Beatmaps/BeatmapInfo.cs | 2 -- osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs | 2 +- osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs | 2 +- osu.Game/Beatmaps/IBeatmap.cs | 2 ++ osu.Game/Rulesets/Difficulty/DifficultyCalculator.cs | 6 ++++++ osu.Game/Screens/Edit/EditorBeatmap.cs | 6 ++++++ osu.Game/Screens/Edit/Setup/DesignSection.cs | 4 ++-- osu.Game/Screens/Play/Player.cs | 2 +- 13 files changed, 25 insertions(+), 11 deletions(-) diff --git a/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapDecoderTest.cs b/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapDecoderTest.cs index ad3721220a..103bafb2d8 100644 --- a/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapDecoderTest.cs +++ b/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapDecoderTest.cs @@ -84,7 +84,7 @@ namespace osu.Game.Tests.Beatmaps.Formats Assert.AreEqual(164471, metadata.PreviewTime); Assert.AreEqual(0.7f, beatmap.StackLeniency); Assert.IsTrue(beatmapInfo.Ruleset.OnlineID == 0); - Assert.IsFalse(beatmapInfo.LetterboxInBreaks); + Assert.IsFalse(beatmap.LetterboxInBreaks); Assert.IsFalse(beatmap.SpecialStyle); Assert.IsFalse(beatmapInfo.WidescreenStoryboard); Assert.IsFalse(beatmapInfo.SamplesMatchPlaybackRate); @@ -953,7 +953,7 @@ namespace osu.Game.Tests.Beatmaps.Formats Assert.That(decoded.AudioLeadIn, Is.EqualTo(0)); Assert.That(decoded.StackLeniency, Is.EqualTo(0.7f)); Assert.That(decoded.SpecialStyle, Is.False); - Assert.That(decoded.BeatmapInfo.LetterboxInBreaks, Is.False); + Assert.That(decoded.LetterboxInBreaks, Is.False); Assert.That(decoded.BeatmapInfo.WidescreenStoryboard, Is.False); Assert.That(decoded.BeatmapInfo.EpilepsyWarning, Is.False); Assert.That(decoded.BeatmapInfo.SamplesMatchPlaybackRate, Is.False); diff --git a/osu.Game.Tests/Beatmaps/Formats/OsuJsonDecoderTest.cs b/osu.Game.Tests/Beatmaps/Formats/OsuJsonDecoderTest.cs index 18f4651e94..c1c996fd42 100644 --- a/osu.Game.Tests/Beatmaps/Formats/OsuJsonDecoderTest.cs +++ b/osu.Game.Tests/Beatmaps/Formats/OsuJsonDecoderTest.cs @@ -55,7 +55,7 @@ namespace osu.Game.Tests.Beatmaps.Formats Assert.AreEqual(0.7f, beatmap.StackLeniency); Assert.AreEqual(false, beatmap.SpecialStyle); Assert.IsTrue(beatmapInfo.Ruleset.OnlineID == 0); - Assert.AreEqual(false, beatmapInfo.LetterboxInBreaks); + Assert.AreEqual(false, beatmap.LetterboxInBreaks); Assert.AreEqual(false, beatmapInfo.WidescreenStoryboard); Assert.AreEqual(CountdownType.None, beatmapInfo.Countdown); Assert.AreEqual(0, beatmapInfo.CountdownOffset); diff --git a/osu.Game/Beatmaps/Beatmap.cs b/osu.Game/Beatmaps/Beatmap.cs index f06d884bd1..614bb4f42a 100644 --- a/osu.Game/Beatmaps/Beatmap.cs +++ b/osu.Game/Beatmaps/Beatmap.cs @@ -120,6 +120,8 @@ namespace osu.Game.Beatmaps public bool SpecialStyle { get; set; } + public bool LetterboxInBreaks { get; set; } + IBeatmap IBeatmap.Clone() => Clone(); public Beatmap Clone() => (Beatmap)MemberwiseClone(); diff --git a/osu.Game/Beatmaps/BeatmapConverter.cs b/osu.Game/Beatmaps/BeatmapConverter.cs index 3b3b68de0a..a56ce58532 100644 --- a/osu.Game/Beatmaps/BeatmapConverter.cs +++ b/osu.Game/Beatmaps/BeatmapConverter.cs @@ -70,6 +70,7 @@ namespace osu.Game.Beatmaps beatmap.AudioLeadIn = original.AudioLeadIn; beatmap.StackLeniency = original.StackLeniency; beatmap.SpecialStyle = original.SpecialStyle; + beatmap.LetterboxInBreaks = original.LetterboxInBreaks; return beatmap; } diff --git a/osu.Game/Beatmaps/BeatmapImporter.cs b/osu.Game/Beatmaps/BeatmapImporter.cs index 435a282b52..c54eece9f8 100644 --- a/osu.Game/Beatmaps/BeatmapImporter.cs +++ b/osu.Game/Beatmaps/BeatmapImporter.cs @@ -414,7 +414,6 @@ namespace osu.Game.Beatmaps Hash = hash, DifficultyName = decodedInfo.DifficultyName, OnlineID = decodedInfo.OnlineID, - LetterboxInBreaks = decodedInfo.LetterboxInBreaks, WidescreenStoryboard = decodedInfo.WidescreenStoryboard, EpilepsyWarning = decodedInfo.EpilepsyWarning, SamplesMatchPlaybackRate = decodedInfo.SamplesMatchPlaybackRate, diff --git a/osu.Game/Beatmaps/BeatmapInfo.cs b/osu.Game/Beatmaps/BeatmapInfo.cs index 4a1fa9b0b4..fafca5e014 100644 --- a/osu.Game/Beatmaps/BeatmapInfo.cs +++ b/osu.Game/Beatmaps/BeatmapInfo.cs @@ -138,8 +138,6 @@ namespace osu.Game.Beatmaps #region Properties we may not want persisted (but also maybe no harm?) - public bool LetterboxInBreaks { get; set; } - public bool WidescreenStoryboard { get; set; } = true; public bool EpilepsyWarning { get; set; } diff --git a/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs b/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs index ea34c7d924..b8469f27dd 100644 --- a/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs +++ b/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs @@ -285,7 +285,7 @@ namespace osu.Game.Beatmaps.Formats break; case @"LetterboxInBreaks": - beatmap.BeatmapInfo.LetterboxInBreaks = Parsing.ParseInt(pair.Value) == 1; + beatmap.LetterboxInBreaks = Parsing.ParseInt(pair.Value) == 1; break; case @"SpecialStyle": diff --git a/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs b/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs index 805ce49ca3..5a3fd0a2f3 100644 --- a/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs +++ b/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs @@ -86,7 +86,7 @@ namespace osu.Game.Beatmaps.Formats $"SampleSet: {toLegacySampleBank(((beatmap.ControlPointInfo as LegacyControlPointInfo)?.SamplePoints.FirstOrDefault() ?? SampleControlPoint.DEFAULT).SampleBank)}")); writer.WriteLine(FormattableString.Invariant($"StackLeniency: {beatmap.StackLeniency}")); writer.WriteLine(FormattableString.Invariant($"Mode: {onlineRulesetID}")); - writer.WriteLine(FormattableString.Invariant($"LetterboxInBreaks: {(beatmap.BeatmapInfo.LetterboxInBreaks ? '1' : '0')}")); + writer.WriteLine(FormattableString.Invariant($"LetterboxInBreaks: {(beatmap.LetterboxInBreaks ? '1' : '0')}")); // if (beatmap.BeatmapInfo.UseSkinSprites) // writer.WriteLine(@"UseSkinSprites: 1"); // if (b.AlwaysShowPlayfield) diff --git a/osu.Game/Beatmaps/IBeatmap.cs b/osu.Game/Beatmaps/IBeatmap.cs index 3ac48c09b4..e5562b608e 100644 --- a/osu.Game/Beatmaps/IBeatmap.cs +++ b/osu.Game/Beatmaps/IBeatmap.cs @@ -74,6 +74,8 @@ namespace osu.Game.Beatmaps bool SpecialStyle { get; internal set; } + bool LetterboxInBreaks { get; internal set; } + /// /// Creates a shallow-clone of this beatmap and returns it. /// diff --git a/osu.Game/Rulesets/Difficulty/DifficultyCalculator.cs b/osu.Game/Rulesets/Difficulty/DifficultyCalculator.cs index 87a20eec0b..04dcb2a552 100644 --- a/osu.Game/Rulesets/Difficulty/DifficultyCalculator.cs +++ b/osu.Game/Rulesets/Difficulty/DifficultyCalculator.cs @@ -355,6 +355,12 @@ namespace osu.Game.Rulesets.Difficulty set => baseBeatmap.SpecialStyle = value; } + public bool LetterboxInBreaks + { + get => baseBeatmap.LetterboxInBreaks; + set => baseBeatmap.LetterboxInBreaks = value; + } + #endregion } } diff --git a/osu.Game/Screens/Edit/EditorBeatmap.cs b/osu.Game/Screens/Edit/EditorBeatmap.cs index 96216c6b1a..7470505712 100644 --- a/osu.Game/Screens/Edit/EditorBeatmap.cs +++ b/osu.Game/Screens/Edit/EditorBeatmap.cs @@ -202,6 +202,12 @@ namespace osu.Game.Screens.Edit set => PlayableBeatmap.SpecialStyle = value; } + public bool LetterboxInBreaks + { + get => PlayableBeatmap.LetterboxInBreaks; + set => PlayableBeatmap.LetterboxInBreaks = value; + } + public IBeatmap Clone() => (EditorBeatmap)MemberwiseClone(); private IList mutableHitObjects => (IList)PlayableBeatmap.HitObjects; diff --git a/osu.Game/Screens/Edit/Setup/DesignSection.cs b/osu.Game/Screens/Edit/Setup/DesignSection.cs index b05a073146..c1fe7f405d 100644 --- a/osu.Game/Screens/Edit/Setup/DesignSection.cs +++ b/osu.Game/Screens/Edit/Setup/DesignSection.cs @@ -80,7 +80,7 @@ namespace osu.Game.Screens.Edit.Setup { Label = EditorSetupStrings.LetterboxDuringBreaks, Description = EditorSetupStrings.LetterboxDuringBreaksDescription, - Current = { Value = Beatmap.BeatmapInfo.LetterboxInBreaks } + Current = { Value = Beatmap.LetterboxInBreaks } }, samplesMatchPlaybackRate = new LabelledSwitchButton { @@ -123,7 +123,7 @@ namespace osu.Game.Screens.Edit.Setup Beatmap.BeatmapInfo.WidescreenStoryboard = widescreenSupport.Current.Value; Beatmap.BeatmapInfo.EpilepsyWarning = epilepsyWarning.Current.Value; - Beatmap.BeatmapInfo.LetterboxInBreaks = letterboxDuringBreaks.Current.Value; + Beatmap.LetterboxInBreaks = letterboxDuringBreaks.Current.Value; Beatmap.BeatmapInfo.SamplesMatchPlaybackRate = samplesMatchPlaybackRate.Current.Value; Beatmap.SaveState(); diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs index 42ff1d74f3..eaadc236f7 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -430,7 +430,7 @@ namespace osu.Game.Screens.Play Children = new[] { DimmableStoryboard.OverlayLayerContainer.CreateProxy(), - BreakOverlay = new BreakOverlay(working.Beatmap.BeatmapInfo.LetterboxInBreaks, ScoreProcessor) + BreakOverlay = new BreakOverlay(working.Beatmap.LetterboxInBreaks, ScoreProcessor) { Clock = DrawableRuleset.FrameStableClock, ProcessCustomClock = false, From 1ab86ebd249e31812ee07af2191957d6115a02c8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 12 Jun 2024 13:23:53 +0200 Subject: [PATCH 0018/1275] Move `WidescreenStoryboard` out of `BeatmapInfo` --- .../Beatmaps/Formats/LegacyBeatmapDecoderTest.cs | 4 ++-- osu.Game.Tests/Beatmaps/Formats/OsuJsonDecoderTest.cs | 2 +- osu.Game.Tests/Visual/Gameplay/TestSceneStoryboard.cs | 2 +- osu.Game/Beatmaps/Beatmap.cs | 2 ++ osu.Game/Beatmaps/BeatmapConverter.cs | 1 + osu.Game/Beatmaps/BeatmapImporter.cs | 1 - osu.Game/Beatmaps/BeatmapInfo.cs | 2 -- osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs | 10 +++++----- osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs | 2 +- osu.Game/Beatmaps/IBeatmap.cs | 2 ++ osu.Game/Rulesets/Difficulty/DifficultyCalculator.cs | 6 ++++++ osu.Game/Screens/Edit/EditorBeatmap.cs | 6 ++++++ osu.Game/Screens/Edit/Setup/DesignSection.cs | 4 ++-- osu.Game/Storyboards/Drawables/DrawableStoryboard.cs | 2 +- osu.Game/Storyboards/Storyboard.cs | 1 + 15 files changed, 31 insertions(+), 16 deletions(-) diff --git a/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapDecoderTest.cs b/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapDecoderTest.cs index 103bafb2d8..cd36b6b986 100644 --- a/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapDecoderTest.cs +++ b/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapDecoderTest.cs @@ -86,7 +86,7 @@ namespace osu.Game.Tests.Beatmaps.Formats Assert.IsTrue(beatmapInfo.Ruleset.OnlineID == 0); Assert.IsFalse(beatmap.LetterboxInBreaks); Assert.IsFalse(beatmap.SpecialStyle); - Assert.IsFalse(beatmapInfo.WidescreenStoryboard); + Assert.IsFalse(beatmap.WidescreenStoryboard); Assert.IsFalse(beatmapInfo.SamplesMatchPlaybackRate); Assert.AreEqual(CountdownType.None, beatmapInfo.Countdown); Assert.AreEqual(0, beatmapInfo.CountdownOffset); @@ -954,7 +954,7 @@ namespace osu.Game.Tests.Beatmaps.Formats Assert.That(decoded.StackLeniency, Is.EqualTo(0.7f)); Assert.That(decoded.SpecialStyle, Is.False); Assert.That(decoded.LetterboxInBreaks, Is.False); - Assert.That(decoded.BeatmapInfo.WidescreenStoryboard, Is.False); + Assert.That(decoded.WidescreenStoryboard, Is.False); Assert.That(decoded.BeatmapInfo.EpilepsyWarning, Is.False); Assert.That(decoded.BeatmapInfo.SamplesMatchPlaybackRate, Is.False); Assert.That(decoded.BeatmapInfo.Countdown, Is.EqualTo(CountdownType.Normal)); diff --git a/osu.Game.Tests/Beatmaps/Formats/OsuJsonDecoderTest.cs b/osu.Game.Tests/Beatmaps/Formats/OsuJsonDecoderTest.cs index c1c996fd42..92715b6aa2 100644 --- a/osu.Game.Tests/Beatmaps/Formats/OsuJsonDecoderTest.cs +++ b/osu.Game.Tests/Beatmaps/Formats/OsuJsonDecoderTest.cs @@ -56,7 +56,7 @@ namespace osu.Game.Tests.Beatmaps.Formats Assert.AreEqual(false, beatmap.SpecialStyle); Assert.IsTrue(beatmapInfo.Ruleset.OnlineID == 0); Assert.AreEqual(false, beatmap.LetterboxInBreaks); - Assert.AreEqual(false, beatmapInfo.WidescreenStoryboard); + Assert.AreEqual(false, beatmap.WidescreenStoryboard); Assert.AreEqual(CountdownType.None, beatmapInfo.Countdown); Assert.AreEqual(0, beatmapInfo.CountdownOffset); } diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneStoryboard.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneStoryboard.cs index 893b9f11f4..95aee43456 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneStoryboard.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneStoryboard.cs @@ -50,7 +50,7 @@ namespace osu.Game.Tests.Visual.Gameplay AddStep("load storyboard with only video", () => { // LegacyStoryboardDecoder doesn't parse WidescreenStoryboard, so it is set manually - loadStoryboard("storyboard_only_video.osu", s => s.BeatmapInfo.WidescreenStoryboard = false); + loadStoryboard("storyboard_only_video.osu", s => s.Beatmap.WidescreenStoryboard = false); }); AddAssert("storyboard is correct width", () => Precision.AlmostEquals(storyboard?.Width ?? 0f, 480 * 16 / 9f)); diff --git a/osu.Game/Beatmaps/Beatmap.cs b/osu.Game/Beatmaps/Beatmap.cs index 614bb4f42a..d909e87417 100644 --- a/osu.Game/Beatmaps/Beatmap.cs +++ b/osu.Game/Beatmaps/Beatmap.cs @@ -122,6 +122,8 @@ namespace osu.Game.Beatmaps public bool LetterboxInBreaks { get; set; } + public bool WidescreenStoryboard { get; set; } = true; + IBeatmap IBeatmap.Clone() => Clone(); public Beatmap Clone() => (Beatmap)MemberwiseClone(); diff --git a/osu.Game/Beatmaps/BeatmapConverter.cs b/osu.Game/Beatmaps/BeatmapConverter.cs index a56ce58532..c097389c56 100644 --- a/osu.Game/Beatmaps/BeatmapConverter.cs +++ b/osu.Game/Beatmaps/BeatmapConverter.cs @@ -71,6 +71,7 @@ namespace osu.Game.Beatmaps beatmap.StackLeniency = original.StackLeniency; beatmap.SpecialStyle = original.SpecialStyle; beatmap.LetterboxInBreaks = original.LetterboxInBreaks; + beatmap.WidescreenStoryboard = original.WidescreenStoryboard; return beatmap; } diff --git a/osu.Game/Beatmaps/BeatmapImporter.cs b/osu.Game/Beatmaps/BeatmapImporter.cs index c54eece9f8..0bb2ddda7d 100644 --- a/osu.Game/Beatmaps/BeatmapImporter.cs +++ b/osu.Game/Beatmaps/BeatmapImporter.cs @@ -414,7 +414,6 @@ namespace osu.Game.Beatmaps Hash = hash, DifficultyName = decodedInfo.DifficultyName, OnlineID = decodedInfo.OnlineID, - WidescreenStoryboard = decodedInfo.WidescreenStoryboard, EpilepsyWarning = decodedInfo.EpilepsyWarning, SamplesMatchPlaybackRate = decodedInfo.SamplesMatchPlaybackRate, DistanceSpacing = decodedInfo.DistanceSpacing, diff --git a/osu.Game/Beatmaps/BeatmapInfo.cs b/osu.Game/Beatmaps/BeatmapInfo.cs index fafca5e014..1bfa65a3fb 100644 --- a/osu.Game/Beatmaps/BeatmapInfo.cs +++ b/osu.Game/Beatmaps/BeatmapInfo.cs @@ -138,8 +138,6 @@ namespace osu.Game.Beatmaps #region Properties we may not want persisted (but also maybe no harm?) - public bool WidescreenStoryboard { get; set; } = true; - public bool EpilepsyWarning { get; set; } public bool SamplesMatchPlaybackRate { get; set; } = true; diff --git a/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs b/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs index b8469f27dd..355114fd0b 100644 --- a/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs +++ b/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs @@ -81,7 +81,7 @@ namespace osu.Game.Beatmaps.Formats this.beatmap = beatmap; this.beatmap.BeatmapInfo.BeatmapVersion = FormatVersion; - applyLegacyDefaults(this.beatmap.BeatmapInfo); + applyLegacyDefaults(this.beatmap); base.ParseStreamInto(stream, beatmap); @@ -183,10 +183,10 @@ namespace osu.Game.Beatmaps.Formats /// This method's intention is to restore those legacy defaults. /// See also: https://osu.ppy.sh/wiki/en/Client/File_formats/Osu_%28file_format%29 /// - private static void applyLegacyDefaults(BeatmapInfo beatmapInfo) + private static void applyLegacyDefaults(Beatmap beatmap) { - beatmapInfo.WidescreenStoryboard = false; - beatmapInfo.SamplesMatchPlaybackRate = false; + beatmap.WidescreenStoryboard = false; + beatmap.BeatmapInfo.SamplesMatchPlaybackRate = false; } protected override void ParseLine(Beatmap beatmap, Section section, string line) @@ -293,7 +293,7 @@ namespace osu.Game.Beatmaps.Formats break; case @"WidescreenStoryboard": - beatmap.BeatmapInfo.WidescreenStoryboard = Parsing.ParseInt(pair.Value) == 1; + beatmap.WidescreenStoryboard = Parsing.ParseInt(pair.Value) == 1; break; case @"EpilepsyWarning": diff --git a/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs b/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs index 5a3fd0a2f3..478b78fa29 100644 --- a/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs +++ b/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs @@ -101,7 +101,7 @@ namespace osu.Game.Beatmaps.Formats writer.WriteLine(FormattableString.Invariant($@"CountdownOffset: {beatmap.BeatmapInfo.CountdownOffset}")); if (onlineRulesetID == 3) writer.WriteLine(FormattableString.Invariant($"SpecialStyle: {(beatmap.SpecialStyle ? '1' : '0')}")); - writer.WriteLine(FormattableString.Invariant($"WidescreenStoryboard: {(beatmap.BeatmapInfo.WidescreenStoryboard ? '1' : '0')}")); + writer.WriteLine(FormattableString.Invariant($"WidescreenStoryboard: {(beatmap.WidescreenStoryboard ? '1' : '0')}")); if (beatmap.BeatmapInfo.SamplesMatchPlaybackRate) writer.WriteLine(@"SamplesMatchPlaybackRate: 1"); } diff --git a/osu.Game/Beatmaps/IBeatmap.cs b/osu.Game/Beatmaps/IBeatmap.cs index e5562b608e..3091e02054 100644 --- a/osu.Game/Beatmaps/IBeatmap.cs +++ b/osu.Game/Beatmaps/IBeatmap.cs @@ -76,6 +76,8 @@ namespace osu.Game.Beatmaps bool LetterboxInBreaks { get; internal set; } + bool WidescreenStoryboard { get; internal set; } + /// /// Creates a shallow-clone of this beatmap and returns it. /// diff --git a/osu.Game/Rulesets/Difficulty/DifficultyCalculator.cs b/osu.Game/Rulesets/Difficulty/DifficultyCalculator.cs index 04dcb2a552..bd051383de 100644 --- a/osu.Game/Rulesets/Difficulty/DifficultyCalculator.cs +++ b/osu.Game/Rulesets/Difficulty/DifficultyCalculator.cs @@ -361,6 +361,12 @@ namespace osu.Game.Rulesets.Difficulty set => baseBeatmap.LetterboxInBreaks = value; } + public bool WidescreenStoryboard + { + get => baseBeatmap.WidescreenStoryboard; + set => baseBeatmap.WidescreenStoryboard = value; + } + #endregion } } diff --git a/osu.Game/Screens/Edit/EditorBeatmap.cs b/osu.Game/Screens/Edit/EditorBeatmap.cs index 7470505712..a217e132d0 100644 --- a/osu.Game/Screens/Edit/EditorBeatmap.cs +++ b/osu.Game/Screens/Edit/EditorBeatmap.cs @@ -208,6 +208,12 @@ namespace osu.Game.Screens.Edit set => PlayableBeatmap.LetterboxInBreaks = value; } + public bool WidescreenStoryboard + { + get => PlayableBeatmap.WidescreenStoryboard; + set => PlayableBeatmap.WidescreenStoryboard = value; + } + public IBeatmap Clone() => (EditorBeatmap)MemberwiseClone(); private IList mutableHitObjects => (IList)PlayableBeatmap.HitObjects; diff --git a/osu.Game/Screens/Edit/Setup/DesignSection.cs b/osu.Game/Screens/Edit/Setup/DesignSection.cs index c1fe7f405d..8c420b979f 100644 --- a/osu.Game/Screens/Edit/Setup/DesignSection.cs +++ b/osu.Game/Screens/Edit/Setup/DesignSection.cs @@ -68,7 +68,7 @@ namespace osu.Game.Screens.Edit.Setup { Label = EditorSetupStrings.WidescreenSupport, Description = EditorSetupStrings.WidescreenSupportDescription, - Current = { Value = Beatmap.BeatmapInfo.WidescreenStoryboard } + Current = { Value = Beatmap.WidescreenStoryboard } }, epilepsyWarning = new LabelledSwitchButton { @@ -121,7 +121,7 @@ namespace osu.Game.Screens.Edit.Setup Beatmap.BeatmapInfo.Countdown = EnableCountdown.Current.Value ? CountdownSpeed.Current.Value : CountdownType.None; Beatmap.BeatmapInfo.CountdownOffset = int.TryParse(CountdownOffset.Current.Value, NumberStyles.None, CultureInfo.InvariantCulture, out int offset) ? offset : 0; - Beatmap.BeatmapInfo.WidescreenStoryboard = widescreenSupport.Current.Value; + Beatmap.WidescreenStoryboard = widescreenSupport.Current.Value; Beatmap.BeatmapInfo.EpilepsyWarning = epilepsyWarning.Current.Value; Beatmap.LetterboxInBreaks = letterboxDuringBreaks.Current.Value; Beatmap.BeatmapInfo.SamplesMatchPlaybackRate = samplesMatchPlaybackRate.Current.Value; diff --git a/osu.Game/Storyboards/Drawables/DrawableStoryboard.cs b/osu.Game/Storyboards/Drawables/DrawableStoryboard.cs index fc5ef12fb8..858c257e85 100644 --- a/osu.Game/Storyboards/Drawables/DrawableStoryboard.cs +++ b/osu.Game/Storyboards/Drawables/DrawableStoryboard.cs @@ -78,7 +78,7 @@ namespace osu.Game.Storyboards.Drawables bool onlyHasVideoElements = Storyboard.Layers.SelectMany(l => l.Elements).All(e => e is StoryboardVideo); - Width = Height * (storyboard.BeatmapInfo.WidescreenStoryboard || onlyHasVideoElements ? 16 / 9f : 4 / 3f); + Width = Height * (storyboard.Beatmap.WidescreenStoryboard || onlyHasVideoElements ? 16 / 9f : 4 / 3f); Anchor = Anchor.Centre; Origin = Anchor.Centre; diff --git a/osu.Game/Storyboards/Storyboard.cs b/osu.Game/Storyboards/Storyboard.cs index 8c43b99702..aca5fc34f4 100644 --- a/osu.Game/Storyboards/Storyboard.cs +++ b/osu.Game/Storyboards/Storyboard.cs @@ -16,6 +16,7 @@ namespace osu.Game.Storyboards public IEnumerable Layers => layers.Values; public BeatmapInfo BeatmapInfo = new BeatmapInfo(); + public IBeatmap Beatmap { get; set; } = new Beatmap(); /// /// Whether the storyboard should prefer textures from the current skin before using local storyboard textures. From f64a0624a5b1808a7c0ca91db99ecc7d679c4ddf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 12 Jun 2024 13:28:41 +0200 Subject: [PATCH 0019/1275] Move `EpilepsyWarning` out of `BeatmapInfo` --- osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapDecoderTest.cs | 2 +- osu.Game.Tests/Visual/Gameplay/TestScenePlayerLoader.cs | 2 +- osu.Game/Beatmaps/Beatmap.cs | 2 ++ osu.Game/Beatmaps/BeatmapConverter.cs | 1 + osu.Game/Beatmaps/BeatmapImporter.cs | 1 - osu.Game/Beatmaps/BeatmapInfo.cs | 2 -- osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs | 2 +- osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs | 2 +- osu.Game/Beatmaps/IBeatmap.cs | 2 ++ osu.Game/Rulesets/Difficulty/DifficultyCalculator.cs | 6 ++++++ osu.Game/Screens/Edit/EditorBeatmap.cs | 6 ++++++ osu.Game/Screens/Edit/Setup/DesignSection.cs | 4 ++-- osu.Game/Screens/Play/PlayerLoader.cs | 2 +- 13 files changed, 24 insertions(+), 10 deletions(-) diff --git a/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapDecoderTest.cs b/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapDecoderTest.cs index cd36b6b986..d3b027d253 100644 --- a/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapDecoderTest.cs +++ b/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapDecoderTest.cs @@ -955,7 +955,7 @@ namespace osu.Game.Tests.Beatmaps.Formats Assert.That(decoded.SpecialStyle, Is.False); Assert.That(decoded.LetterboxInBreaks, Is.False); Assert.That(decoded.WidescreenStoryboard, Is.False); - Assert.That(decoded.BeatmapInfo.EpilepsyWarning, Is.False); + Assert.That(decoded.EpilepsyWarning, Is.False); Assert.That(decoded.BeatmapInfo.SamplesMatchPlaybackRate, Is.False); Assert.That(decoded.BeatmapInfo.Countdown, Is.EqualTo(CountdownType.Normal)); Assert.That(decoded.BeatmapInfo.CountdownOffset, Is.EqualTo(0)); diff --git a/osu.Game.Tests/Visual/Gameplay/TestScenePlayerLoader.cs b/osu.Game.Tests/Visual/Gameplay/TestScenePlayerLoader.cs index c17405c2ec..64211cddf3 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestScenePlayerLoader.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestScenePlayerLoader.cs @@ -138,7 +138,7 @@ namespace osu.Game.Tests.Visual.Gameplay workingBeatmap.Beatmap.AudioLeadIn = 60000; // Set up data for testing disclaimer display. - workingBeatmap.BeatmapInfo.EpilepsyWarning = epilepsyWarning ?? false; + workingBeatmap.Beatmap.EpilepsyWarning = epilepsyWarning ?? false; workingBeatmap.BeatmapInfo.Status = onlineStatus ?? BeatmapOnlineStatus.Ranked; Beatmap.Value = workingBeatmap; diff --git a/osu.Game/Beatmaps/Beatmap.cs b/osu.Game/Beatmaps/Beatmap.cs index d909e87417..7516c8958c 100644 --- a/osu.Game/Beatmaps/Beatmap.cs +++ b/osu.Game/Beatmaps/Beatmap.cs @@ -124,6 +124,8 @@ namespace osu.Game.Beatmaps public bool WidescreenStoryboard { get; set; } = true; + public bool EpilepsyWarning { get; set; } + IBeatmap IBeatmap.Clone() => Clone(); public Beatmap Clone() => (Beatmap)MemberwiseClone(); diff --git a/osu.Game/Beatmaps/BeatmapConverter.cs b/osu.Game/Beatmaps/BeatmapConverter.cs index c097389c56..f9be44599f 100644 --- a/osu.Game/Beatmaps/BeatmapConverter.cs +++ b/osu.Game/Beatmaps/BeatmapConverter.cs @@ -72,6 +72,7 @@ namespace osu.Game.Beatmaps beatmap.SpecialStyle = original.SpecialStyle; beatmap.LetterboxInBreaks = original.LetterboxInBreaks; beatmap.WidescreenStoryboard = original.WidescreenStoryboard; + beatmap.EpilepsyWarning = original.EpilepsyWarning; return beatmap; } diff --git a/osu.Game/Beatmaps/BeatmapImporter.cs b/osu.Game/Beatmaps/BeatmapImporter.cs index 0bb2ddda7d..232c8b7e24 100644 --- a/osu.Game/Beatmaps/BeatmapImporter.cs +++ b/osu.Game/Beatmaps/BeatmapImporter.cs @@ -414,7 +414,6 @@ namespace osu.Game.Beatmaps Hash = hash, DifficultyName = decodedInfo.DifficultyName, OnlineID = decodedInfo.OnlineID, - EpilepsyWarning = decodedInfo.EpilepsyWarning, SamplesMatchPlaybackRate = decodedInfo.SamplesMatchPlaybackRate, DistanceSpacing = decodedInfo.DistanceSpacing, BeatDivisor = decodedInfo.BeatDivisor, diff --git a/osu.Game/Beatmaps/BeatmapInfo.cs b/osu.Game/Beatmaps/BeatmapInfo.cs index 1bfa65a3fb..41a0260250 100644 --- a/osu.Game/Beatmaps/BeatmapInfo.cs +++ b/osu.Game/Beatmaps/BeatmapInfo.cs @@ -138,8 +138,6 @@ namespace osu.Game.Beatmaps #region Properties we may not want persisted (but also maybe no harm?) - public bool EpilepsyWarning { get; set; } - public bool SamplesMatchPlaybackRate { get; set; } = true; /// diff --git a/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs b/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs index 355114fd0b..1152e5f30d 100644 --- a/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs +++ b/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs @@ -297,7 +297,7 @@ namespace osu.Game.Beatmaps.Formats break; case @"EpilepsyWarning": - beatmap.BeatmapInfo.EpilepsyWarning = Parsing.ParseInt(pair.Value) == 1; + beatmap.EpilepsyWarning = Parsing.ParseInt(pair.Value) == 1; break; case @"SamplesMatchPlaybackRate": diff --git a/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs b/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs index 478b78fa29..d7078a71d9 100644 --- a/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs +++ b/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs @@ -95,7 +95,7 @@ namespace osu.Game.Beatmaps.Formats // writer.WriteLine(@"OverlayPosition: " + b.OverlayPosition); // if (!string.IsNullOrEmpty(b.SkinPreference)) // writer.WriteLine(@"SkinPreference:" + b.SkinPreference); - if (beatmap.BeatmapInfo.EpilepsyWarning) + if (beatmap.EpilepsyWarning) writer.WriteLine(@"EpilepsyWarning: 1"); if (beatmap.BeatmapInfo.CountdownOffset > 0) writer.WriteLine(FormattableString.Invariant($@"CountdownOffset: {beatmap.BeatmapInfo.CountdownOffset}")); diff --git a/osu.Game/Beatmaps/IBeatmap.cs b/osu.Game/Beatmaps/IBeatmap.cs index 3091e02054..3d563004d1 100644 --- a/osu.Game/Beatmaps/IBeatmap.cs +++ b/osu.Game/Beatmaps/IBeatmap.cs @@ -78,6 +78,8 @@ namespace osu.Game.Beatmaps bool WidescreenStoryboard { get; internal set; } + bool EpilepsyWarning { get; internal set; } + /// /// Creates a shallow-clone of this beatmap and returns it. /// diff --git a/osu.Game/Rulesets/Difficulty/DifficultyCalculator.cs b/osu.Game/Rulesets/Difficulty/DifficultyCalculator.cs index bd051383de..7f9d2ae6ab 100644 --- a/osu.Game/Rulesets/Difficulty/DifficultyCalculator.cs +++ b/osu.Game/Rulesets/Difficulty/DifficultyCalculator.cs @@ -367,6 +367,12 @@ namespace osu.Game.Rulesets.Difficulty set => baseBeatmap.WidescreenStoryboard = value; } + public bool EpilepsyWarning + { + get => baseBeatmap.EpilepsyWarning; + set => baseBeatmap.EpilepsyWarning = value; + } + #endregion } } diff --git a/osu.Game/Screens/Edit/EditorBeatmap.cs b/osu.Game/Screens/Edit/EditorBeatmap.cs index a217e132d0..063803ab42 100644 --- a/osu.Game/Screens/Edit/EditorBeatmap.cs +++ b/osu.Game/Screens/Edit/EditorBeatmap.cs @@ -214,6 +214,12 @@ namespace osu.Game.Screens.Edit set => PlayableBeatmap.WidescreenStoryboard = value; } + public bool EpilepsyWarning + { + get => PlayableBeatmap.EpilepsyWarning; + set => PlayableBeatmap.EpilepsyWarning = value; + } + public IBeatmap Clone() => (EditorBeatmap)MemberwiseClone(); private IList mutableHitObjects => (IList)PlayableBeatmap.HitObjects; diff --git a/osu.Game/Screens/Edit/Setup/DesignSection.cs b/osu.Game/Screens/Edit/Setup/DesignSection.cs index 8c420b979f..5d729cf4f8 100644 --- a/osu.Game/Screens/Edit/Setup/DesignSection.cs +++ b/osu.Game/Screens/Edit/Setup/DesignSection.cs @@ -74,7 +74,7 @@ namespace osu.Game.Screens.Edit.Setup { Label = EditorSetupStrings.EpilepsyWarning, Description = EditorSetupStrings.EpilepsyWarningDescription, - Current = { Value = Beatmap.BeatmapInfo.EpilepsyWarning } + Current = { Value = Beatmap.EpilepsyWarning } }, letterboxDuringBreaks = new LabelledSwitchButton { @@ -122,7 +122,7 @@ namespace osu.Game.Screens.Edit.Setup Beatmap.BeatmapInfo.CountdownOffset = int.TryParse(CountdownOffset.Current.Value, NumberStyles.None, CultureInfo.InvariantCulture, out int offset) ? offset : 0; Beatmap.WidescreenStoryboard = widescreenSupport.Current.Value; - Beatmap.BeatmapInfo.EpilepsyWarning = epilepsyWarning.Current.Value; + Beatmap.EpilepsyWarning = epilepsyWarning.Current.Value; Beatmap.LetterboxInBreaks = letterboxDuringBreaks.Current.Value; Beatmap.BeatmapInfo.SamplesMatchPlaybackRate = samplesMatchPlaybackRate.Current.Value; diff --git a/osu.Game/Screens/Play/PlayerLoader.cs b/osu.Game/Screens/Play/PlayerLoader.cs index 51a0c94ff0..fc7e7fb58f 100644 --- a/osu.Game/Screens/Play/PlayerLoader.cs +++ b/osu.Game/Screens/Play/PlayerLoader.cs @@ -228,7 +228,7 @@ namespace osu.Game.Screens.Play sampleRestart = new SkinnableSound(new SampleInfo(@"Gameplay/restart", @"pause-retry-click")) }; - if (Beatmap.Value.BeatmapInfo.EpilepsyWarning) + if (Beatmap.Value.Beatmap.EpilepsyWarning) { disclaimers.Add(epilepsyWarning = new PlayerLoaderDisclaimer(PlayerLoaderStrings.EpilepsyWarningTitle, PlayerLoaderStrings.EpilepsyWarningContent)); } From c216283bf4ee5964e793869f2a63d941e58e6d74 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 12 Jun 2024 13:32:23 +0200 Subject: [PATCH 0020/1275] Move `SamplesMatchPlaybackRate` out of `BeatmapInfo` --- osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapDecoderTest.cs | 4 ++-- osu.Game/Beatmaps/Beatmap.cs | 2 ++ osu.Game/Beatmaps/BeatmapConverter.cs | 1 + osu.Game/Beatmaps/BeatmapImporter.cs | 1 - osu.Game/Beatmaps/BeatmapInfo.cs | 2 -- osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs | 4 ++-- osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs | 2 +- osu.Game/Beatmaps/IBeatmap.cs | 2 ++ osu.Game/Rulesets/Difficulty/DifficultyCalculator.cs | 6 ++++++ osu.Game/Screens/Edit/EditorBeatmap.cs | 6 ++++++ osu.Game/Screens/Edit/Setup/DesignSection.cs | 4 ++-- 11 files changed, 24 insertions(+), 10 deletions(-) diff --git a/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapDecoderTest.cs b/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapDecoderTest.cs index d3b027d253..51c1e51d3b 100644 --- a/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapDecoderTest.cs +++ b/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapDecoderTest.cs @@ -87,7 +87,7 @@ namespace osu.Game.Tests.Beatmaps.Formats Assert.IsFalse(beatmap.LetterboxInBreaks); Assert.IsFalse(beatmap.SpecialStyle); Assert.IsFalse(beatmap.WidescreenStoryboard); - Assert.IsFalse(beatmapInfo.SamplesMatchPlaybackRate); + Assert.IsFalse(beatmap.SamplesMatchPlaybackRate); Assert.AreEqual(CountdownType.None, beatmapInfo.Countdown); Assert.AreEqual(0, beatmapInfo.CountdownOffset); } @@ -956,7 +956,7 @@ namespace osu.Game.Tests.Beatmaps.Formats Assert.That(decoded.LetterboxInBreaks, Is.False); Assert.That(decoded.WidescreenStoryboard, Is.False); Assert.That(decoded.EpilepsyWarning, Is.False); - Assert.That(decoded.BeatmapInfo.SamplesMatchPlaybackRate, Is.False); + Assert.That(decoded.SamplesMatchPlaybackRate, Is.False); Assert.That(decoded.BeatmapInfo.Countdown, Is.EqualTo(CountdownType.Normal)); Assert.That(decoded.BeatmapInfo.CountdownOffset, Is.EqualTo(0)); Assert.That(decoded.BeatmapInfo.Metadata.PreviewTime, Is.EqualTo(-1)); diff --git a/osu.Game/Beatmaps/Beatmap.cs b/osu.Game/Beatmaps/Beatmap.cs index 7516c8958c..76864a1d70 100644 --- a/osu.Game/Beatmaps/Beatmap.cs +++ b/osu.Game/Beatmaps/Beatmap.cs @@ -126,6 +126,8 @@ namespace osu.Game.Beatmaps public bool EpilepsyWarning { get; set; } + public bool SamplesMatchPlaybackRate { get; set; } = true; + IBeatmap IBeatmap.Clone() => Clone(); public Beatmap Clone() => (Beatmap)MemberwiseClone(); diff --git a/osu.Game/Beatmaps/BeatmapConverter.cs b/osu.Game/Beatmaps/BeatmapConverter.cs index f9be44599f..b86a445aa8 100644 --- a/osu.Game/Beatmaps/BeatmapConverter.cs +++ b/osu.Game/Beatmaps/BeatmapConverter.cs @@ -73,6 +73,7 @@ namespace osu.Game.Beatmaps beatmap.LetterboxInBreaks = original.LetterboxInBreaks; beatmap.WidescreenStoryboard = original.WidescreenStoryboard; beatmap.EpilepsyWarning = original.EpilepsyWarning; + beatmap.SamplesMatchPlaybackRate = original.SamplesMatchPlaybackRate; return beatmap; } diff --git a/osu.Game/Beatmaps/BeatmapImporter.cs b/osu.Game/Beatmaps/BeatmapImporter.cs index 232c8b7e24..650b0bf510 100644 --- a/osu.Game/Beatmaps/BeatmapImporter.cs +++ b/osu.Game/Beatmaps/BeatmapImporter.cs @@ -414,7 +414,6 @@ namespace osu.Game.Beatmaps Hash = hash, DifficultyName = decodedInfo.DifficultyName, OnlineID = decodedInfo.OnlineID, - SamplesMatchPlaybackRate = decodedInfo.SamplesMatchPlaybackRate, DistanceSpacing = decodedInfo.DistanceSpacing, BeatDivisor = decodedInfo.BeatDivisor, GridSize = decodedInfo.GridSize, diff --git a/osu.Game/Beatmaps/BeatmapInfo.cs b/osu.Game/Beatmaps/BeatmapInfo.cs index 41a0260250..93abfa8d9b 100644 --- a/osu.Game/Beatmaps/BeatmapInfo.cs +++ b/osu.Game/Beatmaps/BeatmapInfo.cs @@ -138,8 +138,6 @@ namespace osu.Game.Beatmaps #region Properties we may not want persisted (but also maybe no harm?) - public bool SamplesMatchPlaybackRate { get; set; } = true; - /// /// The time at which this beatmap was last played by the local user. /// diff --git a/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs b/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs index 1152e5f30d..0c8770782d 100644 --- a/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs +++ b/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs @@ -186,7 +186,7 @@ namespace osu.Game.Beatmaps.Formats private static void applyLegacyDefaults(Beatmap beatmap) { beatmap.WidescreenStoryboard = false; - beatmap.BeatmapInfo.SamplesMatchPlaybackRate = false; + beatmap.SamplesMatchPlaybackRate = false; } protected override void ParseLine(Beatmap beatmap, Section section, string line) @@ -301,7 +301,7 @@ namespace osu.Game.Beatmaps.Formats break; case @"SamplesMatchPlaybackRate": - beatmap.BeatmapInfo.SamplesMatchPlaybackRate = Parsing.ParseInt(pair.Value) == 1; + beatmap.SamplesMatchPlaybackRate = Parsing.ParseInt(pair.Value) == 1; break; case @"Countdown": diff --git a/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs b/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs index d7078a71d9..860ca68f6b 100644 --- a/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs +++ b/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs @@ -102,7 +102,7 @@ namespace osu.Game.Beatmaps.Formats if (onlineRulesetID == 3) writer.WriteLine(FormattableString.Invariant($"SpecialStyle: {(beatmap.SpecialStyle ? '1' : '0')}")); writer.WriteLine(FormattableString.Invariant($"WidescreenStoryboard: {(beatmap.WidescreenStoryboard ? '1' : '0')}")); - if (beatmap.BeatmapInfo.SamplesMatchPlaybackRate) + if (beatmap.SamplesMatchPlaybackRate) writer.WriteLine(@"SamplesMatchPlaybackRate: 1"); } diff --git a/osu.Game/Beatmaps/IBeatmap.cs b/osu.Game/Beatmaps/IBeatmap.cs index 3d563004d1..9900609f18 100644 --- a/osu.Game/Beatmaps/IBeatmap.cs +++ b/osu.Game/Beatmaps/IBeatmap.cs @@ -80,6 +80,8 @@ namespace osu.Game.Beatmaps bool EpilepsyWarning { get; internal set; } + bool SamplesMatchPlaybackRate { get; internal set; } + /// /// Creates a shallow-clone of this beatmap and returns it. /// diff --git a/osu.Game/Rulesets/Difficulty/DifficultyCalculator.cs b/osu.Game/Rulesets/Difficulty/DifficultyCalculator.cs index 7f9d2ae6ab..960eab6f2c 100644 --- a/osu.Game/Rulesets/Difficulty/DifficultyCalculator.cs +++ b/osu.Game/Rulesets/Difficulty/DifficultyCalculator.cs @@ -373,6 +373,12 @@ namespace osu.Game.Rulesets.Difficulty set => baseBeatmap.EpilepsyWarning = value; } + public bool SamplesMatchPlaybackRate + { + get => baseBeatmap.SamplesMatchPlaybackRate; + set => baseBeatmap.SamplesMatchPlaybackRate = value; + } + #endregion } } diff --git a/osu.Game/Screens/Edit/EditorBeatmap.cs b/osu.Game/Screens/Edit/EditorBeatmap.cs index 063803ab42..4303764fa7 100644 --- a/osu.Game/Screens/Edit/EditorBeatmap.cs +++ b/osu.Game/Screens/Edit/EditorBeatmap.cs @@ -220,6 +220,12 @@ namespace osu.Game.Screens.Edit set => PlayableBeatmap.EpilepsyWarning = value; } + public bool SamplesMatchPlaybackRate + { + get => PlayableBeatmap.SamplesMatchPlaybackRate; + set => PlayableBeatmap.SamplesMatchPlaybackRate = value; + } + public IBeatmap Clone() => (EditorBeatmap)MemberwiseClone(); private IList mutableHitObjects => (IList)PlayableBeatmap.HitObjects; diff --git a/osu.Game/Screens/Edit/Setup/DesignSection.cs b/osu.Game/Screens/Edit/Setup/DesignSection.cs index 5d729cf4f8..4c4755064f 100644 --- a/osu.Game/Screens/Edit/Setup/DesignSection.cs +++ b/osu.Game/Screens/Edit/Setup/DesignSection.cs @@ -86,7 +86,7 @@ namespace osu.Game.Screens.Edit.Setup { Label = EditorSetupStrings.SamplesMatchPlaybackRate, Description = EditorSetupStrings.SamplesMatchPlaybackRateDescription, - Current = { Value = Beatmap.BeatmapInfo.SamplesMatchPlaybackRate } + Current = { Value = Beatmap.SamplesMatchPlaybackRate } } }; } @@ -124,7 +124,7 @@ namespace osu.Game.Screens.Edit.Setup Beatmap.WidescreenStoryboard = widescreenSupport.Current.Value; Beatmap.EpilepsyWarning = epilepsyWarning.Current.Value; Beatmap.LetterboxInBreaks = letterboxDuringBreaks.Current.Value; - Beatmap.BeatmapInfo.SamplesMatchPlaybackRate = samplesMatchPlaybackRate.Current.Value; + Beatmap.SamplesMatchPlaybackRate = samplesMatchPlaybackRate.Current.Value; Beatmap.SaveState(); } From 3634307d7ceadc23604014fba8aaa077682f2988 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 12 Jun 2024 13:36:27 +0200 Subject: [PATCH 0021/1275] Move `DistanceSpacing` out of `BeatmapInfo` --- .../Beatmaps/Formats/LegacyBeatmapDecoderTest.cs | 14 +++++++------- .../Beatmaps/Formats/OsuJsonDecoderTest.cs | 2 +- .../Visual/Editing/TestSceneHitObjectComposer.cs | 4 ++-- osu.Game/Beatmaps/Beatmap.cs | 2 ++ osu.Game/Beatmaps/BeatmapConverter.cs | 1 + osu.Game/Beatmaps/BeatmapImporter.cs | 1 - osu.Game/Beatmaps/BeatmapInfo.cs | 14 -------------- osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs | 2 +- osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs | 2 +- osu.Game/Beatmaps/IBeatmap.cs | 13 +++++++++++++ .../Rulesets/Difficulty/DifficultyCalculator.cs | 6 ++++++ .../Rulesets/Edit/ComposerDistanceSnapProvider.cs | 4 ++-- osu.Game/Rulesets/Edit/IDistanceSnapProvider.cs | 2 +- osu.Game/Screens/Edit/EditorBeatmap.cs | 6 ++++++ 14 files changed, 43 insertions(+), 30 deletions(-) diff --git a/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapDecoderTest.cs b/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapDecoderTest.cs index 51c1e51d3b..71dcd38bcd 100644 --- a/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapDecoderTest.cs +++ b/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapDecoderTest.cs @@ -101,7 +101,7 @@ namespace osu.Game.Tests.Beatmaps.Formats using (var resStream = TestResources.OpenResource("Soleily - Renatus (Gamu) [Insane].osu")) using (var stream = new LineBufferedReader(resStream)) { - var beatmapInfo = decoder.Decode(stream).BeatmapInfo; + var beatmap = decoder.Decode(stream); int[] expectedBookmarks = { @@ -109,13 +109,13 @@ namespace osu.Game.Tests.Beatmaps.Formats 95901, 106450, 116999, 119637, 130186, 140735, 151285, 161834, 164471, 175020, 185570, 196119, 206669, 209306 }; - Assert.AreEqual(expectedBookmarks.Length, beatmapInfo.Bookmarks.Length); + Assert.AreEqual(expectedBookmarks.Length, beatmap.BeatmapInfo.Bookmarks.Length); for (int i = 0; i < expectedBookmarks.Length; i++) - Assert.AreEqual(expectedBookmarks[i], beatmapInfo.Bookmarks[i]); - Assert.AreEqual(1.8, beatmapInfo.DistanceSpacing); - Assert.AreEqual(4, beatmapInfo.BeatDivisor); - Assert.AreEqual(4, beatmapInfo.GridSize); - Assert.AreEqual(2, beatmapInfo.TimelineZoom); + Assert.AreEqual(expectedBookmarks[i], beatmap.BeatmapInfo.Bookmarks[i]); + Assert.AreEqual(1.8, beatmap.DistanceSpacing); + Assert.AreEqual(4, beatmap.BeatmapInfo.BeatDivisor); + Assert.AreEqual(4, beatmap.BeatmapInfo.GridSize); + Assert.AreEqual(2, beatmap.BeatmapInfo.TimelineZoom); } } diff --git a/osu.Game.Tests/Beatmaps/Formats/OsuJsonDecoderTest.cs b/osu.Game.Tests/Beatmaps/Formats/OsuJsonDecoderTest.cs index 92715b6aa2..4832ee26b7 100644 --- a/osu.Game.Tests/Beatmaps/Formats/OsuJsonDecoderTest.cs +++ b/osu.Game.Tests/Beatmaps/Formats/OsuJsonDecoderTest.cs @@ -76,7 +76,7 @@ namespace osu.Game.Tests.Beatmaps.Formats Assert.AreEqual(expectedBookmarks.Length, beatmapInfo.Bookmarks.Length); for (int i = 0; i < expectedBookmarks.Length; i++) Assert.AreEqual(expectedBookmarks[i], beatmapInfo.Bookmarks[i]); - Assert.AreEqual(1.8, beatmapInfo.DistanceSpacing); + Assert.AreEqual(1.8, beatmap.DistanceSpacing); Assert.AreEqual(4, beatmapInfo.BeatDivisor); Assert.AreEqual(4, beatmapInfo.GridSize); Assert.AreEqual(2, beatmapInfo.TimelineZoom); diff --git a/osu.Game.Tests/Visual/Editing/TestSceneHitObjectComposer.cs b/osu.Game.Tests/Visual/Editing/TestSceneHitObjectComposer.cs index f392841ac7..d77c86729a 100644 --- a/osu.Game.Tests/Visual/Editing/TestSceneHitObjectComposer.cs +++ b/osu.Game.Tests/Visual/Editing/TestSceneHitObjectComposer.cs @@ -165,7 +165,7 @@ namespace osu.Game.Tests.Visual.Editing { double originalSpacing = 0; - AddStep("retrieve original spacing", () => originalSpacing = editorBeatmap.BeatmapInfo.DistanceSpacing); + AddStep("retrieve original spacing", () => originalSpacing = editorBeatmap.DistanceSpacing); AddStep("hold ctrl", () => InputManager.PressKey(Key.LControl)); AddStep("hold alt", () => InputManager.PressKey(Key.LAlt)); @@ -175,7 +175,7 @@ namespace osu.Game.Tests.Visual.Editing AddStep("release alt", () => InputManager.ReleaseKey(Key.LAlt)); AddStep("release ctrl", () => InputManager.ReleaseKey(Key.LControl)); - AddAssert("distance spacing increased by 0.5", () => editorBeatmap.BeatmapInfo.DistanceSpacing == originalSpacing + 0.5); + AddAssert("distance spacing increased by 0.5", () => editorBeatmap.DistanceSpacing == originalSpacing + 0.5); } public partial class EditorBeatmapContainer : PopoverContainer diff --git a/osu.Game/Beatmaps/Beatmap.cs b/osu.Game/Beatmaps/Beatmap.cs index 76864a1d70..2f7c00af4a 100644 --- a/osu.Game/Beatmaps/Beatmap.cs +++ b/osu.Game/Beatmaps/Beatmap.cs @@ -128,6 +128,8 @@ namespace osu.Game.Beatmaps public bool SamplesMatchPlaybackRate { get; set; } = true; + public double DistanceSpacing { get; set; } = 1.0; + IBeatmap IBeatmap.Clone() => Clone(); public Beatmap Clone() => (Beatmap)MemberwiseClone(); diff --git a/osu.Game/Beatmaps/BeatmapConverter.cs b/osu.Game/Beatmaps/BeatmapConverter.cs index b86a445aa8..eda7f8025f 100644 --- a/osu.Game/Beatmaps/BeatmapConverter.cs +++ b/osu.Game/Beatmaps/BeatmapConverter.cs @@ -74,6 +74,7 @@ namespace osu.Game.Beatmaps beatmap.WidescreenStoryboard = original.WidescreenStoryboard; beatmap.EpilepsyWarning = original.EpilepsyWarning; beatmap.SamplesMatchPlaybackRate = original.SamplesMatchPlaybackRate; + beatmap.DistanceSpacing = original.DistanceSpacing; return beatmap; } diff --git a/osu.Game/Beatmaps/BeatmapImporter.cs b/osu.Game/Beatmaps/BeatmapImporter.cs index 650b0bf510..a8964a365a 100644 --- a/osu.Game/Beatmaps/BeatmapImporter.cs +++ b/osu.Game/Beatmaps/BeatmapImporter.cs @@ -414,7 +414,6 @@ namespace osu.Game.Beatmaps Hash = hash, DifficultyName = decodedInfo.DifficultyName, OnlineID = decodedInfo.OnlineID, - DistanceSpacing = decodedInfo.DistanceSpacing, BeatDivisor = decodedInfo.BeatDivisor, GridSize = decodedInfo.GridSize, TimelineZoom = decodedInfo.TimelineZoom, diff --git a/osu.Game/Beatmaps/BeatmapInfo.cs b/osu.Game/Beatmaps/BeatmapInfo.cs index 93abfa8d9b..8328e3df95 100644 --- a/osu.Game/Beatmaps/BeatmapInfo.cs +++ b/osu.Game/Beatmaps/BeatmapInfo.cs @@ -6,14 +6,12 @@ using System.Diagnostics; using System.Linq; using JetBrains.Annotations; using Newtonsoft.Json; -using osu.Game.Beatmaps.ControlPoints; using osu.Game.Collections; using osu.Game.Database; using osu.Game.Models; using osu.Game.Online.API.Requests.Responses; using osu.Game.Overlays.BeatmapSet.Scores; using osu.Game.Rulesets; -using osu.Game.Rulesets.Edit; using osu.Game.Scoring; using Realms; @@ -143,18 +141,6 @@ namespace osu.Game.Beatmaps /// public DateTimeOffset? LastPlayed { get; set; } - /// - /// The ratio of distance travelled per time unit. - /// Generally used to decouple the spacing between hit objects from the enforced "velocity" of the beatmap (see ). - /// - /// - /// The most common method of understanding is that at a default value of 1.0, the time-to-distance ratio will match the slider velocity of the beatmap - /// at the current point in time. Increasing this value will make hit objects more spaced apart when compared to the cursor movement required to track a slider. - /// - /// This is only a hint property, used by the editor in implementations. It does not directly affect the beatmap or gameplay. - /// - public double DistanceSpacing { get; set; } = 1.0; - public int BeatDivisor { get; set; } = 4; public int GridSize { get; set; } diff --git a/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs b/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs index 0c8770782d..92a26464ee 100644 --- a/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs +++ b/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs @@ -329,7 +329,7 @@ namespace osu.Game.Beatmaps.Formats break; case @"DistanceSpacing": - beatmap.BeatmapInfo.DistanceSpacing = Math.Max(0, Parsing.ParseDouble(pair.Value)); + beatmap.DistanceSpacing = Math.Max(0, Parsing.ParseDouble(pair.Value)); break; case @"BeatDivisor": diff --git a/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs b/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs index 860ca68f6b..a07e8d2226 100644 --- a/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs +++ b/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs @@ -112,7 +112,7 @@ namespace osu.Game.Beatmaps.Formats if (beatmap.BeatmapInfo.Bookmarks.Length > 0) writer.WriteLine(FormattableString.Invariant($"Bookmarks: {string.Join(',', beatmap.BeatmapInfo.Bookmarks)}")); - writer.WriteLine(FormattableString.Invariant($"DistanceSpacing: {beatmap.BeatmapInfo.DistanceSpacing}")); + writer.WriteLine(FormattableString.Invariant($"DistanceSpacing: {beatmap.DistanceSpacing}")); writer.WriteLine(FormattableString.Invariant($"BeatDivisor: {beatmap.BeatmapInfo.BeatDivisor}")); writer.WriteLine(FormattableString.Invariant($"GridSize: {beatmap.BeatmapInfo.GridSize}")); writer.WriteLine(FormattableString.Invariant($"TimelineZoom: {beatmap.BeatmapInfo.TimelineZoom}")); diff --git a/osu.Game/Beatmaps/IBeatmap.cs b/osu.Game/Beatmaps/IBeatmap.cs index 9900609f18..b2c8b7604a 100644 --- a/osu.Game/Beatmaps/IBeatmap.cs +++ b/osu.Game/Beatmaps/IBeatmap.cs @@ -6,6 +6,7 @@ using System.Collections.Generic; using System.Linq; using osu.Game.Beatmaps.ControlPoints; using osu.Game.Beatmaps.Timing; +using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Scoring; @@ -82,6 +83,18 @@ namespace osu.Game.Beatmaps bool SamplesMatchPlaybackRate { get; internal set; } + /// + /// The ratio of distance travelled per time unit. + /// Generally used to decouple the spacing between hit objects from the enforced "velocity" of the beatmap (see ). + /// + /// + /// The most common method of understanding is that at a default value of 1.0, the time-to-distance ratio will match the slider velocity of the beatmap + /// at the current point in time. Increasing this value will make hit objects more spaced apart when compared to the cursor movement required to track a slider. + /// + /// This is only a hint property, used by the editor in implementations. It does not directly affect the beatmap or gameplay. + /// + double DistanceSpacing { get; internal set; } + /// /// Creates a shallow-clone of this beatmap and returns it. /// diff --git a/osu.Game/Rulesets/Difficulty/DifficultyCalculator.cs b/osu.Game/Rulesets/Difficulty/DifficultyCalculator.cs index 960eab6f2c..7473882c15 100644 --- a/osu.Game/Rulesets/Difficulty/DifficultyCalculator.cs +++ b/osu.Game/Rulesets/Difficulty/DifficultyCalculator.cs @@ -379,6 +379,12 @@ namespace osu.Game.Rulesets.Difficulty set => baseBeatmap.SamplesMatchPlaybackRate = value; } + public double DistanceSpacing + { + get => baseBeatmap.DistanceSpacing; + set => baseBeatmap.DistanceSpacing = value; + } + #endregion } } diff --git a/osu.Game/Rulesets/Edit/ComposerDistanceSnapProvider.cs b/osu.Game/Rulesets/Edit/ComposerDistanceSnapProvider.cs index b9850a94a3..665e6ba074 100644 --- a/osu.Game/Rulesets/Edit/ComposerDistanceSnapProvider.cs +++ b/osu.Game/Rulesets/Edit/ComposerDistanceSnapProvider.cs @@ -99,7 +99,7 @@ namespace osu.Game.Rulesets.Edit } }); - DistanceSpacingMultiplier.Value = editorBeatmap.BeatmapInfo.DistanceSpacing; + DistanceSpacingMultiplier.Value = editorBeatmap.DistanceSpacing; DistanceSpacingMultiplier.BindValueChanged(multiplier => { distanceSpacingSlider.ContractedLabelText = $"D. S. ({multiplier.NewValue:0.##x})"; @@ -108,7 +108,7 @@ namespace osu.Game.Rulesets.Edit if (multiplier.NewValue != multiplier.OldValue) onScreenDisplay?.Display(new DistanceSpacingToast(multiplier.NewValue.ToLocalisableString(@"0.##x"), multiplier)); - editorBeatmap.BeatmapInfo.DistanceSpacing = multiplier.NewValue; + editorBeatmap.DistanceSpacing = multiplier.NewValue; }, true); DistanceSpacingMultiplier.BindDisabledChanged(disabled => distanceSpacingSlider.Alpha = disabled ? 0 : 1, true); diff --git a/osu.Game/Rulesets/Edit/IDistanceSnapProvider.cs b/osu.Game/Rulesets/Edit/IDistanceSnapProvider.cs index 380038eadf..c312642fbd 100644 --- a/osu.Game/Rulesets/Edit/IDistanceSnapProvider.cs +++ b/osu.Game/Rulesets/Edit/IDistanceSnapProvider.cs @@ -18,7 +18,7 @@ namespace osu.Game.Rulesets.Edit /// A multiplier which changes the ratio of distance travelled per time unit. /// Importantly, this is provided for manual usage, and not multiplied into any of the methods exposed by this interface. /// - /// + /// Bindable DistanceSpacingMultiplier { get; } /// diff --git a/osu.Game/Screens/Edit/EditorBeatmap.cs b/osu.Game/Screens/Edit/EditorBeatmap.cs index 4303764fa7..aa63dfab8d 100644 --- a/osu.Game/Screens/Edit/EditorBeatmap.cs +++ b/osu.Game/Screens/Edit/EditorBeatmap.cs @@ -226,6 +226,12 @@ namespace osu.Game.Screens.Edit set => PlayableBeatmap.SamplesMatchPlaybackRate = value; } + public double DistanceSpacing + { + get => PlayableBeatmap.DistanceSpacing; + set => PlayableBeatmap.DistanceSpacing = value; + } + public IBeatmap Clone() => (EditorBeatmap)MemberwiseClone(); private IList mutableHitObjects => (IList)PlayableBeatmap.HitObjects; From 6685c5ab741441140e7e9ba4d1e67b29f3ce828a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 12 Jun 2024 13:44:36 +0200 Subject: [PATCH 0022/1275] Move `GridSize` out of `BeatmapInfo` --- .../Editor/TestSceneOsuEditorGrids.cs | 2 +- osu.Game.Rulesets.Osu/Edit/OsuGridToolboxGroup.cs | 4 ++-- osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapDecoderTest.cs | 2 +- osu.Game.Tests/Beatmaps/Formats/OsuJsonDecoderTest.cs | 2 +- osu.Game/Beatmaps/Beatmap.cs | 2 ++ osu.Game/Beatmaps/BeatmapConverter.cs | 1 + osu.Game/Beatmaps/BeatmapImporter.cs | 1 - osu.Game/Beatmaps/BeatmapInfo.cs | 2 -- osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs | 2 +- osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs | 2 +- osu.Game/Beatmaps/IBeatmap.cs | 2 ++ osu.Game/Rulesets/Difficulty/DifficultyCalculator.cs | 6 ++++++ osu.Game/Screens/Edit/EditorBeatmap.cs | 6 ++++++ 13 files changed, 24 insertions(+), 10 deletions(-) diff --git a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneOsuEditorGrids.cs b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneOsuEditorGrids.cs index 48aa74c5bf..cb9347b177 100644 --- a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneOsuEditorGrids.cs +++ b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneOsuEditorGrids.cs @@ -185,6 +185,6 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor private void gridSizeIs(int size) => AddAssert($"grid size is {size}", () => this.ChildrenOfType().Single().Spacing.Value == new Vector2(size) - && EditorBeatmap.BeatmapInfo.GridSize == size); + && EditorBeatmap.GridSize == size); } } diff --git a/osu.Game.Rulesets.Osu/Edit/OsuGridToolboxGroup.cs b/osu.Game.Rulesets.Osu/Edit/OsuGridToolboxGroup.cs index 21cce553b1..3740c54752 100644 --- a/osu.Game.Rulesets.Osu/Edit/OsuGridToolboxGroup.cs +++ b/osu.Game.Rulesets.Osu/Edit/OsuGridToolboxGroup.cs @@ -111,7 +111,7 @@ namespace osu.Game.Rulesets.Osu.Edit }, }; - Spacing.Value = editorBeatmap.BeatmapInfo.GridSize; + Spacing.Value = editorBeatmap.GridSize; } protected override void LoadComplete() @@ -137,7 +137,7 @@ namespace osu.Game.Rulesets.Osu.Edit spacingSlider.ContractedLabelText = $"S: {spacing.NewValue:N0}"; spacingSlider.ExpandedLabelText = $"Spacing: {spacing.NewValue:N0}"; SpacingVector.Value = new Vector2(spacing.NewValue); - editorBeatmap.BeatmapInfo.GridSize = (int)spacing.NewValue; + editorBeatmap.GridSize = (int)spacing.NewValue; }, true); GridLinesRotation.BindValueChanged(rotation => diff --git a/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapDecoderTest.cs b/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapDecoderTest.cs index 71dcd38bcd..a15485cdf1 100644 --- a/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapDecoderTest.cs +++ b/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapDecoderTest.cs @@ -114,7 +114,7 @@ namespace osu.Game.Tests.Beatmaps.Formats Assert.AreEqual(expectedBookmarks[i], beatmap.BeatmapInfo.Bookmarks[i]); Assert.AreEqual(1.8, beatmap.DistanceSpacing); Assert.AreEqual(4, beatmap.BeatmapInfo.BeatDivisor); - Assert.AreEqual(4, beatmap.BeatmapInfo.GridSize); + Assert.AreEqual(4, beatmap.GridSize); Assert.AreEqual(2, beatmap.BeatmapInfo.TimelineZoom); } } diff --git a/osu.Game.Tests/Beatmaps/Formats/OsuJsonDecoderTest.cs b/osu.Game.Tests/Beatmaps/Formats/OsuJsonDecoderTest.cs index 4832ee26b7..95cba082a7 100644 --- a/osu.Game.Tests/Beatmaps/Formats/OsuJsonDecoderTest.cs +++ b/osu.Game.Tests/Beatmaps/Formats/OsuJsonDecoderTest.cs @@ -78,7 +78,7 @@ namespace osu.Game.Tests.Beatmaps.Formats Assert.AreEqual(expectedBookmarks[i], beatmapInfo.Bookmarks[i]); Assert.AreEqual(1.8, beatmap.DistanceSpacing); Assert.AreEqual(4, beatmapInfo.BeatDivisor); - Assert.AreEqual(4, beatmapInfo.GridSize); + Assert.AreEqual(4, beatmap.GridSize); Assert.AreEqual(2, beatmapInfo.TimelineZoom); } diff --git a/osu.Game/Beatmaps/Beatmap.cs b/osu.Game/Beatmaps/Beatmap.cs index 2f7c00af4a..aacbe359b1 100644 --- a/osu.Game/Beatmaps/Beatmap.cs +++ b/osu.Game/Beatmaps/Beatmap.cs @@ -130,6 +130,8 @@ namespace osu.Game.Beatmaps public double DistanceSpacing { get; set; } = 1.0; + public int GridSize { get; set; } + IBeatmap IBeatmap.Clone() => Clone(); public Beatmap Clone() => (Beatmap)MemberwiseClone(); diff --git a/osu.Game/Beatmaps/BeatmapConverter.cs b/osu.Game/Beatmaps/BeatmapConverter.cs index eda7f8025f..a70d449fc5 100644 --- a/osu.Game/Beatmaps/BeatmapConverter.cs +++ b/osu.Game/Beatmaps/BeatmapConverter.cs @@ -75,6 +75,7 @@ namespace osu.Game.Beatmaps beatmap.EpilepsyWarning = original.EpilepsyWarning; beatmap.SamplesMatchPlaybackRate = original.SamplesMatchPlaybackRate; beatmap.DistanceSpacing = original.DistanceSpacing; + beatmap.GridSize = original.GridSize; return beatmap; } diff --git a/osu.Game/Beatmaps/BeatmapImporter.cs b/osu.Game/Beatmaps/BeatmapImporter.cs index a8964a365a..ff9bf4b477 100644 --- a/osu.Game/Beatmaps/BeatmapImporter.cs +++ b/osu.Game/Beatmaps/BeatmapImporter.cs @@ -415,7 +415,6 @@ namespace osu.Game.Beatmaps DifficultyName = decodedInfo.DifficultyName, OnlineID = decodedInfo.OnlineID, BeatDivisor = decodedInfo.BeatDivisor, - GridSize = decodedInfo.GridSize, TimelineZoom = decodedInfo.TimelineZoom, MD5Hash = memoryStream.ComputeMD5Hash(), EndTimeObjectCount = decoded.HitObjects.Count(h => h is IHasDuration), diff --git a/osu.Game/Beatmaps/BeatmapInfo.cs b/osu.Game/Beatmaps/BeatmapInfo.cs index 8328e3df95..6b192063e8 100644 --- a/osu.Game/Beatmaps/BeatmapInfo.cs +++ b/osu.Game/Beatmaps/BeatmapInfo.cs @@ -143,8 +143,6 @@ namespace osu.Game.Beatmaps public int BeatDivisor { get; set; } = 4; - public int GridSize { get; set; } - public double TimelineZoom { get; set; } = 1.0; /// diff --git a/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs b/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs index 92a26464ee..80bfae3036 100644 --- a/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs +++ b/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs @@ -337,7 +337,7 @@ namespace osu.Game.Beatmaps.Formats break; case @"GridSize": - beatmap.BeatmapInfo.GridSize = Parsing.ParseInt(pair.Value); + beatmap.GridSize = Parsing.ParseInt(pair.Value); break; case @"TimelineZoom": diff --git a/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs b/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs index a07e8d2226..1bfba0962c 100644 --- a/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs +++ b/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs @@ -114,7 +114,7 @@ namespace osu.Game.Beatmaps.Formats writer.WriteLine(FormattableString.Invariant($"Bookmarks: {string.Join(',', beatmap.BeatmapInfo.Bookmarks)}")); writer.WriteLine(FormattableString.Invariant($"DistanceSpacing: {beatmap.DistanceSpacing}")); writer.WriteLine(FormattableString.Invariant($"BeatDivisor: {beatmap.BeatmapInfo.BeatDivisor}")); - writer.WriteLine(FormattableString.Invariant($"GridSize: {beatmap.BeatmapInfo.GridSize}")); + writer.WriteLine(FormattableString.Invariant($"GridSize: {beatmap.GridSize}")); writer.WriteLine(FormattableString.Invariant($"TimelineZoom: {beatmap.BeatmapInfo.TimelineZoom}")); } diff --git a/osu.Game/Beatmaps/IBeatmap.cs b/osu.Game/Beatmaps/IBeatmap.cs index b2c8b7604a..ecfc8ea398 100644 --- a/osu.Game/Beatmaps/IBeatmap.cs +++ b/osu.Game/Beatmaps/IBeatmap.cs @@ -95,6 +95,8 @@ namespace osu.Game.Beatmaps /// double DistanceSpacing { get; internal set; } + int GridSize { get; internal set; } + /// /// Creates a shallow-clone of this beatmap and returns it. /// diff --git a/osu.Game/Rulesets/Difficulty/DifficultyCalculator.cs b/osu.Game/Rulesets/Difficulty/DifficultyCalculator.cs index 7473882c15..d4a95558cc 100644 --- a/osu.Game/Rulesets/Difficulty/DifficultyCalculator.cs +++ b/osu.Game/Rulesets/Difficulty/DifficultyCalculator.cs @@ -385,6 +385,12 @@ namespace osu.Game.Rulesets.Difficulty set => baseBeatmap.DistanceSpacing = value; } + public int GridSize + { + get => baseBeatmap.GridSize; + set => baseBeatmap.GridSize = value; + } + #endregion } } diff --git a/osu.Game/Screens/Edit/EditorBeatmap.cs b/osu.Game/Screens/Edit/EditorBeatmap.cs index aa63dfab8d..b1df60126d 100644 --- a/osu.Game/Screens/Edit/EditorBeatmap.cs +++ b/osu.Game/Screens/Edit/EditorBeatmap.cs @@ -232,6 +232,12 @@ namespace osu.Game.Screens.Edit set => PlayableBeatmap.DistanceSpacing = value; } + public int GridSize + { + get => PlayableBeatmap.GridSize; + set => PlayableBeatmap.GridSize = value; + } + public IBeatmap Clone() => (EditorBeatmap)MemberwiseClone(); private IList mutableHitObjects => (IList)PlayableBeatmap.HitObjects; From 7f2a6f6f5a143e44ca427060eee77c5acad84d5d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 12 Jun 2024 13:54:31 +0200 Subject: [PATCH 0023/1275] Move `TimelineZoom` out of `BeatmapInfo` --- .../Beatmaps/Formats/LegacyBeatmapDecoderTest.cs | 2 +- osu.Game.Tests/Beatmaps/Formats/OsuJsonDecoderTest.cs | 2 +- osu.Game.Tests/Visual/Editing/TestSceneEditorSaving.cs | 8 ++++---- osu.Game/Beatmaps/Beatmap.cs | 2 ++ osu.Game/Beatmaps/BeatmapConverter.cs | 1 + osu.Game/Beatmaps/BeatmapImporter.cs | 1 - osu.Game/Beatmaps/BeatmapInfo.cs | 2 -- osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs | 2 +- osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs | 2 +- osu.Game/Beatmaps/IBeatmap.cs | 2 ++ osu.Game/Rulesets/Difficulty/DifficultyCalculator.cs | 6 ++++++ .../Screens/Edit/Compose/Components/Timeline/Timeline.cs | 6 +++--- osu.Game/Screens/Edit/EditorBeatmap.cs | 6 ++++++ 13 files changed, 28 insertions(+), 14 deletions(-) diff --git a/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapDecoderTest.cs b/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapDecoderTest.cs index a15485cdf1..af0e4a8b3c 100644 --- a/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapDecoderTest.cs +++ b/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapDecoderTest.cs @@ -115,7 +115,7 @@ namespace osu.Game.Tests.Beatmaps.Formats Assert.AreEqual(1.8, beatmap.DistanceSpacing); Assert.AreEqual(4, beatmap.BeatmapInfo.BeatDivisor); Assert.AreEqual(4, beatmap.GridSize); - Assert.AreEqual(2, beatmap.BeatmapInfo.TimelineZoom); + Assert.AreEqual(2, beatmap.TimelineZoom); } } diff --git a/osu.Game.Tests/Beatmaps/Formats/OsuJsonDecoderTest.cs b/osu.Game.Tests/Beatmaps/Formats/OsuJsonDecoderTest.cs index 95cba082a7..1fed9633f7 100644 --- a/osu.Game.Tests/Beatmaps/Formats/OsuJsonDecoderTest.cs +++ b/osu.Game.Tests/Beatmaps/Formats/OsuJsonDecoderTest.cs @@ -79,7 +79,7 @@ namespace osu.Game.Tests.Beatmaps.Formats Assert.AreEqual(1.8, beatmap.DistanceSpacing); Assert.AreEqual(4, beatmapInfo.BeatDivisor); Assert.AreEqual(4, beatmap.GridSize); - Assert.AreEqual(2, beatmapInfo.TimelineZoom); + Assert.AreEqual(2, beatmap.TimelineZoom); } [Test] diff --git a/osu.Game.Tests/Visual/Editing/TestSceneEditorSaving.cs b/osu.Game.Tests/Visual/Editing/TestSceneEditorSaving.cs index 64c48e74cf..429b458b9f 100644 --- a/osu.Game.Tests/Visual/Editing/TestSceneEditorSaving.cs +++ b/osu.Game.Tests/Visual/Editing/TestSceneEditorSaving.cs @@ -70,7 +70,7 @@ namespace osu.Game.Tests.Visual.Editing AddStep("Set beat divisor", () => Editor.Dependencies.Get().Value = 16); AddStep("Set timeline zoom", () => { - originalTimelineZoom = EditorBeatmap.BeatmapInfo.TimelineZoom; + originalTimelineZoom = EditorBeatmap.TimelineZoom; var timeline = Editor.ChildrenOfType().Single(); InputManager.MoveMouseTo(timeline); @@ -81,19 +81,19 @@ namespace osu.Game.Tests.Visual.Editing AddAssert("Ensure timeline zoom changed", () => { - changedTimelineZoom = EditorBeatmap.BeatmapInfo.TimelineZoom; + changedTimelineZoom = EditorBeatmap.TimelineZoom; return !Precision.AlmostEquals(changedTimelineZoom, originalTimelineZoom); }); SaveEditor(); AddAssert("Beatmap has correct beat divisor", () => EditorBeatmap.BeatmapInfo.BeatDivisor == 16); - AddAssert("Beatmap has correct timeline zoom", () => EditorBeatmap.BeatmapInfo.TimelineZoom == changedTimelineZoom); + AddAssert("Beatmap has correct timeline zoom", () => EditorBeatmap.TimelineZoom == changedTimelineZoom); ReloadEditorToSameBeatmap(); AddAssert("Beatmap still has correct beat divisor", () => EditorBeatmap.BeatmapInfo.BeatDivisor == 16); - AddAssert("Beatmap still has correct timeline zoom", () => EditorBeatmap.BeatmapInfo.TimelineZoom == changedTimelineZoom); + AddAssert("Beatmap still has correct timeline zoom", () => EditorBeatmap.TimelineZoom == changedTimelineZoom); } [Test] diff --git a/osu.Game/Beatmaps/Beatmap.cs b/osu.Game/Beatmaps/Beatmap.cs index aacbe359b1..35bd935d66 100644 --- a/osu.Game/Beatmaps/Beatmap.cs +++ b/osu.Game/Beatmaps/Beatmap.cs @@ -132,6 +132,8 @@ namespace osu.Game.Beatmaps public int GridSize { get; set; } + public double TimelineZoom { get; set; } = 1.0; + IBeatmap IBeatmap.Clone() => Clone(); public Beatmap Clone() => (Beatmap)MemberwiseClone(); diff --git a/osu.Game/Beatmaps/BeatmapConverter.cs b/osu.Game/Beatmaps/BeatmapConverter.cs index a70d449fc5..140771a5d5 100644 --- a/osu.Game/Beatmaps/BeatmapConverter.cs +++ b/osu.Game/Beatmaps/BeatmapConverter.cs @@ -76,6 +76,7 @@ namespace osu.Game.Beatmaps beatmap.SamplesMatchPlaybackRate = original.SamplesMatchPlaybackRate; beatmap.DistanceSpacing = original.DistanceSpacing; beatmap.GridSize = original.GridSize; + beatmap.TimelineZoom = original.TimelineZoom; return beatmap; } diff --git a/osu.Game/Beatmaps/BeatmapImporter.cs b/osu.Game/Beatmaps/BeatmapImporter.cs index ff9bf4b477..e230f912ab 100644 --- a/osu.Game/Beatmaps/BeatmapImporter.cs +++ b/osu.Game/Beatmaps/BeatmapImporter.cs @@ -415,7 +415,6 @@ namespace osu.Game.Beatmaps DifficultyName = decodedInfo.DifficultyName, OnlineID = decodedInfo.OnlineID, BeatDivisor = decodedInfo.BeatDivisor, - TimelineZoom = decodedInfo.TimelineZoom, MD5Hash = memoryStream.ComputeMD5Hash(), EndTimeObjectCount = decoded.HitObjects.Count(h => h is IHasDuration), TotalObjectCount = decoded.HitObjects.Count diff --git a/osu.Game/Beatmaps/BeatmapInfo.cs b/osu.Game/Beatmaps/BeatmapInfo.cs index 6b192063e8..39cd320ad7 100644 --- a/osu.Game/Beatmaps/BeatmapInfo.cs +++ b/osu.Game/Beatmaps/BeatmapInfo.cs @@ -143,8 +143,6 @@ namespace osu.Game.Beatmaps public int BeatDivisor { get; set; } = 4; - public double TimelineZoom { get; set; } = 1.0; - /// /// The time in milliseconds when last exiting the editor with this beatmap loaded. /// diff --git a/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs b/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs index 80bfae3036..cb2b1820d7 100644 --- a/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs +++ b/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs @@ -341,7 +341,7 @@ namespace osu.Game.Beatmaps.Formats break; case @"TimelineZoom": - beatmap.BeatmapInfo.TimelineZoom = Math.Max(0, Parsing.ParseDouble(pair.Value)); + beatmap.TimelineZoom = Math.Max(0, Parsing.ParseDouble(pair.Value)); break; } } diff --git a/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs b/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs index 1bfba0962c..d705deb5df 100644 --- a/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs +++ b/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs @@ -115,7 +115,7 @@ namespace osu.Game.Beatmaps.Formats writer.WriteLine(FormattableString.Invariant($"DistanceSpacing: {beatmap.DistanceSpacing}")); writer.WriteLine(FormattableString.Invariant($"BeatDivisor: {beatmap.BeatmapInfo.BeatDivisor}")); writer.WriteLine(FormattableString.Invariant($"GridSize: {beatmap.GridSize}")); - writer.WriteLine(FormattableString.Invariant($"TimelineZoom: {beatmap.BeatmapInfo.TimelineZoom}")); + writer.WriteLine(FormattableString.Invariant($"TimelineZoom: {beatmap.TimelineZoom}")); } private void handleMetadata(TextWriter writer) diff --git a/osu.Game/Beatmaps/IBeatmap.cs b/osu.Game/Beatmaps/IBeatmap.cs index ecfc8ea398..99a9d31807 100644 --- a/osu.Game/Beatmaps/IBeatmap.cs +++ b/osu.Game/Beatmaps/IBeatmap.cs @@ -97,6 +97,8 @@ namespace osu.Game.Beatmaps int GridSize { get; internal set; } + double TimelineZoom { get; internal set; } + /// /// Creates a shallow-clone of this beatmap and returns it. /// diff --git a/osu.Game/Rulesets/Difficulty/DifficultyCalculator.cs b/osu.Game/Rulesets/Difficulty/DifficultyCalculator.cs index d4a95558cc..ebdde0fca6 100644 --- a/osu.Game/Rulesets/Difficulty/DifficultyCalculator.cs +++ b/osu.Game/Rulesets/Difficulty/DifficultyCalculator.cs @@ -391,6 +391,12 @@ namespace osu.Game.Rulesets.Difficulty set => baseBeatmap.GridSize = value; } + public double TimelineZoom + { + get => baseBeatmap.TimelineZoom; + set => baseBeatmap.TimelineZoom = value; + } + #endregion } } diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/Timeline.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/Timeline.cs index a2704e550c..7c1f2e3730 100644 --- a/osu.Game/Screens/Edit/Compose/Components/Timeline/Timeline.cs +++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/Timeline.cs @@ -147,7 +147,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline Scheduler.AddOnce(applyVisualOffset, beatmap); }, true); - Zoom = (float)(defaultTimelineZoom * editorBeatmap.BeatmapInfo.TimelineZoom); + Zoom = (float)(defaultTimelineZoom * editorBeatmap.TimelineZoom); } private void applyVisualOffset(IBindable beatmap) @@ -215,7 +215,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline float minimumZoom = getZoomLevelForVisibleMilliseconds(10000); float maximumZoom = getZoomLevelForVisibleMilliseconds(500); - float initialZoom = (float)Math.Clamp(defaultTimelineZoom * (editorBeatmap.BeatmapInfo.TimelineZoom == 0 ? 1 : editorBeatmap.BeatmapInfo.TimelineZoom), minimumZoom, maximumZoom); + float initialZoom = (float)Math.Clamp(defaultTimelineZoom * (editorBeatmap.TimelineZoom == 0 ? 1 : editorBeatmap.TimelineZoom), minimumZoom, maximumZoom); SetupZoom(initialZoom, minimumZoom, maximumZoom); @@ -237,7 +237,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline protected override void OnZoomChanged() { base.OnZoomChanged(); - editorBeatmap.BeatmapInfo.TimelineZoom = Zoom / defaultTimelineZoom; + editorBeatmap.TimelineZoom = Zoom / defaultTimelineZoom; } protected override void UpdateAfterChildren() diff --git a/osu.Game/Screens/Edit/EditorBeatmap.cs b/osu.Game/Screens/Edit/EditorBeatmap.cs index b1df60126d..0d07e16828 100644 --- a/osu.Game/Screens/Edit/EditorBeatmap.cs +++ b/osu.Game/Screens/Edit/EditorBeatmap.cs @@ -238,6 +238,12 @@ namespace osu.Game.Screens.Edit set => PlayableBeatmap.GridSize = value; } + public double TimelineZoom + { + get => PlayableBeatmap.TimelineZoom; + set => PlayableBeatmap.TimelineZoom = value; + } + public IBeatmap Clone() => (EditorBeatmap)MemberwiseClone(); private IList mutableHitObjects => (IList)PlayableBeatmap.HitObjects; From d373f752d667c35ae68ef47eced2dcb3c94920ff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 12 Jun 2024 13:58:00 +0200 Subject: [PATCH 0024/1275] Move `Countdown` out of `BeatmapInfo` --- .../Beatmaps/Formats/LegacyBeatmapDecoderTest.cs | 4 ++-- osu.Game.Tests/Beatmaps/Formats/OsuJsonDecoderTest.cs | 2 +- osu.Game.Tests/Visual/Editing/TestSceneDesignSection.cs | 8 ++++---- osu.Game/Beatmaps/Beatmap.cs | 2 ++ osu.Game/Beatmaps/BeatmapInfo.cs | 3 --- osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs | 2 +- osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs | 2 +- osu.Game/Beatmaps/IBeatmap.cs | 2 ++ osu.Game/Rulesets/Difficulty/DifficultyCalculator.cs | 6 ++++++ osu.Game/Screens/Edit/EditorBeatmap.cs | 6 ++++++ osu.Game/Screens/Edit/Setup/DesignSection.cs | 6 +++--- 11 files changed, 28 insertions(+), 15 deletions(-) diff --git a/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapDecoderTest.cs b/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapDecoderTest.cs index af0e4a8b3c..9cea6ef507 100644 --- a/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapDecoderTest.cs +++ b/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapDecoderTest.cs @@ -88,7 +88,7 @@ namespace osu.Game.Tests.Beatmaps.Formats Assert.IsFalse(beatmap.SpecialStyle); Assert.IsFalse(beatmap.WidescreenStoryboard); Assert.IsFalse(beatmap.SamplesMatchPlaybackRate); - Assert.AreEqual(CountdownType.None, beatmapInfo.Countdown); + Assert.AreEqual(CountdownType.None, beatmap.Countdown); Assert.AreEqual(0, beatmapInfo.CountdownOffset); } } @@ -957,7 +957,7 @@ namespace osu.Game.Tests.Beatmaps.Formats Assert.That(decoded.WidescreenStoryboard, Is.False); Assert.That(decoded.EpilepsyWarning, Is.False); Assert.That(decoded.SamplesMatchPlaybackRate, Is.False); - Assert.That(decoded.BeatmapInfo.Countdown, Is.EqualTo(CountdownType.Normal)); + Assert.That(decoded.Countdown, Is.EqualTo(CountdownType.Normal)); Assert.That(decoded.BeatmapInfo.CountdownOffset, Is.EqualTo(0)); Assert.That(decoded.BeatmapInfo.Metadata.PreviewTime, Is.EqualTo(-1)); Assert.That(decoded.BeatmapInfo.Ruleset.OnlineID, Is.EqualTo(0)); diff --git a/osu.Game.Tests/Beatmaps/Formats/OsuJsonDecoderTest.cs b/osu.Game.Tests/Beatmaps/Formats/OsuJsonDecoderTest.cs index 1fed9633f7..bc6628cea0 100644 --- a/osu.Game.Tests/Beatmaps/Formats/OsuJsonDecoderTest.cs +++ b/osu.Game.Tests/Beatmaps/Formats/OsuJsonDecoderTest.cs @@ -57,7 +57,7 @@ namespace osu.Game.Tests.Beatmaps.Formats Assert.IsTrue(beatmapInfo.Ruleset.OnlineID == 0); Assert.AreEqual(false, beatmap.LetterboxInBreaks); Assert.AreEqual(false, beatmap.WidescreenStoryboard); - Assert.AreEqual(CountdownType.None, beatmapInfo.Countdown); + Assert.AreEqual(CountdownType.None, beatmap.Countdown); Assert.AreEqual(0, beatmapInfo.CountdownOffset); } diff --git a/osu.Game.Tests/Visual/Editing/TestSceneDesignSection.cs b/osu.Game.Tests/Visual/Editing/TestSceneDesignSection.cs index 9a66e1676d..c91c22a145 100644 --- a/osu.Game.Tests/Visual/Editing/TestSceneDesignSection.cs +++ b/osu.Game.Tests/Visual/Editing/TestSceneDesignSection.cs @@ -51,7 +51,7 @@ namespace osu.Game.Tests.Visual.Editing { AddStep("turn countdown off", () => designSection.EnableCountdown.Current.Value = false); - AddAssert("beatmap has correct type", () => editorBeatmap.BeatmapInfo.Countdown == CountdownType.None); + AddAssert("beatmap has correct type", () => editorBeatmap.Countdown == CountdownType.None); AddUntilStep("other controls hidden", () => !designSection.CountdownSettings.IsPresent); } @@ -60,12 +60,12 @@ namespace osu.Game.Tests.Visual.Editing { AddStep("turn countdown on", () => designSection.EnableCountdown.Current.Value = true); - AddAssert("beatmap has correct type", () => editorBeatmap.BeatmapInfo.Countdown == CountdownType.Normal); + AddAssert("beatmap has correct type", () => editorBeatmap.Countdown == CountdownType.Normal); AddUntilStep("other controls shown", () => designSection.CountdownSettings.IsPresent); AddStep("change countdown speed", () => designSection.CountdownSpeed.Current.Value = CountdownType.DoubleSpeed); - AddAssert("beatmap has correct type", () => editorBeatmap.BeatmapInfo.Countdown == CountdownType.DoubleSpeed); + AddAssert("beatmap has correct type", () => editorBeatmap.Countdown == CountdownType.DoubleSpeed); AddUntilStep("other controls still shown", () => designSection.CountdownSettings.IsPresent); } @@ -74,7 +74,7 @@ namespace osu.Game.Tests.Visual.Editing { AddStep("turn countdown on", () => designSection.EnableCountdown.Current.Value = true); - AddAssert("beatmap has correct type", () => editorBeatmap.BeatmapInfo.Countdown == CountdownType.Normal); + AddAssert("beatmap has correct type", () => editorBeatmap.Countdown == CountdownType.Normal); checkOffsetAfter("1", 1); checkOffsetAfter(string.Empty, 0); diff --git a/osu.Game/Beatmaps/Beatmap.cs b/osu.Game/Beatmaps/Beatmap.cs index 35bd935d66..54fb1fd3b1 100644 --- a/osu.Game/Beatmaps/Beatmap.cs +++ b/osu.Game/Beatmaps/Beatmap.cs @@ -134,6 +134,8 @@ namespace osu.Game.Beatmaps public double TimelineZoom { get; set; } = 1.0; + public CountdownType Countdown { get; set; } = CountdownType.Normal; + IBeatmap IBeatmap.Clone() => Clone(); public Beatmap Clone() => (Beatmap)MemberwiseClone(); diff --git a/osu.Game/Beatmaps/BeatmapInfo.cs b/osu.Game/Beatmaps/BeatmapInfo.cs index 39cd320ad7..0214ae4c3a 100644 --- a/osu.Game/Beatmaps/BeatmapInfo.cs +++ b/osu.Game/Beatmaps/BeatmapInfo.cs @@ -148,9 +148,6 @@ namespace osu.Game.Beatmaps /// public double? EditorTimestamp { get; set; } - [Ignored] - public CountdownType Countdown { get; set; } = CountdownType.Normal; - /// /// The number of beats to move the countdown backwards (compared to its default location). /// diff --git a/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs b/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs index cb2b1820d7..48959025c9 100644 --- a/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs +++ b/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs @@ -305,7 +305,7 @@ namespace osu.Game.Beatmaps.Formats break; case @"Countdown": - beatmap.BeatmapInfo.Countdown = Enum.Parse(pair.Value); + beatmap.Countdown = Enum.Parse(pair.Value); break; case @"CountdownOffset": diff --git a/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs b/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs index d705deb5df..73399e93d0 100644 --- a/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs +++ b/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs @@ -81,7 +81,7 @@ namespace osu.Game.Beatmaps.Formats if (!string.IsNullOrEmpty(beatmap.Metadata.AudioFile)) writer.WriteLine(FormattableString.Invariant($"AudioFilename: {Path.GetFileName(beatmap.Metadata.AudioFile)}")); writer.WriteLine(FormattableString.Invariant($"AudioLeadIn: {beatmap.AudioLeadIn}")); writer.WriteLine(FormattableString.Invariant($"PreviewTime: {beatmap.Metadata.PreviewTime}")); - writer.WriteLine(FormattableString.Invariant($"Countdown: {(int)beatmap.BeatmapInfo.Countdown}")); + writer.WriteLine(FormattableString.Invariant($"Countdown: {(int)beatmap.Countdown}")); writer.WriteLine(FormattableString.Invariant( $"SampleSet: {toLegacySampleBank(((beatmap.ControlPointInfo as LegacyControlPointInfo)?.SamplePoints.FirstOrDefault() ?? SampleControlPoint.DEFAULT).SampleBank)}")); writer.WriteLine(FormattableString.Invariant($"StackLeniency: {beatmap.StackLeniency}")); diff --git a/osu.Game/Beatmaps/IBeatmap.cs b/osu.Game/Beatmaps/IBeatmap.cs index 99a9d31807..cf7bd29088 100644 --- a/osu.Game/Beatmaps/IBeatmap.cs +++ b/osu.Game/Beatmaps/IBeatmap.cs @@ -99,6 +99,8 @@ namespace osu.Game.Beatmaps double TimelineZoom { get; internal set; } + CountdownType Countdown { get; internal set; } + /// /// Creates a shallow-clone of this beatmap and returns it. /// diff --git a/osu.Game/Rulesets/Difficulty/DifficultyCalculator.cs b/osu.Game/Rulesets/Difficulty/DifficultyCalculator.cs index ebdde0fca6..a444cc135b 100644 --- a/osu.Game/Rulesets/Difficulty/DifficultyCalculator.cs +++ b/osu.Game/Rulesets/Difficulty/DifficultyCalculator.cs @@ -397,6 +397,12 @@ namespace osu.Game.Rulesets.Difficulty set => baseBeatmap.TimelineZoom = value; } + public CountdownType Countdown + { + get => baseBeatmap.Countdown; + set => baseBeatmap.Countdown = value; + } + #endregion } } diff --git a/osu.Game/Screens/Edit/EditorBeatmap.cs b/osu.Game/Screens/Edit/EditorBeatmap.cs index 0d07e16828..a86d1fbaef 100644 --- a/osu.Game/Screens/Edit/EditorBeatmap.cs +++ b/osu.Game/Screens/Edit/EditorBeatmap.cs @@ -244,6 +244,12 @@ namespace osu.Game.Screens.Edit set => PlayableBeatmap.TimelineZoom = value; } + public CountdownType Countdown + { + get => PlayableBeatmap.Countdown; + set => PlayableBeatmap.Countdown = value; + } + public IBeatmap Clone() => (EditorBeatmap)MemberwiseClone(); private IList mutableHitObjects => (IList)PlayableBeatmap.HitObjects; diff --git a/osu.Game/Screens/Edit/Setup/DesignSection.cs b/osu.Game/Screens/Edit/Setup/DesignSection.cs index 4c4755064f..b40f1bea72 100644 --- a/osu.Game/Screens/Edit/Setup/DesignSection.cs +++ b/osu.Game/Screens/Edit/Setup/DesignSection.cs @@ -38,7 +38,7 @@ namespace osu.Game.Screens.Edit.Setup EnableCountdown = new LabelledSwitchButton { Label = EditorSetupStrings.EnableCountdown, - Current = { Value = Beatmap.BeatmapInfo.Countdown != CountdownType.None }, + Current = { Value = Beatmap.Countdown != CountdownType.None }, Description = EditorSetupStrings.CountdownDescription }, CountdownSettings = new FillFlowContainer @@ -52,7 +52,7 @@ namespace osu.Game.Screens.Edit.Setup CountdownSpeed = new LabelledEnumDropdown { Label = EditorSetupStrings.CountdownSpeed, - Current = { Value = Beatmap.BeatmapInfo.Countdown != CountdownType.None ? Beatmap.BeatmapInfo.Countdown : CountdownType.Normal }, + Current = { Value = Beatmap.Countdown != CountdownType.None ? Beatmap.Countdown : CountdownType.Normal }, Items = Enum.GetValues().Where(type => type != CountdownType.None) }, CountdownOffset = new LabelledNumberBox @@ -118,7 +118,7 @@ namespace osu.Game.Screens.Edit.Setup private void updateBeatmap() { - Beatmap.BeatmapInfo.Countdown = EnableCountdown.Current.Value ? CountdownSpeed.Current.Value : CountdownType.None; + Beatmap.Countdown = EnableCountdown.Current.Value ? CountdownSpeed.Current.Value : CountdownType.None; Beatmap.BeatmapInfo.CountdownOffset = int.TryParse(CountdownOffset.Current.Value, NumberStyles.None, CultureInfo.InvariantCulture, out int offset) ? offset : 0; Beatmap.WidescreenStoryboard = widescreenSupport.Current.Value; From dd50d6fa6e61d02e7e0a995a8e47569a5d7564e5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 12 Jun 2024 14:03:02 +0200 Subject: [PATCH 0025/1275] Move `CountdownOffset` out of `BeatmapInfo` --- osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapDecoderTest.cs | 4 ++-- osu.Game.Tests/Beatmaps/Formats/OsuJsonDecoderTest.cs | 2 +- .../Database/RealmSubscriptionRegistrationTests.cs | 2 +- osu.Game.Tests/Visual/Editing/TestSceneDesignSection.cs | 2 +- osu.Game/Beatmaps/Beatmap.cs | 2 ++ osu.Game/Beatmaps/BeatmapConverter.cs | 1 + osu.Game/Beatmaps/BeatmapInfo.cs | 5 ----- osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs | 2 +- osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs | 4 ++-- osu.Game/Beatmaps/IBeatmap.cs | 5 +++++ osu.Game/Rulesets/Difficulty/DifficultyCalculator.cs | 6 ++++++ osu.Game/Screens/Edit/EditorBeatmap.cs | 6 ++++++ osu.Game/Screens/Edit/Setup/DesignSection.cs | 6 +++--- 13 files changed, 31 insertions(+), 16 deletions(-) diff --git a/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapDecoderTest.cs b/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapDecoderTest.cs index 9cea6ef507..ab0ec7ee39 100644 --- a/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapDecoderTest.cs +++ b/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapDecoderTest.cs @@ -89,7 +89,7 @@ namespace osu.Game.Tests.Beatmaps.Formats Assert.IsFalse(beatmap.WidescreenStoryboard); Assert.IsFalse(beatmap.SamplesMatchPlaybackRate); Assert.AreEqual(CountdownType.None, beatmap.Countdown); - Assert.AreEqual(0, beatmapInfo.CountdownOffset); + Assert.AreEqual(0, beatmap.CountdownOffset); } } @@ -958,7 +958,7 @@ namespace osu.Game.Tests.Beatmaps.Formats Assert.That(decoded.EpilepsyWarning, Is.False); Assert.That(decoded.SamplesMatchPlaybackRate, Is.False); Assert.That(decoded.Countdown, Is.EqualTo(CountdownType.Normal)); - Assert.That(decoded.BeatmapInfo.CountdownOffset, Is.EqualTo(0)); + Assert.That(decoded.CountdownOffset, Is.EqualTo(0)); Assert.That(decoded.BeatmapInfo.Metadata.PreviewTime, Is.EqualTo(-1)); Assert.That(decoded.BeatmapInfo.Ruleset.OnlineID, Is.EqualTo(0)); }); diff --git a/osu.Game.Tests/Beatmaps/Formats/OsuJsonDecoderTest.cs b/osu.Game.Tests/Beatmaps/Formats/OsuJsonDecoderTest.cs index bc6628cea0..e57a4fff62 100644 --- a/osu.Game.Tests/Beatmaps/Formats/OsuJsonDecoderTest.cs +++ b/osu.Game.Tests/Beatmaps/Formats/OsuJsonDecoderTest.cs @@ -58,7 +58,7 @@ namespace osu.Game.Tests.Beatmaps.Formats Assert.AreEqual(false, beatmap.LetterboxInBreaks); Assert.AreEqual(false, beatmap.WidescreenStoryboard); Assert.AreEqual(CountdownType.None, beatmap.Countdown); - Assert.AreEqual(0, beatmapInfo.CountdownOffset); + Assert.AreEqual(0, beatmap.CountdownOffset); } [Test] diff --git a/osu.Game.Tests/Database/RealmSubscriptionRegistrationTests.cs b/osu.Game.Tests/Database/RealmSubscriptionRegistrationTests.cs index 45842a952a..541f3b0417 100644 --- a/osu.Game.Tests/Database/RealmSubscriptionRegistrationTests.cs +++ b/osu.Game.Tests/Database/RealmSubscriptionRegistrationTests.cs @@ -41,7 +41,7 @@ namespace osu.Game.Tests.Database Assert.That(lastChanges?.ModifiedIndices, Is.Empty); Assert.That(lastChanges?.NewModifiedIndices, Is.Empty); - realm.Write(r => r.All().First().Beatmaps.First().CountdownOffset = 5); + realm.Write(r => r.All().First().Beatmaps.First().EditorTimestamp = 5); realm.Run(r => r.Refresh()); Assert.That(collectionChanges, Is.EqualTo(1)); diff --git a/osu.Game.Tests/Visual/Editing/TestSceneDesignSection.cs b/osu.Game.Tests/Visual/Editing/TestSceneDesignSection.cs index c91c22a145..0011a4ceb4 100644 --- a/osu.Game.Tests/Visual/Editing/TestSceneDesignSection.cs +++ b/osu.Game.Tests/Visual/Editing/TestSceneDesignSection.cs @@ -94,7 +94,7 @@ namespace osu.Game.Tests.Visual.Editing AddStep("commit text", () => InputManager.Key(Key.Enter)); AddAssert($"displayed value is {expectedFinalValue}", () => designSection.CountdownOffset.Current.Value == expectedFinalValue.ToString(CultureInfo.InvariantCulture)); - AddAssert($"beatmap value is {expectedFinalValue}", () => editorBeatmap.BeatmapInfo.CountdownOffset == expectedFinalValue); + AddAssert($"beatmap value is {expectedFinalValue}", () => editorBeatmap.CountdownOffset == expectedFinalValue); } private partial class TestDesignSection : DesignSection diff --git a/osu.Game/Beatmaps/Beatmap.cs b/osu.Game/Beatmaps/Beatmap.cs index 54fb1fd3b1..19a7ee3303 100644 --- a/osu.Game/Beatmaps/Beatmap.cs +++ b/osu.Game/Beatmaps/Beatmap.cs @@ -136,6 +136,8 @@ namespace osu.Game.Beatmaps public CountdownType Countdown { get; set; } = CountdownType.Normal; + public int CountdownOffset { get; set; } + IBeatmap IBeatmap.Clone() => Clone(); public Beatmap Clone() => (Beatmap)MemberwiseClone(); diff --git a/osu.Game/Beatmaps/BeatmapConverter.cs b/osu.Game/Beatmaps/BeatmapConverter.cs index 140771a5d5..8e917a179e 100644 --- a/osu.Game/Beatmaps/BeatmapConverter.cs +++ b/osu.Game/Beatmaps/BeatmapConverter.cs @@ -77,6 +77,7 @@ namespace osu.Game.Beatmaps beatmap.DistanceSpacing = original.DistanceSpacing; beatmap.GridSize = original.GridSize; beatmap.TimelineZoom = original.TimelineZoom; + beatmap.CountdownOffset = original.CountdownOffset; return beatmap; } diff --git a/osu.Game/Beatmaps/BeatmapInfo.cs b/osu.Game/Beatmaps/BeatmapInfo.cs index 0214ae4c3a..3ed15f52fb 100644 --- a/osu.Game/Beatmaps/BeatmapInfo.cs +++ b/osu.Game/Beatmaps/BeatmapInfo.cs @@ -148,11 +148,6 @@ namespace osu.Game.Beatmaps /// public double? EditorTimestamp { get; set; } - /// - /// The number of beats to move the countdown backwards (compared to its default location). - /// - public int CountdownOffset { get; set; } - #endregion public bool Equals(BeatmapInfo? other) diff --git a/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs b/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs index 48959025c9..14de31a2a3 100644 --- a/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs +++ b/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs @@ -309,7 +309,7 @@ namespace osu.Game.Beatmaps.Formats break; case @"CountdownOffset": - beatmap.BeatmapInfo.CountdownOffset = Parsing.ParseInt(pair.Value); + beatmap.CountdownOffset = Parsing.ParseInt(pair.Value); break; } } diff --git a/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs b/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs index 73399e93d0..b924b7aea5 100644 --- a/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs +++ b/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs @@ -97,8 +97,8 @@ namespace osu.Game.Beatmaps.Formats // writer.WriteLine(@"SkinPreference:" + b.SkinPreference); if (beatmap.EpilepsyWarning) writer.WriteLine(@"EpilepsyWarning: 1"); - if (beatmap.BeatmapInfo.CountdownOffset > 0) - writer.WriteLine(FormattableString.Invariant($@"CountdownOffset: {beatmap.BeatmapInfo.CountdownOffset}")); + if (beatmap.CountdownOffset > 0) + writer.WriteLine(FormattableString.Invariant($@"CountdownOffset: {beatmap.CountdownOffset}")); if (onlineRulesetID == 3) writer.WriteLine(FormattableString.Invariant($"SpecialStyle: {(beatmap.SpecialStyle ? '1' : '0')}")); writer.WriteLine(FormattableString.Invariant($"WidescreenStoryboard: {(beatmap.WidescreenStoryboard ? '1' : '0')}")); diff --git a/osu.Game/Beatmaps/IBeatmap.cs b/osu.Game/Beatmaps/IBeatmap.cs index cf7bd29088..f08fdfaf6a 100644 --- a/osu.Game/Beatmaps/IBeatmap.cs +++ b/osu.Game/Beatmaps/IBeatmap.cs @@ -101,6 +101,11 @@ namespace osu.Game.Beatmaps CountdownType Countdown { get; internal set; } + /// + /// The number of beats to move the countdown backwards (compared to its default location). + /// + int CountdownOffset { get; internal set; } + /// /// Creates a shallow-clone of this beatmap and returns it. /// diff --git a/osu.Game/Rulesets/Difficulty/DifficultyCalculator.cs b/osu.Game/Rulesets/Difficulty/DifficultyCalculator.cs index a444cc135b..6dd85cefe4 100644 --- a/osu.Game/Rulesets/Difficulty/DifficultyCalculator.cs +++ b/osu.Game/Rulesets/Difficulty/DifficultyCalculator.cs @@ -403,6 +403,12 @@ namespace osu.Game.Rulesets.Difficulty set => baseBeatmap.Countdown = value; } + public int CountdownOffset + { + get => baseBeatmap.CountdownOffset; + set => baseBeatmap.CountdownOffset = value; + } + #endregion } } diff --git a/osu.Game/Screens/Edit/EditorBeatmap.cs b/osu.Game/Screens/Edit/EditorBeatmap.cs index a86d1fbaef..deb46c3d2e 100644 --- a/osu.Game/Screens/Edit/EditorBeatmap.cs +++ b/osu.Game/Screens/Edit/EditorBeatmap.cs @@ -250,6 +250,12 @@ namespace osu.Game.Screens.Edit set => PlayableBeatmap.Countdown = value; } + public int CountdownOffset + { + get => PlayableBeatmap.CountdownOffset; + set => PlayableBeatmap.CountdownOffset = value; + } + public IBeatmap Clone() => (EditorBeatmap)MemberwiseClone(); private IList mutableHitObjects => (IList)PlayableBeatmap.HitObjects; diff --git a/osu.Game/Screens/Edit/Setup/DesignSection.cs b/osu.Game/Screens/Edit/Setup/DesignSection.cs index b40f1bea72..3ed0a78175 100644 --- a/osu.Game/Screens/Edit/Setup/DesignSection.cs +++ b/osu.Game/Screens/Edit/Setup/DesignSection.cs @@ -58,7 +58,7 @@ namespace osu.Game.Screens.Edit.Setup CountdownOffset = new LabelledNumberBox { Label = EditorSetupStrings.CountdownOffset, - Current = { Value = Beatmap.BeatmapInfo.CountdownOffset.ToString() }, + Current = { Value = Beatmap.CountdownOffset.ToString() }, Description = EditorSetupStrings.CountdownOffsetDescription, } } @@ -113,13 +113,13 @@ namespace osu.Game.Screens.Edit.Setup { updateBeatmap(); // update displayed text to ensure parsed value matches display (i.e. if empty string was provided). - CountdownOffset.Current.Value = Beatmap.BeatmapInfo.CountdownOffset.ToString(CultureInfo.InvariantCulture); + CountdownOffset.Current.Value = Beatmap.CountdownOffset.ToString(CultureInfo.InvariantCulture); } private void updateBeatmap() { Beatmap.Countdown = EnableCountdown.Current.Value ? CountdownSpeed.Current.Value : CountdownType.None; - Beatmap.BeatmapInfo.CountdownOffset = int.TryParse(CountdownOffset.Current.Value, NumberStyles.None, CultureInfo.InvariantCulture, out int offset) ? offset : 0; + Beatmap.CountdownOffset = int.TryParse(CountdownOffset.Current.Value, NumberStyles.None, CultureInfo.InvariantCulture, out int offset) ? offset : 0; Beatmap.WidescreenStoryboard = widescreenSupport.Current.Value; Beatmap.EpilepsyWarning = epilepsyWarning.Current.Value; From 9fbf2872e1a63547f44adafc790f8bc57a1c18a7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 12 Jun 2024 14:27:35 +0200 Subject: [PATCH 0026/1275] Remove no longer applicable region marking --- osu.Game/Beatmaps/BeatmapInfo.cs | 4 ---- 1 file changed, 4 deletions(-) diff --git a/osu.Game/Beatmaps/BeatmapInfo.cs b/osu.Game/Beatmaps/BeatmapInfo.cs index 3ed15f52fb..0a6719a96a 100644 --- a/osu.Game/Beatmaps/BeatmapInfo.cs +++ b/osu.Game/Beatmaps/BeatmapInfo.cs @@ -134,8 +134,6 @@ namespace osu.Game.Beatmaps Status = BeatmapOnlineStatus.None; } - #region Properties we may not want persisted (but also maybe no harm?) - /// /// The time at which this beatmap was last played by the local user. /// @@ -148,8 +146,6 @@ namespace osu.Game.Beatmaps /// public double? EditorTimestamp { get; set; } - #endregion - public bool Equals(BeatmapInfo? other) { if (ReferenceEquals(this, other)) return true; From c67e2dc301696654132b2d31f9f07abc4ede47c6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 12 Jun 2024 14:51:20 +0200 Subject: [PATCH 0027/1275] Bump schema version --- osu.Game/Database/RealmAccess.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/osu.Game/Database/RealmAccess.cs b/osu.Game/Database/RealmAccess.cs index 1ece81be50..33b06f32b1 100644 --- a/osu.Game/Database/RealmAccess.cs +++ b/osu.Game/Database/RealmAccess.cs @@ -93,8 +93,9 @@ namespace osu.Game.Database /// 39 2023-12-19 Migrate any EndTimeObjectCount and TotalObjectCount values of 0 to -1 to better identify non-calculated values. /// 40 2023-12-21 Add ScoreInfo.Version to keep track of which build scores were set on. /// 41 2024-04-17 Add ScoreInfo.TotalScoreWithoutMods for future mod multiplier rebalances. + /// 42 2024-06-12 Removed several properties from ScoreInfo which did not need to be persisted to realm. /// - private const int schema_version = 41; + private const int schema_version = 42; /// /// Lock object which is held during sections, blocking realm retrieval during blocking periods. From 04527f3c9da63b5fc54d9afd3c7304b0634a8434 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 13 Jun 2024 09:30:00 +0200 Subject: [PATCH 0028/1275] Fix `TestBeatmap` not transferring newly migrated properties --- osu.Game/Tests/Beatmaps/TestBeatmap.cs | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/osu.Game/Tests/Beatmaps/TestBeatmap.cs b/osu.Game/Tests/Beatmaps/TestBeatmap.cs index de7bcfcfaa..863badbd4a 100644 --- a/osu.Game/Tests/Beatmaps/TestBeatmap.cs +++ b/osu.Game/Tests/Beatmaps/TestBeatmap.cs @@ -28,6 +28,17 @@ namespace osu.Game.Tests.Beatmaps ControlPointInfo = baseBeatmap.ControlPointInfo; Breaks = baseBeatmap.Breaks; UnhandledEventLines = baseBeatmap.UnhandledEventLines; + AudioLeadIn = baseBeatmap.AudioLeadIn; + StackLeniency = baseBeatmap.StackLeniency; + SpecialStyle = baseBeatmap.SpecialStyle; + LetterboxInBreaks = baseBeatmap.LetterboxInBreaks; + WidescreenStoryboard = baseBeatmap.WidescreenStoryboard; + EpilepsyWarning = baseBeatmap.EpilepsyWarning; + SamplesMatchPlaybackRate = baseBeatmap.SamplesMatchPlaybackRate; + DistanceSpacing = baseBeatmap.DistanceSpacing; + GridSize = baseBeatmap.GridSize; + TimelineZoom = baseBeatmap.TimelineZoom; + CountdownOffset = baseBeatmap.CountdownOffset; if (withHitObjects) HitObjects = baseBeatmap.HitObjects; From 1d4d8063622dbc552fd695bfb6561fbe14b63e57 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 23 Jul 2024 12:19:45 +0200 Subject: [PATCH 0029/1275] Fix `WidescreenStoryboard` breakage after moving out of `BeatmapInfo` --- osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs | 4 ++-- .../Beatmaps/Formats/LegacyStoryboardDecoder.cs | 15 +++++++++++++++ osu.Game/Beatmaps/WorkingBeatmap.cs | 7 ++++++- 3 files changed, 23 insertions(+), 3 deletions(-) diff --git a/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs b/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs index f873eaf535..bd81892d95 100644 --- a/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs +++ b/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs @@ -80,7 +80,7 @@ namespace osu.Game.Beatmaps.Formats this.beatmap = beatmap; this.beatmap.BeatmapInfo.BeatmapVersion = FormatVersion; - applyLegacyDefaults(this.beatmap); + ApplyLegacyDefaults(this.beatmap); base.ParseStreamInto(stream, beatmap); @@ -186,7 +186,7 @@ namespace osu.Game.Beatmaps.Formats /// This method's intention is to restore those legacy defaults. /// See also: https://osu.ppy.sh/wiki/en/Client/File_formats/Osu_%28file_format%29 /// - private static void applyLegacyDefaults(Beatmap beatmap) + internal static void ApplyLegacyDefaults(Beatmap beatmap) { beatmap.WidescreenStoryboard = false; beatmap.SamplesMatchPlaybackRate = false; diff --git a/osu.Game/Beatmaps/Formats/LegacyStoryboardDecoder.cs b/osu.Game/Beatmaps/Formats/LegacyStoryboardDecoder.cs index 2f9a256d31..dc96c2ff82 100644 --- a/osu.Game/Beatmaps/Formats/LegacyStoryboardDecoder.cs +++ b/osu.Game/Beatmaps/Formats/LegacyStoryboardDecoder.cs @@ -37,6 +37,17 @@ namespace osu.Game.Beatmaps.Formats SetFallbackDecoder(() => new LegacyStoryboardDecoder()); } + protected override Storyboard CreateTemplateObject() + { + var sb = base.CreateTemplateObject(); + + var beatmap = new Beatmap(); + LegacyBeatmapDecoder.ApplyLegacyDefaults(beatmap); + sb.Beatmap = beatmap; + + return sb; + } + protected override void ParseStreamInto(LineBufferedReader stream, Storyboard storyboard) { this.storyboard = storyboard; @@ -72,6 +83,10 @@ namespace osu.Game.Beatmaps.Formats case "UseSkinSprites": storyboard.UseSkinSprites = pair.Value == "1"; break; + + case @"WidescreenStoryboard": + storyboard.Beatmap.WidescreenStoryboard = Parsing.ParseInt(pair.Value) == 1; + break; } } diff --git a/osu.Game/Beatmaps/WorkingBeatmap.cs b/osu.Game/Beatmaps/WorkingBeatmap.cs index 25159996f3..8b0d3dda6a 100644 --- a/osu.Game/Beatmaps/WorkingBeatmap.cs +++ b/osu.Game/Beatmaps/WorkingBeatmap.cs @@ -62,7 +62,12 @@ namespace osu.Game.Beatmaps #region Resource getters protected virtual Waveform GetWaveform() => new Waveform(null); - protected virtual Storyboard GetStoryboard() => new Storyboard { BeatmapInfo = BeatmapInfo }; + + protected virtual Storyboard GetStoryboard() => new Storyboard + { + BeatmapInfo = BeatmapInfo, + Beatmap = Beatmap, + }; protected abstract IBeatmap GetBeatmap(); public abstract Texture GetBackground(); From 16e69b08a161506d191ddf89e782928da79146d0 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 30 Sep 2024 19:52:51 +0900 Subject: [PATCH 0030/1275] 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 0031/1275] 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 0032/1275] 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 0033/1275] 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 0034/1275] 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 2fd495228c21f9bd8e8700aefdb1b93c69027d3e Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Fri, 25 Oct 2024 02:25:32 -0400 Subject: [PATCH 0035/1275] Fix post-merge errors --- .../Visual/Online/TestSceneUserStatisticsWatcher.cs | 3 ++- osu.Game/Online/API/IAPIProvider.cs | 5 +++++ osu.Game/Online/UserStatisticsWatcher.cs | 4 ++-- osu.Game/OsuGame.cs | 2 +- osu.Game/OsuGameBase.cs | 11 ++++------- 5 files changed, 14 insertions(+), 11 deletions(-) diff --git a/osu.Game.Tests/Visual/Online/TestSceneUserStatisticsWatcher.cs b/osu.Game.Tests/Visual/Online/TestSceneUserStatisticsWatcher.cs index 1454f8c183..e5ccad703e 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneUserStatisticsWatcher.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneUserStatisticsWatcher.cs @@ -25,6 +25,7 @@ namespace osu.Game.Tests.Visual.Online { protected override bool UseOnlineAPI => false; + private LocalUserStatisticsProvider statisticsProvider = null!; private UserStatisticsWatcher watcher = null!; [Resolved] @@ -109,7 +110,7 @@ namespace osu.Game.Tests.Visual.Online { Clear(); Add(statisticsProvider = new LocalUserStatisticsProvider()); - Add(watcher = new SoloStatisticsWatcher(statisticsProvider)); + Add(watcher = new UserStatisticsWatcher(statisticsProvider)); }); } diff --git a/osu.Game/Online/API/IAPIProvider.cs b/osu.Game/Online/API/IAPIProvider.cs index cea2d20d8d..e4d6b07037 100644 --- a/osu.Game/Online/API/IAPIProvider.cs +++ b/osu.Game/Online/API/IAPIProvider.cs @@ -124,6 +124,11 @@ namespace osu.Game.Online.API /// void Logout(); + /// + /// Schedule a callback to run on the update thread. + /// + internal void Schedule(Action action); + /// /// Constructs a new . May be null if not supported. /// diff --git a/osu.Game/Online/UserStatisticsWatcher.cs b/osu.Game/Online/UserStatisticsWatcher.cs index ea50966ad0..b63bdff17f 100644 --- a/osu.Game/Online/UserStatisticsWatcher.cs +++ b/osu.Game/Online/UserStatisticsWatcher.cs @@ -36,7 +36,7 @@ namespace osu.Game.Online private Dictionary? latestStatistics; - public SoloStatisticsWatcher(LocalUserStatisticsProvider? statisticsProvider = null) + public UserStatisticsWatcher(LocalUserStatisticsProvider? statisticsProvider = null) { this.statisticsProvider = statisticsProvider; } @@ -118,7 +118,7 @@ namespace osu.Game.Online { string rulesetName = scoreInfo.Ruleset.ShortName; - statisticsProvider?.UpdateStatistics(updatedStatistics, callback.Score.Ruleset); + statisticsProvider?.UpdateStatistics(updatedStatistics, scoreInfo.Ruleset); if (latestStatistics == null) return; diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs index dce24c6ee7..b420578024 100644 --- a/osu.Game/OsuGame.cs +++ b/osu.Game/OsuGame.cs @@ -1069,7 +1069,7 @@ namespace osu.Game ScreenStack.Push(CreateLoader().With(l => l.RelativeSizeAxes = Axes.Both)); }); - loadComponentSingleFile(new UserStatisticsWatcher(), Add, true); + loadComponentSingleFile(new UserStatisticsWatcher(LocalUserStatisticsProvider), Add, true); loadComponentSingleFile(Toolbar = new Toolbar { OnHome = delegate diff --git a/osu.Game/OsuGameBase.cs b/osu.Game/OsuGameBase.cs index f4b2f21ea9..7404eb232f 100644 --- a/osu.Game/OsuGameBase.cs +++ b/osu.Game/OsuGameBase.cs @@ -212,8 +212,8 @@ namespace osu.Game protected MultiplayerClient MultiplayerClient { get; private set; } private MetadataClient metadataClient; - private SoloStatisticsWatcher soloStatisticsWatcher; - private LocalUserStatisticsProvider localUserStatisticsProvider; + + protected LocalUserStatisticsProvider LocalUserStatisticsProvider { get; private set; } private RealmAccess realm; @@ -330,9 +330,7 @@ namespace osu.Game dependencies.CacheAs(SpectatorClient = new OnlineSpectatorClient(endpoints)); dependencies.CacheAs(MultiplayerClient = new OnlineMultiplayerClient(endpoints)); dependencies.CacheAs(metadataClient = new OnlineMetadataClient(endpoints)); - - dependencies.CacheAs(localUserStatisticsProvider = new LocalUserStatisticsProvider()); - dependencies.CacheAs(soloStatisticsWatcher = new SoloStatisticsWatcher(localUserStatisticsProvider)); + dependencies.CacheAs(LocalUserStatisticsProvider = new LocalUserStatisticsProvider()); base.Content.Add(new BeatmapOnlineChangeIngest(beatmapUpdater, realm, metadataClient)); @@ -375,8 +373,7 @@ namespace osu.Game base.Content.Add(SpectatorClient); base.Content.Add(MultiplayerClient); base.Content.Add(metadataClient); - base.Content.Add(localUserStatisticsProvider); - base.Content.Add(soloStatisticsWatcher); + base.Content.Add(LocalUserStatisticsProvider); base.Content.Add(rulesetConfigCache); From 3a57b21c89e6917a25ba9a748a7be7e1d86985e2 Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Fri, 25 Oct 2024 02:38:41 -0400 Subject: [PATCH 0036/1275] Move `LocalUserStatisticsProvider` to non-base game class and make dependency optional --- osu.Game/OsuGame.cs | 5 ++++- osu.Game/OsuGameBase.cs | 4 ---- osu.Game/Users/UserRankPanel.cs | 17 ++++++++++------- 3 files changed, 14 insertions(+), 12 deletions(-) diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs index b420578024..f7e6184dac 100644 --- a/osu.Game/OsuGame.cs +++ b/osu.Game/OsuGame.cs @@ -1069,7 +1069,10 @@ namespace osu.Game ScreenStack.Push(CreateLoader().With(l => l.RelativeSizeAxes = Axes.Both)); }); - loadComponentSingleFile(new UserStatisticsWatcher(LocalUserStatisticsProvider), Add, true); + LocalUserStatisticsProvider statisticsProvider; + + loadComponentSingleFile(statisticsProvider = new LocalUserStatisticsProvider(), Add, true); + loadComponentSingleFile(new UserStatisticsWatcher(statisticsProvider), Add, true); loadComponentSingleFile(Toolbar = new Toolbar { OnHome = delegate diff --git a/osu.Game/OsuGameBase.cs b/osu.Game/OsuGameBase.cs index 7404eb232f..d4704d1c72 100644 --- a/osu.Game/OsuGameBase.cs +++ b/osu.Game/OsuGameBase.cs @@ -213,8 +213,6 @@ namespace osu.Game private MetadataClient metadataClient; - protected LocalUserStatisticsProvider LocalUserStatisticsProvider { get; private set; } - private RealmAccess realm; protected SafeAreaContainer SafeAreaContainer { get; private set; } @@ -330,7 +328,6 @@ namespace osu.Game dependencies.CacheAs(SpectatorClient = new OnlineSpectatorClient(endpoints)); dependencies.CacheAs(MultiplayerClient = new OnlineMultiplayerClient(endpoints)); dependencies.CacheAs(metadataClient = new OnlineMetadataClient(endpoints)); - dependencies.CacheAs(LocalUserStatisticsProvider = new LocalUserStatisticsProvider()); base.Content.Add(new BeatmapOnlineChangeIngest(beatmapUpdater, realm, metadataClient)); @@ -373,7 +370,6 @@ namespace osu.Game base.Content.Add(SpectatorClient); base.Content.Add(MultiplayerClient); base.Content.Add(metadataClient); - base.Content.Add(LocalUserStatisticsProvider); base.Content.Add(rulesetConfigCache); diff --git a/osu.Game/Users/UserRankPanel.cs b/osu.Game/Users/UserRankPanel.cs index 70885940e1..a761ddaea7 100644 --- a/osu.Game/Users/UserRankPanel.cs +++ b/osu.Game/Users/UserRankPanel.cs @@ -45,19 +45,22 @@ namespace osu.Game.Users } [Resolved] - private LocalUserStatisticsProvider statisticsProvider { get; set; } = null!; + private LocalUserStatisticsProvider? statisticsProvider { get; set; } protected override void LoadComplete() { base.LoadComplete(); - statistics.BindTo(statisticsProvider.Statistics); - statistics.BindValueChanged(stats => + if (statisticsProvider != null) { - loadingLayer.State.Value = stats.NewValue == null ? Visibility.Visible : Visibility.Hidden; - globalRankDisplay.Content = stats.NewValue?.GlobalRank?.ToLocalisableString("\\##,##0") ?? "-"; - countryRankDisplay.Content = stats.NewValue?.CountryRank?.ToLocalisableString("\\##,##0") ?? "-"; - }, true); + statistics.BindTo(statisticsProvider.Statistics); + statistics.BindValueChanged(stats => + { + loadingLayer.State.Value = stats.NewValue == null ? Visibility.Visible : Visibility.Hidden; + globalRankDisplay.Content = stats.NewValue?.GlobalRank?.ToLocalisableString("\\##,##0") ?? "-"; + countryRankDisplay.Content = stats.NewValue?.CountryRank?.ToLocalisableString("\\##,##0") ?? "-"; + }, true); + } } protected override Drawable CreateLayout() From 44dd81363ac98fc6648e048928f42431bf0464dc Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Fri, 25 Oct 2024 03:06:41 -0400 Subject: [PATCH 0037/1275] Make `UserStatisticsWatcher` fully rely on `LocalUserStatisticsProvider` --- .../Online/LocalUserStatisticsProvider.cs | 7 +++ osu.Game/Online/UserStatisticsWatcher.cs | 49 ++----------------- 2 files changed, 12 insertions(+), 44 deletions(-) diff --git a/osu.Game/Online/LocalUserStatisticsProvider.cs b/osu.Game/Online/LocalUserStatisticsProvider.cs index e2f016b336..372bb090d6 100644 --- a/osu.Game/Online/LocalUserStatisticsProvider.cs +++ b/osu.Game/Online/LocalUserStatisticsProvider.cs @@ -33,6 +33,13 @@ namespace osu.Game.Online private readonly Dictionary allStatistics = new Dictionary(); + /// + /// Returns the currently available for the given ruleset. + /// This may return null if the requested statistics has not been fetched yet. + /// + /// The ruleset to return the corresponding for. + internal UserStatistics? GetStatisticsFor(RulesetInfo ruleset) => allStatistics.GetValueOrDefault(ruleset.ShortName); + protected override void LoadComplete() { base.LoadComplete(); diff --git a/osu.Game/Online/UserStatisticsWatcher.cs b/osu.Game/Online/UserStatisticsWatcher.cs index b63bdff17f..162204e4e8 100644 --- a/osu.Game/Online/UserStatisticsWatcher.cs +++ b/osu.Game/Online/UserStatisticsWatcher.cs @@ -2,7 +2,6 @@ // See the LICENCE file in the repository root for full licence text. using System.Collections.Generic; -using System.Linq; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Extensions.ObjectExtensions; @@ -10,7 +9,6 @@ using osu.Framework.Graphics; using osu.Game.Extensions; using osu.Game.Online.API; using osu.Game.Online.API.Requests; -using osu.Game.Online.API.Requests.Responses; using osu.Game.Online.Spectator; using osu.Game.Scoring; using osu.Game.Users; @@ -34,8 +32,6 @@ namespace osu.Game.Online private readonly Dictionary watchedScores = new Dictionary(); - private Dictionary? latestStatistics; - public UserStatisticsWatcher(LocalUserStatisticsProvider? statisticsProvider = null) { this.statisticsProvider = statisticsProvider; @@ -44,8 +40,6 @@ namespace osu.Game.Online protected override void LoadComplete() { base.LoadComplete(); - - api.LocalUser.BindValueChanged(user => onUserChanged(user.NewValue), true); spectatorClient.OnUserScoreProcessed += userScoreProcessed; } @@ -67,35 +61,6 @@ namespace osu.Game.Online }); } - private void onUserChanged(APIUser? localUser) => Schedule(() => - { - latestStatistics = null; - - if (localUser == null || localUser.OnlineID <= 1) - return; - - var userRequest = new GetUsersRequest(new[] { localUser.OnlineID }); - userRequest.Success += initialiseUserStatistics; - api.Queue(userRequest); - }); - - private void initialiseUserStatistics(GetUsersResponse response) => Schedule(() => - { - var user = response.Users.SingleOrDefault(); - - // possible if the user is restricted or similar. - if (user == null) - return; - - latestStatistics = new Dictionary(); - - if (user.RulesetsStatistics != null) - { - foreach (var rulesetStats in user.RulesetsStatistics) - latestStatistics.Add(rulesetStats.Key, rulesetStats.Value); - } - }); - private void userScoreProcessed(int userId, long scoreId) { if (userId != api.LocalUser.Value?.OnlineID) @@ -116,18 +81,14 @@ namespace osu.Game.Online private void dispatchStatisticsUpdate(ScoreInfo scoreInfo, UserStatistics updatedStatistics) { - string rulesetName = scoreInfo.Ruleset.ShortName; - - statisticsProvider?.UpdateStatistics(updatedStatistics, scoreInfo.Ruleset); - - if (latestStatistics == null) + if (statisticsProvider == null) return; - latestStatistics.TryGetValue(rulesetName, out UserStatistics? latestRulesetStatistics); - latestRulesetStatistics ??= new UserStatistics(); + var latestRulesetStatistics = statisticsProvider.GetStatisticsFor(scoreInfo.Ruleset); + statisticsProvider.UpdateStatistics(updatedStatistics, scoreInfo.Ruleset); - latestUpdate.Value = new UserStatisticsUpdate(scoreInfo, latestRulesetStatistics, updatedStatistics); - latestStatistics[rulesetName] = updatedStatistics; + if (latestRulesetStatistics != null) + latestUpdate.Value = new UserStatisticsUpdate(scoreInfo, latestRulesetStatistics, updatedStatistics); } protected override void Dispose(bool isDisposing) From 663b769c710644cd12479b4a690cc24503f538bc Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Fri, 25 Oct 2024 03:30:43 -0400 Subject: [PATCH 0038/1275] Update `DiscordRichPresence` to use new statistics provider component --- osu.Desktop/DiscordRichPresence.cs | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/osu.Desktop/DiscordRichPresence.cs b/osu.Desktop/DiscordRichPresence.cs index 5a7a01df1b..3ad4112733 100644 --- a/osu.Desktop/DiscordRichPresence.cs +++ b/osu.Desktop/DiscordRichPresence.cs @@ -15,6 +15,7 @@ using osu.Framework.Threading; using osu.Game; using osu.Game.Configuration; using osu.Game.Extensions; +using osu.Game.Online; using osu.Game.Online.API; using osu.Game.Online.API.Requests.Responses; using osu.Game.Online.Multiplayer; @@ -47,6 +48,9 @@ namespace osu.Desktop [Resolved] private MultiplayerClient multiplayerClient { get; set; } = null!; + [Resolved] + private LocalUserStatisticsProvider statisticsProvider { get; set; } = null!; + [Resolved] private OsuConfigManager config { get; set; } = null!; @@ -65,6 +69,7 @@ namespace osu.Desktop }; private IBindable? user; + private IBindable? localStatistics; [BackgroundDependencyLoader] private void load() @@ -117,6 +122,10 @@ namespace osu.Desktop status.BindValueChanged(_ => schedulePresenceUpdate()); activity.BindValueChanged(_ => schedulePresenceUpdate()); privacyMode.BindValueChanged(_ => schedulePresenceUpdate()); + + localStatistics = statisticsProvider.Statistics.GetBoundCopy(); + localStatistics.BindValueChanged(_ => schedulePresenceUpdate()); + multiplayerClient.RoomUpdated += onRoomUpdated; } @@ -158,7 +167,7 @@ namespace osu.Desktop private void updatePresence(bool hideIdentifiableInformation) { - if (user == null) + if (user == null || localStatistics == null) return; // user activity @@ -228,12 +237,7 @@ namespace osu.Desktop if (privacyMode.Value == DiscordRichPresenceMode.Limited) presence.Assets.LargeImageText = string.Empty; else - { - if (user.Value.RulesetsStatistics != null && user.Value.RulesetsStatistics.TryGetValue(ruleset.Value.ShortName, out UserStatistics? statistics)) - presence.Assets.LargeImageText = $"{user.Value.Username}" + (statistics.GlobalRank > 0 ? $" (rank #{statistics.GlobalRank:N0})" : string.Empty); - else - presence.Assets.LargeImageText = $"{user.Value.Username}" + (user.Value.Statistics?.GlobalRank > 0 ? $" (rank #{user.Value.Statistics.GlobalRank:N0})" : string.Empty); - } + presence.Assets.LargeImageText = $"{user.Value.Username}" + (localStatistics.Value?.GlobalRank > 0 ? $" (rank #{localStatistics.Value?.GlobalRank:N0})" : string.Empty); // small image presence.Assets.SmallImageKey = ruleset.Value.IsLegacyRuleset() ? $"mode_{ruleset.Value.OnlineID}" : "mode_custom"; From 979065c4212a425c9438c157089895c2eeac48de Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Sat, 26 Oct 2024 23:09:16 -0400 Subject: [PATCH 0039/1275] Reorder code slightly --- .../Online/LocalUserStatisticsProvider.cs | 28 +++++++++---------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/osu.Game/Online/LocalUserStatisticsProvider.cs b/osu.Game/Online/LocalUserStatisticsProvider.cs index 372bb090d6..e64f88759e 100644 --- a/osu.Game/Online/LocalUserStatisticsProvider.cs +++ b/osu.Game/Online/LocalUserStatisticsProvider.cs @@ -14,31 +14,24 @@ using osu.Game.Users; namespace osu.Game.Online { /// - /// A component that is responsible for providing the latest statistics of the logged-in user for the game-wide selected ruleset. + /// A component that keeps track of the latest statistics for the local user. /// public partial class LocalUserStatisticsProvider : Component { - /// - /// The statistics of the logged-in user for the game-wide selected ruleset. - /// - public IBindable Statistics => statistics; - - private readonly Bindable statistics = new Bindable(); - [Resolved] private IBindable ruleset { get; set; } = null!; [Resolved] private IAPIProvider api { get; set; } = null!; - private readonly Dictionary allStatistics = new Dictionary(); - /// - /// Returns the currently available for the given ruleset. - /// This may return null if the requested statistics has not been fetched yet. + /// The statistics of the local user for the game-wide selected ruleset. /// - /// The ruleset to return the corresponding for. - internal UserStatistics? GetStatisticsFor(RulesetInfo ruleset) => allStatistics.GetValueOrDefault(ruleset.ShortName); + public IBindable Statistics => statistics; + + private readonly Bindable statistics = new Bindable(); + + private readonly Dictionary allStatistics = new Dictionary(); protected override void LoadComplete() { @@ -90,6 +83,13 @@ namespace osu.Game.Online api.Queue(currentRequest); } + /// + /// Returns the currently available for the given ruleset. + /// This may return null if the requested statistics has not been fetched yet. + /// + /// The ruleset to return the corresponding for. + internal UserStatistics? GetStatisticsFor(RulesetInfo ruleset) => allStatistics.GetValueOrDefault(ruleset.ShortName); + internal void UpdateStatistics(UserStatistics statistics, RulesetInfo statisticsRuleset) { allStatistics[statisticsRuleset.ShortName] = statistics; From 99518f4a564ed2e14895c5744a25f3af4138db64 Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Mon, 4 Nov 2024 04:28:16 -0500 Subject: [PATCH 0040/1275] 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 4a628287e260e0120adc2dd102fcfc8c81930a78 Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Sun, 17 Nov 2024 18:13:37 -0500 Subject: [PATCH 0041/1275] Decouple game-wide ruleset bindable and refactor `LocalUserStatisticsProvider` This also throws away the logic of updating `API.LocalUser.Value.Statistics`. Components should rely on `LocalUserStatisticsProvider` instead for proper behaviour and ability to update on statistics updates. --- osu.Desktop/DiscordRichPresence.cs | 13 ++- .../Menus/TestSceneToolbarUserButton.cs | 12 +-- .../TestSceneLocalUserStatisticsProvider.cs | 88 +++++++++++------ .../Visual/Online/TestSceneUserPanel.cs | 10 +- .../Online/TestSceneUserStatisticsWatcher.cs | 19 ++-- .../Visual/Ranking/TestSceneOverallRanking.cs | 2 +- .../Ranking/TestSceneStatisticsPanel.cs | 6 +- .../Online/API/Requests/Responses/APIUser.cs | 12 ++- .../Online/LocalUserStatisticsProvider.cs | 98 ++++++++----------- ...e.cs => ScoreBasedUserStatisticsUpdate.cs} | 6 +- osu.Game/Online/UserStatisticsWatcher.cs | 38 ++++--- .../TransientUserStatisticsUpdateDisplay.cs | 4 +- .../Ranking/Statistics/User/OverallRanking.cs | 4 +- .../Statistics/User/RankingChangeRow.cs | 4 +- .../Ranking/Statistics/UserStatisticsPanel.cs | 4 +- osu.Game/Users/UserRankPanel.cs | 26 +++-- 16 files changed, 188 insertions(+), 158 deletions(-) rename osu.Game/Online/{UserStatisticsUpdate.cs => ScoreBasedUserStatisticsUpdate.cs} (84%) diff --git a/osu.Desktop/DiscordRichPresence.cs b/osu.Desktop/DiscordRichPresence.cs index 3ad4112733..c9529d2f5e 100644 --- a/osu.Desktop/DiscordRichPresence.cs +++ b/osu.Desktop/DiscordRichPresence.cs @@ -69,7 +69,7 @@ namespace osu.Desktop }; private IBindable? user; - private IBindable? localStatistics; + private IBindable? statisticsUpdate; [BackgroundDependencyLoader] private void load() @@ -123,8 +123,8 @@ namespace osu.Desktop activity.BindValueChanged(_ => schedulePresenceUpdate()); privacyMode.BindValueChanged(_ => schedulePresenceUpdate()); - localStatistics = statisticsProvider.Statistics.GetBoundCopy(); - localStatistics.BindValueChanged(_ => schedulePresenceUpdate()); + statisticsUpdate = statisticsProvider.StatisticsUpdate.GetBoundCopy(); + statisticsUpdate.BindValueChanged(_ => schedulePresenceUpdate()); multiplayerClient.RoomUpdated += onRoomUpdated; } @@ -167,7 +167,7 @@ namespace osu.Desktop private void updatePresence(bool hideIdentifiableInformation) { - if (user == null || localStatistics == null) + if (user == null) return; // user activity @@ -237,7 +237,10 @@ namespace osu.Desktop if (privacyMode.Value == DiscordRichPresenceMode.Limited) presence.Assets.LargeImageText = string.Empty; else - presence.Assets.LargeImageText = $"{user.Value.Username}" + (localStatistics.Value?.GlobalRank > 0 ? $" (rank #{localStatistics.Value?.GlobalRank:N0})" : string.Empty); + { + var statistics = statisticsProvider.GetStatisticsFor(ruleset.Value); + presence.Assets.LargeImageText = $"{user.Value.Username}" + (statistics?.GlobalRank > 0 ? $" (rank #{statistics.GlobalRank:N0})" : string.Empty); + } // small image presence.Assets.SmallImageKey = ruleset.Value.IsLegacyRuleset() ? $"mode_{ruleset.Value.OnlineID}" : "mode_custom"; diff --git a/osu.Game.Tests/Visual/Menus/TestSceneToolbarUserButton.cs b/osu.Game.Tests/Visual/Menus/TestSceneToolbarUserButton.cs index 71a45e2398..1af4af8f6b 100644 --- a/osu.Game.Tests/Visual/Menus/TestSceneToolbarUserButton.cs +++ b/osu.Game.Tests/Visual/Menus/TestSceneToolbarUserButton.cs @@ -101,7 +101,7 @@ namespace osu.Game.Tests.Visual.Menus AddStep("Gain", () => { var transientUpdateDisplay = this.ChildrenOfType().Single(); - transientUpdateDisplay.LatestUpdate.Value = new UserStatisticsUpdate( + transientUpdateDisplay.LatestUpdate.Value = new ScoreBasedUserStatisticsUpdate( new ScoreInfo(), new UserStatistics { @@ -118,7 +118,7 @@ namespace osu.Game.Tests.Visual.Menus AddStep("Loss", () => { var transientUpdateDisplay = this.ChildrenOfType().Single(); - transientUpdateDisplay.LatestUpdate.Value = new UserStatisticsUpdate( + transientUpdateDisplay.LatestUpdate.Value = new ScoreBasedUserStatisticsUpdate( new ScoreInfo(), new UserStatistics { @@ -136,7 +136,7 @@ namespace osu.Game.Tests.Visual.Menus AddStep("Tiny increase in PP", () => { var transientUpdateDisplay = this.ChildrenOfType().Single(); - transientUpdateDisplay.LatestUpdate.Value = new UserStatisticsUpdate( + transientUpdateDisplay.LatestUpdate.Value = new ScoreBasedUserStatisticsUpdate( new ScoreInfo(), new UserStatistics { @@ -153,7 +153,7 @@ namespace osu.Game.Tests.Visual.Menus AddStep("No change 1", () => { var transientUpdateDisplay = this.ChildrenOfType().Single(); - transientUpdateDisplay.LatestUpdate.Value = new UserStatisticsUpdate( + transientUpdateDisplay.LatestUpdate.Value = new ScoreBasedUserStatisticsUpdate( new ScoreInfo(), new UserStatistics { @@ -170,7 +170,7 @@ namespace osu.Game.Tests.Visual.Menus AddStep("Was null", () => { var transientUpdateDisplay = this.ChildrenOfType().Single(); - transientUpdateDisplay.LatestUpdate.Value = new UserStatisticsUpdate( + transientUpdateDisplay.LatestUpdate.Value = new ScoreBasedUserStatisticsUpdate( new ScoreInfo(), new UserStatistics { @@ -187,7 +187,7 @@ namespace osu.Game.Tests.Visual.Menus AddStep("Became null", () => { var transientUpdateDisplay = this.ChildrenOfType().Single(); - transientUpdateDisplay.LatestUpdate.Value = new UserStatisticsUpdate( + transientUpdateDisplay.LatestUpdate.Value = new ScoreBasedUserStatisticsUpdate( new ScoreInfo(), new UserStatistics { diff --git a/osu.Game.Tests/Visual/Online/TestSceneLocalUserStatisticsProvider.cs b/osu.Game.Tests/Visual/Online/TestSceneLocalUserStatisticsProvider.cs index 1a27fd1de5..342d805be4 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneLocalUserStatisticsProvider.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneLocalUserStatisticsProvider.cs @@ -3,14 +3,15 @@ using System.Collections.Generic; using NUnit.Framework; -using osu.Framework.Extensions.ObjectExtensions; +using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Testing; -using osu.Game.Graphics.Sprites; +using osu.Game.Graphics.Containers; using osu.Game.Online; using osu.Game.Online.API; using osu.Game.Online.API.Requests; using osu.Game.Online.API.Requests.Responses; +using osu.Game.Rulesets; using osu.Game.Rulesets.Catch; using osu.Game.Rulesets.Mania; using osu.Game.Rulesets.Osu; @@ -34,7 +35,7 @@ namespace osu.Game.Tests.Visual.Online AddStep("setup provider", () => { - OsuSpriteText text; + OsuTextFlowContainer text; ((DummyAPIAccess)API).HandleRequest = r => { @@ -59,17 +60,31 @@ namespace osu.Game.Tests.Visual.Online Clear(); Add(statisticsProvider = new LocalUserStatisticsProvider()); - Add(text = new OsuSpriteText + Add(text = new OsuTextFlowContainer { Anchor = Anchor.Centre, Origin = Anchor.Centre, }); - statisticsProvider.Statistics.BindValueChanged(s => + statisticsProvider.StatisticsUpdate.BindValueChanged(s => { - text.Text = s.NewValue == null - ? "Statistics: (null)" - : $"Statistics: (total score: {s.NewValue.TotalScore:N0})"; + text.Clear(); + + foreach (var ruleset in Dependencies.Get().AvailableRulesets) + { + text.AddText(statisticsProvider.GetStatisticsFor(ruleset) is UserStatistics statistics + ? $"{ruleset.Name} statistics: (total score: {statistics.TotalScore})" + : $"{ruleset.Name} statistics: (null)"); + text.NewLine(); + } + + if (s.NewValue == null) + text.AddText("latest update: (null)"); + else + { + text.AddText($"latest update: {s.NewValue.Ruleset}" + + $" ({(s.NewValue.OldStatistics?.TotalScore.ToString() ?? "null")} -> {s.NewValue.NewStatistics.TotalScore})"); + } }); Ruleset.Value = new OsuRuleset().RulesetInfo; @@ -79,19 +94,10 @@ namespace osu.Game.Tests.Visual.Online [Test] public void TestInitialStatistics() { - AddAssert("initial statistics populated", () => statisticsProvider.Statistics.Value.AsNonNull().TotalScore, () => Is.EqualTo(4_000_000)); - } - - [Test] - public void TestRulesetChanges() - { - AddAssert("statistics from osu", () => statisticsProvider.Statistics.Value.AsNonNull().TotalScore, () => Is.EqualTo(4_000_000)); - AddStep("change ruleset to taiko", () => Ruleset.Value = new TaikoRuleset().RulesetInfo); - AddAssert("statistics from taiko", () => statisticsProvider.Statistics.Value.AsNonNull().TotalScore, () => Is.EqualTo(3_000_000)); - AddStep("change ruleset to catch", () => Ruleset.Value = new CatchRuleset().RulesetInfo); - AddAssert("statistics from catch", () => statisticsProvider.Statistics.Value.AsNonNull().TotalScore, () => Is.EqualTo(2_000_000)); - AddStep("change ruleset to mania", () => Ruleset.Value = new ManiaRuleset().RulesetInfo); - AddAssert("statistics from mania", () => statisticsProvider.Statistics.Value.AsNonNull().TotalScore, () => Is.EqualTo(1_000_000)); + AddAssert("osu statistics populated", () => statisticsProvider.GetStatisticsFor(new OsuRuleset().RulesetInfo)!.TotalScore, () => Is.EqualTo(4_000_000)); + AddAssert("taiko statistics populated", () => statisticsProvider.GetStatisticsFor(new TaikoRuleset().RulesetInfo)!.TotalScore, () => Is.EqualTo(3_000_000)); + AddAssert("catch statistics populated", () => statisticsProvider.GetStatisticsFor(new CatchRuleset().RulesetInfo)!.TotalScore, () => Is.EqualTo(2_000_000)); + AddAssert("mania statistics populated", () => statisticsProvider.GetStatisticsFor(new ManiaRuleset().RulesetInfo)!.TotalScore, () => Is.EqualTo(1_000_000)); } [Test] @@ -105,18 +111,44 @@ namespace osu.Game.Tests.Visual.Online serverSideStatistics[(1000, "taiko")] = new UserStatistics { TotalScore = 6_000_000 }; }); - AddAssert("statistics matches user 1001 from osu", () => statisticsProvider.Statistics.Value.AsNonNull().TotalScore, () => Is.EqualTo(4_000_000)); + AddAssert("statistics matches user 1001 in osu", + () => statisticsProvider.GetStatisticsFor(new OsuRuleset().RulesetInfo)!.TotalScore, + () => Is.EqualTo(4_000_000)); - AddStep("change ruleset to taiko", () => Ruleset.Value = new TaikoRuleset().RulesetInfo); - AddAssert("statistics matches user 1001 from taiko", () => statisticsProvider.Statistics.Value.AsNonNull().TotalScore, () => Is.EqualTo(3_000_000)); + AddAssert("statistics matches user 1001 in taiko", + () => statisticsProvider.GetStatisticsFor(new TaikoRuleset().RulesetInfo)!.TotalScore, + () => Is.EqualTo(3_000_000)); - AddStep("change ruleset to osu", () => Ruleset.Value = new OsuRuleset().RulesetInfo); setUser(1000, false); - AddAssert("statistics matches user 1000 from osu", () => statisticsProvider.Statistics.Value.AsNonNull().TotalScore, () => Is.EqualTo(5_000_000)); + AddAssert("statistics matches user 1000 in osu", + () => statisticsProvider.GetStatisticsFor(new OsuRuleset().RulesetInfo)!.TotalScore, + () => Is.EqualTo(5_000_000)); - AddStep("change ruleset to osu", () => Ruleset.Value = new TaikoRuleset().RulesetInfo); - AddAssert("statistics matches user 1000 from taiko", () => statisticsProvider.Statistics.Value.AsNonNull().TotalScore, () => Is.EqualTo(6_000_000)); + AddAssert("statistics matches user 1000 in taiko", + () => statisticsProvider.GetStatisticsFor(new TaikoRuleset().RulesetInfo)!.TotalScore, + () => Is.EqualTo(6_000_000)); + } + + [Test] + public void TestRefetchStatistics() + { + setUser(1001); + + AddStep("update statistics server side", + () => serverSideStatistics[(1001, "osu")] = new UserStatistics { TotalScore = 9_000_000 }); + + AddAssert("statistics match old score", + () => statisticsProvider.GetStatisticsFor(new OsuRuleset().RulesetInfo)!.TotalScore, + () => Is.EqualTo(4_000_000)); + + AddStep("request refetch", () => statisticsProvider.RefetchStatistics(new OsuRuleset().RulesetInfo)); + AddUntilStep("statistics update raised", + () => statisticsProvider.StatisticsUpdate.Value.NewStatistics.TotalScore, + () => Is.EqualTo(9_000_000)); + AddAssert("statistics match new score", + () => statisticsProvider.GetStatisticsFor(new OsuRuleset().RulesetInfo)!.TotalScore, + () => Is.EqualTo(9_000_000)); } private UserStatistics tryGetStatistics(int userId, string rulesetName) diff --git a/osu.Game.Tests/Visual/Online/TestSceneUserPanel.cs b/osu.Game.Tests/Visual/Online/TestSceneUserPanel.cs index 365dce551c..e291b90361 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneUserPanel.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneUserPanel.cs @@ -34,8 +34,8 @@ namespace osu.Game.Tests.Visual.Online [Cached] private readonly OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Purple); - [Cached] - private readonly LocalUserStatisticsProvider statisticsProvider = new LocalUserStatisticsProvider(); + [Cached(typeof(LocalUserStatisticsProvider))] + private readonly TestUserStatisticsProvider statisticsProvider = new TestUserStatisticsProvider(); [Resolved] private IRulesetStore rulesetStore { get; set; } @@ -206,5 +206,11 @@ namespace osu.Game.Tests.Visual.Online public new TextFlowContainer LastVisitMessage => base.LastVisitMessage; } + + private partial class TestUserStatisticsProvider : LocalUserStatisticsProvider + { + public new void UpdateStatistics(UserStatistics newStatistics, RulesetInfo ruleset) + => base.UpdateStatistics(newStatistics, ruleset); + } } } diff --git a/osu.Game.Tests/Visual/Online/TestSceneUserStatisticsWatcher.cs b/osu.Game.Tests/Visual/Online/TestSceneUserStatisticsWatcher.cs index e5ccad703e..c91dfe9eb7 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneUserStatisticsWatcher.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneUserStatisticsWatcher.cs @@ -126,7 +126,7 @@ namespace osu.Game.Tests.Visual.Online var ruleset = new OsuRuleset().RulesetInfo; - UserStatisticsUpdate? update = null; + ScoreBasedUserStatisticsUpdate? update = null; registerForUpdates(scoreId, ruleset, receivedUpdate => update = receivedUpdate); feignScoreProcessing(userId, ruleset, 5_000_000); @@ -149,7 +149,7 @@ namespace osu.Game.Tests.Visual.Online // note ordering - in this test processing completes *before* the registration is added. feignScoreProcessing(userId, ruleset, 5_000_000); - UserStatisticsUpdate? update = null; + ScoreBasedUserStatisticsUpdate? update = null; registerForUpdates(scoreId, ruleset, receivedUpdate => update = receivedUpdate); AddStep("signal score processed", () => ((ISpectatorClient)spectatorClient).UserScoreProcessed(userId, scoreId)); @@ -167,7 +167,7 @@ namespace osu.Game.Tests.Visual.Online long scoreId = getScoreId(); var ruleset = new OsuRuleset().RulesetInfo; - UserStatisticsUpdate? update = null; + ScoreBasedUserStatisticsUpdate? update = null; registerForUpdates(scoreId, ruleset, receivedUpdate => update = receivedUpdate); feignScoreProcessing(userId, ruleset, 5_000_000); @@ -194,7 +194,7 @@ namespace osu.Game.Tests.Visual.Online long scoreId = getScoreId(); var ruleset = new OsuRuleset().RulesetInfo; - UserStatisticsUpdate? update = null; + ScoreBasedUserStatisticsUpdate? update = null; registerForUpdates(scoreId, ruleset, receivedUpdate => update = receivedUpdate); feignScoreProcessing(userId, ruleset, 5_000_000); @@ -215,7 +215,7 @@ namespace osu.Game.Tests.Visual.Online long scoreId = getScoreId(); var ruleset = new OsuRuleset().RulesetInfo; - UserStatisticsUpdate? update = null; + ScoreBasedUserStatisticsUpdate? update = null; registerForUpdates(scoreId, ruleset, receivedUpdate => update = receivedUpdate); feignScoreProcessing(userId, ruleset, 5_000_000); @@ -244,7 +244,7 @@ namespace osu.Game.Tests.Visual.Online feignScoreProcessing(userId, ruleset, 6_000_000); - UserStatisticsUpdate? update = null; + ScoreBasedUserStatisticsUpdate? update = null; registerForUpdates(secondScoreId, ruleset, receivedUpdate => update = receivedUpdate); AddStep("signal score processed", () => ((ISpectatorClient)spectatorClient).UserScoreProcessed(userId, secondScoreId)); @@ -262,15 +262,14 @@ namespace osu.Game.Tests.Visual.Online var ruleset = new OsuRuleset().RulesetInfo; - UserStatisticsUpdate? update = null; + ScoreBasedUserStatisticsUpdate? update = null; registerForUpdates(scoreId, ruleset, receivedUpdate => update = receivedUpdate); feignScoreProcessing(userId, ruleset, 5_000_000); AddStep("signal score processed", () => ((ISpectatorClient)spectatorClient).UserScoreProcessed(userId, scoreId)); AddUntilStep("update received", () => update != null); - AddAssert("local user values are correct", () => dummyAPI.LocalUser.Value.Statistics.TotalScore, () => Is.EqualTo(5_000_000)); - AddAssert("statistics values are correct", () => statisticsProvider.Statistics.Value!.TotalScore, () => Is.EqualTo(5_000_000)); + AddAssert("statistics values are correct", () => dummyAPI.LocalUser.Value.Statistics.TotalScore, () => Is.EqualTo(5_000_000)); } private int nextUserId = 2000; @@ -292,7 +291,7 @@ namespace osu.Game.Tests.Visual.Online }); } - private void registerForUpdates(long scoreId, RulesetInfo rulesetInfo, Action onUpdateReady) => + private void registerForUpdates(long scoreId, RulesetInfo rulesetInfo, Action onUpdateReady) => AddStep("register for updates", () => { watcher.RegisterForStatisticsUpdateAfter( diff --git a/osu.Game.Tests/Visual/Ranking/TestSceneOverallRanking.cs b/osu.Game.Tests/Visual/Ranking/TestSceneOverallRanking.cs index ffc7d88a34..b406ea369f 100644 --- a/osu.Game.Tests/Visual/Ranking/TestSceneOverallRanking.cs +++ b/osu.Game.Tests/Visual/Ranking/TestSceneOverallRanking.cs @@ -112,6 +112,6 @@ namespace osu.Game.Tests.Visual.Ranking }); private void displayUpdate(UserStatistics before, UserStatistics after) => - AddStep("display update", () => overallRanking.StatisticsUpdate.Value = new UserStatisticsUpdate(new ScoreInfo(), before, after)); + AddStep("display update", () => overallRanking.StatisticsUpdate.Value = new ScoreBasedUserStatisticsUpdate(new ScoreInfo(), before, after)); } } diff --git a/osu.Game.Tests/Visual/Ranking/TestSceneStatisticsPanel.cs b/osu.Game.Tests/Visual/Ranking/TestSceneStatisticsPanel.cs index f46f76cbb8..c12b9d29bc 100644 --- a/osu.Game.Tests/Visual/Ranking/TestSceneStatisticsPanel.cs +++ b/osu.Game.Tests/Visual/Ranking/TestSceneStatisticsPanel.cs @@ -91,12 +91,12 @@ namespace osu.Game.Tests.Visual.Ranking UserStatisticsWatcher userStatisticsWatcher = null!; ScoreInfo score = null!; - AddStep("create user statistics watcher", () => Add(userStatisticsWatcher = new UserStatisticsWatcher())); + AddStep("create user statistics watcher", () => Add(userStatisticsWatcher = new UserStatisticsWatcher(new LocalUserStatisticsProvider()))); AddStep("set user statistics update", () => { score = TestResources.CreateTestScoreInfo(); score.OnlineID = 1234; - ((Bindable)userStatisticsWatcher.LatestUpdate).Value = new UserStatisticsUpdate(score, + ((Bindable)userStatisticsWatcher.LatestUpdate).Value = new ScoreBasedUserStatisticsUpdate(score, new UserStatistics { Level = new UserStatistics.LevelInfo @@ -157,7 +157,7 @@ namespace osu.Game.Tests.Visual.Ranking Score = { Value = score }, DisplayedUserStatisticsUpdate = { - Value = new UserStatisticsUpdate(score, new UserStatistics + Value = new ScoreBasedUserStatisticsUpdate(score, new UserStatistics { Level = new UserStatistics.LevelInfo { diff --git a/osu.Game/Online/API/Requests/Responses/APIUser.cs b/osu.Game/Online/API/Requests/Responses/APIUser.cs index 5d80fde515..452b5f7654 100644 --- a/osu.Game/Online/API/Requests/Responses/APIUser.cs +++ b/osu.Game/Online/API/Requests/Responses/APIUser.cs @@ -223,12 +223,14 @@ namespace osu.Game.Online.API.Requests.Responses /// /// User statistics for the requested ruleset (in the case of a or response). - /// Otherwise empty. /// + /// + /// This returns null when accessed from . Use instead. + /// [JsonProperty(@"statistics")] public UserStatistics Statistics { - get => statistics ??= new UserStatistics(); + get => statistics; set { if (statistics != null) @@ -242,7 +244,11 @@ namespace osu.Game.Online.API.Requests.Responses [JsonProperty(@"rank_history")] private APIRankHistory rankHistory { - set => Statistics.RankHistory = value; + set + { + statistics ??= new UserStatistics(); + statistics.RankHistory = value; + } } [JsonProperty(@"active_tournament_banners")] diff --git a/osu.Game/Online/LocalUserStatisticsProvider.cs b/osu.Game/Online/LocalUserStatisticsProvider.cs index e64f88759e..ea4688a307 100644 --- a/osu.Game/Online/LocalUserStatisticsProvider.cs +++ b/osu.Game/Online/LocalUserStatisticsProvider.cs @@ -2,6 +2,7 @@ // 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; @@ -18,84 +19,63 @@ namespace osu.Game.Online /// public partial class LocalUserStatisticsProvider : Component { + private readonly Bindable statisticsUpdate = new Bindable(); + + /// + /// A bindable communicating updates to the local user's statistics on any ruleset. + /// This does not guarantee the presence of old statistics, as it is invoked on initial population of statistics. + /// + public IBindable StatisticsUpdate => statisticsUpdate; + [Resolved] - private IBindable ruleset { get; set; } = null!; + private RulesetStore rulesets { get; set; } = null!; [Resolved] private IAPIProvider api { get; set; } = null!; + private readonly Dictionary statisticsCache = new Dictionary(); + private readonly Dictionary statisticsRequests = new Dictionary(); + /// - /// The statistics of the local user for the game-wide selected ruleset. + /// Returns the currently available for the given ruleset. + /// This may return null if the requested statistics has not been fetched before yet. /// - public IBindable Statistics => statistics; - - private readonly Bindable statistics = new Bindable(); - - private readonly Dictionary allStatistics = new Dictionary(); + /// The ruleset to return the corresponding for. + public UserStatistics? GetStatisticsFor(RulesetInfo ruleset) => statisticsCache.GetValueOrDefault(ruleset.ShortName); protected override void LoadComplete() { base.LoadComplete(); - - statistics.BindValueChanged(v => - { - if (api.LocalUser.Value != null && v.NewValue != null) - api.LocalUser.Value.Statistics = v.NewValue; - }); - - ruleset.BindValueChanged(_ => updateStatisticsBindable()); - - api.LocalUser.BindValueChanged(_ => - { - allStatistics.Clear(); - updateStatisticsBindable(); - }, true); + api.LocalUser.BindValueChanged(_ => initialiseStatistics(), true); } - private GetUserRequest? currentRequest; - - private void updateStatisticsBindable() => Schedule(() => + private void initialiseStatistics() { - statistics.Value = null; + statisticsCache.Clear(); - if (api.LocalUser.Value == null || api.LocalUser.Value.OnlineID <= 1 || !ruleset.Value.IsLegacyRuleset()) - { - statistics.Value = new UserStatistics(); - return; - } - - if (currentRequest?.CompletionState == APIRequestCompletionState.Waiting) - { - currentRequest.Cancel(); - currentRequest = null; - } - - if (allStatistics.TryGetValue(ruleset.Value.ShortName, out var existing)) - statistics.Value = existing; - else - requestStatistics(ruleset.Value); - }); - - private void requestStatistics(RulesetInfo ruleset) - { - currentRequest = new GetUserRequest(api.LocalUser.Value.OnlineID, ruleset); - currentRequest.Success += u => statistics.Value = allStatistics[ruleset.ShortName] = u.Statistics; - api.Queue(currentRequest); + foreach (var ruleset in rulesets.AvailableRulesets.Where(r => r.IsLegacyRuleset())) + RefetchStatistics(ruleset); } - /// - /// Returns the currently available for the given ruleset. - /// This may return null if the requested statistics has not been fetched yet. - /// - /// The ruleset to return the corresponding for. - internal UserStatistics? GetStatisticsFor(RulesetInfo ruleset) => allStatistics.GetValueOrDefault(ruleset.ShortName); - - internal void UpdateStatistics(UserStatistics statistics, RulesetInfo statisticsRuleset) + public void RefetchStatistics(RulesetInfo ruleset) { - allStatistics[statisticsRuleset.ShortName] = statistics; + if (statisticsRequests.TryGetValue(ruleset.ShortName, out var previousRequest)) + previousRequest.Cancel(); - if (statisticsRuleset.ShortName == ruleset.Value.ShortName) - updateStatisticsBindable(); + var request = statisticsRequests[ruleset.ShortName] = new GetUserRequest(api.LocalUser.Value.Id, ruleset); + request.Success += u => UpdateStatistics(u.Statistics, ruleset); + api.Queue(request); + } + + protected void UpdateStatistics(UserStatistics newStatistics, RulesetInfo ruleset) + { + var oldStatistics = statisticsCache.GetValueOrDefault(ruleset.ShortName); + + statisticsRequests.Remove(ruleset.ShortName); + statisticsCache[ruleset.ShortName] = newStatistics; + statisticsUpdate.Value = new UserStatisticsUpdate(ruleset, oldStatistics, newStatistics); } } + + public record UserStatisticsUpdate(RulesetInfo Ruleset, UserStatistics? OldStatistics, UserStatistics NewStatistics); } diff --git a/osu.Game/Online/UserStatisticsUpdate.cs b/osu.Game/Online/ScoreBasedUserStatisticsUpdate.cs similarity index 84% rename from osu.Game/Online/UserStatisticsUpdate.cs rename to osu.Game/Online/ScoreBasedUserStatisticsUpdate.cs index f85b219ef0..dc55c57c68 100644 --- a/osu.Game/Online/UserStatisticsUpdate.cs +++ b/osu.Game/Online/ScoreBasedUserStatisticsUpdate.cs @@ -9,7 +9,7 @@ namespace osu.Game.Online /// /// Contains data about the change in a user's profile statistics after completing a score. /// - public class UserStatisticsUpdate + public class ScoreBasedUserStatisticsUpdate { /// /// The score set by the user that triggered the update. @@ -27,12 +27,12 @@ namespace osu.Game.Online public UserStatistics After { get; } /// - /// Creates a new . + /// Creates a new . /// /// The score set by the user that triggered the update. /// The user's profile statistics prior to the score being set. /// The user's profile statistics after the score was set. - public UserStatisticsUpdate(ScoreInfo score, UserStatistics before, UserStatistics after) + public ScoreBasedUserStatisticsUpdate(ScoreInfo score, UserStatistics before, UserStatistics after) { Score = score; Before = before; diff --git a/osu.Game/Online/UserStatisticsWatcher.cs b/osu.Game/Online/UserStatisticsWatcher.cs index 162204e4e8..bd3c4b819f 100644 --- a/osu.Game/Online/UserStatisticsWatcher.cs +++ b/osu.Game/Online/UserStatisticsWatcher.cs @@ -8,10 +8,8 @@ using osu.Framework.Extensions.ObjectExtensions; using osu.Framework.Graphics; using osu.Game.Extensions; using osu.Game.Online.API; -using osu.Game.Online.API.Requests; using osu.Game.Online.Spectator; using osu.Game.Scoring; -using osu.Game.Users; namespace osu.Game.Online { @@ -20,9 +18,12 @@ namespace osu.Game.Online /// public partial class UserStatisticsWatcher : Component { - private readonly LocalUserStatisticsProvider? statisticsProvider; - public IBindable LatestUpdate => latestUpdate; - private readonly Bindable latestUpdate = new Bindable(); + private readonly LocalUserStatisticsProvider statisticsProvider; + + public IBindable LatestUpdate => latestUpdate; + private readonly Bindable latestUpdate = new Bindable(); + + private ScoreInfo? scorePendingUpdate; [Resolved] private SpectatorClient spectatorClient { get; set; } = null!; @@ -32,7 +33,7 @@ namespace osu.Game.Online private readonly Dictionary watchedScores = new Dictionary(); - public UserStatisticsWatcher(LocalUserStatisticsProvider? statisticsProvider = null) + public UserStatisticsWatcher(LocalUserStatisticsProvider statisticsProvider) { this.statisticsProvider = statisticsProvider; } @@ -40,7 +41,9 @@ namespace osu.Game.Online protected override void LoadComplete() { base.LoadComplete(); + spectatorClient.OnUserScoreProcessed += userScoreProcessed; + statisticsProvider.StatisticsUpdate.ValueChanged += onStatisticsUpdated; } /// @@ -69,27 +72,20 @@ namespace osu.Game.Online if (!watchedScores.Remove(scoreId, out var scoreInfo)) return; - requestStatisticsUpdate(userId, scoreInfo); + scorePendingUpdate = scoreInfo; + statisticsProvider.RefetchStatistics(scoreInfo.Ruleset); } - private void requestStatisticsUpdate(int userId, ScoreInfo scoreInfo) + private void onStatisticsUpdated(ValueChangedEvent update) => Schedule(() => { - var request = new GetUserRequest(userId, scoreInfo.Ruleset); - request.Success += user => Schedule(() => dispatchStatisticsUpdate(scoreInfo, user.Statistics)); - api.Queue(request); - } - - private void dispatchStatisticsUpdate(ScoreInfo scoreInfo, UserStatistics updatedStatistics) - { - if (statisticsProvider == null) + if (scorePendingUpdate == null || !update.NewValue.Ruleset.Equals(scorePendingUpdate.Ruleset)) return; - var latestRulesetStatistics = statisticsProvider.GetStatisticsFor(scoreInfo.Ruleset); - statisticsProvider.UpdateStatistics(updatedStatistics, scoreInfo.Ruleset); + if (update.NewValue.OldStatistics != null) + latestUpdate.Value = new ScoreBasedUserStatisticsUpdate(scorePendingUpdate, update.NewValue.OldStatistics, update.NewValue.NewStatistics); - if (latestRulesetStatistics != null) - latestUpdate.Value = new UserStatisticsUpdate(scoreInfo, latestRulesetStatistics, updatedStatistics); - } + scorePendingUpdate = null; + }); protected override void Dispose(bool isDisposing) { diff --git a/osu.Game/Overlays/Toolbar/TransientUserStatisticsUpdateDisplay.cs b/osu.Game/Overlays/Toolbar/TransientUserStatisticsUpdateDisplay.cs index 07c2e72774..d5891da936 100644 --- a/osu.Game/Overlays/Toolbar/TransientUserStatisticsUpdateDisplay.cs +++ b/osu.Game/Overlays/Toolbar/TransientUserStatisticsUpdateDisplay.cs @@ -21,7 +21,7 @@ namespace osu.Game.Overlays.Toolbar { public partial class TransientUserStatisticsUpdateDisplay : CompositeDrawable { - public Bindable LatestUpdate { get; } = new Bindable(); + public Bindable LatestUpdate { get; } = new Bindable(); private Statistic globalRank = null!; private Statistic pp = null!; @@ -48,7 +48,7 @@ namespace osu.Game.Overlays.Toolbar }; if (userStatisticsWatcher != null) - ((IBindable)LatestUpdate).BindTo(userStatisticsWatcher.LatestUpdate); + ((IBindable)LatestUpdate).BindTo(userStatisticsWatcher.LatestUpdate); } protected override void LoadComplete() diff --git a/osu.Game/Screens/Ranking/Statistics/User/OverallRanking.cs b/osu.Game/Screens/Ranking/Statistics/User/OverallRanking.cs index 1e60e09486..171a3f0f65 100644 --- a/osu.Game/Screens/Ranking/Statistics/User/OverallRanking.cs +++ b/osu.Game/Screens/Ranking/Statistics/User/OverallRanking.cs @@ -14,7 +14,7 @@ namespace osu.Game.Screens.Ranking.Statistics.User { private const float transition_duration = 300; - public Bindable StatisticsUpdate { get; } = new Bindable(); + public Bindable StatisticsUpdate { get; } = new Bindable(); private LoadingLayer loadingLayer = null!; private GridContainer content = null!; @@ -86,7 +86,7 @@ namespace osu.Game.Screens.Ranking.Statistics.User FinishTransforms(true); } - private void onUpdateReceived(ValueChangedEvent update) + private void onUpdateReceived(ValueChangedEvent update) { if (update.NewValue == null) { diff --git a/osu.Game/Screens/Ranking/Statistics/User/RankingChangeRow.cs b/osu.Game/Screens/Ranking/Statistics/User/RankingChangeRow.cs index e5f07d9891..e6a6530345 100644 --- a/osu.Game/Screens/Ranking/Statistics/User/RankingChangeRow.cs +++ b/osu.Game/Screens/Ranking/Statistics/User/RankingChangeRow.cs @@ -19,7 +19,7 @@ namespace osu.Game.Screens.Ranking.Statistics.User { public abstract partial class RankingChangeRow : CompositeDrawable { - public Bindable StatisticsUpdate { get; } = new Bindable(); + public Bindable StatisticsUpdate { get; } = new Bindable(); private readonly Func accessor; @@ -113,7 +113,7 @@ namespace osu.Game.Screens.Ranking.Statistics.User StatisticsUpdate.BindValueChanged(onStatisticsUpdate, true); } - private void onStatisticsUpdate(ValueChangedEvent statisticsUpdate) + private void onStatisticsUpdate(ValueChangedEvent statisticsUpdate) { var update = statisticsUpdate.NewValue; diff --git a/osu.Game/Screens/Ranking/Statistics/UserStatisticsPanel.cs b/osu.Game/Screens/Ranking/Statistics/UserStatisticsPanel.cs index 4e9c07ab7b..86fed4a9bb 100644 --- a/osu.Game/Screens/Ranking/Statistics/UserStatisticsPanel.cs +++ b/osu.Game/Screens/Ranking/Statistics/UserStatisticsPanel.cs @@ -18,9 +18,9 @@ namespace osu.Game.Screens.Ranking.Statistics { private readonly ScoreInfo achievedScore; - internal readonly Bindable DisplayedUserStatisticsUpdate = new Bindable(); + internal readonly Bindable DisplayedUserStatisticsUpdate = new Bindable(); - private IBindable latestGlobalStatisticsUpdate = null!; + private IBindable latestGlobalStatisticsUpdate = null!; public UserStatisticsPanel(ScoreInfo achievedScore) { diff --git a/osu.Game/Users/UserRankPanel.cs b/osu.Game/Users/UserRankPanel.cs index 5804fce4c1..6fa926998e 100644 --- a/osu.Game/Users/UserRankPanel.cs +++ b/osu.Game/Users/UserRankPanel.cs @@ -12,6 +12,7 @@ using osu.Game.Online; using osu.Game.Online.API.Requests.Responses; using osu.Game.Overlays.Profile.Header.Components; using osu.Game.Resources.Localisation.Web; +using osu.Game.Rulesets; using osuTK; namespace osu.Game.Users @@ -29,8 +30,6 @@ namespace osu.Game.Users private ProfileValueDisplay countryRankDisplay = null!; private LoadingLayer loadingLayer = null!; - private readonly IBindable statistics = new Bindable(); - public UserRankPanel(APIUser user) : base(user) { @@ -47,22 +46,31 @@ namespace osu.Game.Users [Resolved] private LocalUserStatisticsProvider? statisticsProvider { get; set; } + [Resolved] + private IBindable ruleset { get; set; } = null!; + + private IBindable statisticsUpdate = null!; + protected override void LoadComplete() { base.LoadComplete(); if (statisticsProvider != null) { - statistics.BindTo(statisticsProvider.Statistics); - statistics.BindValueChanged(stats => - { - loadingLayer.State.Value = stats.NewValue == null ? Visibility.Visible : Visibility.Hidden; - globalRankDisplay.Content = stats.NewValue?.GlobalRank?.ToLocalisableString("\\##,##0") ?? "-"; - countryRankDisplay.Content = stats.NewValue?.CountryRank?.ToLocalisableString("\\##,##0") ?? "-"; - }, true); + statisticsUpdate = statisticsProvider.StatisticsUpdate.GetBoundCopy(); + statisticsUpdate.BindValueChanged(_ => updateDisplay(), true); } } + private void updateDisplay() + { + var statistics = statisticsProvider?.GetStatisticsFor(ruleset.Value); + + loadingLayer.State.Value = statistics == null ? Visibility.Visible : Visibility.Hidden; + globalRankDisplay.Content = statistics?.GlobalRank?.ToLocalisableString("\\##,##0") ?? "-"; + countryRankDisplay.Content = statistics?.CountryRank?.ToLocalisableString("\\##,##0") ?? "-"; + } + protected override Drawable CreateLayout() { FillFlowContainer details; From 28f87407f6abb1075de854a10cb110fa8d0be3d3 Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Sun, 17 Nov 2024 18:15:38 -0500 Subject: [PATCH 0042/1275] Make `DifficultyRecommender` rely on the statistics provider --- osu.Game/Beatmaps/DifficultyRecommender.cs | 74 +++++++++++----------- osu.Game/OsuGame.cs | 6 +- 2 files changed, 40 insertions(+), 40 deletions(-) diff --git a/osu.Game/Beatmaps/DifficultyRecommender.cs b/osu.Game/Beatmaps/DifficultyRecommender.cs index ec00756fd9..4d883e5327 100644 --- a/osu.Game/Beatmaps/DifficultyRecommender.cs +++ b/osu.Game/Beatmaps/DifficultyRecommender.cs @@ -10,7 +10,7 @@ using JetBrains.Annotations; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; -using osu.Game.Online.API; +using osu.Game.Online; using osu.Game.Rulesets; namespace osu.Game.Beatmaps @@ -21,18 +21,49 @@ namespace osu.Game.Beatmaps /// public partial class DifficultyRecommender : Component { - [Resolved] - private IAPIProvider api { get; set; } + private readonly LocalUserStatisticsProvider statisticsProvider; [Resolved] private Bindable ruleset { get; set; } private readonly Dictionary recommendedDifficultyMapping = new Dictionary(); - [BackgroundDependencyLoader] - private void load() + /// + /// Rulesets ordered descending by their respective recommended difficulties. + /// The currently selected ruleset will always be first. + /// + private IEnumerable orderedRulesets { - api.LocalUser.BindValueChanged(_ => populateValues(), true); + get + { + if (LoadState < LoadState.Ready || ruleset.Value == null) + return Enumerable.Empty(); + + return recommendedDifficultyMapping + .OrderByDescending(pair => pair.Value) + .Select(pair => pair.Key) + .Where(r => !r.Equals(ruleset.Value.ShortName, StringComparison.Ordinal)) + .Prepend(ruleset.Value.ShortName); + } + } + + private IBindable statisticsUpdate = null!; + + public DifficultyRecommender(LocalUserStatisticsProvider statisticsProvider) + { + this.statisticsProvider = statisticsProvider; + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + statisticsUpdate = statisticsProvider.StatisticsUpdate.GetBoundCopy(); + statisticsUpdate.BindValueChanged(u => + { + // algorithm taken from https://github.com/ppy/osu-web/blob/e6e2825516449e3d0f3f5e1852c6bdd3428c3437/app/Models/User.php#L1505 + recommendedDifficultyMapping[u.NewValue.Ruleset.ShortName] = Math.Pow((double)(u.NewValue.NewStatistics.PP ?? 0), 0.4) * 0.195; + }, true); } /// @@ -63,36 +94,5 @@ namespace osu.Game.Beatmaps return null; } - - private void populateValues() - { - if (api.LocalUser.Value.RulesetsStatistics == null) - return; - - foreach (var kvp in api.LocalUser.Value.RulesetsStatistics) - { - // algorithm taken from https://github.com/ppy/osu-web/blob/e6e2825516449e3d0f3f5e1852c6bdd3428c3437/app/Models/User.php#L1505 - recommendedDifficultyMapping[kvp.Key] = Math.Pow((double)(kvp.Value.PP ?? 0), 0.4) * 0.195; - } - } - - /// - /// Rulesets ordered descending by their respective recommended difficulties. - /// The currently selected ruleset will always be first. - /// - private IEnumerable orderedRulesets - { - get - { - if (LoadState < LoadState.Ready || ruleset.Value == null) - return Enumerable.Empty(); - - return recommendedDifficultyMapping - .OrderByDescending(pair => pair.Value) - .Select(pair => pair.Key) - .Where(r => !r.Equals(ruleset.Value.ShortName, StringComparison.Ordinal)) - .Prepend(ruleset.Value.ShortName); - } - } } } diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs index f7e6184dac..b87ad33902 100644 --- a/osu.Game/OsuGame.cs +++ b/osu.Game/OsuGame.cs @@ -148,8 +148,7 @@ namespace osu.Game [Resolved] private FrameworkConfigManager frameworkConfig { get; set; } - [Cached] - private readonly DifficultyRecommender difficultyRecommender = new DifficultyRecommender(); + private DifficultyRecommender difficultyRecommender; [Cached] private readonly LegacyImportManager legacyImportManager = new LegacyImportManager(); @@ -1142,7 +1141,8 @@ namespace osu.Game loadComponentSingleFile(new BackgroundDataStoreProcessor(), Add); loadComponentSingleFile(new DetachedBeatmapStore(), Add, true); - Add(difficultyRecommender); + loadComponentSingleFile(difficultyRecommender = new DifficultyRecommender(statisticsProvider), Add, true); + Add(externalLinkOpener = new ExternalLinkOpener()); Add(new MusicKeyBindingHandler()); Add(new OnlineStatusNotifier(() => ScreenStack.CurrentScreen)); From 07609b62677d24bfc17e9a1b387775186566c26a Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Sun, 17 Nov 2024 18:32:12 -0500 Subject: [PATCH 0043/1275] Fix `UserRankPanel` not updating on ruleset change --- osu.Game/Users/UserRankPanel.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/osu.Game/Users/UserRankPanel.cs b/osu.Game/Users/UserRankPanel.cs index 6fa926998e..4f2a252539 100644 --- a/osu.Game/Users/UserRankPanel.cs +++ b/osu.Game/Users/UserRankPanel.cs @@ -58,7 +58,8 @@ namespace osu.Game.Users if (statisticsProvider != null) { statisticsUpdate = statisticsProvider.StatisticsUpdate.GetBoundCopy(); - statisticsUpdate.BindValueChanged(_ => updateDisplay(), true); + statisticsUpdate.BindValueChanged(_ => updateDisplay()); + ruleset.BindValueChanged(_ => updateDisplay(), true); } } From 1847b679dbfcc1279c82e6e04f73d3b3ce3a9e0a Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Sun, 17 Nov 2024 18:45:05 -0500 Subject: [PATCH 0044/1275] Only update user rank panel display when ruleset matches Nothing behaviourally different, just reduce number of redundant calls. --- osu.Game/Users/UserRankPanel.cs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/osu.Game/Users/UserRankPanel.cs b/osu.Game/Users/UserRankPanel.cs index 4f2a252539..c66dd8ef49 100644 --- a/osu.Game/Users/UserRankPanel.cs +++ b/osu.Game/Users/UserRankPanel.cs @@ -58,7 +58,12 @@ namespace osu.Game.Users if (statisticsProvider != null) { statisticsUpdate = statisticsProvider.StatisticsUpdate.GetBoundCopy(); - statisticsUpdate.BindValueChanged(_ => updateDisplay()); + statisticsUpdate.BindValueChanged(u => + { + if (u.NewValue.Ruleset.Equals(ruleset.Value)) + updateDisplay(); + }); + ruleset.BindValueChanged(_ => updateDisplay(), true); } } From caf56afba6bb12383ae5d2695c5c8136c6d479cc Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Sun, 17 Nov 2024 19:13:23 -0500 Subject: [PATCH 0045/1275] Fix various test failures --- .../Online/TestSceneUserStatisticsWatcher.cs | 2 +- .../TestSceneBeatmapRecommendations.cs | 32 +++++++++++-------- osu.Game/Beatmaps/DifficultyRecommender.cs | 3 ++ .../Online/API/Requests/Responses/APIUser.cs | 8 ++--- 4 files changed, 25 insertions(+), 20 deletions(-) diff --git a/osu.Game.Tests/Visual/Online/TestSceneUserStatisticsWatcher.cs b/osu.Game.Tests/Visual/Online/TestSceneUserStatisticsWatcher.cs index c91dfe9eb7..d410b7f3a4 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneUserStatisticsWatcher.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneUserStatisticsWatcher.cs @@ -269,7 +269,7 @@ namespace osu.Game.Tests.Visual.Online AddStep("signal score processed", () => ((ISpectatorClient)spectatorClient).UserScoreProcessed(userId, scoreId)); AddUntilStep("update received", () => update != null); - AddAssert("statistics values are correct", () => dummyAPI.LocalUser.Value.Statistics.TotalScore, () => Is.EqualTo(5_000_000)); + AddAssert("statistics values are correct", () => statisticsProvider.GetStatisticsFor(ruleset)!.TotalScore, () => Is.EqualTo(5_000_000)); } private int nextUserId = 2000; diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapRecommendations.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapRecommendations.cs index 66862e1b78..d5b0b3b1b0 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapRecommendations.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapRecommendations.cs @@ -8,14 +8,14 @@ using System.Collections.Generic; using System.Diagnostics; using System.Linq; using NUnit.Framework; -using osu.Framework.Allocation; -using osu.Framework.Extensions.IEnumerableExtensions; using osu.Framework.Platform; using osu.Framework.Testing; using osu.Game.Beatmaps; using osu.Game.Database; using osu.Game.Extensions; using osu.Game.Online.API; +using osu.Game.Online.API.Requests; +using osu.Game.Online.API.Requests.Responses; using osu.Game.Rulesets; using osu.Game.Rulesets.Catch; using osu.Game.Rulesets.Mania; @@ -28,25 +28,31 @@ namespace osu.Game.Tests.Visual.SongSelect { public partial class TestSceneBeatmapRecommendations : OsuGameTestScene { - [Resolved] - private IRulesetStore rulesetStore { get; set; } - [SetUpSteps] public override void SetUpSteps() { AddStep("populate ruleset statistics", () => { - Dictionary rulesetStatistics = new Dictionary(); - - rulesetStore.AvailableRulesets.Where(ruleset => ruleset.IsLegacyRuleset()).ForEach(rulesetInfo => + ((DummyAPIAccess)API).HandleRequest += r => { - rulesetStatistics[rulesetInfo.ShortName] = new UserStatistics + switch (r) { - PP = getNecessaryPP(rulesetInfo.OnlineID) - }; - }); + case GetUserRequest userRequest: + userRequest.TriggerSuccess(new APIUser + { + Id = 99, + Statistics = new UserStatistics + { + PP = getNecessaryPP(userRequest.Ruleset?.OnlineID ?? 0) + } + }); - API.LocalUser.Value.RulesetsStatistics = rulesetStatistics; + return true; + + default: + return false; + } + }; }); decimal getNecessaryPP(int? rulesetID) diff --git a/osu.Game/Beatmaps/DifficultyRecommender.cs b/osu.Game/Beatmaps/DifficultyRecommender.cs index 4d883e5327..0c75f19658 100644 --- a/osu.Game/Beatmaps/DifficultyRecommender.cs +++ b/osu.Game/Beatmaps/DifficultyRecommender.cs @@ -61,6 +61,9 @@ namespace osu.Game.Beatmaps statisticsUpdate = statisticsProvider.StatisticsUpdate.GetBoundCopy(); statisticsUpdate.BindValueChanged(u => { + if (u.NewValue == null) + return; + // algorithm taken from https://github.com/ppy/osu-web/blob/e6e2825516449e3d0f3f5e1852c6bdd3428c3437/app/Models/User.php#L1505 recommendedDifficultyMapping[u.NewValue.Ruleset.ShortName] = Math.Pow((double)(u.NewValue.NewStatistics.PP ?? 0), 0.4) * 0.195; }, true); diff --git a/osu.Game/Online/API/Requests/Responses/APIUser.cs b/osu.Game/Online/API/Requests/Responses/APIUser.cs index 452b5f7654..a829484506 100644 --- a/osu.Game/Online/API/Requests/Responses/APIUser.cs +++ b/osu.Game/Online/API/Requests/Responses/APIUser.cs @@ -230,7 +230,7 @@ namespace osu.Game.Online.API.Requests.Responses [JsonProperty(@"statistics")] public UserStatistics Statistics { - get => statistics; + get => statistics ??= new UserStatistics(); set { if (statistics != null) @@ -244,11 +244,7 @@ namespace osu.Game.Online.API.Requests.Responses [JsonProperty(@"rank_history")] private APIRankHistory rankHistory { - set - { - statistics ??= new UserStatistics(); - statistics.RankHistory = value; - } + set => Statistics.RankHistory = value; } [JsonProperty(@"active_tournament_banners")] From b1068336638f426231b39aeeba0b27b515c1d1a5 Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Sun, 17 Nov 2024 20:35:59 -0500 Subject: [PATCH 0046/1275] Fix more test / component breakage --- .../TestSceneBeatmapRecommendations.cs | 2 +- osu.Game/Beatmaps/DifficultyRecommender.cs | 35 +++++++++++++------ osu.Game/OsuGame.cs | 3 +- 3 files changed, 26 insertions(+), 14 deletions(-) diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapRecommendations.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapRecommendations.cs index d5b0b3b1b0..bd5c43d242 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapRecommendations.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapRecommendations.cs @@ -33,7 +33,7 @@ namespace osu.Game.Tests.Visual.SongSelect { AddStep("populate ruleset statistics", () => { - ((DummyAPIAccess)API).HandleRequest += r => + ((DummyAPIAccess)API).HandleRequest = r => { switch (r) { diff --git a/osu.Game/Beatmaps/DifficultyRecommender.cs b/osu.Game/Beatmaps/DifficultyRecommender.cs index 0c75f19658..bf81356407 100644 --- a/osu.Game/Beatmaps/DifficultyRecommender.cs +++ b/osu.Game/Beatmaps/DifficultyRecommender.cs @@ -12,6 +12,7 @@ using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Game.Online; using osu.Game.Rulesets; +using osu.Game.Users; namespace osu.Game.Beatmaps { @@ -24,7 +25,10 @@ namespace osu.Game.Beatmaps private readonly LocalUserStatisticsProvider statisticsProvider; [Resolved] - private Bindable ruleset { get; set; } + private Bindable gameRuleset { get; set; } + + [Resolved] + private RulesetStore rulesets { get; set; } = null!; private readonly Dictionary recommendedDifficultyMapping = new Dictionary(); @@ -36,14 +40,14 @@ namespace osu.Game.Beatmaps { get { - if (LoadState < LoadState.Ready || ruleset.Value == null) + if (LoadState < LoadState.Ready || gameRuleset.Value == null) return Enumerable.Empty(); return recommendedDifficultyMapping .OrderByDescending(pair => pair.Value) .Select(pair => pair.Key) - .Where(r => !r.Equals(ruleset.Value.ShortName, StringComparison.Ordinal)) - .Prepend(ruleset.Value.ShortName); + .Where(r => !r.Equals(gameRuleset.Value.ShortName, StringComparison.Ordinal)) + .Prepend(gameRuleset.Value.ShortName); } } @@ -54,19 +58,28 @@ namespace osu.Game.Beatmaps this.statisticsProvider = statisticsProvider; } + [BackgroundDependencyLoader] + private void load() + { + foreach (var ruleset in rulesets.AvailableRulesets) + { + if (statisticsProvider.GetStatisticsFor(ruleset) is UserStatistics statistics) + updateMapping(ruleset, statistics); + } + } + protected override void LoadComplete() { base.LoadComplete(); statisticsUpdate = statisticsProvider.StatisticsUpdate.GetBoundCopy(); - statisticsUpdate.BindValueChanged(u => - { - if (u.NewValue == null) - return; + statisticsUpdate.ValueChanged += u => updateMapping(u.NewValue.Ruleset, u.NewValue.NewStatistics); + } - // algorithm taken from https://github.com/ppy/osu-web/blob/e6e2825516449e3d0f3f5e1852c6bdd3428c3437/app/Models/User.php#L1505 - recommendedDifficultyMapping[u.NewValue.Ruleset.ShortName] = Math.Pow((double)(u.NewValue.NewStatistics.PP ?? 0), 0.4) * 0.195; - }, true); + private void updateMapping(RulesetInfo ruleset, UserStatistics statistics) + { + // algorithm taken from https://github.com/ppy/osu-web/blob/e6e2825516449e3d0f3f5e1852c6bdd3428c3437/app/Models/User.php#L1505 + recommendedDifficultyMapping[ruleset.ShortName] = Math.Pow((double)(statistics.PP ?? 0), 0.4) * 0.195; } /// diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs index b87ad33902..a92b1f4d36 100644 --- a/osu.Game/OsuGame.cs +++ b/osu.Game/OsuGame.cs @@ -1071,6 +1071,7 @@ namespace osu.Game LocalUserStatisticsProvider statisticsProvider; loadComponentSingleFile(statisticsProvider = new LocalUserStatisticsProvider(), Add, true); + loadComponentSingleFile(difficultyRecommender = new DifficultyRecommender(statisticsProvider), Add, true); loadComponentSingleFile(new UserStatisticsWatcher(statisticsProvider), Add, true); loadComponentSingleFile(Toolbar = new Toolbar { @@ -1141,8 +1142,6 @@ namespace osu.Game loadComponentSingleFile(new BackgroundDataStoreProcessor(), Add); loadComponentSingleFile(new DetachedBeatmapStore(), Add, true); - loadComponentSingleFile(difficultyRecommender = new DifficultyRecommender(statisticsProvider), Add, true); - Add(externalLinkOpener = new ExternalLinkOpener()); Add(new MusicKeyBindingHandler()); Add(new OnlineStatusNotifier(() => ScreenStack.CurrentScreen)); From 29e7adcd3b195bc466cb7c434ce493c32e3e94c5 Mon Sep 17 00:00:00 2001 From: Sheppsu <49356627+Sheppsu@users.noreply.github.com> Date: Mon, 18 Nov 2024 03:57:50 -0500 Subject: [PATCH 0047/1275] add player settings to multi spectator screen --- .../Spectate/MultiSpectatorScreen.cs | 3 +- .../Spectate/MultiSpectatorSettings.cs | 81 +++++++++++++++++++ 2 files changed, 83 insertions(+), 1 deletion(-) create mode 100644 osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorSettings.cs diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorScreen.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorScreen.cs index cb00763e6b..841aaf7a45 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorScreen.cs @@ -126,7 +126,8 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate syncManager = new SpectatorSyncManager(masterClockContainer) { ReadyToStart = performInitialSeek, - } + }, + new MultiSpectatorSettings() }; for (int i = 0; i < Users.Count; i++) diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorSettings.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorSettings.cs new file mode 100644 index 0000000000..7ed6b95110 --- /dev/null +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorSettings.cs @@ -0,0 +1,81 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Bindables; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Sprites; +using osu.Game.Graphics.UserInterface; +using osu.Game.Screens.Play.HUD; +using osuTK; + +namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate +{ + public partial class MultiSpectatorSettings : CompositeDrawable + { + private const double slide_duration = 200; + + private readonly PlayerSettingsOverlay playerSettingsOverlay; + private readonly Container slidingContainer; + + private readonly BindableBool opened = new BindableBool(); + + public MultiSpectatorSettings() + { + Origin = Anchor.TopLeft; + Anchor = Anchor.TopRight; + AutoSizeAxes = Axes.Both; + + InternalChildren = new Drawable[] + { + slidingContainer = new Container + { + Origin = Anchor.TopLeft, + Anchor = Anchor.TopLeft, + AutoSizeAxes = Axes.Both, + Children = new Drawable[] + { + new IconButton + { + Icon = FontAwesome.Solid.Cog, + Origin = Anchor.TopLeft, + Anchor = Anchor.TopLeft, + Scale = new Vector2(1f), + Position = new Vector2(-30, 0), + Action = () => opened.Toggle() + }, + playerSettingsOverlay = new PlayerSettingsOverlay() + } + } + }; + + playerSettingsOverlay.Show(); + + opened.BindValueChanged(value => + { + if (value.NewValue) + open(); + else + close(); + }); + } + + private void open() + { + slidingContainer.MoveToOffset(new Vector2(-playerSettingsOverlay.Width, 0), slide_duration, Easing.Out).Then().OnComplete(c => + { + c.Origin = Anchor.TopRight; + c.Position = Vector2.Zero; + }); + } + + private void close() + { + slidingContainer.MoveToOffset(new Vector2(playerSettingsOverlay.Width, 0), slide_duration, Easing.Out).Then().OnComplete(c => + { + c.Origin = Anchor.TopLeft; + c.Position = Vector2.Zero; + }); + } + } +} From dcf4674c6c6ac567d80ecdd0145fb8080012e89e Mon Sep 17 00:00:00 2001 From: StanR Date: Mon, 18 Nov 2024 14:01:17 +0500 Subject: [PATCH 0048/1275] Scale down beatmap cards in profile overlay --- .../Profile/Sections/Beatmaps/PaginatedBeatmapContainer.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/osu.Game/Overlays/Profile/Sections/Beatmaps/PaginatedBeatmapContainer.cs b/osu.Game/Overlays/Profile/Sections/Beatmaps/PaginatedBeatmapContainer.cs index b237a0ee05..8a47ae6830 100644 --- a/osu.Game/Overlays/Profile/Sections/Beatmaps/PaginatedBeatmapContainer.cs +++ b/osu.Game/Overlays/Profile/Sections/Beatmaps/PaginatedBeatmapContainer.cs @@ -72,6 +72,7 @@ namespace osu.Game.Overlays.Profile.Sections.Beatmaps { Anchor = Anchor.TopCentre, Origin = Anchor.TopCentre, + Scale = new Vector2(0.8f) } : null; } From 7d4062d2adba94f38805f9b59ad047ce3822f260 Mon Sep 17 00:00:00 2001 From: Sheppsu <49356627+Sheppsu@users.noreply.github.com> Date: Mon, 18 Nov 2024 04:04:28 -0500 Subject: [PATCH 0049/1275] remove redundant Scale attribute --- .../OnlinePlay/Multiplayer/Spectate/MultiSpectatorSettings.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorSettings.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorSettings.cs index 7ed6b95110..64c798b092 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorSettings.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorSettings.cs @@ -40,7 +40,6 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate Icon = FontAwesome.Solid.Cog, Origin = Anchor.TopLeft, Anchor = Anchor.TopLeft, - Scale = new Vector2(1f), Position = new Vector2(-30, 0), Action = () => opened.Toggle() }, From 4066186b24efaf90333b63298c8b23856b419711 Mon Sep 17 00:00:00 2001 From: StanR Date: Mon, 18 Nov 2024 14:48:51 +0500 Subject: [PATCH 0050/1275] Scale beatmap cards down by ~0.8 --- .../Online/TestSceneUserProfileOverlay.cs | 11 ++++++ .../Drawables/BeatmapSetOnlineStatusPill.cs | 2 +- .../Beatmaps/Drawables/Cards/BeatmapCard.cs | 4 +-- .../Drawables/Cards/BeatmapCardExtra.cs | 34 +++++++++---------- .../Cards/BeatmapCardExtraInfoRow.cs | 7 ++-- .../Drawables/Cards/BeatmapCardNormal.cs | 32 ++++++++--------- .../Cards/Statistics/BeatmapCardStatistic.cs | 6 ++-- osu.Game/Overlays/BeatmapListingOverlay.cs | 1 - .../Beatmaps/PaginatedBeatmapContainer.cs | 3 +- 9 files changed, 55 insertions(+), 45 deletions(-) diff --git a/osu.Game.Tests/Visual/Online/TestSceneUserProfileOverlay.cs b/osu.Game.Tests/Visual/Online/TestSceneUserProfileOverlay.cs index 006610dccd..d16ed46bd2 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneUserProfileOverlay.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneUserProfileOverlay.cs @@ -2,6 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using System; +using System.Collections.Generic; using System.Linq; using NUnit.Framework; using osu.Framework.Graphics; @@ -58,6 +59,16 @@ namespace osu.Game.Tests.Visual.Online return true; } + if (req is GetUserBeatmapsRequest getUserBeatmapsRequest) + { + getUserBeatmapsRequest.TriggerSuccess(new List + { + CreateAPIBeatmapSet(), + CreateAPIBeatmapSet() + }); + return true; + } + return false; }; }); diff --git a/osu.Game/Beatmaps/Drawables/BeatmapSetOnlineStatusPill.cs b/osu.Game/Beatmaps/Drawables/BeatmapSetOnlineStatusPill.cs index f18355505a..599d1b380a 100644 --- a/osu.Game/Beatmaps/Drawables/BeatmapSetOnlineStatusPill.cs +++ b/osu.Game/Beatmaps/Drawables/BeatmapSetOnlineStatusPill.cs @@ -86,7 +86,7 @@ namespace osu.Game.Beatmaps.Drawables }; Status = BeatmapOnlineStatus.None; - TextPadding = new MarginPadding { Horizontal = 5, Bottom = 1 }; + TextPadding = new MarginPadding { Horizontal = 4, Bottom = 1 }; } protected override void LoadComplete() diff --git a/osu.Game/Beatmaps/Drawables/Cards/BeatmapCard.cs b/osu.Game/Beatmaps/Drawables/Cards/BeatmapCard.cs index 25e42bcbf7..56103c1d6d 100644 --- a/osu.Game/Beatmaps/Drawables/Cards/BeatmapCard.cs +++ b/osu.Game/Beatmaps/Drawables/Cards/BeatmapCard.cs @@ -20,9 +20,9 @@ namespace osu.Game.Beatmaps.Drawables.Cards public abstract partial class BeatmapCard : OsuClickableContainer, IHasContextMenu { public const float TRANSITION_DURATION = 340; - public const float CORNER_RADIUS = 10; + public const float CORNER_RADIUS = 8; - protected const float WIDTH = 430; + protected const float WIDTH = 345; public IBindable Expanded { get; } diff --git a/osu.Game/Beatmaps/Drawables/Cards/BeatmapCardExtra.cs b/osu.Game/Beatmaps/Drawables/Cards/BeatmapCardExtra.cs index 2c2761ff0c..ebd0113379 100644 --- a/osu.Game/Beatmaps/Drawables/Cards/BeatmapCardExtra.cs +++ b/osu.Game/Beatmaps/Drawables/Cards/BeatmapCardExtra.cs @@ -22,7 +22,7 @@ namespace osu.Game.Beatmaps.Drawables.Cards protected override Drawable IdleContent => idleBottomContent; protected override Drawable DownloadInProgressContent => downloadProgressBar; - private const float height = 140; + private const float height = 112; [Cached] private readonly BeatmapCardContent content; @@ -68,7 +68,7 @@ namespace osu.Game.Beatmaps.Drawables.Cards Padding = new MarginPadding { Right = CORNER_RADIUS }, Child = leftIconArea = new FillFlowContainer { - Margin = new MarginPadding(5), + Margin = new MarginPadding(4), AutoSizeAxes = Axes.Both, Direction = FillDirection.Horizontal, Spacing = new Vector2(1) @@ -80,7 +80,7 @@ namespace osu.Game.Beatmaps.Drawables.Cards Width = WIDTH - height + CORNER_RADIUS, FavouriteState = { BindTarget = FavouriteState }, ButtonsCollapsedWidth = CORNER_RADIUS, - ButtonsExpandedWidth = 30, + ButtonsExpandedWidth = 24, Children = new Drawable[] { new FillFlowContainer @@ -109,7 +109,7 @@ namespace osu.Game.Beatmaps.Drawables.Cards new TruncatingSpriteText { Text = new RomanisableString(BeatmapSet.TitleUnicode, BeatmapSet.Title), - Font = OsuFont.Default.With(size: 22.5f, weight: FontWeight.SemiBold), + Font = OsuFont.Default.With(size: 18f, weight: FontWeight.SemiBold), RelativeSizeAxes = Axes.X, }, titleBadgeArea = new FillFlowContainer @@ -142,7 +142,7 @@ namespace osu.Game.Beatmaps.Drawables.Cards new TruncatingSpriteText { Text = createArtistText(), - Font = OsuFont.Default.With(size: 17.5f, weight: FontWeight.SemiBold), + Font = OsuFont.Default.With(size: 14f, weight: FontWeight.SemiBold), RelativeSizeAxes = Axes.X, }, Empty() @@ -154,7 +154,7 @@ namespace osu.Game.Beatmaps.Drawables.Cards RelativeSizeAxes = Axes.X, Text = BeatmapSet.Source, Shadow = false, - Font = OsuFont.GetFont(size: 14, weight: FontWeight.SemiBold), + Font = OsuFont.GetFont(size: 11f, weight: FontWeight.SemiBold), Colour = colourProvider.Content2 }, } @@ -173,18 +173,18 @@ namespace osu.Game.Beatmaps.Drawables.Cards RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y, Direction = FillDirection.Vertical, - Spacing = new Vector2(0, 3), + Spacing = new Vector2(0, 2), AlwaysPresent = true, Children = new Drawable[] { new LinkFlowContainer(s => { s.Shadow = false; - s.Font = OsuFont.GetFont(size: 14, weight: FontWeight.SemiBold); + s.Font = OsuFont.GetFont(size: 11f, weight: FontWeight.SemiBold); }).With(d => { d.AutoSizeAxes = Axes.Both; - d.Margin = new MarginPadding { Top = 2 }; + d.Margin = new MarginPadding { Top = 1 }; d.AddText("mapped by ", t => t.Colour = colourProvider.Content2); d.AddUserLink(BeatmapSet.Author); }), @@ -215,7 +215,7 @@ namespace osu.Game.Beatmaps.Drawables.Cards downloadProgressBar = new BeatmapCardDownloadProgressBar { RelativeSizeAxes = Axes.X, - Height = 6, + Height = 5, Anchor = Anchor.Centre, Origin = Anchor.Centre, State = { BindTarget = DownloadTracker.State }, @@ -231,17 +231,17 @@ namespace osu.Game.Beatmaps.Drawables.Cards { RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y, - Padding = new MarginPadding { Horizontal = 10, Vertical = 13 }, + Padding = new MarginPadding { Horizontal = 8, Vertical = 10 }, Child = new BeatmapCardDifficultyList(BeatmapSet) }; c.Expanded.BindTarget = Expanded; }); if (BeatmapSet.HasVideo) - leftIconArea.Add(new VideoIconPill { IconSize = new Vector2(20) }); + leftIconArea.Add(new VideoIconPill { IconSize = new Vector2(16) }); if (BeatmapSet.HasStoryboard) - leftIconArea.Add(new StoryboardIconPill { IconSize = new Vector2(20) }); + leftIconArea.Add(new StoryboardIconPill { IconSize = new Vector2(16) }); if (BeatmapSet.FeaturedInSpotlight) { @@ -249,7 +249,7 @@ namespace osu.Game.Beatmaps.Drawables.Cards { Anchor = Anchor.BottomRight, Origin = Anchor.BottomRight, - Margin = new MarginPadding { Left = 5 } + Margin = new MarginPadding { Left = 4 } }); } @@ -259,7 +259,7 @@ namespace osu.Game.Beatmaps.Drawables.Cards { Anchor = Anchor.BottomRight, Origin = Anchor.BottomRight, - Margin = new MarginPadding { Left = 5 } + Margin = new MarginPadding { Left = 4 } }); } @@ -269,7 +269,7 @@ namespace osu.Game.Beatmaps.Drawables.Cards { Anchor = Anchor.BottomRight, Origin = Anchor.BottomRight, - Margin = new MarginPadding { Left = 5 } + Margin = new MarginPadding { Left = 4 } }; } @@ -288,7 +288,7 @@ namespace osu.Game.Beatmaps.Drawables.Cards { BeatmapCardStatistic withMargin(BeatmapCardStatistic original) { - original.Margin = new MarginPadding { Right = 10 }; + original.Margin = new MarginPadding { Right = 8 }; return original; } diff --git a/osu.Game/Beatmaps/Drawables/Cards/BeatmapCardExtraInfoRow.cs b/osu.Game/Beatmaps/Drawables/Cards/BeatmapCardExtraInfoRow.cs index 3a1b8f7e86..a11ef0f95c 100644 --- a/osu.Game/Beatmaps/Drawables/Cards/BeatmapCardExtraInfoRow.cs +++ b/osu.Game/Beatmaps/Drawables/Cards/BeatmapCardExtraInfoRow.cs @@ -25,7 +25,7 @@ namespace osu.Game.Beatmaps.Drawables.Cards RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y, Direction = FillDirection.Horizontal, - Spacing = new Vector2(4, 0), + Spacing = new Vector2(3, 0), Children = new Drawable[] { new BeatmapSetOnlineStatusPill @@ -33,13 +33,14 @@ namespace osu.Game.Beatmaps.Drawables.Cards AutoSizeAxes = Axes.Both, Status = beatmapSet.Status, Anchor = Anchor.CentreLeft, - Origin = Anchor.CentreLeft + Origin = Anchor.CentreLeft, + TextSize = 13f }, new DifficultySpectrumDisplay(beatmapSet) { Anchor = Anchor.CentreLeft, Origin = Anchor.CentreLeft, - DotSize = new Vector2(6, 12) + DotSize = new Vector2(5, 10) } } }; diff --git a/osu.Game/Beatmaps/Drawables/Cards/BeatmapCardNormal.cs b/osu.Game/Beatmaps/Drawables/Cards/BeatmapCardNormal.cs index 46ab7ec5f6..724919f3bd 100644 --- a/osu.Game/Beatmaps/Drawables/Cards/BeatmapCardNormal.cs +++ b/osu.Game/Beatmaps/Drawables/Cards/BeatmapCardNormal.cs @@ -23,7 +23,7 @@ namespace osu.Game.Beatmaps.Drawables.Cards protected override Drawable IdleContent => idleBottomContent; protected override Drawable DownloadInProgressContent => downloadProgressBar; - public const float HEIGHT = 100; + public const float HEIGHT = 80; [Cached] private readonly BeatmapCardContent content; @@ -69,7 +69,7 @@ namespace osu.Game.Beatmaps.Drawables.Cards Padding = new MarginPadding { Right = CORNER_RADIUS }, Child = leftIconArea = new FillFlowContainer { - Margin = new MarginPadding(5), + Margin = new MarginPadding(4), AutoSizeAxes = Axes.Both, Direction = FillDirection.Horizontal, Spacing = new Vector2(1) @@ -81,7 +81,7 @@ namespace osu.Game.Beatmaps.Drawables.Cards Width = WIDTH - HEIGHT + CORNER_RADIUS, FavouriteState = { BindTarget = FavouriteState }, ButtonsCollapsedWidth = CORNER_RADIUS, - ButtonsExpandedWidth = 30, + ButtonsExpandedWidth = 24, Children = new Drawable[] { new FillFlowContainer @@ -110,7 +110,7 @@ namespace osu.Game.Beatmaps.Drawables.Cards new TruncatingSpriteText { Text = new RomanisableString(BeatmapSet.TitleUnicode, BeatmapSet.Title), - Font = OsuFont.Default.With(size: 22.5f, weight: FontWeight.SemiBold), + Font = OsuFont.Default.With(size: 18f, weight: FontWeight.SemiBold), RelativeSizeAxes = Axes.X, }, titleBadgeArea = new FillFlowContainer @@ -143,7 +143,7 @@ namespace osu.Game.Beatmaps.Drawables.Cards new TruncatingSpriteText { Text = createArtistText(), - Font = OsuFont.Default.With(size: 17.5f, weight: FontWeight.SemiBold), + Font = OsuFont.Default.With(size: 14f, weight: FontWeight.SemiBold), RelativeSizeAxes = Axes.X, }, Empty() @@ -153,11 +153,11 @@ namespace osu.Game.Beatmaps.Drawables.Cards new LinkFlowContainer(s => { s.Shadow = false; - s.Font = OsuFont.GetFont(size: 14, weight: FontWeight.SemiBold); + s.Font = OsuFont.GetFont(size: 11f, weight: FontWeight.SemiBold); }).With(d => { d.AutoSizeAxes = Axes.Both; - d.Margin = new MarginPadding { Top = 2 }; + d.Margin = new MarginPadding { Top = 1 }; d.AddText("mapped by ", t => t.Colour = colourProvider.Content2); d.AddUserLink(BeatmapSet.Author); }), @@ -177,7 +177,7 @@ namespace osu.Game.Beatmaps.Drawables.Cards RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y, Direction = FillDirection.Vertical, - Spacing = new Vector2(0, 3), + Spacing = new Vector2(0, 2), AlwaysPresent = true, Children = new Drawable[] { @@ -186,7 +186,7 @@ namespace osu.Game.Beatmaps.Drawables.Cards RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y, Direction = FillDirection.Horizontal, - Spacing = new Vector2(10, 0), + Spacing = new Vector2(8, 0), Alpha = 0, AlwaysPresent = true, ChildrenEnumerable = createStatistics() @@ -197,7 +197,7 @@ namespace osu.Game.Beatmaps.Drawables.Cards downloadProgressBar = new BeatmapCardDownloadProgressBar { RelativeSizeAxes = Axes.X, - Height = 6, + Height = 5, Anchor = Anchor.Centre, Origin = Anchor.Centre, State = { BindTarget = DownloadTracker.State }, @@ -213,17 +213,17 @@ namespace osu.Game.Beatmaps.Drawables.Cards { RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y, - Padding = new MarginPadding { Horizontal = 10, Vertical = 13 }, + Padding = new MarginPadding { Horizontal = 8, Vertical = 10 }, Child = new BeatmapCardDifficultyList(BeatmapSet) }; c.Expanded.BindTarget = Expanded; }); if (BeatmapSet.HasVideo) - leftIconArea.Add(new VideoIconPill { IconSize = new Vector2(20) }); + leftIconArea.Add(new VideoIconPill { IconSize = new Vector2(16) }); if (BeatmapSet.HasStoryboard) - leftIconArea.Add(new StoryboardIconPill { IconSize = new Vector2(20) }); + leftIconArea.Add(new StoryboardIconPill { IconSize = new Vector2(16) }); if (BeatmapSet.FeaturedInSpotlight) { @@ -231,7 +231,7 @@ namespace osu.Game.Beatmaps.Drawables.Cards { Anchor = Anchor.BottomRight, Origin = Anchor.BottomRight, - Margin = new MarginPadding { Left = 5 } + Margin = new MarginPadding { Left = 4 } }); } @@ -241,7 +241,7 @@ namespace osu.Game.Beatmaps.Drawables.Cards { Anchor = Anchor.BottomRight, Origin = Anchor.BottomRight, - Margin = new MarginPadding { Left = 5 } + Margin = new MarginPadding { Left = 4 } }); } @@ -251,7 +251,7 @@ namespace osu.Game.Beatmaps.Drawables.Cards { Anchor = Anchor.BottomRight, Origin = Anchor.BottomRight, - Margin = new MarginPadding { Left = 5 } + Margin = new MarginPadding { Left = 4 } }; } } diff --git a/osu.Game/Beatmaps/Drawables/Cards/Statistics/BeatmapCardStatistic.cs b/osu.Game/Beatmaps/Drawables/Cards/Statistics/BeatmapCardStatistic.cs index 6fd7142c05..ece52d0fa9 100644 --- a/osu.Game/Beatmaps/Drawables/Cards/Statistics/BeatmapCardStatistic.cs +++ b/osu.Game/Beatmaps/Drawables/Cards/Statistics/BeatmapCardStatistic.cs @@ -46,21 +46,21 @@ namespace osu.Game.Beatmaps.Drawables.Cards.Statistics { AutoSizeAxes = Axes.Both, Direction = FillDirection.Horizontal, - Spacing = new Vector2(5, 0), + Spacing = new Vector2(4, 0), Children = new Drawable[] { spriteIcon = new SpriteIcon { Anchor = Anchor.CentreLeft, Origin = Anchor.CentreLeft, - Size = new Vector2(10), + Size = new Vector2(8), Margin = new MarginPadding { Top = 1 } }, spriteText = new OsuSpriteText { Anchor = Anchor.CentreLeft, Origin = Anchor.CentreLeft, - Font = OsuFont.Default.With(size: 14) + Font = OsuFont.Default.With(size: 11) } } }; diff --git a/osu.Game/Overlays/BeatmapListingOverlay.cs b/osu.Game/Overlays/BeatmapListingOverlay.cs index b47e2b82c0..f83368fa41 100644 --- a/osu.Game/Overlays/BeatmapListingOverlay.cs +++ b/osu.Game/Overlays/BeatmapListingOverlay.cs @@ -198,7 +198,6 @@ namespace osu.Game.Overlays { c.Anchor = Anchor.TopCentre; c.Origin = Anchor.TopCentre; - c.Scale = new Vector2(0.8f); })).ToArray(); private static ReverseChildIDFillFlowContainer createCardContainerFor(IEnumerable newCards) diff --git a/osu.Game/Overlays/Profile/Sections/Beatmaps/PaginatedBeatmapContainer.cs b/osu.Game/Overlays/Profile/Sections/Beatmaps/PaginatedBeatmapContainer.cs index 8a47ae6830..df657aa55b 100644 --- a/osu.Game/Overlays/Profile/Sections/Beatmaps/PaginatedBeatmapContainer.cs +++ b/osu.Game/Overlays/Profile/Sections/Beatmaps/PaginatedBeatmapContainer.cs @@ -71,8 +71,7 @@ namespace osu.Game.Overlays.Profile.Sections.Beatmaps ? new BeatmapCardNormal(model) { Anchor = Anchor.TopCentre, - Origin = Anchor.TopCentre, - Scale = new Vector2(0.8f) + Origin = Anchor.TopCentre } : null; } From 74daf85e489bb5402504729303b0f2dd1fa14e2f Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Mon, 18 Nov 2024 06:40:14 -0500 Subject: [PATCH 0051/1275] Replace bindable with an event --- osu.Desktop/DiscordRichPresence.cs | 10 ++++--- .../TestSceneLocalUserStatisticsProvider.cs | 26 ++++++++++------- osu.Game/Beatmaps/DifficultyRecommender.cs | 16 ++++++++--- .../Online/LocalUserStatisticsProvider.cs | 16 ++++++----- osu.Game/Online/UserStatisticsWatcher.cs | 10 +++---- osu.Game/Users/UserRankPanel.cs | 28 +++++++++++-------- 6 files changed, 65 insertions(+), 41 deletions(-) diff --git a/osu.Desktop/DiscordRichPresence.cs b/osu.Desktop/DiscordRichPresence.cs index c9529d2f5e..c08185ddbe 100644 --- a/osu.Desktop/DiscordRichPresence.cs +++ b/osu.Desktop/DiscordRichPresence.cs @@ -69,7 +69,6 @@ namespace osu.Desktop }; private IBindable? user; - private IBindable? statisticsUpdate; [BackgroundDependencyLoader] private void load() @@ -123,10 +122,8 @@ namespace osu.Desktop activity.BindValueChanged(_ => schedulePresenceUpdate()); privacyMode.BindValueChanged(_ => schedulePresenceUpdate()); - statisticsUpdate = statisticsProvider.StatisticsUpdate.GetBoundCopy(); - statisticsUpdate.BindValueChanged(_ => schedulePresenceUpdate()); - multiplayerClient.RoomUpdated += onRoomUpdated; + statisticsProvider.StatisticsUpdated += onStatisticsUpdated; } private void onReady(object _, ReadyMessage __) @@ -142,6 +139,8 @@ namespace osu.Desktop private void onRoomUpdated() => schedulePresenceUpdate(); + private void onStatisticsUpdated(UserStatisticsUpdate _) => schedulePresenceUpdate(); + private ScheduledDelegate? presenceUpdateDelegate; private void schedulePresenceUpdate() @@ -353,6 +352,9 @@ namespace osu.Desktop if (multiplayerClient.IsNotNull()) multiplayerClient.RoomUpdated -= onRoomUpdated; + if (statisticsProvider.IsNotNull()) + statisticsProvider.StatisticsUpdated -= onStatisticsUpdated; + client.Dispose(); base.Dispose(isDisposing); } diff --git a/osu.Game.Tests/Visual/Online/TestSceneLocalUserStatisticsProvider.cs b/osu.Game.Tests/Visual/Online/TestSceneLocalUserStatisticsProvider.cs index 342d805be4..f24a9333c1 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneLocalUserStatisticsProvider.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneLocalUserStatisticsProvider.cs @@ -66,7 +66,7 @@ namespace osu.Game.Tests.Visual.Online Origin = Anchor.Centre, }); - statisticsProvider.StatisticsUpdate.BindValueChanged(s => + statisticsProvider.StatisticsUpdated += update => { text.Clear(); @@ -78,14 +78,9 @@ namespace osu.Game.Tests.Visual.Online text.NewLine(); } - if (s.NewValue == null) - text.AddText("latest update: (null)"); - else - { - text.AddText($"latest update: {s.NewValue.Ruleset}" - + $" ({(s.NewValue.OldStatistics?.TotalScore.ToString() ?? "null")} -> {s.NewValue.NewStatistics.TotalScore})"); - } - }); + text.AddText($"latest update: {update.Ruleset}" + + $" ({(update.OldStatistics?.TotalScore.ToString() ?? "null")} -> {update.NewStatistics.TotalScore})"); + }; Ruleset.Value = new OsuRuleset().RulesetInfo; }); @@ -133,6 +128,8 @@ namespace osu.Game.Tests.Visual.Online [Test] public void TestRefetchStatistics() { + UserStatisticsUpdate? update = null; + setUser(1001); AddStep("update statistics server side", @@ -142,13 +139,22 @@ namespace osu.Game.Tests.Visual.Online () => statisticsProvider.GetStatisticsFor(new OsuRuleset().RulesetInfo)!.TotalScore, () => Is.EqualTo(4_000_000)); + AddStep("setup event", () => + { + update = null; + statisticsProvider.StatisticsUpdated -= onStatisticsUpdated; + statisticsProvider.StatisticsUpdated += onStatisticsUpdated; + }); + AddStep("request refetch", () => statisticsProvider.RefetchStatistics(new OsuRuleset().RulesetInfo)); AddUntilStep("statistics update raised", - () => statisticsProvider.StatisticsUpdate.Value.NewStatistics.TotalScore, + () => update?.NewStatistics.TotalScore, () => Is.EqualTo(9_000_000)); AddAssert("statistics match new score", () => statisticsProvider.GetStatisticsFor(new OsuRuleset().RulesetInfo)!.TotalScore, () => Is.EqualTo(9_000_000)); + + void onStatisticsUpdated(UserStatisticsUpdate u) => update = u; } private UserStatistics tryGetStatistics(int userId, string rulesetName) diff --git a/osu.Game/Beatmaps/DifficultyRecommender.cs b/osu.Game/Beatmaps/DifficultyRecommender.cs index bf81356407..d132b86052 100644 --- a/osu.Game/Beatmaps/DifficultyRecommender.cs +++ b/osu.Game/Beatmaps/DifficultyRecommender.cs @@ -9,6 +9,7 @@ using System.Linq; using JetBrains.Annotations; using osu.Framework.Allocation; using osu.Framework.Bindables; +using osu.Framework.Extensions.ObjectExtensions; using osu.Framework.Graphics; using osu.Game.Online; using osu.Game.Rulesets; @@ -51,8 +52,6 @@ namespace osu.Game.Beatmaps } } - private IBindable statisticsUpdate = null!; - public DifficultyRecommender(LocalUserStatisticsProvider statisticsProvider) { this.statisticsProvider = statisticsProvider; @@ -72,10 +71,11 @@ namespace osu.Game.Beatmaps { base.LoadComplete(); - statisticsUpdate = statisticsProvider.StatisticsUpdate.GetBoundCopy(); - statisticsUpdate.ValueChanged += u => updateMapping(u.NewValue.Ruleset, u.NewValue.NewStatistics); + statisticsProvider.StatisticsUpdated += onStatisticsUpdated; } + private void onStatisticsUpdated(UserStatisticsUpdate update) => updateMapping(update.Ruleset, update.NewStatistics); + private void updateMapping(RulesetInfo ruleset, UserStatistics statistics) { // algorithm taken from https://github.com/ppy/osu-web/blob/e6e2825516449e3d0f3f5e1852c6bdd3428c3437/app/Models/User.php#L1505 @@ -110,5 +110,13 @@ namespace osu.Game.Beatmaps return null; } + + protected override void Dispose(bool isDisposing) + { + if (statisticsProvider.IsNotNull()) + statisticsProvider.StatisticsUpdated -= onStatisticsUpdated; + + base.Dispose(isDisposing); + } } } diff --git a/osu.Game/Online/LocalUserStatisticsProvider.cs b/osu.Game/Online/LocalUserStatisticsProvider.cs index ea4688a307..a17041c996 100644 --- a/osu.Game/Online/LocalUserStatisticsProvider.cs +++ b/osu.Game/Online/LocalUserStatisticsProvider.cs @@ -1,10 +1,10 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System; using System.Collections.Generic; using System.Linq; using osu.Framework.Allocation; -using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Game.Extensions; using osu.Game.Online.API; @@ -19,13 +19,15 @@ namespace osu.Game.Online /// public partial class LocalUserStatisticsProvider : Component { - private readonly Bindable statisticsUpdate = new Bindable(); - /// - /// A bindable communicating updates to the local user's statistics on any ruleset. - /// This does not guarantee the presence of old statistics, as it is invoked on initial population of statistics. + /// Invoked whenever a change occured to the statistics of any ruleset, + /// either due to change in local user (log out and log in) or as a result of score submission. /// - public IBindable StatisticsUpdate => statisticsUpdate; + /// + /// This does not guarantee the presence of the old statistics, + /// specifically in the case of initial population or change in local user. + /// + public event Action? StatisticsUpdated; [Resolved] private RulesetStore rulesets { get; set; } = null!; @@ -73,7 +75,7 @@ namespace osu.Game.Online statisticsRequests.Remove(ruleset.ShortName); statisticsCache[ruleset.ShortName] = newStatistics; - statisticsUpdate.Value = new UserStatisticsUpdate(ruleset, oldStatistics, newStatistics); + StatisticsUpdated?.Invoke(new UserStatisticsUpdate(ruleset, oldStatistics, newStatistics)); } } diff --git a/osu.Game/Online/UserStatisticsWatcher.cs b/osu.Game/Online/UserStatisticsWatcher.cs index bd3c4b819f..8ed1ff594d 100644 --- a/osu.Game/Online/UserStatisticsWatcher.cs +++ b/osu.Game/Online/UserStatisticsWatcher.cs @@ -43,7 +43,7 @@ namespace osu.Game.Online base.LoadComplete(); spectatorClient.OnUserScoreProcessed += userScoreProcessed; - statisticsProvider.StatisticsUpdate.ValueChanged += onStatisticsUpdated; + statisticsProvider.StatisticsUpdated += onStatisticsUpdated; } /// @@ -76,13 +76,13 @@ namespace osu.Game.Online statisticsProvider.RefetchStatistics(scoreInfo.Ruleset); } - private void onStatisticsUpdated(ValueChangedEvent update) => Schedule(() => + private void onStatisticsUpdated(UserStatisticsUpdate update) => Schedule(() => { - if (scorePendingUpdate == null || !update.NewValue.Ruleset.Equals(scorePendingUpdate.Ruleset)) + if (scorePendingUpdate == null || !update.Ruleset.Equals(scorePendingUpdate.Ruleset)) return; - if (update.NewValue.OldStatistics != null) - latestUpdate.Value = new ScoreBasedUserStatisticsUpdate(scorePendingUpdate, update.NewValue.OldStatistics, update.NewValue.NewStatistics); + if (update.OldStatistics != null) + latestUpdate.Value = new ScoreBasedUserStatisticsUpdate(scorePendingUpdate, update.OldStatistics, update.NewStatistics); scorePendingUpdate = null; }); diff --git a/osu.Game/Users/UserRankPanel.cs b/osu.Game/Users/UserRankPanel.cs index c66dd8ef49..5e3ae172be 100644 --- a/osu.Game/Users/UserRankPanel.cs +++ b/osu.Game/Users/UserRankPanel.cs @@ -4,6 +4,7 @@ using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Extensions.LocalisationExtensions; +using osu.Framework.Extensions.ObjectExtensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Input.Events; @@ -49,23 +50,20 @@ namespace osu.Game.Users [Resolved] private IBindable ruleset { get; set; } = null!; - private IBindable statisticsUpdate = null!; - protected override void LoadComplete() { base.LoadComplete(); if (statisticsProvider != null) - { - statisticsUpdate = statisticsProvider.StatisticsUpdate.GetBoundCopy(); - statisticsUpdate.BindValueChanged(u => - { - if (u.NewValue.Ruleset.Equals(ruleset.Value)) - updateDisplay(); - }); + statisticsProvider.StatisticsUpdated += onStatisticsUpdated; - ruleset.BindValueChanged(_ => updateDisplay(), true); - } + ruleset.BindValueChanged(_ => updateDisplay(), true); + } + + private void onStatisticsUpdated(UserStatisticsUpdate update) + { + if (update.Ruleset.Equals(ruleset.Value)) + updateDisplay(); } private void updateDisplay() @@ -231,5 +229,13 @@ namespace osu.Game.Users } protected override Drawable? CreateBackground() => null; + + protected override void Dispose(bool isDisposing) + { + if (statisticsProvider.IsNotNull()) + statisticsProvider.StatisticsUpdated -= onStatisticsUpdated; + + base.Dispose(isDisposing); + } } } From 0b52080a52d29e022a70de6fc1a4c3417411969f Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Mon, 18 Nov 2024 06:46:57 -0500 Subject: [PATCH 0052/1275] Handle logged out user --- osu.Game/Online/LocalUserStatisticsProvider.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/osu.Game/Online/LocalUserStatisticsProvider.cs b/osu.Game/Online/LocalUserStatisticsProvider.cs index a17041c996..a25f5b05aa 100644 --- a/osu.Game/Online/LocalUserStatisticsProvider.cs +++ b/osu.Game/Online/LocalUserStatisticsProvider.cs @@ -55,6 +55,9 @@ namespace osu.Game.Online { statisticsCache.Clear(); + if (api.LocalUser.Value == null || api.LocalUser.Value.Id <= 1) + return; + foreach (var ruleset in rulesets.AvailableRulesets.Where(r => r.IsLegacyRuleset())) RefetchStatistics(ruleset); } From 5ac3bb73ee2569b62f5a928606b16bfc2b969a9e Mon Sep 17 00:00:00 2001 From: Darius Wattimena Date: Mon, 18 Nov 2024 22:19:08 +0100 Subject: [PATCH 0053/1275] Adds an option to the catch editor to convert sliders to fruits --- .../JuiceStreamSelectionBlueprint.cs | 69 +++++++++++++++++++ 1 file changed, 69 insertions(+) diff --git a/osu.Game.Rulesets.Catch/Edit/Blueprints/JuiceStreamSelectionBlueprint.cs b/osu.Game.Rulesets.Catch/Edit/Blueprints/JuiceStreamSelectionBlueprint.cs index 3eb8d6c018..2f2ccae38b 100644 --- a/osu.Game.Rulesets.Catch/Edit/Blueprints/JuiceStreamSelectionBlueprint.cs +++ b/osu.Game.Rulesets.Catch/Edit/Blueprints/JuiceStreamSelectionBlueprint.cs @@ -10,10 +10,12 @@ using osu.Framework.Graphics.Primitives; using osu.Framework.Graphics.UserInterface; using osu.Framework.Input.Bindings; using osu.Framework.Input.Events; +using osu.Framework.Utils; using osu.Game.Graphics.UserInterface; using osu.Game.Rulesets.Catch.Edit.Blueprints.Components; using osu.Game.Rulesets.Catch.Objects; using osu.Game.Rulesets.Objects; +using osu.Game.Rulesets.Objects.Types; using osu.Game.Screens.Edit; using osuTK; using osuTK.Input; @@ -54,6 +56,12 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints [Resolved] private EditorBeatmap? editorBeatmap { get; set; } + [Resolved] + private IEditorChangeHandler? changeHandler { get; set; } + + [Resolved] + private BindableBeatDivisor? beatDivisor { get; set; } + public JuiceStreamSelectionBlueprint(JuiceStream hitObject) : base(hitObject) { @@ -119,6 +127,20 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints return base.OnMouseDown(e); } + protected override bool OnKeyDown(KeyDownEvent e) + { + if (!IsSelected) + return false; + + if (e.Key == Key.F && e.ControlPressed && e.ShiftPressed) + { + convertToFruits(); + return true; + } + + return false; + } + private void onDefaultsApplied(HitObject _) { computeObjectBounds(); @@ -168,6 +190,48 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints lastSliderPathVersion = HitObject.Path.Version.Value; } + private void convertToFruits() + { + if (editorBeatmap == null || beatDivisor == null) + return; + + var timingPoint = editorBeatmap.ControlPointInfo.TimingPointAt(HitObject.StartTime); + double streamSpacing = timingPoint.BeatLength / beatDivisor.Value; + + changeHandler?.BeginChange(); + + int i = 0; + double time = HitObject.StartTime; + + while (!Precision.DefinitelyBigger(time, HitObject.GetEndTime(), 1)) + { + // positionWithRepeats is a fractional number in the range of [0, HitObject.SpanCount()] + // and indicates how many fractional spans of a slider have passed up to time. + double positionWithRepeats = (time - HitObject.StartTime) / HitObject.Duration * HitObject.SpanCount(); + double pathPosition = positionWithRepeats - (int)positionWithRepeats; + // every second span is in the reverse direction - need to reverse the path position. + if (positionWithRepeats % 2 >= 1) + pathPosition = 1 - pathPosition; + + float fruitXValue = HitObject.OriginalX + HitObject.Path.PositionAt(pathPosition).X; + + editorBeatmap.Add(new Fruit + { + StartTime = time, + OriginalX = fruitXValue, + NewCombo = i == 0 && HitObject.NewCombo, + Samples = HitObject.Samples.Select(s => s.With()).ToList() + }); + + i += 1; + time = HitObject.StartTime + i * streamSpacing; + } + + editorBeatmap.Remove(HitObject); + + changeHandler?.EndChange(); + } + private IEnumerable getContextMenuItems() { yield return new OsuMenuItem("Add vertex", MenuItemType.Standard, () => @@ -177,6 +241,11 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints { Hotkey = new Hotkey(new KeyCombination(InputKey.Control, InputKey.MouseLeft)) }; + + yield return new OsuMenuItem("Convert to fruits", MenuItemType.Destructive, convertToFruits) + { + Hotkey = new Hotkey(new KeyCombination(InputKey.Control, InputKey.Shift, InputKey.F)) + }; } protected override void Dispose(bool isDisposing) From 00e3b20ff0bb3d6f001fc375034cef8406fab799 Mon Sep 17 00:00:00 2001 From: Darius Wattimena Date: Mon, 18 Nov 2024 22:30:15 +0100 Subject: [PATCH 0054/1275] Change text to stream instead of fruits as that is the term by catch mappers --- .../Edit/Blueprints/JuiceStreamSelectionBlueprint.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Game.Rulesets.Catch/Edit/Blueprints/JuiceStreamSelectionBlueprint.cs b/osu.Game.Rulesets.Catch/Edit/Blueprints/JuiceStreamSelectionBlueprint.cs index 2f2ccae38b..a61478f5d5 100644 --- a/osu.Game.Rulesets.Catch/Edit/Blueprints/JuiceStreamSelectionBlueprint.cs +++ b/osu.Game.Rulesets.Catch/Edit/Blueprints/JuiceStreamSelectionBlueprint.cs @@ -134,7 +134,7 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints if (e.Key == Key.F && e.ControlPressed && e.ShiftPressed) { - convertToFruits(); + convertToStream(); return true; } @@ -190,7 +190,7 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints lastSliderPathVersion = HitObject.Path.Version.Value; } - private void convertToFruits() + private void convertToStream() { if (editorBeatmap == null || beatDivisor == null) return; @@ -242,7 +242,7 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints Hotkey = new Hotkey(new KeyCombination(InputKey.Control, InputKey.MouseLeft)) }; - yield return new OsuMenuItem("Convert to fruits", MenuItemType.Destructive, convertToFruits) + yield return new OsuMenuItem("Convert to stream", MenuItemType.Destructive, convertToStream) { Hotkey = new Hotkey(new KeyCombination(InputKey.Control, InputKey.Shift, InputKey.F)) }; From a679f0736ed05367d2ab471d364914c024bc9d90 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 7 Nov 2024 14:58:06 +0100 Subject: [PATCH 0055/1275] Add ability to close playlists within grace period after creation --- .../API/Requests/ClosePlaylistRequest.cs | 27 ++++++++++++ .../OnlinePlay/Lounge/DrawableLoungeRoom.cs | 42 ++++++++++++++++--- .../OnlinePlay/Lounge/LoungeSubScreen.cs | 2 + .../Playlists/ClosePlaylistDialog.cs | 19 +++++++++ 4 files changed, 85 insertions(+), 5 deletions(-) create mode 100644 osu.Game/Online/API/Requests/ClosePlaylistRequest.cs create mode 100644 osu.Game/Screens/OnlinePlay/Playlists/ClosePlaylistDialog.cs diff --git a/osu.Game/Online/API/Requests/ClosePlaylistRequest.cs b/osu.Game/Online/API/Requests/ClosePlaylistRequest.cs new file mode 100644 index 0000000000..545266491e --- /dev/null +++ b/osu.Game/Online/API/Requests/ClosePlaylistRequest.cs @@ -0,0 +1,27 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Net.Http; +using osu.Framework.IO.Network; + +namespace osu.Game.Online.API.Requests +{ + public class ClosePlaylistRequest : APIRequest + { + private readonly long roomId; + + public ClosePlaylistRequest(long roomId) + { + this.roomId = roomId; + } + + protected override WebRequest CreateWebRequest() + { + var request = base.CreateWebRequest(); + request.Method = HttpMethod.Delete; + return request; + } + + protected override string Target => $@"rooms/{roomId}"; + } +} diff --git a/osu.Game/Screens/OnlinePlay/Lounge/DrawableLoungeRoom.cs b/osu.Game/Screens/OnlinePlay/Lounge/DrawableLoungeRoom.cs index d396d18b4f..76de649ef8 100644 --- a/osu.Game/Screens/OnlinePlay/Lounge/DrawableLoungeRoom.cs +++ b/osu.Game/Screens/OnlinePlay/Lounge/DrawableLoungeRoom.cs @@ -1,6 +1,7 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System; using System.Collections.Generic; using System.ComponentModel; using osu.Framework.Allocation; @@ -22,9 +23,13 @@ using osu.Game.Graphics.Sprites; using osu.Game.Graphics.UserInterface; using osu.Game.Graphics.UserInterfaceV2; using osu.Game.Input.Bindings; +using osu.Game.Online.API; +using osu.Game.Online.API.Requests; using osu.Game.Online.Rooms; +using osu.Game.Overlays; using osu.Game.Screens.OnlinePlay.Components; using osu.Game.Screens.OnlinePlay.Lounge.Components; +using osu.Game.Screens.OnlinePlay.Playlists; using osuTK; using osuTK.Graphics; using Container = osu.Framework.Graphics.Containers.Container; @@ -48,6 +53,12 @@ namespace osu.Game.Screens.OnlinePlay.Lounge [Resolved(canBeNull: true)] private LoungeSubScreen? lounge { get; set; } + [Resolved] + private IDialogOverlay? dialogOverlay { get; set; } + + [Resolved] + private IAPIProvider api { get; set; } = null!; + private readonly BindableWithCurrent selectedRoom = new BindableWithCurrent(); private Sample? sampleSelect; private Sample? sampleJoin; @@ -144,13 +155,34 @@ namespace osu.Game.Screens.OnlinePlay.Lounge public Popover GetPopover() => new PasswordEntryPopover(Room); - public MenuItem[] ContextMenuItems => new MenuItem[] + public MenuItem[] ContextMenuItems { - new OsuMenuItem("Create copy", MenuItemType.Standard, () => + get { - lounge?.OpenCopy(Room); - }) - }; + var items = new List + { + new OsuMenuItem("Create copy", MenuItemType.Standard, () => + { + lounge?.OpenCopy(Room); + }) + }; + + if (Room.Type == MatchType.Playlists && Room.Host?.Id == api.LocalUser.Value.Id && Room.StartDate?.AddMinutes(5) >= DateTimeOffset.Now) + { + items.Add(new OsuMenuItem("Close playlist", MenuItemType.Destructive, () => + { + dialogOverlay?.Push(new ClosePlaylistDialog(Room, () => + { + var request = new ClosePlaylistRequest(Room.RoomID!.Value); + request.Success += () => lounge?.RefreshRooms(); + api.Queue(request); + })); + })); + } + + return items.ToArray(); + } + } public bool OnPressed(KeyBindingPressEvent e) { diff --git a/osu.Game/Screens/OnlinePlay/Lounge/LoungeSubScreen.cs b/osu.Game/Screens/OnlinePlay/Lounge/LoungeSubScreen.cs index 90288a1067..e3ec97e157 100644 --- a/osu.Game/Screens/OnlinePlay/Lounge/LoungeSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Lounge/LoungeSubScreen.cs @@ -382,6 +382,8 @@ namespace osu.Game.Screens.OnlinePlay.Lounge this.Push(CreateRoomSubScreen(room)); } + public void RefreshRooms() => ListingPollingComponent.PollImmediately(); + private void updateLoadingLayer() { if (operationInProgress.Value || !ListingPollingComponent.InitialRoomsReceived.Value) diff --git a/osu.Game/Screens/OnlinePlay/Playlists/ClosePlaylistDialog.cs b/osu.Game/Screens/OnlinePlay/Playlists/ClosePlaylistDialog.cs new file mode 100644 index 0000000000..08fed037d3 --- /dev/null +++ b/osu.Game/Screens/OnlinePlay/Playlists/ClosePlaylistDialog.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 System; +using osu.Game.Online.Rooms; +using osu.Game.Overlays.Dialog; + +namespace osu.Game.Screens.OnlinePlay.Playlists +{ + public partial class ClosePlaylistDialog : DeletionDialog + { + public ClosePlaylistDialog(Room room, Action closeAction) + { + HeaderText = "Are you sure you want to close the following playlist:"; + BodyText = room.Name; + DangerousAction = closeAction; + } + } +} From a76b4418b9159ad25aaa67ec9b1ced2c7e588c46 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 22 Nov 2024 16:55:37 +0900 Subject: [PATCH 0056/1275] Change some beatmap default settings to match stable - Countdown should [be off by default](https://github.com/peppy/osu-stable-reference/blob/9a0748563812b5085a0ef5f8600b997408330eab/osu!/GameplayElements/Beatmaps/Beatmap.cs#L372) - Samples match playback rate [also](https://github.com/peppy/osu-stable-reference/blob/9a0748563812b5085a0ef5f8600b997408330eab/osu!/GameplayElements/Beatmaps/Beatmap.cs#L210) --- osu.Game/Beatmaps/BeatmapInfo.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Beatmaps/BeatmapInfo.cs b/osu.Game/Beatmaps/BeatmapInfo.cs index f1463eb632..d94c09d40f 100644 --- a/osu.Game/Beatmaps/BeatmapInfo.cs +++ b/osu.Game/Beatmaps/BeatmapInfo.cs @@ -150,7 +150,7 @@ namespace osu.Game.Beatmaps public bool EpilepsyWarning { get; set; } - public bool SamplesMatchPlaybackRate { get; set; } = true; + public bool SamplesMatchPlaybackRate { get; set; } /// /// The time at which this beatmap was last played by the local user. @@ -181,7 +181,7 @@ namespace osu.Game.Beatmaps public double? EditorTimestamp { get; set; } [Ignored] - public CountdownType Countdown { get; set; } = CountdownType.Normal; + public CountdownType Countdown { get; set; } = CountdownType.None; /// /// The number of beats to move the countdown backwards (compared to its default location). From 29757ffdf2260a22f736e11dd1d2100a26ac11ce Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 22 Nov 2024 17:36:28 +0900 Subject: [PATCH 0057/1275] Allow setting osu!mania scroll speed to single decimal precision Addresses https://github.com/ppy/osu/discussions/30663. --- .../Configuration/ManiaRulesetConfigManager.cs | 6 +++--- .../Edit/DrawableManiaEditorRuleset.cs | 2 +- osu.Game.Rulesets.Mania/ManiaSettingsSubsection.cs | 8 ++++---- osu.Game.Rulesets.Mania/UI/DrawableManiaRuleset.cs | 4 ++-- osu.Game/Localisation/RulesetSettingsStrings.cs | 4 ++-- 5 files changed, 12 insertions(+), 12 deletions(-) diff --git a/osu.Game.Rulesets.Mania/Configuration/ManiaRulesetConfigManager.cs b/osu.Game.Rulesets.Mania/Configuration/ManiaRulesetConfigManager.cs index f975c7f1d4..d9cc224ad1 100644 --- a/osu.Game.Rulesets.Mania/Configuration/ManiaRulesetConfigManager.cs +++ b/osu.Game.Rulesets.Mania/Configuration/ManiaRulesetConfigManager.cs @@ -21,7 +21,7 @@ namespace osu.Game.Rulesets.Mania.Configuration { base.InitialiseDefaults(); - SetDefault(ManiaRulesetSetting.ScrollSpeed, 8, 1, 40); + SetDefault(ManiaRulesetSetting.ScrollSpeed, 8.0, 1.0, 40.0, 0.1); SetDefault(ManiaRulesetSetting.ScrollDirection, ManiaScrollingDirection.Down); SetDefault(ManiaRulesetSetting.TimingBasedNoteColouring, false); @@ -31,7 +31,7 @@ namespace osu.Game.Rulesets.Mania.Configuration if (Get(ManiaRulesetSetting.ScrollTime) is double scrollTime) { - SetValue(ManiaRulesetSetting.ScrollSpeed, (int)Math.Round(DrawableManiaRuleset.MAX_TIME_RANGE / scrollTime)); + SetValue(ManiaRulesetSetting.ScrollSpeed, Math.Round(DrawableManiaRuleset.MAX_TIME_RANGE / scrollTime)); SetValue(ManiaRulesetSetting.ScrollTime, null); } #pragma warning restore CS0618 @@ -39,7 +39,7 @@ namespace osu.Game.Rulesets.Mania.Configuration public override TrackedSettings CreateTrackedSettings() => new TrackedSettings { - new TrackedSetting(ManiaRulesetSetting.ScrollSpeed, + new TrackedSetting(ManiaRulesetSetting.ScrollSpeed, speed => new SettingDescription( rawValue: speed, name: RulesetSettingsStrings.ScrollSpeed, diff --git a/osu.Game.Rulesets.Mania/Edit/DrawableManiaEditorRuleset.cs b/osu.Game.Rulesets.Mania/Edit/DrawableManiaEditorRuleset.cs index 4c4cf519ce..181bc7341c 100644 --- a/osu.Game.Rulesets.Mania/Edit/DrawableManiaEditorRuleset.cs +++ b/osu.Game.Rulesets.Mania/Edit/DrawableManiaEditorRuleset.cs @@ -44,7 +44,7 @@ namespace osu.Game.Rulesets.Mania.Edit protected override void Update() { - TargetTimeRange = TimelineTimeRange == null || ShowSpeedChanges.Value ? ComputeScrollTime(Config.Get(ManiaRulesetSetting.ScrollSpeed)) : TimelineTimeRange.Value; + TargetTimeRange = TimelineTimeRange == null || ShowSpeedChanges.Value ? ComputeScrollTime(Config.Get(ManiaRulesetSetting.ScrollSpeed)) : TimelineTimeRange.Value; base.Update(); } } diff --git a/osu.Game.Rulesets.Mania/ManiaSettingsSubsection.cs b/osu.Game.Rulesets.Mania/ManiaSettingsSubsection.cs index 30eca0636c..17add32513 100644 --- a/osu.Game.Rulesets.Mania/ManiaSettingsSubsection.cs +++ b/osu.Game.Rulesets.Mania/ManiaSettingsSubsection.cs @@ -33,11 +33,11 @@ namespace osu.Game.Rulesets.Mania LabelText = RulesetSettingsStrings.ScrollingDirection, Current = config.GetBindable(ManiaRulesetSetting.ScrollDirection) }, - new SettingsSlider + new SettingsSlider { LabelText = RulesetSettingsStrings.ScrollSpeed, - Current = config.GetBindable(ManiaRulesetSetting.ScrollSpeed), - KeyboardStep = 5 + Current = config.GetBindable(ManiaRulesetSetting.ScrollSpeed), + KeyboardStep = 1 }, new SettingsCheckbox { @@ -48,7 +48,7 @@ namespace osu.Game.Rulesets.Mania }; } - private partial class ManiaScrollSlider : RoundedSliderBar + private partial class ManiaScrollSlider : RoundedSliderBar { public override LocalisableString TooltipText => RulesetSettingsStrings.ScrollSpeedTooltip((int)DrawableManiaRuleset.ComputeScrollTime(Current.Value), Current.Value); } diff --git a/osu.Game.Rulesets.Mania/UI/DrawableManiaRuleset.cs b/osu.Game.Rulesets.Mania/UI/DrawableManiaRuleset.cs index aed53e157a..d173ae4143 100644 --- a/osu.Game.Rulesets.Mania/UI/DrawableManiaRuleset.cs +++ b/osu.Game.Rulesets.Mania/UI/DrawableManiaRuleset.cs @@ -56,7 +56,7 @@ namespace osu.Game.Rulesets.Mania.UI protected new ManiaRulesetConfigManager Config => (ManiaRulesetConfigManager)base.Config; private readonly Bindable configDirection = new Bindable(); - private readonly BindableInt configScrollSpeed = new BindableInt(); + private readonly BindableDouble configScrollSpeed = new BindableDouble(); private double currentTimeRange; protected double TargetTimeRange; @@ -160,7 +160,7 @@ namespace osu.Game.Rulesets.Mania.UI /// /// The scroll speed. /// The scroll time. - public static double ComputeScrollTime(int scrollSpeed) => MAX_TIME_RANGE / scrollSpeed; + public static double ComputeScrollTime(double scrollSpeed) => MAX_TIME_RANGE / scrollSpeed; public override PlayfieldAdjustmentContainer CreatePlayfieldAdjustmentContainer() => new ManiaPlayfieldAdjustmentContainer(); diff --git a/osu.Game/Localisation/RulesetSettingsStrings.cs b/osu.Game/Localisation/RulesetSettingsStrings.cs index e3d51f1124..9434cd53de 100644 --- a/osu.Game/Localisation/RulesetSettingsStrings.cs +++ b/osu.Game/Localisation/RulesetSettingsStrings.cs @@ -80,9 +80,9 @@ namespace osu.Game.Localisation public static LocalisableString TimingBasedColouring => new TranslatableString(getKey(@"Timing_based_colouring"), @"Timing-based note colouring"); /// - /// "{0}ms (speed {1})" + /// "{0}ms (speed {1:N1})" /// - public static LocalisableString ScrollSpeedTooltip(int scrollTime, int scrollSpeed) => new TranslatableString(getKey(@"ruleset"), @"{0}ms (speed {1})", scrollTime, scrollSpeed); + public static LocalisableString ScrollSpeedTooltip(int scrollTime, double scrollSpeed) => new TranslatableString(getKey(@"ruleset"), @"{0}ms (speed {1:N1})", scrollTime, scrollSpeed); /// /// "Touch control scheme" From 69c2c988a1e1d4c7ff3cbbbd70a6a8836ecdab00 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 22 Nov 2024 09:54:56 +0100 Subject: [PATCH 0058/1275] Add extra check to ensure closed rooms can't be closed harder --- osu.Game/Screens/OnlinePlay/Lounge/DrawableLoungeRoom.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/osu.Game/Screens/OnlinePlay/Lounge/DrawableLoungeRoom.cs b/osu.Game/Screens/OnlinePlay/Lounge/DrawableLoungeRoom.cs index 76de649ef8..7d36cec7ba 100644 --- a/osu.Game/Screens/OnlinePlay/Lounge/DrawableLoungeRoom.cs +++ b/osu.Game/Screens/OnlinePlay/Lounge/DrawableLoungeRoom.cs @@ -26,6 +26,7 @@ using osu.Game.Input.Bindings; using osu.Game.Online.API; using osu.Game.Online.API.Requests; using osu.Game.Online.Rooms; +using osu.Game.Online.Rooms.RoomStatuses; using osu.Game.Overlays; using osu.Game.Screens.OnlinePlay.Components; using osu.Game.Screens.OnlinePlay.Lounge.Components; @@ -167,7 +168,7 @@ namespace osu.Game.Screens.OnlinePlay.Lounge }) }; - if (Room.Type == MatchType.Playlists && Room.Host?.Id == api.LocalUser.Value.Id && Room.StartDate?.AddMinutes(5) >= DateTimeOffset.Now) + if (Room.Type == MatchType.Playlists && Room.Host?.Id == api.LocalUser.Value.Id && Room.StartDate?.AddMinutes(5) >= DateTimeOffset.Now && Room.Status is not RoomStatusEnded) { items.Add(new OsuMenuItem("Close playlist", MenuItemType.Destructive, () => { From cfc38df88940eb1ea0cbb0e87fea36bd1476a3ab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 22 Nov 2024 09:55:28 +0100 Subject: [PATCH 0059/1275] Add close button to playlists footer --- .../TestScenePlaylistsRoomSubScreen.cs | 32 +++++++ .../Screens/OnlinePlay/Match/RoomSubScreen.cs | 12 +-- .../Playlists/PlaylistsRoomFooter.cs | 95 +++++++++++++++++-- .../Playlists/PlaylistsRoomSubScreen.cs | 15 ++- 4 files changed, 140 insertions(+), 14 deletions(-) diff --git a/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsRoomSubScreen.cs b/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsRoomSubScreen.cs index 4306fc1e6a..5f9e06fda5 100644 --- a/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsRoomSubScreen.cs +++ b/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsRoomSubScreen.cs @@ -2,8 +2,13 @@ // See the LICENCE file in the repository root for full licence text. using System; +using System.Linq; using NUnit.Framework; +using osu.Framework.Allocation; using osu.Framework.Screens; +using osu.Framework.Testing; +using osu.Game.Graphics.UserInterface; +using osu.Game.Online.API; using osu.Game.Online.API.Requests.Responses; using osu.Game.Online.Rooms; using osu.Game.Online.Rooms.RoomStatuses; @@ -14,6 +19,9 @@ namespace osu.Game.Tests.Visual.Playlists { public partial class TestScenePlaylistsRoomSubScreen : OnlinePlayTestScene { + [Resolved] + private IAPIProvider api { get; set; } = null!; + protected new TestRoomManager RoomManager => (TestRoomManager)base.RoomManager; [Test] @@ -37,5 +45,29 @@ namespace osu.Game.Tests.Visual.Playlists AddUntilStep("wait for screen load", () => roomScreen.IsCurrentScreen()); AddAssert("status is still ended", () => roomScreen.Room.Status, Is.TypeOf); } + + [Test] + public void TestCloseButtonGoesAwayAfterGracePeriod() + { + Room room = null!; + PlaylistsRoomSubScreen roomScreen = null!; + + AddStep("create room", () => + { + RoomManager.AddRoom(room = new Room + { + Name = @"Test Room", + Host = api.LocalUser.Value, + Category = RoomCategory.Normal, + StartDate = DateTimeOffset.Now.AddMinutes(-5).AddSeconds(3), + EndDate = DateTimeOffset.Now.AddMinutes(30) + }); + }); + + AddStep("push screen", () => LoadScreen(roomScreen = new PlaylistsRoomSubScreen(room))); + AddUntilStep("wait for screen load", () => roomScreen.IsCurrentScreen()); + AddAssert("close button present", () => roomScreen.ChildrenOfType().Any()); + AddUntilStep("wait for close button to disappear", () => !roomScreen.ChildrenOfType().Any()); + } } } diff --git a/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs b/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs index ffea3878fa..4ef31c02c3 100644 --- a/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs @@ -71,7 +71,7 @@ namespace osu.Game.Screens.OnlinePlay.Match protected RulesetStore Rulesets { get; private set; } = null!; [Resolved] - private IAPIProvider api { get; set; } = null!; + protected IAPIProvider API { get; private set; } = null!; [Resolved(canBeNull: true)] protected OnlinePlayScreen? ParentScreen { get; private set; } @@ -80,7 +80,7 @@ namespace osu.Game.Screens.OnlinePlay.Match private PreviewTrackManager previewTrackManager { get; set; } = null!; [Resolved(canBeNull: true)] - private IDialogOverlay? dialogOverlay { get; set; } + protected IDialogOverlay? DialogOverlay { get; private set; } [Cached] private readonly OnlinePlayBeatmapAvailabilityTracker beatmapAvailabilityTracker = new OnlinePlayBeatmapAvailabilityTracker(); @@ -282,7 +282,7 @@ namespace osu.Game.Screens.OnlinePlay.Match } } - protected virtual bool IsConnected => api.State.Value == APIState.Online; + protected virtual bool IsConnected => API.State.Value == APIState.Online; public override bool OnBackButton() { @@ -361,17 +361,17 @@ namespace osu.Game.Screens.OnlinePlay.Match bool hasUnsavedChanges = Room.RoomID == null && Room.Playlist.Count > 0; - if (dialogOverlay == null || !hasUnsavedChanges) + if (DialogOverlay == null || !hasUnsavedChanges) return true; // if the dialog is already displayed, block exiting until the user explicitly makes a decision. - if (dialogOverlay.CurrentDialog is ConfirmDiscardChangesDialog discardChangesDialog) + if (DialogOverlay.CurrentDialog is ConfirmDiscardChangesDialog discardChangesDialog) { discardChangesDialog.Flash(); return false; } - dialogOverlay.Push(new ConfirmDiscardChangesDialog(() => + DialogOverlay.Push(new ConfirmDiscardChangesDialog(() => { ExitConfirmed = true; settingsOverlay.Hide(); diff --git a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomFooter.cs b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomFooter.cs index 0d837423a6..7838bd2fc8 100644 --- a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomFooter.cs +++ b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomFooter.cs @@ -2,9 +2,14 @@ // See the LICENCE file in the repository root for full licence text. using System; +using System.ComponentModel; +using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; +using osu.Game.Graphics.UserInterface; +using osu.Game.Online.API; using osu.Game.Online.Rooms; +using osu.Game.Online.Rooms.RoomStatuses; using osuTK; namespace osu.Game.Screens.OnlinePlay.Playlists @@ -12,22 +17,98 @@ namespace osu.Game.Screens.OnlinePlay.Playlists public partial class PlaylistsRoomFooter : CompositeDrawable { public Action? OnStart; + public Action? OnClose; + + private readonly Room room; + private DangerousRoundedButton closeButton = null!; + + [Resolved] + private IAPIProvider api { get; set; } = null!; public PlaylistsRoomFooter(Room room) + { + this.room = room; + } + + [BackgroundDependencyLoader] + private void load() { RelativeSizeAxes = Axes.Both; - InternalChildren = new[] + InternalChild = new FillFlowContainer { - new PlaylistsReadyButton(room) + AutoSizeAxes = Axes.X, + RelativeSizeAxes = Axes.Y, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Direction = FillDirection.Horizontal, + Spacing = new Vector2(10), + Children = new Drawable[] { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - RelativeSizeAxes = Axes.Y, - Size = new Vector2(600, 1), - Action = () => OnStart?.Invoke() + new PlaylistsReadyButton(room) + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + RelativeSizeAxes = Axes.Y, + Size = new Vector2(600, 1), + Action = () => OnStart?.Invoke() + }, + closeButton = new DangerousRoundedButton + { + Text = "Close", + Action = () => OnClose?.Invoke(), + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Size = new Vector2(200, 1), + Alpha = 0, + RelativeSizeAxes = Axes.Y, + } } }; } + + protected override void LoadComplete() + { + base.LoadComplete(); + + room.PropertyChanged += onRoomChanged; + updateState(); + } + + private void hideCloseButton() + { + closeButton?.ResizeWidthTo(0, 100, Easing.OutQuint) + .Then().FadeOut().Expire(); + } + + private void onRoomChanged(object? sender, PropertyChangedEventArgs e) + { + if (e.PropertyName == nameof(Room.Status) || e.PropertyName == nameof(Room.Host) || e.PropertyName == nameof(Room.StartDate)) + updateState(); + } + + private void updateState() + { + TimeSpan? deletionGracePeriodRemaining = room.StartDate?.AddMinutes(5) - DateTimeOffset.Now; + + if (room.Host?.Id == api.LocalUser.Value.Id) + { + if (deletionGracePeriodRemaining > TimeSpan.Zero && room.Status is not RoomStatusEnded) + { + closeButton.FadeIn(); + using (BeginDelayedSequence(deletionGracePeriodRemaining.Value.TotalMilliseconds)) + hideCloseButton(); + } + else if (closeButton.Alpha > 0) + hideCloseButton(); + } + } + + protected override void Dispose(bool isDisposing) + { + base.Dispose(isDisposing); + + room.PropertyChanged -= onRoomChanged; + } } } diff --git a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs index 44d1841fb8..bac99123d5 100644 --- a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs @@ -12,7 +12,9 @@ using osu.Framework.Logging; using osu.Framework.Screens; using osu.Game.Graphics.Cursor; using osu.Game.Input; +using osu.Game.Online.API.Requests; using osu.Game.Online.Rooms; +using osu.Game.Online.Rooms.RoomStatuses; using osu.Game.Screens.OnlinePlay.Components; using osu.Game.Screens.OnlinePlay.Match; using osu.Game.Screens.OnlinePlay.Match.Components; @@ -255,7 +257,8 @@ namespace osu.Game.Screens.OnlinePlay.Playlists protected override Drawable CreateFooter() => new PlaylistsRoomFooter(Room) { - OnStart = StartPlay + OnStart = StartPlay, + OnClose = closePlaylist, }; protected override RoomSettingsOverlay CreateRoomSettingsOverlay(Room room) => new PlaylistsRoomSettingsOverlay(room) @@ -273,6 +276,16 @@ namespace osu.Game.Screens.OnlinePlay.Playlists Logger.Log($"Polling adjusted (selection: {selectionPollingComponent.TimeBetweenPolls.Value})"); } + private void closePlaylist() + { + DialogOverlay?.Push(new ClosePlaylistDialog(Room, () => + { + var request = new ClosePlaylistRequest(Room.RoomID!.Value); + request.Success += () => Room.Status = new RoomStatusEnded(); + API.Queue(request); + })); + } + protected override Screen CreateGameplayScreen(PlaylistItem selectedItem) { return new PlayerLoader(() => new PlaylistsPlayer(Room, selectedItem) From 8b68859d9d2ef11ee48a425a818a7f4f390b13c9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 22 Nov 2024 09:55:40 +0100 Subject: [PATCH 0060/1275] Fix `Room.CopyFrom()` skipping a field Was making the close button not display when creating a room anew. --- osu.Game/Online/Rooms/Room.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/osu.Game/Online/Rooms/Room.cs b/osu.Game/Online/Rooms/Room.cs index 486f70c0ed..094fe4ce56 100644 --- a/osu.Game/Online/Rooms/Room.cs +++ b/osu.Game/Online/Rooms/Room.cs @@ -379,6 +379,7 @@ namespace osu.Game.Online.Rooms Type = other.Type; MaxParticipants = other.MaxParticipants; ParticipantCount = other.ParticipantCount; + StartDate = other.StartDate; EndDate = other.EndDate; UserScore = other.UserScore; QueueMode = other.QueueMode; From c590bef4c3ce5550e0b1af2ad6ab1625348e58d5 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 22 Nov 2024 19:05:29 +0900 Subject: [PATCH 0061/1275] Remove legacy default setter for `SamplesMatchPlaybackRate` now that it's the default --- osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs b/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs index 3d8c8a6e7a..4d7ac355e0 100644 --- a/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs +++ b/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs @@ -192,7 +192,6 @@ namespace osu.Game.Beatmaps.Formats private static void applyLegacyDefaults(BeatmapInfo beatmapInfo) { beatmapInfo.WidescreenStoryboard = false; - beatmapInfo.SamplesMatchPlaybackRate = false; } protected override void ParseLine(Beatmap beatmap, Section section, string line) From ead7e99c591aa037110635a304d9368a8ea2435a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 22 Nov 2024 11:06:36 +0100 Subject: [PATCH 0062/1275] Fix incorrect comment --- osu.Game/Database/RealmAccess.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Database/RealmAccess.cs b/osu.Game/Database/RealmAccess.cs index 698acc9822..a520040ad1 100644 --- a/osu.Game/Database/RealmAccess.cs +++ b/osu.Game/Database/RealmAccess.cs @@ -94,7 +94,7 @@ namespace osu.Game.Database /// 41 2024-04-17 Add ScoreInfo.TotalScoreWithoutMods for future mod multiplier rebalances. /// 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 ScoreInfo which did not need to be persisted to realm. + /// 44 2024-11-22 Removed several properties from BeatmapInfo which did not need to be persisted to realm. /// private const int schema_version = 44; From c844d65a81da4606eee240ad58774d38953cedb4 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 22 Nov 2024 19:11:14 +0900 Subject: [PATCH 0063/1275] Use `TryGetValue` wherever possible Rider says so. --- .../Skinning/Legacy/ManiaLegacySkinTransformer.cs | 4 ++-- osu.Game/Overlays/Chat/ChannelList/ChannelList.cs | 7 +++---- osu.Game/Tests/Visual/Spectator/TestSpectatorClient.cs | 4 ++-- 3 files changed, 7 insertions(+), 8 deletions(-) diff --git a/osu.Game.Rulesets.Mania/Skinning/Legacy/ManiaLegacySkinTransformer.cs b/osu.Game.Rulesets.Mania/Skinning/Legacy/ManiaLegacySkinTransformer.cs index cb42b2b62a..8f425edc44 100644 --- a/osu.Game.Rulesets.Mania/Skinning/Legacy/ManiaLegacySkinTransformer.cs +++ b/osu.Game.Rulesets.Mania/Skinning/Legacy/ManiaLegacySkinTransformer.cs @@ -164,10 +164,10 @@ namespace osu.Game.Rulesets.Mania.Skinning.Legacy private Drawable getResult(HitResult result) { - if (!hit_result_mapping.ContainsKey(result)) + if (!hit_result_mapping.TryGetValue(result, out var value)) return null; - string filename = this.GetManiaSkinConfig(hit_result_mapping[result])?.Value + string filename = this.GetManiaSkinConfig(value)?.Value ?? default_hit_result_skin_filenames[result]; var animation = this.GetAnimation(filename, true, true, frameLength: 1000 / 20d); diff --git a/osu.Game/Overlays/Chat/ChannelList/ChannelList.cs b/osu.Game/Overlays/Chat/ChannelList/ChannelList.cs index fc0060d86a..3b657e7056 100644 --- a/osu.Game/Overlays/Chat/ChannelList/ChannelList.cs +++ b/osu.Game/Overlays/Chat/ChannelList/ChannelList.cs @@ -120,10 +120,9 @@ namespace osu.Game.Overlays.Chat.ChannelList public void RemoveChannel(Channel channel) { - if (!channelMap.ContainsKey(channel)) + if (!channelMap.TryGetValue(channel, out var item)) return; - ChannelListItem item = channelMap[channel]; FillFlowContainer flow = getFlowForChannel(channel); channelMap.Remove(channel); @@ -134,10 +133,10 @@ namespace osu.Game.Overlays.Chat.ChannelList public ChannelListItem GetItem(Channel channel) { - if (!channelMap.ContainsKey(channel)) + if (!channelMap.TryGetValue(channel, out var item)) throw new ArgumentOutOfRangeException(); - return channelMap[channel]; + return item; } public void ScrollChannelIntoView(Channel channel) => scroll.ScrollIntoView(GetItem(channel)); diff --git a/osu.Game/Tests/Visual/Spectator/TestSpectatorClient.cs b/osu.Game/Tests/Visual/Spectator/TestSpectatorClient.cs index c27e7f15ca..a311531088 100644 --- a/osu.Game/Tests/Visual/Spectator/TestSpectatorClient.cs +++ b/osu.Game/Tests/Visual/Spectator/TestSpectatorClient.cs @@ -78,12 +78,12 @@ namespace osu.Game.Tests.Visual.Spectator /// The spectator state to end play with. public void SendEndPlay(int userId, SpectatedUserState state = SpectatedUserState.Quit) { - if (!userBeatmapDictionary.ContainsKey(userId)) + if (!userBeatmapDictionary.TryGetValue(userId, out int value)) return; ((ISpectatorClient)this).UserFinishedPlaying(userId, new SpectatorState { - BeatmapID = userBeatmapDictionary[userId], + BeatmapID = value, RulesetID = 0, Mods = userModsDictionary[userId], State = state From 9930922769f092d5edd3968404bdd266ad8367fa Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 22 Nov 2024 19:51:55 +0900 Subject: [PATCH 0064/1275] Fix chat channel listing not being ordered to expectations - Public channels (and announcements) are now alphabetically ordered. - Private message channels are now ordered by most recent activity. Closes https://github.com/ppy/osu/issues/30835. --- .../Overlays/Chat/ChannelList/ChannelList.cs | 66 ++++++++++++++----- 1 file changed, 51 insertions(+), 15 deletions(-) diff --git a/osu.Game/Overlays/Chat/ChannelList/ChannelList.cs b/osu.Game/Overlays/Chat/ChannelList/ChannelList.cs index 3b657e7056..a2ec385a7e 100644 --- a/osu.Game/Overlays/Chat/ChannelList/ChannelList.cs +++ b/osu.Game/Overlays/Chat/ChannelList/ChannelList.cs @@ -77,10 +77,10 @@ namespace osu.Game.Overlays.Chat.ChannelList RelativeSizeAxes = Axes.X, } }, - announceChannelGroup = new ChannelGroup(ChatStrings.ChannelsListTitleANNOUNCE.ToUpper()), - publicChannelGroup = new ChannelGroup(ChatStrings.ChannelsListTitlePUBLIC.ToUpper()), + announceChannelGroup = new ChannelGroup(ChatStrings.ChannelsListTitleANNOUNCE.ToUpper(), false), + publicChannelGroup = new ChannelGroup(ChatStrings.ChannelsListTitlePUBLIC.ToUpper(), false), selector = new ChannelListItem(ChannelListingChannel), - privateChannelGroup = new ChannelGroup(ChatStrings.ChannelsListTitlePM.ToUpper()), + privateChannelGroup = new ChannelGroup(ChatStrings.ChannelsListTitlePM.ToUpper(), true), }, }, }, @@ -111,9 +111,9 @@ namespace osu.Game.Overlays.Chat.ChannelList item.OnRequestSelect += chan => OnRequestSelect?.Invoke(chan); item.OnRequestLeave += chan => OnRequestLeave?.Invoke(chan); - FillFlowContainer flow = getFlowForChannel(channel); + ChannelGroup group = getGroupFromChannel(channel); channelMap.Add(channel, item); - flow.Add(item); + group.AddChannel(item); updateVisibility(); } @@ -123,10 +123,10 @@ namespace osu.Game.Overlays.Chat.ChannelList if (!channelMap.TryGetValue(channel, out var item)) return; - FillFlowContainer flow = getFlowForChannel(channel); + ChannelGroup group = getGroupFromChannel(channel); channelMap.Remove(channel); - flow.Remove(item, true); + group.RemoveChannel(item); updateVisibility(); } @@ -141,21 +141,21 @@ namespace osu.Game.Overlays.Chat.ChannelList public void ScrollChannelIntoView(Channel channel) => scroll.ScrollIntoView(GetItem(channel)); - private FillFlowContainer getFlowForChannel(Channel channel) + private ChannelGroup getGroupFromChannel(Channel channel) { switch (channel.Type) { case ChannelType.Public: - return publicChannelGroup.ItemFlow; + return publicChannelGroup; case ChannelType.PM: - return privateChannelGroup.ItemFlow; + return privateChannelGroup; case ChannelType.Announce: - return announceChannelGroup.ItemFlow; + return announceChannelGroup; default: - return publicChannelGroup.ItemFlow; + return publicChannelGroup; } } @@ -169,9 +169,9 @@ namespace osu.Game.Overlays.Chat.ChannelList private partial class ChannelGroup : FillFlowContainer { - public readonly FillFlowContainer ItemFlow; + public readonly ChannelListItemFlow ItemFlow; - public ChannelGroup(LocalisableString label) + public ChannelGroup(LocalisableString label, bool sortByRecent) { Direction = FillDirection.Vertical; RelativeSizeAxes = Axes.X; @@ -186,7 +186,7 @@ namespace osu.Game.Overlays.Chat.ChannelList Margin = new MarginPadding { Left = 18, Bottom = 5 }, Font = OsuFont.Torus.With(size: 12, weight: FontWeight.SemiBold), }, - ItemFlow = new FillFlowContainer + ItemFlow = new ChannelListItemFlow(sortByRecent) { Direction = FillDirection.Vertical, RelativeSizeAxes = Axes.X, @@ -194,6 +194,42 @@ namespace osu.Game.Overlays.Chat.ChannelList }, }; } + + public partial class ChannelListItemFlow : FillFlowContainer + { + private readonly bool sortByRecent; + + public ChannelListItemFlow(bool sortByRecent) + { + this.sortByRecent = sortByRecent; + } + + public void Reflow() => InvalidateLayout(); + + public override IEnumerable FlowingChildren => sortByRecent + ? base.FlowingChildren.OfType().OrderByDescending(i => i.Channel.LastMessageId) + : base.FlowingChildren.OfType().OrderBy(i => i.Channel.Name); + } + + public void AddChannel(ChannelListItem item) + { + ItemFlow.Add(item); + + item.Channel.NewMessagesArrived += newMessagesArrived; + item.Channel.PendingMessageResolved += pendingMessageResolved; + + ItemFlow.Reflow(); + } + + public void RemoveChannel(ChannelListItem item) + { + item.Channel.NewMessagesArrived -= newMessagesArrived; + item.Channel.PendingMessageResolved -= pendingMessageResolved; + ItemFlow.Remove(item, true); + } + + private void pendingMessageResolved(LocalEchoMessage _, Message __) => ItemFlow.Reflow(); + private void newMessagesArrived(IEnumerable _) => ItemFlow.Reflow(); } private partial class ChannelSearchTextBox : BasicSearchTextBox From 82a63228de1984249136d9593405921127346d69 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Fri, 22 Nov 2024 20:40:01 +0900 Subject: [PATCH 0065/1275] Improve handling of multiplayer room status --- .../Multiplayer/TestSceneDrawableRoom.cs | 16 ++++---- .../TestScenePlaylistsRoomSubScreen.cs | 41 ------------------- osu.Game/Graphics/OsuColour.cs | 21 ++++++++++ .../Online/Multiplayer/MultiplayerClient.cs | 10 ++--- osu.Game/Online/Rooms/Room.cs | 21 ++-------- osu.Game/Online/Rooms/RoomStatus.cs | 14 ++----- .../Rooms/RoomStatuses/RoomStatusEnded.cs | 14 ------- .../Rooms/RoomStatuses/RoomStatusOpen.cs | 14 ------- .../RoomStatuses/RoomStatusOpenPrivate.cs | 14 ------- .../Rooms/RoomStatuses/RoomStatusPlaying.cs | 14 ------- .../Components/StatusColouredContainer.cs | 15 +++++-- .../Lounge/Components/RoomStatusPill.cs | 24 +++++++++-- .../Multiplayer/MultiplayerRoomManager.cs | 3 +- 13 files changed, 74 insertions(+), 147 deletions(-) delete mode 100644 osu.Game.Tests/Visual/Playlists/TestScenePlaylistsRoomSubScreen.cs delete mode 100644 osu.Game/Online/Rooms/RoomStatuses/RoomStatusEnded.cs delete mode 100644 osu.Game/Online/Rooms/RoomStatuses/RoomStatusOpen.cs delete mode 100644 osu.Game/Online/Rooms/RoomStatuses/RoomStatusOpenPrivate.cs delete mode 100644 osu.Game/Online/Rooms/RoomStatuses/RoomStatusPlaying.cs diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneDrawableRoom.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneDrawableRoom.cs index e5938a796c..abfe613b65 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneDrawableRoom.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneDrawableRoom.cs @@ -14,7 +14,6 @@ using osu.Game.Graphics.Sprites; using osu.Game.Online.API.Requests.Responses; using osu.Game.Online.Multiplayer; using osu.Game.Online.Rooms; -using osu.Game.Online.Rooms.RoomStatuses; using osu.Game.Overlays; using osu.Game.Rulesets.Osu; using osu.Game.Screens.OnlinePlay.Lounge; @@ -76,7 +75,6 @@ namespace osu.Game.Tests.Visual.Multiplayer createLoungeRoom(new Room { Name = "Multiplayer room", - Status = new RoomStatusOpen(), EndDate = DateTimeOffset.Now.AddDays(1), Type = MatchType.HeadToHead, Playlist = [item1], @@ -85,7 +83,6 @@ namespace osu.Game.Tests.Visual.Multiplayer createLoungeRoom(new Room { Name = "Private room", - Status = new RoomStatusOpenPrivate(), Password = "*", EndDate = DateTimeOffset.Now.AddDays(1), Type = MatchType.HeadToHead, @@ -95,27 +92,29 @@ namespace osu.Game.Tests.Visual.Multiplayer createLoungeRoom(new Room { Name = "Playlist room with multiple beatmaps", - Status = new RoomStatusPlaying(), + Status = RoomStatus.Playing, EndDate = DateTimeOffset.Now.AddDays(1), Playlist = [item1, item2], CurrentPlaylistItem = item1 }), createLoungeRoom(new Room { - Name = "Finished room", - Status = new RoomStatusEnded(), + Name = "Closing soon", + EndDate = DateTimeOffset.Now.AddSeconds(5), + }), + createLoungeRoom(new Room + { + Name = "Closed room", EndDate = DateTimeOffset.Now, }), createLoungeRoom(new Room { Name = "Spotlight room", - Status = new RoomStatusOpen(), Category = RoomCategory.Spotlight, }), createLoungeRoom(new Room { Name = "Featured artist room", - Status = new RoomStatusOpen(), Category = RoomCategory.FeaturedArtist, }), } @@ -136,7 +135,6 @@ namespace osu.Game.Tests.Visual.Multiplayer AddStep("create room", () => Child = drawableRoom = createLoungeRoom(room = new Room { Name = "Room with password", - Status = new RoomStatusOpen(), Type = MatchType.HeadToHead, })); diff --git a/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsRoomSubScreen.cs b/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsRoomSubScreen.cs deleted file mode 100644 index 4306fc1e6a..0000000000 --- a/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsRoomSubScreen.cs +++ /dev/null @@ -1,41 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -using System; -using NUnit.Framework; -using osu.Framework.Screens; -using osu.Game.Online.API.Requests.Responses; -using osu.Game.Online.Rooms; -using osu.Game.Online.Rooms.RoomStatuses; -using osu.Game.Screens.OnlinePlay.Playlists; -using osu.Game.Tests.Visual.OnlinePlay; - -namespace osu.Game.Tests.Visual.Playlists -{ - public partial class TestScenePlaylistsRoomSubScreen : OnlinePlayTestScene - { - protected new TestRoomManager RoomManager => (TestRoomManager)base.RoomManager; - - [Test] - public void TestStatusUpdateOnEnter() - { - Room room = null!; - PlaylistsRoomSubScreen roomScreen = null!; - - AddStep("create room", () => - { - RoomManager.AddRoom(room = new Room - { - Name = @"Test Room", - Host = new APIUser { Username = @"Host" }, - Category = RoomCategory.Normal, - EndDate = DateTimeOffset.Now.AddMinutes(-1) - }); - }); - - AddStep("push screen", () => LoadScreen(roomScreen = new PlaylistsRoomSubScreen(room))); - AddUntilStep("wait for screen load", () => roomScreen.IsCurrentScreen()); - AddAssert("status is still ended", () => roomScreen.Room.Status, Is.TypeOf); - } - } -} diff --git a/osu.Game/Graphics/OsuColour.cs b/osu.Game/Graphics/OsuColour.cs index c479d0cfe4..20e65323f8 100644 --- a/osu.Game/Graphics/OsuColour.cs +++ b/osu.Game/Graphics/OsuColour.cs @@ -195,6 +195,27 @@ namespace osu.Game.Graphics } } + /// + /// Retrieves the accent colour representing a 's current status. + /// + public Color4 ForRoomStatus(Room room) + { + if (DateTimeOffset.Now >= room.EndDate) + return YellowDarker; + + switch (room.Status) + { + case RoomStatus.Playing: + return Purple; + + default: + if (room.HasPassword) + return GreenDark; + + return GreenLight; + } + } + /// /// Retrieves colour for a . /// See https://www.figma.com/file/YHWhp9wZ089YXgB7pe6L1k/Tier-Colours diff --git a/osu.Game/Online/Multiplayer/MultiplayerClient.cs b/osu.Game/Online/Multiplayer/MultiplayerClient.cs index 998a34931d..4a28124583 100644 --- a/osu.Game/Online/Multiplayer/MultiplayerClient.cs +++ b/osu.Game/Online/Multiplayer/MultiplayerClient.cs @@ -18,7 +18,6 @@ using osu.Game.Online.API.Requests; using osu.Game.Online.API.Requests.Responses; using osu.Game.Online.Multiplayer.Countdown; using osu.Game.Online.Rooms; -using osu.Game.Online.Rooms.RoomStatuses; using osu.Game.Overlays.Notifications; using osu.Game.Rulesets; using osu.Game.Rulesets.Mods; @@ -395,15 +394,17 @@ namespace osu.Game.Online.Multiplayer switch (state) { case MultiplayerRoomState.Open: - APIRoom.Status = APIRoom.HasPassword ? new RoomStatusOpenPrivate() : new RoomStatusOpen(); + APIRoom.Status = RoomStatus.Idle; break; + case MultiplayerRoomState.WaitingForLoad: case MultiplayerRoomState.Playing: - APIRoom.Status = new RoomStatusPlaying(); + APIRoom.Status = RoomStatus.Playing; break; case MultiplayerRoomState.Closed: - APIRoom.Status = new RoomStatusEnded(); + APIRoom.EndDate = DateTimeOffset.Now; + APIRoom.Status = RoomStatus.Idle; break; } @@ -821,7 +822,6 @@ namespace osu.Game.Online.Multiplayer Room.Settings = settings; APIRoom.Name = Room.Settings.Name; APIRoom.Password = Room.Settings.Password; - APIRoom.Status = string.IsNullOrEmpty(Room.Settings.Password) ? new RoomStatusOpen() : new RoomStatusOpenPrivate(); APIRoom.Type = Room.Settings.MatchType; APIRoom.QueueMode = Room.Settings.QueueMode; APIRoom.AutoStartDuration = Room.Settings.AutoStartDuration; diff --git a/osu.Game/Online/Rooms/Room.cs b/osu.Game/Online/Rooms/Room.cs index e1813c7e4e..6e073bdcd7 100644 --- a/osu.Game/Online/Rooms/Room.cs +++ b/osu.Game/Online/Rooms/Room.cs @@ -6,12 +6,10 @@ using System.Collections.Generic; using System.ComponentModel; using System.Linq; using System.Runtime.CompilerServices; -using System.Runtime.Serialization; using Newtonsoft.Json; using osu.Game.IO.Serialization.Converters; using osu.Game.Online.API.Requests.Responses; using osu.Game.Online.Multiplayer; -using osu.Game.Online.Rooms.RoomStatuses; namespace osu.Game.Online.Rooms { @@ -248,7 +246,7 @@ namespace osu.Game.Online.Rooms } /// - /// The current room status. + /// The current status of the room. /// public RoomStatus Status { @@ -265,18 +263,6 @@ namespace osu.Game.Online.Rooms set => SetField(ref availability, value); } - [OnDeserialized] - private void onDeserialised(StreamingContext context) - { - // API doesn't populate status so let's do it here. - if (EndDate != null && DateTimeOffset.Now >= EndDate) - Status = new RoomStatusEnded(); - else if (HasPassword) - Status = new RoomStatusOpenPrivate(); - else - Status = new RoomStatusOpen(); - } - [JsonProperty("id")] private long? roomId; @@ -349,8 +335,9 @@ namespace osu.Game.Online.Rooms [JsonProperty("channel_id")] private int channelId; - // Not serialised (see: GetRoomsRequest). - private RoomStatus status = new RoomStatusOpen(); + [JsonProperty("status")] + [JsonConverter(typeof(SnakeCaseStringEnumConverter))] + private RoomStatus status; // Not yet serialised (not implemented). private RoomAvailability availability; diff --git a/osu.Game/Online/Rooms/RoomStatus.cs b/osu.Game/Online/Rooms/RoomStatus.cs index 4b890b00b7..d048486f19 100644 --- a/osu.Game/Online/Rooms/RoomStatus.cs +++ b/osu.Game/Online/Rooms/RoomStatus.cs @@ -1,19 +1,11 @@ // 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.Game.Graphics; -using osuTK.Graphics; - namespace osu.Game.Online.Rooms { - public abstract class RoomStatus + public enum RoomStatus { - public abstract string Message { get; } - public abstract Color4 GetAppropriateColour(OsuColour colours); - - public override int GetHashCode() => GetType().GetHashCode(); - public override bool Equals(object obj) => GetType() == obj?.GetType(); + Idle, + Playing, } } diff --git a/osu.Game/Online/Rooms/RoomStatuses/RoomStatusEnded.cs b/osu.Game/Online/Rooms/RoomStatuses/RoomStatusEnded.cs deleted file mode 100644 index 0fc27d26b8..0000000000 --- a/osu.Game/Online/Rooms/RoomStatuses/RoomStatusEnded.cs +++ /dev/null @@ -1,14 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -using osu.Game.Graphics; -using osuTK.Graphics; - -namespace osu.Game.Online.Rooms.RoomStatuses -{ - public class RoomStatusEnded : RoomStatus - { - public override string Message => "Ended"; - public override Color4 GetAppropriateColour(OsuColour colours) => colours.YellowDarker; - } -} diff --git a/osu.Game/Online/Rooms/RoomStatuses/RoomStatusOpen.cs b/osu.Game/Online/Rooms/RoomStatuses/RoomStatusOpen.cs deleted file mode 100644 index 5cc664cf36..0000000000 --- a/osu.Game/Online/Rooms/RoomStatuses/RoomStatusOpen.cs +++ /dev/null @@ -1,14 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -using osu.Game.Graphics; -using osuTK.Graphics; - -namespace osu.Game.Online.Rooms.RoomStatuses -{ - public class RoomStatusOpen : RoomStatus - { - public override string Message => "Open"; - public override Color4 GetAppropriateColour(OsuColour colours) => colours.GreenLight; - } -} diff --git a/osu.Game/Online/Rooms/RoomStatuses/RoomStatusOpenPrivate.cs b/osu.Game/Online/Rooms/RoomStatuses/RoomStatusOpenPrivate.cs deleted file mode 100644 index d71e706c76..0000000000 --- a/osu.Game/Online/Rooms/RoomStatuses/RoomStatusOpenPrivate.cs +++ /dev/null @@ -1,14 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -using osu.Game.Graphics; -using osuTK.Graphics; - -namespace osu.Game.Online.Rooms.RoomStatuses -{ - public class RoomStatusOpenPrivate : RoomStatus - { - public override string Message => "Open (Private)"; - public override Color4 GetAppropriateColour(OsuColour colours) => colours.GreenDark; - } -} diff --git a/osu.Game/Online/Rooms/RoomStatuses/RoomStatusPlaying.cs b/osu.Game/Online/Rooms/RoomStatuses/RoomStatusPlaying.cs deleted file mode 100644 index 4d0c93b8ab..0000000000 --- a/osu.Game/Online/Rooms/RoomStatuses/RoomStatusPlaying.cs +++ /dev/null @@ -1,14 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -using osu.Game.Graphics; -using osuTK.Graphics; - -namespace osu.Game.Online.Rooms.RoomStatuses -{ - public class RoomStatusPlaying : RoomStatus - { - public override string Message => "Playing"; - public override Color4 GetAppropriateColour(OsuColour colours) => colours.Purple; - } -} diff --git a/osu.Game/Screens/OnlinePlay/Components/StatusColouredContainer.cs b/osu.Game/Screens/OnlinePlay/Components/StatusColouredContainer.cs index 2b1233506f..a811ee3371 100644 --- a/osu.Game/Screens/OnlinePlay/Components/StatusColouredContainer.cs +++ b/osu.Game/Screens/OnlinePlay/Components/StatusColouredContainer.cs @@ -29,18 +29,27 @@ namespace osu.Game.Screens.OnlinePlay.Components base.LoadComplete(); room.PropertyChanged += onRoomPropertyChanged; + + Scheduler.AddDelayed(updateRoomStatus, 5000, true); updateRoomStatus(); } private void onRoomPropertyChanged(object? sender, PropertyChangedEventArgs e) { - if (e.PropertyName == nameof(Room.Status)) - updateRoomStatus(); + switch (e.PropertyName) + { + case nameof(Room.Category): + case nameof(Room.Status): + case nameof(Room.EndDate): + case nameof(Room.HasPassword): + updateRoomStatus(); + break; + } } private void updateRoomStatus() { - this.FadeColour(colours.ForRoomCategory(room.Category) ?? room.Status.GetAppropriateColour(colours), transitionDuration); + this.FadeColour(colours.ForRoomCategory(room.Category) ?? colours.ForRoomStatus(room), transitionDuration); } protected override void Dispose(bool isDisposing) diff --git a/osu.Game/Screens/OnlinePlay/Lounge/Components/RoomStatusPill.cs b/osu.Game/Screens/OnlinePlay/Lounge/Components/RoomStatusPill.cs index b3dc617fd6..cc495d19d6 100644 --- a/osu.Game/Screens/OnlinePlay/Lounge/Components/RoomStatusPill.cs +++ b/osu.Game/Screens/OnlinePlay/Lounge/Components/RoomStatusPill.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.ComponentModel; using osu.Framework.Allocation; using osu.Framework.Graphics; @@ -35,8 +36,9 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components Pill.Background.Alpha = 1; room.PropertyChanged += onRoomPropertyChanged; - updateDisplay(); + Scheduler.AddDelayed(updateDisplay, 5000, true); + updateDisplay(); FinishTransforms(true); } @@ -46,6 +48,7 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components { case nameof(Room.Status): case nameof(Room.EndDate): + case nameof(Room.HasPassword): updateDisplay(); break; } @@ -53,8 +56,23 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components private void updateDisplay() { - Pill.Background.FadeColour(room.Status.GetAppropriateColour(colours), 100); - TextFlow.Text = room.Status.Message; + Pill.Background.FadeColour(colours.ForRoomStatus(room), 100); + + if (DateTimeOffset.Now >= room.EndDate) + TextFlow.Text = "Ended"; + else + { + switch (room.Status) + { + case RoomStatus.Playing: + TextFlow.Text = "Playing"; + break; + + default: + TextFlow.Text = room.HasPassword ? "Open (Private)" : "Open"; + break; + } + } } protected override void Dispose(bool isDisposing) diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerRoomManager.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerRoomManager.cs index e16582a6e1..b6f4b0e8d9 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerRoomManager.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerRoomManager.cs @@ -8,7 +8,6 @@ using osu.Framework.Extensions.ExceptionExtensions; using osu.Framework.Logging; using osu.Game.Online.Multiplayer; using osu.Game.Online.Rooms; -using osu.Game.Online.Rooms.RoomStatuses; using osu.Game.Screens.OnlinePlay.Components; namespace osu.Game.Screens.OnlinePlay.Multiplayer @@ -31,7 +30,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer // this is done here as a pre-check to avoid clicking on already closed rooms in the lounge from triggering a server join. // should probably be done at a higher level, but due to the current structure of things this is the easiest place for now. - if (room.Status is RoomStatusEnded) + if (DateTimeOffset.Now >= room.EndDate) { onError?.Invoke("Cannot join an ended room."); return; From 5ebaab7e9aafcd2c220bbc737c1f5354a217dc11 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Fri, 22 Nov 2024 21:04:57 +0900 Subject: [PATCH 0066/1275] Add localisation --- .../Localisation/RoomStatusPillStrings.cs | 34 +++++++++++++++++++ .../Lounge/Components/RoomStatusPill.cs | 7 ++-- 2 files changed, 38 insertions(+), 3 deletions(-) create mode 100644 osu.Game/Localisation/RoomStatusPillStrings.cs diff --git a/osu.Game/Localisation/RoomStatusPillStrings.cs b/osu.Game/Localisation/RoomStatusPillStrings.cs new file mode 100644 index 0000000000..5b4aa776ab --- /dev/null +++ b/osu.Game/Localisation/RoomStatusPillStrings.cs @@ -0,0 +1,34 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Localisation; + +namespace osu.Game.Localisation +{ + public static class RoomStatusPillStrings + { + private const string prefix = @"osu.Game.Resources.Localisation.RoomStatusPill"; + + /// + /// "Ended" + /// + public static LocalisableString Ended => new TranslatableString(getKey(@"ended"), @"Ended"); + + /// + /// "Playing" + /// + public static LocalisableString Playing => new TranslatableString(getKey(@"playing"), @"Playing"); + + /// + /// "Open (Private)" + /// + public static LocalisableString OpenPrivate => new TranslatableString(getKey(@"open_private"), @"Open (Private)"); + + /// + /// "Open" + /// + public static LocalisableString Open => new TranslatableString(getKey(@"open"), @"Open"); + + private static string getKey(string key) => $@"{prefix}:{key}"; + } +} \ No newline at end of file diff --git a/osu.Game/Screens/OnlinePlay/Lounge/Components/RoomStatusPill.cs b/osu.Game/Screens/OnlinePlay/Lounge/Components/RoomStatusPill.cs index cc495d19d6..5d2c4b28e6 100644 --- a/osu.Game/Screens/OnlinePlay/Lounge/Components/RoomStatusPill.cs +++ b/osu.Game/Screens/OnlinePlay/Lounge/Components/RoomStatusPill.cs @@ -8,6 +8,7 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Sprites; using osu.Game.Graphics; using osu.Game.Online.Rooms; +using osu.Game.Localisation; namespace osu.Game.Screens.OnlinePlay.Lounge.Components { @@ -59,17 +60,17 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components Pill.Background.FadeColour(colours.ForRoomStatus(room), 100); if (DateTimeOffset.Now >= room.EndDate) - TextFlow.Text = "Ended"; + TextFlow.Text = RoomStatusPillStrings.Ended; else { switch (room.Status) { case RoomStatus.Playing: - TextFlow.Text = "Playing"; + TextFlow.Text = RoomStatusPillStrings.Playing; break; default: - TextFlow.Text = room.HasPassword ? "Open (Private)" : "Open"; + TextFlow.Text = room.HasPassword ? RoomStatusPillStrings.OpenPrivate : RoomStatusPillStrings.Open; break; } } From 1b8db7cfd6c7fc094c6c1367bd3a2c267f7b4947 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Fri, 22 Nov 2024 21:27:44 +0900 Subject: [PATCH 0067/1275] Fix test --- osu.Game.Tests/Visual/Multiplayer/TestSceneDrawableRoom.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneDrawableRoom.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneDrawableRoom.cs index abfe613b65..021c0abf1d 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneDrawableRoom.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneDrawableRoom.cs @@ -121,9 +121,9 @@ namespace osu.Game.Tests.Visual.Multiplayer }; }); - AddUntilStep("wait for panel load", () => rooms.Count == 6); + AddUntilStep("wait for panel load", () => rooms.Count == 7); AddUntilStep("correct status text", () => rooms.ChildrenOfType().Count(s => s.Text.ToString().StartsWith("Currently playing", StringComparison.Ordinal)) == 2); - AddUntilStep("correct status text", () => rooms.ChildrenOfType().Count(s => s.Text.ToString().StartsWith("Ready to play", StringComparison.Ordinal)) == 4); + AddUntilStep("correct status text", () => rooms.ChildrenOfType().Count(s => s.Text.ToString().StartsWith("Ready to play", StringComparison.Ordinal)) == 5); } [Test] From 62837c7e53de8e9130c216b3b6c3158ef2504d78 Mon Sep 17 00:00:00 2001 From: Joseph Madamba Date: Sat, 9 Nov 2024 10:27:23 -0800 Subject: [PATCH 0068/1275] Fix discord "view beatmap" button being shown when editing and hide identifiable information is set --- osu.Desktop/DiscordRichPresence.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/osu.Desktop/DiscordRichPresence.cs b/osu.Desktop/DiscordRichPresence.cs index 5a7a01df1b..ba61f4be34 100644 --- a/osu.Desktop/DiscordRichPresence.cs +++ b/osu.Desktop/DiscordRichPresence.cs @@ -167,7 +167,9 @@ namespace osu.Desktop presence.State = clampLength(activity.Value.GetStatus(hideIdentifiableInformation)); presence.Details = clampLength(activity.Value.GetDetails(hideIdentifiableInformation) ?? string.Empty); - if (getBeatmapID(activity.Value) is int beatmapId && beatmapId > 0) + if (getBeatmapID(activity.Value) is int beatmapId + && beatmapId > 0 + && !(activity.Value is UserActivity.EditingBeatmap && hideIdentifiableInformation)) { presence.Buttons = new[] { From 3713bb48b775c00da2fbbb6d86b98fc7b8ddafa5 Mon Sep 17 00:00:00 2001 From: Sheppsu <49356627+Sheppsu@users.noreply.github.com> Date: Sat, 23 Nov 2024 01:09:58 -0500 Subject: [PATCH 0069/1275] expand and contract settings from hover --- .../Spectate/MultiSpectatorSettings.cs | 78 +++++++------------ 1 file changed, 29 insertions(+), 49 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorSettings.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorSettings.cs index 64c798b092..dfb26d104a 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorSettings.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorSettings.cs @@ -1,80 +1,60 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Sprites; +using osu.Framework.Input.Events; +using osu.Game.Graphics.Containers; using osu.Game.Graphics.UserInterface; using osu.Game.Screens.Play.HUD; -using osuTK; namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate { - public partial class MultiSpectatorSettings : CompositeDrawable + public partial class MultiSpectatorSettings : ExpandingContainer { - private const double slide_duration = 200; - - private readonly PlayerSettingsOverlay playerSettingsOverlay; - private readonly Container slidingContainer; - - private readonly BindableBool opened = new BindableBool(); + public const float CONTRACTED_WIDTH = 30; + public const int EXPANDED_WIDTH = 300; public MultiSpectatorSettings() + : base(CONTRACTED_WIDTH, EXPANDED_WIDTH) { - Origin = Anchor.TopLeft; + Origin = Anchor.TopRight; Anchor = Anchor.TopRight; - AutoSizeAxes = Axes.Both; - InternalChildren = new Drawable[] + PlayerSettingsOverlay playerSettingsOverlay; + + InternalChild = new FillFlowContainer { - slidingContainer = new Container + Origin = Anchor.TopLeft, + Anchor = Anchor.TopLeft, + Direction = FillDirection.Horizontal, + AutoSizeAxes = Axes.Both, + Children = new Drawable[] { - Origin = Anchor.TopLeft, - Anchor = Anchor.TopLeft, - AutoSizeAxes = Axes.Both, - Children = new Drawable[] + new IconButton { - new IconButton - { - Icon = FontAwesome.Solid.Cog, - Origin = Anchor.TopLeft, - Anchor = Anchor.TopLeft, - Position = new Vector2(-30, 0), - Action = () => opened.Toggle() - }, - playerSettingsOverlay = new PlayerSettingsOverlay() + Icon = FontAwesome.Solid.Cog, + Origin = Anchor.TopLeft, + Anchor = Anchor.TopLeft, + Action = () => Expanded.Toggle() + }, + playerSettingsOverlay = new PlayerSettingsOverlay + { + Anchor = Anchor.TopLeft, + Origin = Anchor.TopLeft } } }; playerSettingsOverlay.Show(); - - opened.BindValueChanged(value => - { - if (value.NewValue) - open(); - else - close(); - }); } - private void open() + protected override void OnHoverLost(HoverLostEvent e) { - slidingContainer.MoveToOffset(new Vector2(-playerSettingsOverlay.Width, 0), slide_duration, Easing.Out).Then().OnComplete(c => - { - c.Origin = Anchor.TopRight; - c.Position = Vector2.Zero; - }); - } - - private void close() - { - slidingContainer.MoveToOffset(new Vector2(playerSettingsOverlay.Width, 0), slide_duration, Easing.Out).Then().OnComplete(c => - { - c.Origin = Anchor.TopLeft; - c.Position = Vector2.Zero; - }); + // Prevent unexpanding when hovering player settings + if (!Contains(e.ScreenSpaceMousePosition)) + base.OnHoverLost(e); } } } From eed02c2ab143b0fd1973906aa5a3d629f581eb49 Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Sat, 23 Nov 2024 15:45:29 -0500 Subject: [PATCH 0070/1275] Fix daily challenge results screen beginning score fetch from user highest --- .../DailyChallenge/DailyChallenge.cs | 2 +- .../DailyChallenge/DailyChallengePlayer.cs | 41 +++++++++++++++++++ 2 files changed, 42 insertions(+), 1 deletion(-) create mode 100644 osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallengePlayer.cs diff --git a/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallenge.cs b/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallenge.cs index 0dc7e7930a..6cb8a87a2a 100644 --- a/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallenge.cs +++ b/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallenge.cs @@ -532,7 +532,7 @@ namespace osu.Game.Screens.OnlinePlay.DailyChallenge private void startPlay() { sampleStart?.Play(); - this.Push(new PlayerLoader(() => new PlaylistsPlayer(room, playlistItem) + this.Push(new PlayerLoader(() => new DailyChallengePlayer(room, playlistItem) { Exited = () => Scheduler.AddOnce(() => leaderboard.RefetchScores()) })); diff --git a/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallengePlayer.cs b/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallengePlayer.cs new file mode 100644 index 0000000000..cfc0898e5a --- /dev/null +++ b/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallengePlayer.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.Diagnostics; +using osu.Game.Online.Rooms; +using osu.Game.Scoring; +using osu.Game.Screens.OnlinePlay.Playlists; +using osu.Game.Screens.Play; +using osu.Game.Screens.Ranking; + +namespace osu.Game.Screens.OnlinePlay.DailyChallenge +{ + public partial class DailyChallengePlayer : PlaylistsPlayer + { + public DailyChallengePlayer(Room room, PlaylistItem playlistItem, PlayerConfiguration? configuration = null) + : base(room, playlistItem, configuration) + { + } + + protected override ResultsScreen CreateResults(ScoreInfo score) + { + Debug.Assert(Room.RoomID != null); + + if (score.OnlineID >= 0) + { + return new PlaylistItemScoreResultsScreen(Room.RoomID.Value, PlaylistItem, score.OnlineID) + { + AllowRetry = true, + ShowUserStatistics = true, + }; + } + + // If the score has failed submission, fall back to displaying scores from user's highest. + return new PlaylistItemUserResultsScreen(score, Room.RoomID.Value, PlaylistItem) + { + AllowRetry = true, + ShowUserStatistics = true, + }; + } + } +} From 8f5d513d461affec8f3812beb18136d3228d08d4 Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Sat, 23 Nov 2024 22:16:11 -0500 Subject: [PATCH 0071/1275] Fix room auto start duration setting applied to the wrong component --- .../Multiplayer/Match/MultiplayerMatchSettingsOverlay.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerMatchSettingsOverlay.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerMatchSettingsOverlay.cs index 1dbef079d4..79617f172c 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerMatchSettingsOverlay.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerMatchSettingsOverlay.cs @@ -438,7 +438,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match => MaxParticipantsField.Text = room.MaxParticipants?.ToString(); private void updateRoomAutoStartDuration() - => typeLabel.Text = room.AutoStartDuration.GetLocalisableDescription(); + => startModeDropdown.Current.Value = (StartMode)room.AutoStartDuration.TotalSeconds; private void updateRoomPlaylist() => drawablePlaylist.Items.ReplaceRange(0, drawablePlaylist.Items.Count, room.Playlist); From cab26c70c1f2fca14b3feaa2abff5385963a401b Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Sat, 23 Nov 2024 22:27:56 -0500 Subject: [PATCH 0072/1275] Fix editor grid settings not displaying decimal portion in slider tooltips --- osu.Game.Rulesets.Osu/Edit/OsuGridToolboxGroup.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/osu.Game.Rulesets.Osu/Edit/OsuGridToolboxGroup.cs b/osu.Game.Rulesets.Osu/Edit/OsuGridToolboxGroup.cs index 768a764ad1..2fe0d51034 100644 --- a/osu.Game.Rulesets.Osu/Edit/OsuGridToolboxGroup.cs +++ b/osu.Game.Rulesets.Osu/Edit/OsuGridToolboxGroup.cs @@ -38,6 +38,7 @@ namespace osu.Game.Rulesets.Osu.Edit { MinValue = 0f, MaxValue = OsuPlayfield.BASE_SIZE.X, + Precision = 0.01f, }; /// @@ -47,6 +48,7 @@ namespace osu.Game.Rulesets.Osu.Edit { MinValue = 0f, MaxValue = OsuPlayfield.BASE_SIZE.Y, + Precision = 0.01f, }; /// @@ -56,6 +58,7 @@ namespace osu.Game.Rulesets.Osu.Edit { MinValue = 4f, MaxValue = 128f, + Precision = 0.01f, }; /// @@ -65,6 +68,7 @@ namespace osu.Game.Rulesets.Osu.Edit { MinValue = -180f, MaxValue = 180f, + Precision = 0.01f, }; /// From 259ad8ae0f55f7802d252529b0cc80373c5504a5 Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Sat, 23 Nov 2024 23:28:22 -0500 Subject: [PATCH 0073/1275] Add failing test cases --- .../Editing/TestSceneEditorBeatmapCreation.cs | 131 +++++++++++++++--- 1 file changed, 113 insertions(+), 18 deletions(-) diff --git a/osu.Game.Tests/Visual/Editing/TestSceneEditorBeatmapCreation.cs b/osu.Game.Tests/Visual/Editing/TestSceneEditorBeatmapCreation.cs index db87987815..b15ee0cab8 100644 --- a/osu.Game.Tests/Visual/Editing/TestSceneEditorBeatmapCreation.cs +++ b/osu.Game.Tests/Visual/Editing/TestSceneEditorBeatmapCreation.cs @@ -104,28 +104,12 @@ namespace osu.Game.Tests.Visual.Editing { var setup = Editor.ChildrenOfType().First(); - string temp = TestResources.GetTestBeatmapForImport(); - - string extractedFolder = $"{temp}_extracted"; - Directory.CreateDirectory(extractedFolder); - - try + return setFile(TestResources.GetTestBeatmapForImport(), extractedFolder => { - using (var zip = ZipArchive.Open(temp)) - zip.WriteToDirectory(extractedFolder); - bool success = setup.ChildrenOfType().First().ChangeAudioTrack(new FileInfo(Path.Combine(extractedFolder, "03. Renatus - Soleily 192kbps.mp3"))); - - // ensure audio file is copied to beatmap as "audio.mp3" rather than original filename. Assert.That(Beatmap.Value.Metadata.AudioFile == "audio.mp3"); - return success; - } - finally - { - File.Delete(temp); - Directory.Delete(extractedFolder, true); - } + }); }); AddAssert("track is not virtual", () => Beatmap.Value.Track is not TrackVirtual); @@ -530,5 +514,116 @@ namespace osu.Game.Tests.Visual.Editing return set != null && set.PerformRead(s => s.Beatmaps.Count == 3 && s.Files.Count == 3); }); } + + [Test] + public void TestMultipleBackgroundFiles() + { + AddStep("enter setup mode", () => InputManager.Key(Key.F4)); + AddAssert("set background", () => setBackground(expected: "bg.jpg")); + + AddStep("save", () => Editor.Save()); + AddStep("create new difficulty", () => Editor.CreateNewDifficulty(new OsuRuleset().RulesetInfo)); + AddUntilStep("wait for dialog", () => DialogOverlay.CurrentDialog is CreateNewDifficultyDialog); + AddStep("confirm creation with no objects", () => DialogOverlay.CurrentDialog!.PerformOkAction()); + AddUntilStep("wait for created", () => + { + string? difficultyName = Editor.ChildrenOfType().SingleOrDefault()?.BeatmapInfo.DifficultyName; + return difficultyName != null && difficultyName == "New Difficulty"; + }); + + AddAssert("new difficulty uses same background", () => Beatmap.Value.Metadata.BackgroundFile == "bg.jpg"); + AddStep("enter setup mode", () => InputManager.Key(Key.F4)); + AddUntilStep("wait for load", () => Editor.ChildrenOfType().Any()); + AddAssert("set background", () => setBackground(expected: "bg (1).jpg")); + AddAssert("new difficulty uses new background", () => Beatmap.Value.Metadata.BackgroundFile == "bg (1).jpg"); + + AddStep("save", () => Editor.Save()); + AddStep("switch to previous difficulty", () => Editor.SwitchToDifficulty(Beatmap.Value.BeatmapSetInfo.Beatmaps.First())); + AddAssert("old difficulty uses old background", () => Beatmap.Value.Metadata.BackgroundFile == "bg.jpg"); + AddAssert("old background not removed", () => Beatmap.Value.BeatmapSetInfo.Files.Any(f => f.Filename == "bg.jpg")); + + AddStep("enter setup mode", () => InputManager.Key(Key.F4)); + AddUntilStep("wait for load", () => Editor.ChildrenOfType().Any()); + AddStep("set background", () => setBackground(expected: "bg.jpg")); + AddAssert("other background not removed", () => Beatmap.Value.BeatmapSetInfo.Files.Any(f => f.Filename == "bg (1).jpg")); + + bool setBackground(string expected) + { + var setup = Editor.ChildrenOfType().First(); + + return setFile(TestResources.GetQuickTestBeatmapForImport(), extractedFolder => + { + bool success = setup.ChildrenOfType().First().ChangeBackgroundImage(new FileInfo(Path.Combine(extractedFolder, "machinetop_background.jpg"))); + Assert.That(Beatmap.Value.Metadata.BackgroundFile, Is.EqualTo(expected)); + return success; + }); + } + } + + [Test] + public void TestMultipleAudioFiles() + { + AddStep("enter setup mode", () => InputManager.Key(Key.F4)); + AddAssert("set audio", () => setAudio(expected: "audio.mp3")); + + AddStep("save", () => Editor.Save()); + AddStep("create new difficulty", () => Editor.CreateNewDifficulty(new OsuRuleset().RulesetInfo)); + AddUntilStep("wait for dialog", () => DialogOverlay.CurrentDialog is CreateNewDifficultyDialog); + AddStep("confirm creation with no objects", () => DialogOverlay.CurrentDialog!.PerformOkAction()); + AddUntilStep("wait for created", () => + { + string? difficultyName = Editor.ChildrenOfType().SingleOrDefault()?.BeatmapInfo.DifficultyName; + return difficultyName != null && difficultyName == "New Difficulty"; + }); + + AddAssert("new difficulty uses same audio", () => Beatmap.Value.Metadata.AudioFile == "audio.mp3"); + AddStep("enter setup mode", () => InputManager.Key(Key.F4)); + AddUntilStep("wait for load", () => Editor.ChildrenOfType().Any()); + AddAssert("set audio", () => setAudio(expected: "audio (1).mp3")); + AddAssert("new difficulty uses new audio", () => Beatmap.Value.Metadata.AudioFile == "audio (1).mp3"); + + AddStep("save", () => Editor.Save()); + AddStep("switch to previous difficulty", () => Editor.SwitchToDifficulty(Beatmap.Value.BeatmapSetInfo.Beatmaps.First())); + AddAssert("old difficulty uses old audio", () => Beatmap.Value.Metadata.AudioFile == "audio.mp3"); + AddAssert("old audio not removed", () => Beatmap.Value.BeatmapSetInfo.Files.Any(f => f.Filename == "audio.mp3")); + + AddStep("enter setup mode", () => InputManager.Key(Key.F4)); + AddUntilStep("wait for load", () => Editor.ChildrenOfType().Any()); + AddStep("set audio", () => setAudio(expected: "audio.mp3")); + AddAssert("other audio not removed", () => Beatmap.Value.BeatmapSetInfo.Files.Any(f => f.Filename == "audio (1).mp3")); + + bool setAudio(string expected) + { + var setup = Editor.ChildrenOfType().First(); + + return setFile(TestResources.GetTestBeatmapForImport(), extractedFolder => + { + bool success = setup.ChildrenOfType().First().ChangeAudioTrack(new FileInfo(Path.Combine(extractedFolder, "03. Renatus - Soleily 192kbps.mp3"))); + Assert.That(Beatmap.Value.Metadata.AudioFile, Is.EqualTo(expected)); + return success; + }); + } + } + + private bool setFile(string archivePath, Func func) + { + string temp = archivePath; + + string extractedFolder = $"{temp}_extracted"; + Directory.CreateDirectory(extractedFolder); + + try + { + using (var zip = ZipArchive.Open(temp)) + zip.WriteToDirectory(extractedFolder); + + return func(extractedFolder); + } + finally + { + File.Delete(temp); + Directory.Delete(extractedFolder, true); + } + } } } From 871c365fd8d0ff1525f07462bf2173e3848c7de9 Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Sun, 24 Nov 2024 00:32:27 -0500 Subject: [PATCH 0074/1275] Preserve existing beatmap background/audio files if used elsewhere --- .../Screens/Edit/Setup/ResourcesSection.cs | 58 ++++++++++++------- 1 file changed, 37 insertions(+), 21 deletions(-) diff --git a/osu.Game/Screens/Edit/Setup/ResourcesSection.cs b/osu.Game/Screens/Edit/Setup/ResourcesSection.cs index 845c21b598..8ab26a74a2 100644 --- a/osu.Game/Screens/Edit/Setup/ResourcesSection.cs +++ b/osu.Game/Screens/Edit/Setup/ResourcesSection.cs @@ -77,27 +77,35 @@ namespace osu.Game.Screens.Edit.Setup if (!source.Exists) return false; + var beatmap = working.Value.BeatmapInfo; var set = working.Value.BeatmapSetInfo; - var destination = new FileInfo($@"bg{source.Extension}"); + string[] filenames = set.Files.Select(f => f.Filename).Where(f => + f.StartsWith(@"bg", StringComparison.OrdinalIgnoreCase) && + f.EndsWith(source.Extension, StringComparison.OrdinalIgnoreCase)).ToArray(); - // remove the previous background for now. - // in the future we probably want to check if this is being used elsewhere (other difficulties?) - var oldFile = set.GetFile(working.Value.Metadata.BackgroundFile); + string currentFilename = working.Value.Metadata.BackgroundFile; + string? newFilename = null; + + var oldFile = set.GetFile(currentFilename); + + if (oldFile != null && set.Beatmaps.Where(b => !b.Equals(beatmap)).All(b => b.Metadata.BackgroundFile != currentFilename)) + { + beatmaps.DeleteFile(set, oldFile); + newFilename = currentFilename; + } + + newFilename ??= NamingUtils.GetNextBestFilename(filenames, $@"bg{source.Extension}"); using (var stream = source.OpenRead()) - { - if (oldFile != null) - beatmaps.DeleteFile(set, oldFile); + beatmaps.AddFile(set, stream, newFilename); - beatmaps.AddFile(set, stream, destination.Name); - } + working.Value.Metadata.BackgroundFile = newFilename; + updateAllDifficultiesButton.Enabled.Value = true; editorBeatmap.SaveState(); - working.Value.Metadata.BackgroundFile = destination.Name; headerBackground.UpdateBackground(); - editor?.ApplyToBackground(bg => bg.RefreshBackground()); return true; @@ -108,23 +116,31 @@ namespace osu.Game.Screens.Edit.Setup if (!source.Exists) return false; + var beatmap = working.Value.BeatmapInfo; var set = working.Value.BeatmapSetInfo; - var destination = new FileInfo($@"audio{source.Extension}"); + string[] filenames = set.Files.Select(f => f.Filename).Where(f => + f.StartsWith(@"audio", StringComparison.OrdinalIgnoreCase) && + f.EndsWith(source.Extension, StringComparison.OrdinalIgnoreCase)).ToArray(); - // remove the previous audio track for now. - // in the future we probably want to check if this is being used elsewhere (other difficulties?) - var oldFile = set.GetFile(working.Value.Metadata.AudioFile); + string currentFilename = working.Value.Metadata.AudioFile; + string? newFilename = null; - using (var stream = source.OpenRead()) + var oldFile = set.GetFile(currentFilename); + + if (oldFile != null && set.Beatmaps.Where(b => !b.Equals(beatmap)).All(b => b.Metadata.AudioFile != currentFilename)) { - if (oldFile != null) - beatmaps.DeleteFile(set, oldFile); - - beatmaps.AddFile(set, stream, destination.Name); + beatmaps.DeleteFile(set, oldFile); + newFilename = currentFilename; } - working.Value.Metadata.AudioFile = destination.Name; + newFilename ??= NamingUtils.GetNextBestFilename(filenames, $@"audio{source.Extension}"); + + using (var stream = source.OpenRead()) + beatmaps.AddFile(set, stream, newFilename); + + working.Value.Metadata.AudioFile = newFilename; + updateAllDifficultiesButton.Enabled.Value = true; editorBeatmap.SaveState(); music.ReloadCurrentTrack(); From 8e20dc7e9def53de9cc45706fec3e9d77a2c00d4 Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Sun, 24 Nov 2024 00:32:50 -0500 Subject: [PATCH 0075/1275] Add option to update all difficulties with new background/audio file --- .../Screens/Edit/Setup/ResourcesSection.cs | 47 +++++++++++++++++++ 1 file changed, 47 insertions(+) diff --git a/osu.Game/Screens/Edit/Setup/ResourcesSection.cs b/osu.Game/Screens/Edit/Setup/ResourcesSection.cs index 8ab26a74a2..50c7072f84 100644 --- a/osu.Game/Screens/Edit/Setup/ResourcesSection.cs +++ b/osu.Game/Screens/Edit/Setup/ResourcesSection.cs @@ -1,7 +1,9 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System; using System.IO; +using System.Linq; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; @@ -10,6 +12,8 @@ using osu.Game.Beatmaps; using osu.Game.Graphics.UserInterfaceV2; using osu.Game.Overlays; using osu.Game.Localisation; +using osu.Game.Models; +using osu.Game.Utils; namespace osu.Game.Screens.Edit.Setup { @@ -36,6 +40,7 @@ namespace osu.Game.Screens.Edit.Setup private Editor? editor { get; set; } private SetupScreenHeaderBackground headerBackground = null!; + private RoundedButton updateAllDifficultiesButton = null!; [BackgroundDependencyLoader] private void load() @@ -58,6 +63,13 @@ namespace osu.Game.Screens.Edit.Setup Caption = EditorSetupStrings.AudioTrack, PlaceholderText = EditorSetupStrings.ClickToSelectTrack, }, + updateAllDifficultiesButton = new RoundedButton + { + RelativeSizeAxes = Axes.X, + Text = "Update all difficulties", + Action = updateAllDifficulties, + Enabled = { Value = false }, + } }; backgroundChooser.PreviewContainer.Add(headerBackground); @@ -148,6 +160,41 @@ namespace osu.Game.Screens.Edit.Setup return true; } + private void updateAllDifficulties() + { + var beatmap = working.Value.BeatmapInfo; + var set = working.Value.BeatmapSetInfo; + + string backgroundFile = working.Value.Metadata.BackgroundFile; + string audioFile = working.Value.Metadata.AudioFile; + + foreach (var otherBeatmap in set.Beatmaps.Where(b => !b.Equals(beatmap))) + { + var otherWorking = beatmaps.GetWorkingBeatmap(otherBeatmap); + + if (!string.Equals(otherBeatmap.Metadata.BackgroundFile, backgroundFile, StringComparison.OrdinalIgnoreCase)) + { + if (set.GetFile(otherBeatmap.Metadata.BackgroundFile) is RealmNamedFileUsage file) + beatmaps.DeleteFile(set, file); + + otherBeatmap.Metadata.BackgroundFile = backgroundFile; + } + + if (!string.Equals(otherBeatmap.Metadata.AudioFile, audioFile, StringComparison.OrdinalIgnoreCase)) + { + if (set.GetFile(otherBeatmap.Metadata.AudioFile) is RealmNamedFileUsage file) + beatmaps.DeleteFile(set, file); + + otherBeatmap.Metadata.AudioFile = audioFile; + } + + beatmaps.Save(otherBeatmap, otherWorking.Beatmap); + } + + editorBeatmap.SaveState(); + updateAllDifficultiesButton.Enabled.Value = false; + } + private void backgroundChanged(ValueChangedEvent file) { if (file.NewValue == null || !ChangeBackgroundImage(file.NewValue)) From e348b3a7aa929b31116751e665c2b1bf402a3df9 Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Sun, 24 Nov 2024 00:34:03 -0500 Subject: [PATCH 0076/1275] Only enable button if there are multiple difficulties --- osu.Game/Screens/Edit/Setup/ResourcesSection.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/Edit/Setup/ResourcesSection.cs b/osu.Game/Screens/Edit/Setup/ResourcesSection.cs index 50c7072f84..5c904f6ce1 100644 --- a/osu.Game/Screens/Edit/Setup/ResourcesSection.cs +++ b/osu.Game/Screens/Edit/Setup/ResourcesSection.cs @@ -113,7 +113,7 @@ namespace osu.Game.Screens.Edit.Setup beatmaps.AddFile(set, stream, newFilename); working.Value.Metadata.BackgroundFile = newFilename; - updateAllDifficultiesButton.Enabled.Value = true; + updateAllDifficultiesButton.Enabled.Value = set.Beatmaps.Count > 1; editorBeatmap.SaveState(); @@ -152,7 +152,7 @@ namespace osu.Game.Screens.Edit.Setup beatmaps.AddFile(set, stream, newFilename); working.Value.Metadata.AudioFile = newFilename; - updateAllDifficultiesButton.Enabled.Value = true; + updateAllDifficultiesButton.Enabled.Value = set.Beatmaps.Count > 1; editorBeatmap.SaveState(); music.ReloadCurrentTrack(); From dc210d59b5a4b4954cd87194c941857d20637d9b Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Sun, 24 Nov 2024 00:45:11 -0500 Subject: [PATCH 0077/1275] Add test coverage for sync button --- .../Editing/TestSceneEditorBeatmapCreation.cs | 56 +++++++++++++++++++ 1 file changed, 56 insertions(+) diff --git a/osu.Game.Tests/Visual/Editing/TestSceneEditorBeatmapCreation.cs b/osu.Game.Tests/Visual/Editing/TestSceneEditorBeatmapCreation.cs index b15ee0cab8..9fabed346b 100644 --- a/osu.Game.Tests/Visual/Editing/TestSceneEditorBeatmapCreation.cs +++ b/osu.Game.Tests/Visual/Editing/TestSceneEditorBeatmapCreation.cs @@ -15,6 +15,8 @@ using osu.Game.Beatmaps; using osu.Game.Beatmaps.ControlPoints; using osu.Game.Collections; using osu.Game.Database; +using osu.Game.Graphics.UserInterfaceV2; +using osu.Game.Localisation; using osu.Game.Overlays.Dialog; using osu.Game.Rulesets; using osu.Game.Rulesets.Catch; @@ -605,6 +607,60 @@ namespace osu.Game.Tests.Visual.Editing } } + [Test] + public void TestUpdateBackgroundOnAllDifficulties() + { + AddStep("enter setup mode", () => InputManager.Key(Key.F4)); + AddAssert("button disabled", () => !getButton().Enabled.Value); + AddAssert("set background", () => setBackground(expected: "bg.jpg")); + + // there is only one diff so this should still be disabled. + AddAssert("button still disabled", () => !getButton().Enabled.Value); + + AddStep("save", () => Editor.Save()); + AddStep("create new difficulty", () => Editor.CreateNewDifficulty(new OsuRuleset().RulesetInfo)); + AddUntilStep("wait for dialog", () => DialogOverlay.CurrentDialog is CreateNewDifficultyDialog); + AddStep("confirm creation with no objects", () => DialogOverlay.CurrentDialog!.PerformOkAction()); + AddUntilStep("wait for created", () => + { + string? difficultyName = Editor.ChildrenOfType().SingleOrDefault()?.BeatmapInfo.DifficultyName; + return difficultyName != null && difficultyName == "New Difficulty"; + }); + + AddAssert("new difficulty uses same background", () => Beatmap.Value.Metadata.BackgroundFile == "bg.jpg"); + AddStep("enter setup mode", () => InputManager.Key(Key.F4)); + AddUntilStep("wait for load", () => Editor.ChildrenOfType().Any()); + AddAssert("button disabled", () => !getButton().Enabled.Value); + AddAssert("set background", () => setBackground(expected: "bg (1).jpg")); + AddAssert("new background added", () => Beatmap.Value.BeatmapSetInfo.Files.Any(f => f.Filename == "bg (1).jpg")); + AddAssert("new difficulty uses new background", () => Beatmap.Value.Metadata.BackgroundFile == "bg (1).jpg"); + + AddAssert("button enabled", () => getButton().Enabled.Value); + AddStep("press button", () => getButton().TriggerClick()); + + AddAssert("new difficulty still uses new background", () => Beatmap.Value.BeatmapSetInfo.Beatmaps[1].Metadata.BackgroundFile == "bg (1).jpg"); + AddAssert("old difficulty uses new background", () => Beatmap.Value.BeatmapSetInfo.Beatmaps[0].Metadata.BackgroundFile == "bg (1).jpg"); + AddAssert("old background removed", () => Beatmap.Value.BeatmapSetInfo.Files.All(f => f.Filename != "bg.jpg")); + + AddStep("save", () => Editor.Save()); + AddStep("switch to previous difficulty", () => Editor.SwitchToDifficulty(Beatmap.Value.BeatmapSetInfo.Beatmaps.First())); + AddAssert("old difficulty still uses new background", () => Beatmap.Value.Metadata.BackgroundFile == "bg (1).jpg"); + + bool setBackground(string expected) + { + var setup = Editor.ChildrenOfType().First(); + + return setFile(TestResources.GetQuickTestBeatmapForImport(), extractedFolder => + { + bool success = setup.ChildrenOfType().First().ChangeBackgroundImage(new FileInfo(Path.Combine(extractedFolder, "machinetop_background.jpg"))); + Assert.That(Beatmap.Value.Metadata.BackgroundFile, Is.EqualTo(expected)); + return success; + }); + } + + RoundedButton getButton() => Editor.ChildrenOfType().Single(b => b.Text == EditorSetupStrings.ResourcesUpdateAllDifficulties); + } + private bool setFile(string archivePath, Func func) { string temp = archivePath; From c8b13b726dc23feebffa628deab6fa0f2252813a Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Sun, 24 Nov 2024 00:45:17 -0500 Subject: [PATCH 0078/1275] Add localisation support --- osu.Game/Localisation/EditorSetupStrings.cs | 5 +++++ osu.Game/Screens/Edit/Setup/ResourcesSection.cs | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/osu.Game/Localisation/EditorSetupStrings.cs b/osu.Game/Localisation/EditorSetupStrings.cs index 350517734f..60e677757e 100644 --- a/osu.Game/Localisation/EditorSetupStrings.cs +++ b/osu.Game/Localisation/EditorSetupStrings.cs @@ -188,6 +188,11 @@ namespace osu.Game.Localisation /// public static LocalisableString AudioTrack => new TranslatableString(getKey(@"audio_track"), @"Audio Track"); + /// + /// "Update all difficulties" + /// + public static LocalisableString ResourcesUpdateAllDifficulties => new TranslatableString(getKey(@"resources_update_all_difficulties"), @"Update all difficulties"); + /// /// "Click to select a track" /// diff --git a/osu.Game/Screens/Edit/Setup/ResourcesSection.cs b/osu.Game/Screens/Edit/Setup/ResourcesSection.cs index 5c904f6ce1..aa28e56218 100644 --- a/osu.Game/Screens/Edit/Setup/ResourcesSection.cs +++ b/osu.Game/Screens/Edit/Setup/ResourcesSection.cs @@ -66,7 +66,7 @@ namespace osu.Game.Screens.Edit.Setup updateAllDifficultiesButton = new RoundedButton { RelativeSizeAxes = Axes.X, - Text = "Update all difficulties", + Text = EditorSetupStrings.ResourcesUpdateAllDifficulties, Action = updateAllDifficulties, Enabled = { Value = false }, } From a872f749740b8f1bcf755add2cbe0d7298fa489e Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Sun, 24 Nov 2024 01:02:34 -0500 Subject: [PATCH 0079/1275] Make sync button only affect changed resource type --- .../Screens/Edit/Setup/ResourcesSection.cs | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/osu.Game/Screens/Edit/Setup/ResourcesSection.cs b/osu.Game/Screens/Edit/Setup/ResourcesSection.cs index aa28e56218..8c9b9796ed 100644 --- a/osu.Game/Screens/Edit/Setup/ResourcesSection.cs +++ b/osu.Game/Screens/Edit/Setup/ResourcesSection.cs @@ -84,6 +84,9 @@ namespace osu.Game.Screens.Edit.Setup audioTrackChooser.Current.BindValueChanged(audioTrackChanged); } + private string? newBackgroundFile; + private string? newAudioFile; + public bool ChangeBackgroundImage(FileInfo source) { if (!source.Exists) @@ -112,7 +115,7 @@ namespace osu.Game.Screens.Edit.Setup using (var stream = source.OpenRead()) beatmaps.AddFile(set, stream, newFilename); - working.Value.Metadata.BackgroundFile = newFilename; + working.Value.Metadata.BackgroundFile = newBackgroundFile = newFilename; updateAllDifficultiesButton.Enabled.Value = set.Beatmaps.Count > 1; editorBeatmap.SaveState(); @@ -151,7 +154,7 @@ namespace osu.Game.Screens.Edit.Setup using (var stream = source.OpenRead()) beatmaps.AddFile(set, stream, newFilename); - working.Value.Metadata.AudioFile = newFilename; + working.Value.Metadata.AudioFile = newAudioFile = newFilename; updateAllDifficultiesButton.Enabled.Value = set.Beatmaps.Count > 1; editorBeatmap.SaveState(); @@ -165,27 +168,24 @@ namespace osu.Game.Screens.Edit.Setup var beatmap = working.Value.BeatmapInfo; var set = working.Value.BeatmapSetInfo; - string backgroundFile = working.Value.Metadata.BackgroundFile; - string audioFile = working.Value.Metadata.AudioFile; - foreach (var otherBeatmap in set.Beatmaps.Where(b => !b.Equals(beatmap))) { var otherWorking = beatmaps.GetWorkingBeatmap(otherBeatmap); - if (!string.Equals(otherBeatmap.Metadata.BackgroundFile, backgroundFile, StringComparison.OrdinalIgnoreCase)) + if (newBackgroundFile != null && !string.Equals(otherBeatmap.Metadata.BackgroundFile, newBackgroundFile, StringComparison.OrdinalIgnoreCase)) { if (set.GetFile(otherBeatmap.Metadata.BackgroundFile) is RealmNamedFileUsage file) beatmaps.DeleteFile(set, file); - otherBeatmap.Metadata.BackgroundFile = backgroundFile; + otherBeatmap.Metadata.BackgroundFile = newBackgroundFile; } - if (!string.Equals(otherBeatmap.Metadata.AudioFile, audioFile, StringComparison.OrdinalIgnoreCase)) + if (newAudioFile != null && !string.Equals(otherBeatmap.Metadata.AudioFile, newAudioFile, StringComparison.OrdinalIgnoreCase)) { if (set.GetFile(otherBeatmap.Metadata.AudioFile) is RealmNamedFileUsage file) beatmaps.DeleteFile(set, file); - otherBeatmap.Metadata.AudioFile = audioFile; + otherBeatmap.Metadata.AudioFile = newAudioFile; } beatmaps.Save(otherBeatmap, otherWorking.Beatmap); From 95a6226413a29f82b64040ebd081939a354d7a06 Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Sun, 24 Nov 2024 01:11:28 -0500 Subject: [PATCH 0080/1275] Only enable button if there are different filenames --- .../Screens/Edit/Setup/ResourcesSection.cs | 23 +++++++++++++------ 1 file changed, 16 insertions(+), 7 deletions(-) diff --git a/osu.Game/Screens/Edit/Setup/ResourcesSection.cs b/osu.Game/Screens/Edit/Setup/ResourcesSection.cs index 8c9b9796ed..863cf9f241 100644 --- a/osu.Game/Screens/Edit/Setup/ResourcesSection.cs +++ b/osu.Game/Screens/Edit/Setup/ResourcesSection.cs @@ -40,7 +40,7 @@ namespace osu.Game.Screens.Edit.Setup private Editor? editor { get; set; } private SetupScreenHeaderBackground headerBackground = null!; - private RoundedButton updateAllDifficultiesButton = null!; + private RoundedButton syncResourcesButton = null!; [BackgroundDependencyLoader] private void load() @@ -63,11 +63,11 @@ namespace osu.Game.Screens.Edit.Setup Caption = EditorSetupStrings.AudioTrack, PlaceholderText = EditorSetupStrings.ClickToSelectTrack, }, - updateAllDifficultiesButton = new RoundedButton + syncResourcesButton = new RoundedButton { RelativeSizeAxes = Axes.X, Text = EditorSetupStrings.ResourcesUpdateAllDifficulties, - Action = updateAllDifficulties, + Action = syncResources, Enabled = { Value = false }, } }; @@ -116,7 +116,7 @@ namespace osu.Game.Screens.Edit.Setup beatmaps.AddFile(set, stream, newFilename); working.Value.Metadata.BackgroundFile = newBackgroundFile = newFilename; - updateAllDifficultiesButton.Enabled.Value = set.Beatmaps.Count > 1; + syncResourcesButton.Enabled.Value = set.Beatmaps.Count > 1; editorBeatmap.SaveState(); @@ -155,7 +155,7 @@ namespace osu.Game.Screens.Edit.Setup beatmaps.AddFile(set, stream, newFilename); working.Value.Metadata.AudioFile = newAudioFile = newFilename; - updateAllDifficultiesButton.Enabled.Value = set.Beatmaps.Count > 1; + updateSyncResourcesButton(); editorBeatmap.SaveState(); music.ReloadCurrentTrack(); @@ -163,7 +163,16 @@ namespace osu.Game.Screens.Edit.Setup return true; } - private void updateAllDifficulties() + private void updateSyncResourcesButton() + { + var set = working.Value.BeatmapSetInfo; + + syncResourcesButton.Enabled.Value = + (newBackgroundFile != null && set.Beatmaps.DistinctBy(b => b.Metadata.BackgroundFile, StringComparer.OrdinalIgnoreCase).Count() > 1) || + (newAudioFile != null && set.Beatmaps.DistinctBy(b => b.Metadata.AudioFile, StringComparer.OrdinalIgnoreCase).Count() > 1); + } + + private void syncResources() { var beatmap = working.Value.BeatmapInfo; var set = working.Value.BeatmapSetInfo; @@ -192,7 +201,7 @@ namespace osu.Game.Screens.Edit.Setup } editorBeatmap.SaveState(); - updateAllDifficultiesButton.Enabled.Value = false; + syncResourcesButton.Enabled.Value = false; } private void backgroundChanged(ValueChangedEvent file) From 3480da22d2dcde78dd23cb20d8131784ac9d5522 Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Sun, 24 Nov 2024 01:13:39 -0500 Subject: [PATCH 0081/1275] Remove no-op `SaveState` call --- osu.Game/Screens/Edit/Setup/ResourcesSection.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/osu.Game/Screens/Edit/Setup/ResourcesSection.cs b/osu.Game/Screens/Edit/Setup/ResourcesSection.cs index 863cf9f241..a52e42c7c0 100644 --- a/osu.Game/Screens/Edit/Setup/ResourcesSection.cs +++ b/osu.Game/Screens/Edit/Setup/ResourcesSection.cs @@ -200,7 +200,6 @@ namespace osu.Game.Screens.Edit.Setup beatmaps.Save(otherBeatmap, otherWorking.Beatmap); } - editorBeatmap.SaveState(); syncResourcesButton.Enabled.Value = false; } From 631bfadd68f2a58ef51f7d17c10271bfddc34de5 Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Sun, 24 Nov 2024 04:10:01 -0500 Subject: [PATCH 0082/1275] Replace event subscription with callback in `UserStatisticsWatcher` Also no longer cancels previous API requests as there's no actual need to do it. --- .../Online/LocalUserStatisticsProvider.cs | 19 +++++++---------- osu.Game/Online/UserStatisticsWatcher.cs | 21 +++++-------------- 2 files changed, 13 insertions(+), 27 deletions(-) diff --git a/osu.Game/Online/LocalUserStatisticsProvider.cs b/osu.Game/Online/LocalUserStatisticsProvider.cs index a25f5b05aa..5fa2b40715 100644 --- a/osu.Game/Online/LocalUserStatisticsProvider.cs +++ b/osu.Game/Online/LocalUserStatisticsProvider.cs @@ -36,7 +36,6 @@ namespace osu.Game.Online private IAPIProvider api { get; set; } = null!; private readonly Dictionary statisticsCache = new Dictionary(); - private readonly Dictionary statisticsRequests = new Dictionary(); /// /// Returns the currently available for the given ruleset. @@ -62,23 +61,21 @@ namespace osu.Game.Online RefetchStatistics(ruleset); } - public void RefetchStatistics(RulesetInfo ruleset) + public void RefetchStatistics(RulesetInfo ruleset, Action? callback = null) { - if (statisticsRequests.TryGetValue(ruleset.ShortName, out var previousRequest)) - previousRequest.Cancel(); - - var request = statisticsRequests[ruleset.ShortName] = new GetUserRequest(api.LocalUser.Value.Id, ruleset); - request.Success += u => UpdateStatistics(u.Statistics, ruleset); + var request = new GetUserRequest(api.LocalUser.Value.Id, ruleset); + request.Success += u => UpdateStatistics(u.Statistics, ruleset, callback); api.Queue(request); } - protected void UpdateStatistics(UserStatistics newStatistics, RulesetInfo ruleset) + protected void UpdateStatistics(UserStatistics newStatistics, RulesetInfo ruleset, Action? callback = null) { var oldStatistics = statisticsCache.GetValueOrDefault(ruleset.ShortName); - - statisticsRequests.Remove(ruleset.ShortName); statisticsCache[ruleset.ShortName] = newStatistics; - StatisticsUpdated?.Invoke(new UserStatisticsUpdate(ruleset, oldStatistics, newStatistics)); + + var update = new UserStatisticsUpdate(ruleset, oldStatistics, newStatistics); + callback?.Invoke(update); + StatisticsUpdated?.Invoke(update); } } diff --git a/osu.Game/Online/UserStatisticsWatcher.cs b/osu.Game/Online/UserStatisticsWatcher.cs index 8ed1ff594d..73ca3c9f53 100644 --- a/osu.Game/Online/UserStatisticsWatcher.cs +++ b/osu.Game/Online/UserStatisticsWatcher.cs @@ -23,8 +23,6 @@ namespace osu.Game.Online public IBindable LatestUpdate => latestUpdate; private readonly Bindable latestUpdate = new Bindable(); - private ScoreInfo? scorePendingUpdate; - [Resolved] private SpectatorClient spectatorClient { get; set; } = null!; @@ -43,7 +41,6 @@ namespace osu.Game.Online base.LoadComplete(); spectatorClient.OnUserScoreProcessed += userScoreProcessed; - statisticsProvider.StatisticsUpdated += onStatisticsUpdated; } /// @@ -72,21 +69,13 @@ namespace osu.Game.Online if (!watchedScores.Remove(scoreId, out var scoreInfo)) return; - scorePendingUpdate = scoreInfo; - statisticsProvider.RefetchStatistics(scoreInfo.Ruleset); + statisticsProvider.RefetchStatistics(scoreInfo.Ruleset, u => Schedule(() => + { + if (u.OldStatistics != null) + latestUpdate.Value = new ScoreBasedUserStatisticsUpdate(scoreInfo, u.OldStatistics, u.NewStatistics); + })); } - private void onStatisticsUpdated(UserStatisticsUpdate update) => Schedule(() => - { - if (scorePendingUpdate == null || !update.Ruleset.Equals(scorePendingUpdate.Ruleset)) - return; - - if (update.OldStatistics != null) - latestUpdate.Value = new ScoreBasedUserStatisticsUpdate(scorePendingUpdate, update.OldStatistics, update.NewStatistics); - - scorePendingUpdate = null; - }); - protected override void Dispose(bool isDisposing) { if (spectatorClient.IsNotNull()) From f3155bfc7d83029e9170562598c0dd6ec342f54c Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Sun, 24 Nov 2024 04:24:31 -0500 Subject: [PATCH 0083/1275] Fix pause shortcut on multiplayer not delayed --- osu.Game/Screens/Play/HUD/HoldForMenuButton.cs | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/Play/HUD/HoldForMenuButton.cs b/osu.Game/Screens/Play/HUD/HoldForMenuButton.cs index 806985e19d..5d3d5774d0 100644 --- a/osu.Game/Screens/Play/HUD/HoldForMenuButton.cs +++ b/osu.Game/Screens/Play/HUD/HoldForMenuButton.cs @@ -299,7 +299,13 @@ namespace osu.Game.Screens.Play.HUD { case GlobalAction.Back: if (!pendingAnimation) - Confirm(); + { + if (IsDangerousAction) + BeginConfirm(); + else + Confirm(); + } + return true; case GlobalAction.PauseGameplay: @@ -307,7 +313,13 @@ namespace osu.Game.Screens.Play.HUD if (ReplayLoaded.Value) return false; if (!pendingAnimation) - Confirm(); + { + if (IsDangerousAction) + BeginConfirm(); + else + Confirm(); + } + return true; } From aa1358b2b4576b2d9d08365503cb610328068255 Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Sun, 24 Nov 2024 04:33:03 -0500 Subject: [PATCH 0084/1275] Enable NRT and fix code --- osu.Game.Tests/Visual/Online/TestSceneUserPanel.cs | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/osu.Game.Tests/Visual/Online/TestSceneUserPanel.cs b/osu.Game.Tests/Visual/Online/TestSceneUserPanel.cs index e291b90361..dfb8213acf 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneUserPanel.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneUserPanel.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 NUnit.Framework; using osu.Framework.Allocation; @@ -25,11 +23,11 @@ namespace osu.Game.Tests.Visual.Online [TestFixture] public partial class TestSceneUserPanel : OsuTestScene { - private readonly Bindable activity = new Bindable(); + private readonly Bindable activity = new Bindable(); private readonly Bindable status = new Bindable(); - private UserGridPanel boundPanel1; - private TestUserListPanel boundPanel2; + private UserGridPanel boundPanel1 = null!; + private TestUserListPanel boundPanel2 = null!; [Cached] private readonly OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Purple); @@ -38,7 +36,7 @@ namespace osu.Game.Tests.Visual.Online private readonly TestUserStatisticsProvider statisticsProvider = new TestUserStatisticsProvider(); [Resolved] - private IRulesetStore rulesetStore { get; set; } + private IRulesetStore rulesetStore { get; set; } = null!; [SetUp] public void SetUp() => Schedule(() => @@ -209,8 +207,8 @@ namespace osu.Game.Tests.Visual.Online private partial class TestUserStatisticsProvider : LocalUserStatisticsProvider { - public new void UpdateStatistics(UserStatistics newStatistics, RulesetInfo ruleset) - => base.UpdateStatistics(newStatistics, ruleset); + public new void UpdateStatistics(UserStatistics newStatistics, RulesetInfo ruleset, Action? callback = null) + => base.UpdateStatistics(newStatistics, ruleset, callback); } } } From 242079346661b3bb5734ef6db066627addea2681 Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Sun, 24 Nov 2024 05:39:42 -0500 Subject: [PATCH 0085/1275] Allow controlling back button visibility state from screens --- osu.Game/OsuGame.cs | 13 ++++++++----- osu.Game/Screens/IOsuScreen.cs | 12 ++++++++++++ osu.Game/Screens/OsuScreen.cs | 5 +++++ 3 files changed, 25 insertions(+), 5 deletions(-) diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs index dce24c6ee7..4d6bc4fd14 100644 --- a/osu.Game/OsuGame.cs +++ b/osu.Game/OsuGame.cs @@ -1581,12 +1581,20 @@ namespace osu.Game if (current is IOsuScreen currentOsuScreen) { + if (currentOsuScreen.AllowBackButton) + BackButton.State.UnbindFrom(currentOsuScreen.BackButtonState); + OverlayActivationMode.UnbindFrom(currentOsuScreen.OverlayActivationMode); API.Activity.UnbindFrom(currentOsuScreen.Activity); } if (newScreen is IOsuScreen newOsuScreen) { + if (newOsuScreen.AllowBackButton) + ((IBindable)BackButton.State).BindTo(newOsuScreen.BackButtonState); + else + BackButton.Hide(); + OverlayActivationMode.BindTo(newOsuScreen.OverlayActivationMode); API.Activity.BindTo(newOsuScreen.Activity); @@ -1597,11 +1605,6 @@ namespace osu.Game else Toolbar.Show(); - if (newOsuScreen.AllowBackButton) - BackButton.Show(); - else - BackButton.Hide(); - if (newOsuScreen.ShowFooter) { BackButton.Hide(); diff --git a/osu.Game/Screens/IOsuScreen.cs b/osu.Game/Screens/IOsuScreen.cs index b80c1f87a4..7025460daa 100644 --- a/osu.Game/Screens/IOsuScreen.cs +++ b/osu.Game/Screens/IOsuScreen.cs @@ -3,8 +3,11 @@ using System.Collections.Generic; using osu.Framework.Bindables; +using osu.Framework.Graphics.Containers; using osu.Framework.Screens; using osu.Game.Beatmaps; +using osu.Game.Graphics.UserInterface; +using osu.Game.Input.Bindings; using osu.Game.Overlays; using osu.Game.Rulesets; using osu.Game.Screens.Footer; @@ -59,6 +62,15 @@ namespace osu.Game.Screens /// IBindable OverlayActivationMode { get; } + /// + /// Controls the visibility state of to better work with screen-specific transitions (i.e. quick restart in player). + /// The back button can still be triggered by the action even while hidden. + /// + /// + /// This is ignored when is set to false. + /// + IBindable BackButtonState { get; } + /// /// The current for this screen. /// diff --git a/osu.Game/Screens/OsuScreen.cs b/osu.Game/Screens/OsuScreen.cs index 695a074907..2c5c889154 100644 --- a/osu.Game/Screens/OsuScreen.cs +++ b/osu.Game/Screens/OsuScreen.cs @@ -11,6 +11,7 @@ using osu.Framework.Audio; using osu.Framework.Audio.Sample; using osu.Framework.Bindables; using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; using osu.Framework.Screens; using osu.Game.Beatmaps; using osu.Game.Overlays; @@ -56,6 +57,10 @@ namespace osu.Game.Screens IBindable IOsuScreen.OverlayActivationMode => OverlayActivationMode; + public readonly Bindable BackButtonState = new Bindable(Visibility.Visible); + + IBindable IOsuScreen.BackButtonState => BackButtonState; + public virtual bool CursorVisible => true; protected new OsuGameBase Game => base.Game as OsuGameBase; From ae9119eef044c348805e4b2488fb768fc342e621 Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Sun, 24 Nov 2024 05:40:06 -0500 Subject: [PATCH 0086/1275] Hide back button when quick-restarting unless load time takes long --- osu.Game/Screens/Play/PlayerLoader.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/osu.Game/Screens/Play/PlayerLoader.cs b/osu.Game/Screens/Play/PlayerLoader.cs index 3e36c630db..a6e171ba02 100644 --- a/osu.Game/Screens/Play/PlayerLoader.cs +++ b/osu.Game/Screens/Play/PlayerLoader.cs @@ -478,6 +478,8 @@ namespace osu.Game.Screens.Play if (quickRestart) { + BackButtonState.Value = Visibility.Hidden; + // A quick restart starts by triggering a fade to black AddInternal(quickRestartBlackLayer = new Box { @@ -496,6 +498,8 @@ namespace osu.Game.Screens.Play .Delay(quick_restart_initial_delay) .ScaleTo(1) .FadeInFromZero(500, Easing.OutQuint); + + this.Delay(quick_restart_initial_delay).Schedule(() => BackButtonState.Value = Visibility.Visible); } else { From 53b390667a844ca98dd0d7f1d1be0ebffd6c2133 Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Sun, 24 Nov 2024 06:04:36 -0500 Subject: [PATCH 0087/1275] Fix failing test --- osu.Game.Tests/Visual/Menus/TestSceneLoginOverlay.cs | 6 ++++++ osu.Game.Tests/Visual/Online/TestSceneUserPanel.cs | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/osu.Game.Tests/Visual/Menus/TestSceneLoginOverlay.cs b/osu.Game.Tests/Visual/Menus/TestSceneLoginOverlay.cs index 460d7814e0..609bc6e166 100644 --- a/osu.Game.Tests/Visual/Menus/TestSceneLoginOverlay.cs +++ b/osu.Game.Tests/Visual/Menus/TestSceneLoginOverlay.cs @@ -10,11 +10,13 @@ using osu.Framework.Graphics.Containers; using osu.Framework.Testing; using osu.Game.Configuration; using osu.Game.Graphics.UserInterface; +using osu.Game.Online; using osu.Game.Online.API; using osu.Game.Online.API.Requests; using osu.Game.Overlays; using osu.Game.Overlays.Login; using osu.Game.Overlays.Settings; +using osu.Game.Tests.Visual.Online; using osu.Game.Users; using osu.Game.Users.Drawables; using osuTK.Input; @@ -31,6 +33,9 @@ namespace osu.Game.Tests.Visual.Menus [Resolved] private OsuConfigManager configManager { get; set; } = null!; + [Cached(typeof(LocalUserStatisticsProvider))] + private readonly TestSceneUserPanel.TestUserStatisticsProvider statisticsProvider = new TestSceneUserPanel.TestUserStatisticsProvider(); + [BackgroundDependencyLoader] private void load() { @@ -170,6 +175,7 @@ namespace osu.Game.Tests.Visual.Menus AddStep("enter code", () => loginOverlay.ChildrenOfType().First().Text = "88800088"); assertAPIState(APIState.Online); + AddStep("feed statistics", () => statisticsProvider.UpdateStatistics(new UserStatistics(), Ruleset.Value)); AddStep("click on flag", () => { InputManager.MoveMouseTo(loginOverlay.ChildrenOfType().First()); diff --git a/osu.Game.Tests/Visual/Online/TestSceneUserPanel.cs b/osu.Game.Tests/Visual/Online/TestSceneUserPanel.cs index dfb8213acf..3f1d961588 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneUserPanel.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneUserPanel.cs @@ -205,7 +205,7 @@ namespace osu.Game.Tests.Visual.Online public new TextFlowContainer LastVisitMessage => base.LastVisitMessage; } - private partial class TestUserStatisticsProvider : LocalUserStatisticsProvider + public partial class TestUserStatisticsProvider : LocalUserStatisticsProvider { public new void UpdateStatistics(UserStatistics newStatistics, RulesetInfo ruleset, Action? callback = null) => base.UpdateStatistics(newStatistics, ruleset, callback); From 8611ed31c2dce59da516b20fa73bd547add2991f Mon Sep 17 00:00:00 2001 From: "tsrk." Date: Sun, 24 Nov 2024 14:22:56 +0100 Subject: [PATCH 0088/1275] refactor(MenuTip): add localisation support Signed-off-by: tsrk. --- osu.Game/Localisation/MenuTipStrings.cs | 154 ++++++++++++++++++++++++ osu.Game/Screens/Menu/MenuTip.cs | 66 +++++----- 2 files changed, 188 insertions(+), 32 deletions(-) create mode 100644 osu.Game/Localisation/MenuTipStrings.cs diff --git a/osu.Game/Localisation/MenuTipStrings.cs b/osu.Game/Localisation/MenuTipStrings.cs new file mode 100644 index 0000000000..e955040f37 --- /dev/null +++ b/osu.Game/Localisation/MenuTipStrings.cs @@ -0,0 +1,154 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Localisation; + +namespace osu.Game.Localisation +{ + public static class MenuTipStrings + { + private const string prefix = @"osu.Game.Resources.Localisation.MenuTip"; + + /// + /// "Press Ctrl-T anywhere in the game to toggle the toolbar!" + /// + public static LocalisableString ToggleToolbarShortcut => new TranslatableString(getKey(@"toggle_toolbar_shortcut"), @"Press Ctrl-T anywhere in the game to toggle the toolbar!"); + + /// + /// "Press Ctrl-O anywhere in the game to access options!" + /// + public static LocalisableString GameSettingsShortcut => new TranslatableString(getKey(@"game_settings_shortcut"), @"Press Ctrl-O anywhere in the game to access options!"); + + /// + /// "All settings are dynamic and take effect in real-time. Try changing the skin while watching autoplay!" + /// + public static LocalisableString DynamicSettings => new TranslatableString(getKey(@"dynamic_settings"), @"All settings are dynamic and take effect in real-time. Try changing the skin while watching autoplay!"); + + /// + /// "New features are coming online every update. Make sure to stay up-to-date!" + /// + public static LocalisableString NewFeaturesAreComingOnline => new TranslatableString(getKey(@"new_features_are_coming_online"), @"New features are coming online every update. Make sure to stay up-to-date!"); + + /// + /// "If you find the UI too large or small, try adjusting UI scale in settings!" + /// + public static LocalisableString UIScalingSettings => new TranslatableString(getKey(@"ui_scaling_settings"), @"If you find the UI too large or small, try adjusting UI scale in settings!"); + + /// + /// "Try adjusting the "Screen Scaling" mode to change your gameplay or UI area, even in fullscreen!" + /// + public static LocalisableString ScreenScalingSettings => new TranslatableString(getKey(@"screen_scaling_settings"), @"Try adjusting the ""Screen Scaling"" mode to change your gameplay or UI area, even in fullscreen!"); + + /// + /// "What used to be "osu!direct" is available to all users just like on the website. You can access it anywhere using Ctrl-B!" + /// + public static LocalisableString FreeOsuDirect => new TranslatableString(getKey(@"free_osu_direct"), @"What used to be ""osu!direct"" is available to all users just like on the website. You can access it anywhere using Ctrl-B!"); + + /// + /// "Seeking in replays is available by dragging on the progress bar at the bottom of the screen or by using the left and right arrow keys!" + /// + public static LocalisableString ReplaySeeking => new TranslatableString(getKey(@"replay_seeking"), @"Seeking in replays is available by dragging on the progress bar at the bottom of the screen or by using the left and right arrow keys!"); + + /// + /// "Try scrolling right in mod select to find a bunch of new fun mods!" + /// + public static LocalisableString TryNewMods => new TranslatableString(getKey(@"try_new_mods"), @"Try scrolling right in mod select to find a bunch of new fun mods!"); + + /// + /// "Most of the web content (profiles, rankings, etc.) are available natively in-game from the icons on the toolbar!" + /// + public static LocalisableString EmbeddedWebContent => new TranslatableString(getKey(@"embedded_web_content"), @"Most of the web content (profiles, rankings, etc.) are available natively in-game from the icons on the toolbar!"); + + /// + /// "Get more details, hide or delete a beatmap by right-clicking on its panel at song select!" + /// + public static LocalisableString BeatmapRightClick => new TranslatableString(getKey(@"beatmap_right_click"), @"Get more details, hide or delete a beatmap by right-clicking on its panel at song select!"); + + /// + /// "Check out the "playlists" system, which lets users create their own custom and permanent leaderboards!" + /// + public static LocalisableString DiscoverPlaylists => new TranslatableString(getKey(@"discover_playlists"), @"Check out the ""playlists"" system, which lets users create their own custom and permanent leaderboards!"); + + /// + /// "Toggle advanced frame / thread statistics with Ctrl-F11!" + /// + public static LocalisableString ToggleAdvancedFPSCounter => new TranslatableString(getKey(@"toggle_advanced_fps_counter"), @"Toggle advanced frame / thread statistics with Ctrl-F11!"); + + /// + /// "You can pause during a replay by pressing Space!" + /// + public static LocalisableString ReplayPausing => new TranslatableString(getKey(@"replay_pausing"), @"You can pause during a replay by pressing Space!"); + + /// + /// "Most of the hotkeys in the game are configurable and can be changed to anything you want. Check the bindings panel under input settings!" + /// + public static LocalisableString ConfigurableHotkeys => new TranslatableString(getKey(@"configurable_hotkeys"), @"Most of the hotkeys in the game are configurable and can be changed to anything you want. Check the bindings panel under input settings!"); + + /// + /// "Your gameplay HUD can be customized by using the skin layout editor. Open it at any time via Ctrl-Shift-S!" + /// + public static LocalisableString SkinEditor => new TranslatableString(getKey(@"skin_editor"), @"Your gameplay HUD can be customized by using the skin layout editor. Open it at any time via Ctrl-Shift-S!"); + + /// + /// "You can create mod presets to make toggling your favorite mod combinations easier!" + /// + public static LocalisableString ModPresets => new TranslatableString(getKey(@"mod_presets"), @"You can create mod presets to make toggling your favorite mod combinations easier!"); + + /// + /// "Many mods have customisation settings that drastically change how they function. Click the Mod Customisation button in mod select to view settings!" + /// + public static LocalisableString ModCustomisationSettings => new TranslatableString(getKey(@"mod_customisation_settings"), @"Many mods have customisation settings that drastically change how they function. Click the Mod Customisation button in mod select to view settings!"); + + /// + /// "Press Ctrl-Shift-R to switch to a random skin!" + /// + public static LocalisableString RandomSkinShortcut => new TranslatableString(getKey(@"random_skin_shortcut"), @"Press Ctrl-Shift-R to switch to a random skin!"); + + /// + /// "While watching a replay, press Ctrl-H to toggle replay settings!" + /// + public static LocalisableString ToggleReplaySettingsShortcut => new TranslatableString(getKey(@"toggle_replay_settings_shortcut"), @"While watching a replay, press Ctrl-H to toggle replay settings!"); + + /// + /// "You can easily copy the mods from scores on a leaderboard by right-clicking on them!" + /// + public static LocalisableString CopyModsFromScore => new TranslatableString(getKey(@"copy_mods_from_score"), @"You can easily copy the mods from scores on a leaderboard by right-clicking on them!"); + + /// + /// "Ctrl-Enter at song select will start a beatmap in autoplay mode!" + /// + public static LocalisableString AutoplayBeatmapShortcut => new TranslatableString(getKey(@"autoplay_beatmap_shortcut"), @"Ctrl-Enter at song select will start a beatmap in autoplay mode!"); + + /// + /// "Multithreading support means that even with low "FPS" your input and judgements will be accurate!" + /// + public static LocalisableString MultithreadingSupport => new TranslatableString(getKey(@"multithreading_support"), @"Multithreading support means that even with low ""FPS"" your input and judgements will be accurate!"); + + /// + /// "All delete operations are temporary until exiting. Restore accidentally deleted content from the maintenance settings!" + /// + public static LocalisableString TemporaryDeleteOperations => new TranslatableString(getKey(@"temporary_delete_operations"), @"All delete operations are temporary until exiting. Restore accidentally deleted content from the maintenance settings!"); + + /// + /// "Take a look under the hood at performance counters and enable verbose performance logging with Ctrl-F2!" + /// + public static LocalisableString GlobalStatisticsShortcut => new TranslatableString(getKey(@"global_statistics_shortcut"), @"Take a look under the hood at performance counters and enable verbose performance logging with Ctrl-F2!"); + + /// + /// "When your gameplay HUD is hidden, you can press and hold Ctrl to view it temporarily!" + /// + public static LocalisableString PeekHUDWhenHidden => new TranslatableString(getKey(@"peek_hud_when_hidden"), @"When your gameplay HUD is hidden, you can press and hold Ctrl to view it temporarily!"); + + /// + /// "Drag and drop any image into the skin editor to load it in quickly!" + /// + public static LocalisableString DragAndDropImageInSkinEditor => new TranslatableString(getKey(@"drag_and_drop_image_in_skin_editor"), @"Drag and drop any image into the skin editor to load it in quickly!"); + + /// + /// "a tip for you:" + /// + public static LocalisableString MenuTipTitle => new TranslatableString(getKey(@"menu_tip_title"), @"a tip for you:"); + + private static string getKey(string key) => $@"{prefix}:{key}"; + } +} diff --git a/osu.Game/Screens/Menu/MenuTip.cs b/osu.Game/Screens/Menu/MenuTip.cs index 58eeb7e82d..3fc5fe57fb 100644 --- a/osu.Game/Screens/Menu/MenuTip.cs +++ b/osu.Game/Screens/Menu/MenuTip.cs @@ -7,12 +7,14 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Sprites; +using osu.Framework.Localisation; using osu.Framework.Utils; using osu.Game.Configuration; using osu.Game.Graphics; using osu.Game.Graphics.Containers; using osuTK; using osuTK.Graphics; +using osu.Game.Localisation; namespace osu.Game.Screens.Menu { @@ -78,49 +80,49 @@ namespace osu.Game.Screens.Menu static void formatRegular(SpriteText t) => t.Font = OsuFont.GetFont(size: 16, weight: FontWeight.Regular); static void formatSemiBold(SpriteText t) => t.Font = OsuFont.GetFont(size: 16, weight: FontWeight.SemiBold); - string tip = getRandomTip(); + var tip = getRandomTip(); textFlow.Clear(); - textFlow.AddParagraph("a tip for you:", formatSemiBold); + textFlow.AddParagraph(MenuTipStrings.MenuTipTitle, formatSemiBold); textFlow.AddParagraph(tip, formatRegular); this.FadeInFromZero(200, Easing.OutQuint) - .Delay(1000 + 80 * tip.Length) + .Delay(1000 + 80 * tip.ToString().Length) .Then() .FadeOutFromOne(2000, Easing.OutQuint); } - private string getRandomTip() + private LocalisableString getRandomTip() { - string[] tips = + LocalisableString[] tips = { - "Press Ctrl-T anywhere in the game to toggle the toolbar!", - "Press Ctrl-O anywhere in the game to access options!", - "All settings are dynamic and take effect in real-time. Try changing the skin while watching autoplay!", - "New features are coming online every update. Make sure to stay up-to-date!", - "If you find the UI too large or small, try adjusting UI scale in settings!", - "Try adjusting the \"Screen Scaling\" mode to change your gameplay or UI area, even in fullscreen!", - "What used to be \"osu!direct\" is available to all users just like on the website. You can access it anywhere using Ctrl-B!", - "Seeking in replays is available by dragging on the progress bar at the bottom of the screen or by using the left and right arrow keys!", - "Multithreading support means that even with low \"FPS\" your input and judgements will be accurate!", - "Try scrolling right in mod select to find a bunch of new fun mods!", - "Most of the web content (profiles, rankings, etc.) are available natively in-game from the icons on the toolbar!", - "Get more details, hide or delete a beatmap by right-clicking on its panel at song select!", - "All delete operations are temporary until exiting. Restore accidentally deleted content from the maintenance settings!", - "Check out the \"playlists\" system, which lets users create their own custom and permanent leaderboards!", - "Toggle advanced frame / thread statistics with Ctrl-F11!", - "Take a look under the hood at performance counters and enable verbose performance logging with Ctrl-F2!", - "You can pause during a replay by pressing Space!", - "Most of the hotkeys in the game are configurable and can be changed to anything you want. Check the bindings panel under input settings!", - "When your gameplay HUD is hidden, you can press and hold Ctrl to view it temporarily!", - "Your gameplay HUD can be customized by using the skin layout editor. Open it at any time via Ctrl-Shift-S!", - "Drag and drop any image into the skin editor to load it in quickly!", - "You can create mod presets to make toggling your favorite mod combinations easier!", - "Many mods have customisation settings that drastically change how they function. Click the Mod Customisation button in mod select to view settings!", - "Press Ctrl-Shift-R to switch to a random skin!", - "While watching a replay, press Ctrl-H to toggle replay settings!", - "You can easily copy the mods from scores on a leaderboard by right-clicking on them!", - "Ctrl-Enter at song select will start a beatmap in autoplay mode!" + MenuTipStrings.ToggleToolbarShortcut, + MenuTipStrings.GameSettingsShortcut, + MenuTipStrings.DynamicSettings, + MenuTipStrings.NewFeaturesAreComingOnline, + MenuTipStrings.UIScalingSettings, + MenuTipStrings.ScreenScalingSettings, + MenuTipStrings.FreeOsuDirect, + MenuTipStrings.ReplaySeeking, + MenuTipStrings.MultithreadingSupport, + MenuTipStrings.TryNewMods, + MenuTipStrings.EmbeddedWebContent, + MenuTipStrings.BeatmapRightClick, + MenuTipStrings.TemporaryDeleteOperations, + MenuTipStrings.DiscoverPlaylists, + MenuTipStrings.ToggleAdvancedFPSCounter, + MenuTipStrings.GlobalStatisticsShortcut, + MenuTipStrings.ReplayPausing, + MenuTipStrings.ConfigurableHotkeys, + MenuTipStrings.PeekHUDWhenHidden, + MenuTipStrings.SkinEditor, + MenuTipStrings.DragAndDropImageInSkinEditor, + MenuTipStrings.ModPresets, + MenuTipStrings.ModCustomisationSettings, + MenuTipStrings.RandomSkinShortcut, + MenuTipStrings.ToggleReplaySettingsShortcut, + MenuTipStrings.CopyModsFromScore, + MenuTipStrings.AutoplayBeatmapShortcut }; return tips[RNG.Next(0, tips.Length)]; From 146838555999a69386aebb3d67f6af4bc860e1ba Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Sun, 24 Nov 2024 22:38:48 -0500 Subject: [PATCH 0089/1275] Reset new file states after syncing --- osu.Game/Screens/Edit/Setup/ResourcesSection.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/osu.Game/Screens/Edit/Setup/ResourcesSection.cs b/osu.Game/Screens/Edit/Setup/ResourcesSection.cs index a52e42c7c0..90603a6366 100644 --- a/osu.Game/Screens/Edit/Setup/ResourcesSection.cs +++ b/osu.Game/Screens/Edit/Setup/ResourcesSection.cs @@ -200,6 +200,8 @@ namespace osu.Game.Screens.Edit.Setup beatmaps.Save(otherBeatmap, otherWorking.Beatmap); } + newAudioFile = null; + newBackgroundFile = null; syncResourcesButton.Enabled.Value = false; } From a1916d12db3be7e55fd7ac1d184a1725cca4266d Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 25 Nov 2024 18:41:50 +0900 Subject: [PATCH 0090/1275] Ensure UR benchmark has hitwindows populated --- osu.Game.Benchmarks/BenchmarkUnstableRate.cs | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/osu.Game.Benchmarks/BenchmarkUnstableRate.cs b/osu.Game.Benchmarks/BenchmarkUnstableRate.cs index aa229c7d06..7b6c839648 100644 --- a/osu.Game.Benchmarks/BenchmarkUnstableRate.cs +++ b/osu.Game.Benchmarks/BenchmarkUnstableRate.cs @@ -4,7 +4,9 @@ using System.Collections.Generic; using BenchmarkDotNet.Attributes; using osu.Framework.Utils; -using osu.Game.Rulesets.Objects; +using osu.Game.Beatmaps; +using osu.Game.Beatmaps.ControlPoints; +using osu.Game.Rulesets.Osu.Objects; using osu.Game.Rulesets.Scoring; namespace osu.Game.Benchmarks @@ -18,8 +20,14 @@ namespace osu.Game.Benchmarks base.SetUp(); events = new List(); - for (int i = 0; i < 1000; i++) - events.Add(new HitEvent(RNG.NextDouble(-200.0, 200.0), RNG.NextDouble(1.0, 2.0), HitResult.Great, new HitObject(), null, null)); + for (int i = 0; i < 2048; i++) + { + // Ensure the object has hit windows populated. + var hitObject = new HitCircle(); + hitObject.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty()); + + events.Add(new HitEvent(RNG.NextDouble(-200.0, 200.0), RNG.NextDouble(1.0, 2.0), HitResult.Great, hitObject, null, null)); + } } [Benchmark] From 605fe71f46eaf2944b659389cca3cfa03011e5de Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 25 Nov 2024 19:17:32 +0900 Subject: [PATCH 0091/1275] Make empty hitwindows readonly static and slightly improve comparison performance --- osu.Game/Rulesets/Mods/ModAdaptiveSpeed.cs | 2 +- osu.Game/Rulesets/Scoring/HitEventExtensions.cs | 2 +- osu.Game/Rulesets/Scoring/HitWindows.cs | 4 ++-- osu.Game/Screens/Play/HUD/UnstableRateCounter.cs | 2 +- .../Ranking/Statistics/HitEventTimingDistributionGraph.cs | 2 +- 5 files changed, 6 insertions(+), 6 deletions(-) diff --git a/osu.Game/Rulesets/Mods/ModAdaptiveSpeed.cs b/osu.Game/Rulesets/Mods/ModAdaptiveSpeed.cs index 19554b6504..4ca937bf86 100644 --- a/osu.Game/Rulesets/Mods/ModAdaptiveSpeed.cs +++ b/osu.Game/Rulesets/Mods/ModAdaptiveSpeed.cs @@ -205,7 +205,7 @@ namespace osu.Game.Rulesets.Mods { foreach (var hitObject in hitObjects) { - if (!(hitObject.HitWindows is HitWindows.EmptyHitWindows)) + if (hitObject.HitWindows != HitWindows.Empty) yield return hitObject; foreach (HitObject nested in getAllApplicableHitObjects(hitObject.NestedHitObjects)) diff --git a/osu.Game/Rulesets/Scoring/HitEventExtensions.cs b/osu.Game/Rulesets/Scoring/HitEventExtensions.cs index fc4eef13ba..672c229875 100644 --- a/osu.Game/Rulesets/Scoring/HitEventExtensions.cs +++ b/osu.Game/Rulesets/Scoring/HitEventExtensions.cs @@ -65,6 +65,6 @@ namespace osu.Game.Rulesets.Scoring return timeOffsets.Average(); } - public static bool AffectsUnstableRate(HitEvent e) => !(e.HitObject.HitWindows is HitWindows.EmptyHitWindows) && e.Result.IsHit(); + public static bool AffectsUnstableRate(HitEvent e) => e.HitObject.HitWindows != HitWindows.Empty && e.Result.IsHit(); } } diff --git a/osu.Game/Rulesets/Scoring/HitWindows.cs b/osu.Game/Rulesets/Scoring/HitWindows.cs index 2d008b58ba..a6a268fc78 100644 --- a/osu.Game/Rulesets/Scoring/HitWindows.cs +++ b/osu.Game/Rulesets/Scoring/HitWindows.cs @@ -36,7 +36,7 @@ namespace osu.Game.Rulesets.Scoring /// An empty with only and . /// No time values are provided (meaning instantaneous hit or miss). /// - public static HitWindows Empty => new EmptyHitWindows(); + public static HitWindows Empty { get; } = new EmptyHitWindows(); public HitWindows() { @@ -182,7 +182,7 @@ namespace osu.Game.Rulesets.Scoring /// protected virtual DifficultyRange[] GetRanges() => base_ranges; - public class EmptyHitWindows : HitWindows + private class EmptyHitWindows : HitWindows { private static readonly DifficultyRange[] ranges = { diff --git a/osu.Game/Screens/Play/HUD/UnstableRateCounter.cs b/osu.Game/Screens/Play/HUD/UnstableRateCounter.cs index ab7ab6b3a0..6fe5e818c4 100644 --- a/osu.Game/Screens/Play/HUD/UnstableRateCounter.cs +++ b/osu.Game/Screens/Play/HUD/UnstableRateCounter.cs @@ -45,7 +45,7 @@ namespace osu.Game.Screens.Play.HUD } private bool changesUnstableRate(JudgementResult judgement) - => !(judgement.HitObject.HitWindows is HitWindows.EmptyHitWindows) && judgement.IsHit; + => judgement.HitObject.HitWindows != HitWindows.Empty && judgement.IsHit; protected override void LoadComplete() { diff --git a/osu.Game/Screens/Ranking/Statistics/HitEventTimingDistributionGraph.cs b/osu.Game/Screens/Ranking/Statistics/HitEventTimingDistributionGraph.cs index a9b93e0ffc..a80aeaa5dd 100644 --- a/osu.Game/Screens/Ranking/Statistics/HitEventTimingDistributionGraph.cs +++ b/osu.Game/Screens/Ranking/Statistics/HitEventTimingDistributionGraph.cs @@ -58,7 +58,7 @@ namespace osu.Game.Screens.Ranking.Statistics /// The s to display the timing distribution of. public HitEventTimingDistributionGraph(IReadOnlyList hitEvents) { - this.hitEvents = hitEvents.Where(e => !(e.HitObject.HitWindows is HitWindows.EmptyHitWindows) && e.Result.IsBasic() && e.Result.IsHit()).ToList(); + this.hitEvents = hitEvents.Where(e => e.HitObject.HitWindows != HitWindows.Empty && e.Result.IsBasic() && e.Result.IsHit()).ToList(); bins = Enumerable.Range(0, total_timing_distribution_bins).Select(_ => new Dictionary()).ToArray>(); } From 33d725e889481843601fe1f647a88ba866a8c6ce Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 25 Nov 2024 19:24:58 +0900 Subject: [PATCH 0092/1275] Address unstable rate calculations as a list for marginal gains --- osu.Game.Tests/NonVisual/Ranking/UnstableRateTest.cs | 3 ++- osu.Game/Rulesets/Scoring/HitEventExtensions.cs | 2 +- osu.Game/Screens/Play/HUD/UnstableRateCounter.cs | 3 --- osu.Game/Screens/Ranking/Statistics/UnstableRate.cs | 2 +- 4 files changed, 4 insertions(+), 6 deletions(-) diff --git a/osu.Game.Tests/NonVisual/Ranking/UnstableRateTest.cs b/osu.Game.Tests/NonVisual/Ranking/UnstableRateTest.cs index 5a416d05d7..94a0e34d0d 100644 --- a/osu.Game.Tests/NonVisual/Ranking/UnstableRateTest.cs +++ b/osu.Game.Tests/NonVisual/Ranking/UnstableRateTest.cs @@ -20,7 +20,8 @@ namespace osu.Game.Tests.NonVisual.Ranking public void TestDistributedHits() { var events = Enumerable.Range(-5, 11) - .Select(t => new HitEvent(t - 5, 1.0, HitResult.Great, new HitObject(), null, null)); + .Select(t => new HitEvent(t - 5, 1.0, HitResult.Great, new HitObject(), null, null)) + .ToList(); var unstableRate = new UnstableRate(events); diff --git a/osu.Game/Rulesets/Scoring/HitEventExtensions.cs b/osu.Game/Rulesets/Scoring/HitEventExtensions.cs index 672c229875..e79504d1ec 100644 --- a/osu.Game/Rulesets/Scoring/HitEventExtensions.cs +++ b/osu.Game/Rulesets/Scoring/HitEventExtensions.cs @@ -20,7 +20,7 @@ namespace osu.Game.Rulesets.Scoring /// A non-null value if unstable rate could be calculated, /// and if unstable rate cannot be calculated due to being empty. /// - public static double? CalculateUnstableRate(this IEnumerable hitEvents) + public static double? CalculateUnstableRate(this IReadOnlyList hitEvents) { Debug.Assert(hitEvents.All(ev => ev.GameplayRate != null)); diff --git a/osu.Game/Screens/Play/HUD/UnstableRateCounter.cs b/osu.Game/Screens/Play/HUD/UnstableRateCounter.cs index 6fe5e818c4..3c9ab87022 100644 --- a/osu.Game/Screens/Play/HUD/UnstableRateCounter.cs +++ b/osu.Game/Screens/Play/HUD/UnstableRateCounter.cs @@ -44,9 +44,6 @@ namespace osu.Game.Screens.Play.HUD DrawableCount.FadeTo(e.NewValue ? 1 : alpha_when_invalid, 1000, Easing.OutQuint)); } - private bool changesUnstableRate(JudgementResult judgement) - => judgement.HitObject.HitWindows != HitWindows.Empty && judgement.IsHit; - protected override void LoadComplete() { base.LoadComplete(); diff --git a/osu.Game/Screens/Ranking/Statistics/UnstableRate.cs b/osu.Game/Screens/Ranking/Statistics/UnstableRate.cs index cc3535a426..10b18d09c9 100644 --- a/osu.Game/Screens/Ranking/Statistics/UnstableRate.cs +++ b/osu.Game/Screens/Ranking/Statistics/UnstableRate.cs @@ -15,7 +15,7 @@ namespace osu.Game.Screens.Ranking.Statistics /// Creates and computes an statistic. /// /// Sequence of s to calculate the unstable rate based on. - public UnstableRate(IEnumerable hitEvents) + public UnstableRate(IReadOnlyList hitEvents) : base("Unstable Rate") { Value = hitEvents.CalculateUnstableRate(); From c8847e8da86bbfdbc2801c8e5aef1c9a95389c59 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 25 Nov 2024 12:53:40 +0100 Subject: [PATCH 0093/1275] Fix incorrect unit test --- osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapDecoderTest.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapDecoderTest.cs b/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapDecoderTest.cs index 54ebebeb7b..b5c299ed9d 100644 --- a/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapDecoderTest.cs +++ b/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapDecoderTest.cs @@ -1000,7 +1000,7 @@ namespace osu.Game.Tests.Beatmaps.Formats Assert.That(decoded.BeatmapInfo.WidescreenStoryboard, Is.False); Assert.That(decoded.BeatmapInfo.EpilepsyWarning, Is.False); Assert.That(decoded.BeatmapInfo.SamplesMatchPlaybackRate, Is.False); - Assert.That(decoded.BeatmapInfo.Countdown, Is.EqualTo(CountdownType.Normal)); + Assert.That(decoded.BeatmapInfo.Countdown, Is.EqualTo(CountdownType.None)); Assert.That(decoded.BeatmapInfo.CountdownOffset, Is.EqualTo(0)); Assert.That(decoded.BeatmapInfo.Metadata.PreviewTime, Is.EqualTo(-1)); Assert.That(decoded.BeatmapInfo.Ruleset.OnlineID, Is.EqualTo(0)); From 5668258182537bdfaa6d6419e75a3d6f497e1d86 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 25 Nov 2024 19:43:42 +0900 Subject: [PATCH 0094/1275] Add incremental processing --- osu.Game.Benchmarks/BenchmarkUnstableRate.cs | 26 ++++++++++-- .../NonVisual/Ranking/UnstableRateTest.cs | 42 ++++++++++++++++++- .../Rulesets/Scoring/HitEventExtensions.cs | 14 ++++--- .../Screens/Play/HUD/UnstableRateCounter.cs | 2 +- .../Ranking/Statistics/UnstableRate.cs | 2 +- 5 files changed, 74 insertions(+), 12 deletions(-) diff --git a/osu.Game.Benchmarks/BenchmarkUnstableRate.cs b/osu.Game.Benchmarks/BenchmarkUnstableRate.cs index 7b6c839648..4d3023e92e 100644 --- a/osu.Game.Benchmarks/BenchmarkUnstableRate.cs +++ b/osu.Game.Benchmarks/BenchmarkUnstableRate.cs @@ -13,27 +13,45 @@ namespace osu.Game.Benchmarks { public class BenchmarkUnstableRate : BenchmarkTest { - private List events = null!; + private readonly List> incrementalEventLists = new List>(); public override void SetUp() { base.SetUp(); - events = new List(); + + var events = new List(); for (int i = 0; i < 2048; i++) { // Ensure the object has hit windows populated. var hitObject = new HitCircle(); hitObject.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty()); - events.Add(new HitEvent(RNG.NextDouble(-200.0, 200.0), RNG.NextDouble(1.0, 2.0), HitResult.Great, hitObject, null, null)); + + incrementalEventLists.Add(new List(events)); } } [Benchmark] public void CalculateUnstableRate() { - _ = events.CalculateUnstableRate(); + for (int i = 0; i < 2048; i++) + { + var events = incrementalEventLists[i]; + _ = events.CalculateUnstableRate(); + } + } + + [Benchmark] + public void CalculateUnstableRateUsingIncrementalCalculation() + { + HitEventExtensions.UnstableRateCalculationResult? last = null; + + for (int i = 0; i < 2048; i++) + { + var events = incrementalEventLists[i]; + last = events.CalculateUnstableRate(last); + } } } } diff --git a/osu.Game.Tests/NonVisual/Ranking/UnstableRateTest.cs b/osu.Game.Tests/NonVisual/Ranking/UnstableRateTest.cs index 94a0e34d0d..03dc91b5d4 100644 --- a/osu.Game.Tests/NonVisual/Ranking/UnstableRateTest.cs +++ b/osu.Game.Tests/NonVisual/Ranking/UnstableRateTest.cs @@ -26,7 +26,47 @@ namespace osu.Game.Tests.NonVisual.Ranking var unstableRate = new UnstableRate(events); Assert.IsNotNull(unstableRate.Value); - Assert.IsTrue(Precision.AlmostEquals(unstableRate.Value.Value, 10 * Math.Sqrt(10))); + Assert.AreEqual(unstableRate.Value.Value, 10 * Math.Sqrt(10), Precision.DOUBLE_EPSILON); + } + + [Test] + public void TestDistributedHitsIncrementalRewind() + { + var events = Enumerable.Range(-5, 11) + .Select(t => new HitEvent(t - 5, 1.0, HitResult.Great, new HitObject(), null, null)) + .ToList(); + + HitEventExtensions.UnstableRateCalculationResult result = null; + + for (int i = 0; i < events.Count; i++) + { + result = events.GetRange(0, i + 1) + .CalculateUnstableRate(result); + } + + result = events.GetRange(0, 2).CalculateUnstableRate(result); + + Assert.IsNotNull(result!.Result); + Assert.AreEqual(5, result.Result, Precision.DOUBLE_EPSILON); + } + + [Test] + public void TestDistributedHitsIncremental() + { + var events = Enumerable.Range(-5, 11) + .Select(t => new HitEvent(t - 5, 1.0, HitResult.Great, new HitObject(), null, null)) + .ToList(); + + HitEventExtensions.UnstableRateCalculationResult result = null; + + for (int i = 0; i < events.Count; i++) + { + result = events.GetRange(0, i + 1) + .CalculateUnstableRate(result); + } + + Assert.IsNotNull(result!.Result); + Assert.AreEqual(10 * Math.Sqrt(10), result.Result, Precision.DOUBLE_EPSILON); } [Test] diff --git a/osu.Game/Rulesets/Scoring/HitEventExtensions.cs b/osu.Game/Rulesets/Scoring/HitEventExtensions.cs index e79504d1ec..115ffb67f7 100644 --- a/osu.Game/Rulesets/Scoring/HitEventExtensions.cs +++ b/osu.Game/Rulesets/Scoring/HitEventExtensions.cs @@ -20,16 +20,18 @@ namespace osu.Game.Rulesets.Scoring /// A non-null value if unstable rate could be calculated, /// and if unstable rate cannot be calculated due to being empty. /// - public static double? CalculateUnstableRate(this IReadOnlyList hitEvents) + public static UnstableRateCalculationResult? CalculateUnstableRate(this IReadOnlyList hitEvents, UnstableRateCalculationResult? previousResult = null) { Debug.Assert(hitEvents.All(ev => ev.GameplayRate != null)); int count = 0; - double mean = 0; - double sumOfSquares = 0; + double mean = previousResult?.Mean ?? 0; + double sumOfSquares = previousResult?.SumOfSquares ?? 0; - foreach (var e in hitEvents) + for (int i = previousResult?.CalculatedHitEventsCount - 1 ?? 0; i < hitEvents.Count; i++) { + HitEvent e = hitEvents[i]; + if (!AffectsUnstableRate(e)) continue; @@ -45,7 +47,7 @@ namespace osu.Game.Rulesets.Scoring if (count == 0) return null; - return 10.0 * Math.Sqrt(sumOfSquares / count); + return new UnstableRateCalculationResult(hitEvents.Count, sumOfSquares, mean, 10.0 * Math.Sqrt(sumOfSquares / count)); } /// @@ -66,5 +68,7 @@ namespace osu.Game.Rulesets.Scoring } public static bool AffectsUnstableRate(HitEvent e) => e.HitObject.HitWindows != HitWindows.Empty && e.Result.IsHit(); + + public record UnstableRateCalculationResult(int CalculatedHitEventsCount, double SumOfSquares, double Mean, double Result); } } diff --git a/osu.Game/Screens/Play/HUD/UnstableRateCounter.cs b/osu.Game/Screens/Play/HUD/UnstableRateCounter.cs index 3c9ab87022..db271a21c5 100644 --- a/osu.Game/Screens/Play/HUD/UnstableRateCounter.cs +++ b/osu.Game/Screens/Play/HUD/UnstableRateCounter.cs @@ -57,7 +57,7 @@ namespace osu.Game.Screens.Play.HUD private void updateDisplay() { - double? unstableRate = scoreProcessor.HitEvents.CalculateUnstableRate(); + double? unstableRate = scoreProcessor.HitEvents.CalculateUnstableRate()?.Result; valid.Value = unstableRate != null; if (unstableRate != null) diff --git a/osu.Game/Screens/Ranking/Statistics/UnstableRate.cs b/osu.Game/Screens/Ranking/Statistics/UnstableRate.cs index 10b18d09c9..d114bed156 100644 --- a/osu.Game/Screens/Ranking/Statistics/UnstableRate.cs +++ b/osu.Game/Screens/Ranking/Statistics/UnstableRate.cs @@ -18,7 +18,7 @@ namespace osu.Game.Screens.Ranking.Statistics public UnstableRate(IReadOnlyList hitEvents) : base("Unstable Rate") { - Value = hitEvents.CalculateUnstableRate(); + Value = hitEvents.CalculateUnstableRate()?.Result; } protected override string DisplayValue(double? value) => value == null ? "(not available)" : value.Value.ToString(@"N2"); From ea68d4b33abbd920e267001fb4785ae000031f54 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 25 Nov 2024 20:36:43 +0900 Subject: [PATCH 0095/1275] Use class instead of record for lower allocations --- .../Rulesets/Scoring/HitEventExtensions.cs | 33 ++++++++++++------- 1 file changed, 21 insertions(+), 12 deletions(-) diff --git a/osu.Game/Rulesets/Scoring/HitEventExtensions.cs b/osu.Game/Rulesets/Scoring/HitEventExtensions.cs index 115ffb67f7..d9eb8b0c37 100644 --- a/osu.Game/Rulesets/Scoring/HitEventExtensions.cs +++ b/osu.Game/Rulesets/Scoring/HitEventExtensions.cs @@ -20,34 +20,36 @@ namespace osu.Game.Rulesets.Scoring /// A non-null value if unstable rate could be calculated, /// and if unstable rate cannot be calculated due to being empty. /// - public static UnstableRateCalculationResult? CalculateUnstableRate(this IReadOnlyList hitEvents, UnstableRateCalculationResult? previousResult = null) + public static UnstableRateCalculationResult? CalculateUnstableRate(this IReadOnlyList hitEvents, UnstableRateCalculationResult? result = null) { Debug.Assert(hitEvents.All(ev => ev.GameplayRate != null)); - int count = 0; - double mean = previousResult?.Mean ?? 0; - double sumOfSquares = previousResult?.SumOfSquares ?? 0; + result ??= new UnstableRateCalculationResult(); - for (int i = previousResult?.CalculatedHitEventsCount - 1 ?? 0; i < hitEvents.Count; i++) + // Handle rewinding in the simplest way possible. + if (hitEvents.Count < result.NextProcessableIndex + 1) + result = new UnstableRateCalculationResult(); + + for (int i = result.NextProcessableIndex; i < hitEvents.Count; i++) { HitEvent e = hitEvents[i]; if (!AffectsUnstableRate(e)) continue; - count++; + result.NextProcessableIndex++; // Division by gameplay rate is to account for TimeOffset scaling with gameplay rate. double currentValue = e.TimeOffset / e.GameplayRate!.Value; - double nextMean = mean + (currentValue - mean) / count; - sumOfSquares += (currentValue - mean) * (currentValue - nextMean); - mean = nextMean; + double nextMean = result.Mean + (currentValue - result.Mean) / result.NextProcessableIndex; + result.SumOfSquares += (currentValue - result.Mean) * (currentValue - nextMean); + result.Mean = nextMean; } - if (count == 0) + if (result.NextProcessableIndex == 0) return null; - return new UnstableRateCalculationResult(hitEvents.Count, sumOfSquares, mean, 10.0 * Math.Sqrt(sumOfSquares / count)); + return result; } /// @@ -69,6 +71,13 @@ namespace osu.Game.Rulesets.Scoring public static bool AffectsUnstableRate(HitEvent e) => e.HitObject.HitWindows != HitWindows.Empty && e.Result.IsHit(); - public record UnstableRateCalculationResult(int CalculatedHitEventsCount, double SumOfSquares, double Mean, double Result); + public class UnstableRateCalculationResult + { + public int NextProcessableIndex; + public double SumOfSquares; + public double Mean; + + public double Result => 10.0 * Math.Sqrt(SumOfSquares / NextProcessableIndex); + } } } From bbe8f2ec44cf7f2fa76c23dddbfcc33bea7045ee Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 25 Nov 2024 20:49:30 +0900 Subject: [PATCH 0096/1275] Only update unstable rate counter when an applicable hitobject is reached --- osu.Game/Rulesets/Scoring/HitEventExtensions.cs | 4 +++- osu.Game/Screens/Play/HUD/UnstableRateCounter.cs | 13 +++++++++++-- 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/osu.Game/Rulesets/Scoring/HitEventExtensions.cs b/osu.Game/Rulesets/Scoring/HitEventExtensions.cs index d9eb8b0c37..3236ce83dd 100644 --- a/osu.Game/Rulesets/Scoring/HitEventExtensions.cs +++ b/osu.Game/Rulesets/Scoring/HitEventExtensions.cs @@ -5,6 +5,7 @@ using System; using System.Collections.Generic; using System.Diagnostics; using System.Linq; +using osu.Game.Rulesets.Objects; namespace osu.Game.Rulesets.Scoring { @@ -69,7 +70,8 @@ namespace osu.Game.Rulesets.Scoring return timeOffsets.Average(); } - public static bool AffectsUnstableRate(HitEvent e) => e.HitObject.HitWindows != HitWindows.Empty && e.Result.IsHit(); + public static bool AffectsUnstableRate(HitEvent e) => AffectsUnstableRate(e.HitObject, e.Result); + public static bool AffectsUnstableRate(HitObject hitObject, HitResult result) => hitObject.HitWindows != HitWindows.Empty && result.IsHit(); public class UnstableRateCalculationResult { diff --git a/osu.Game/Screens/Play/HUD/UnstableRateCounter.cs b/osu.Game/Screens/Play/HUD/UnstableRateCounter.cs index db271a21c5..a856a09388 100644 --- a/osu.Game/Screens/Play/HUD/UnstableRateCounter.cs +++ b/osu.Game/Screens/Play/HUD/UnstableRateCounter.cs @@ -28,6 +28,8 @@ namespace osu.Game.Screens.Play.HUD private const float alpha_when_invalid = 0.3f; private readonly Bindable valid = new Bindable(); + private HitEventExtensions.UnstableRateCalculationResult? unstableRateResult; + [Resolved] private ScoreProcessor scoreProcessor { get; set; } = null!; @@ -53,13 +55,20 @@ namespace osu.Game.Screens.Play.HUD updateDisplay(); } - private void updateDisplay(JudgementResult _) => Scheduler.AddOnce(updateDisplay); + private void updateDisplay(JudgementResult result) + { + if (HitEventExtensions.AffectsUnstableRate(result.HitObject, result.Type)) + Scheduler.AddOnce(updateDisplay); + } private void updateDisplay() { - double? unstableRate = scoreProcessor.HitEvents.CalculateUnstableRate()?.Result; + unstableRateResult = scoreProcessor.HitEvents.CalculateUnstableRate(unstableRateResult); + + double? unstableRate = unstableRateResult?.Result; valid.Value = unstableRate != null; + if (unstableRate != null) Current.Value = (int)Math.Round(unstableRate.Value); } From 0a3f3c3210dd50f5a50c7fb0df875299abc9ffe8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 25 Nov 2024 13:14:22 +0100 Subject: [PATCH 0097/1275] Add guard against fetching statistics for non-legacy rulesets --- osu.Game/Online/LocalUserStatisticsProvider.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/osu.Game/Online/LocalUserStatisticsProvider.cs b/osu.Game/Online/LocalUserStatisticsProvider.cs index 5fa2b40715..79122b4186 100644 --- a/osu.Game/Online/LocalUserStatisticsProvider.cs +++ b/osu.Game/Online/LocalUserStatisticsProvider.cs @@ -63,6 +63,9 @@ namespace osu.Game.Online public void RefetchStatistics(RulesetInfo ruleset, Action? callback = null) { + if (!ruleset.IsLegacyRuleset()) + throw new InvalidOperationException($@"Retrieving statistics is not supported for ruleset {ruleset.ShortName}"); + var request = new GetUserRequest(api.LocalUser.Value.Id, ruleset); request.Success += u => UpdateStatistics(u.Statistics, ruleset, callback); api.Queue(request); From d903d381d50d792d3452f07c117f7069866c66fc Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 26 Nov 2024 12:10:34 +0900 Subject: [PATCH 0098/1275] Rename `NextProcessableIndex` to `EventCount` in line with actual functionality --- osu.Game/Rulesets/Scoring/HitEventExtensions.cs | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/osu.Game/Rulesets/Scoring/HitEventExtensions.cs b/osu.Game/Rulesets/Scoring/HitEventExtensions.cs index 3236ce83dd..7442c6ccc3 100644 --- a/osu.Game/Rulesets/Scoring/HitEventExtensions.cs +++ b/osu.Game/Rulesets/Scoring/HitEventExtensions.cs @@ -28,26 +28,26 @@ namespace osu.Game.Rulesets.Scoring result ??= new UnstableRateCalculationResult(); // Handle rewinding in the simplest way possible. - if (hitEvents.Count < result.NextProcessableIndex + 1) + if (hitEvents.Count < result.EventCount + 1) result = new UnstableRateCalculationResult(); - for (int i = result.NextProcessableIndex; i < hitEvents.Count; i++) + for (int i = result.EventCount; i < hitEvents.Count; i++) { HitEvent e = hitEvents[i]; if (!AffectsUnstableRate(e)) continue; - result.NextProcessableIndex++; + result.EventCount++; // Division by gameplay rate is to account for TimeOffset scaling with gameplay rate. double currentValue = e.TimeOffset / e.GameplayRate!.Value; - double nextMean = result.Mean + (currentValue - result.Mean) / result.NextProcessableIndex; + double nextMean = result.Mean + (currentValue - result.Mean) / result.EventCount; result.SumOfSquares += (currentValue - result.Mean) * (currentValue - nextMean); result.Mean = nextMean; } - if (result.NextProcessableIndex == 0) + if (result.EventCount == 0) return null; return result; @@ -75,11 +75,11 @@ namespace osu.Game.Rulesets.Scoring public class UnstableRateCalculationResult { - public int NextProcessableIndex; + public int EventCount; public double SumOfSquares; public double Mean; - public double Result => 10.0 * Math.Sqrt(SumOfSquares / NextProcessableIndex); + public double Result => 10.0 * Math.Sqrt(SumOfSquares / EventCount); } } } From d6cf1db0f5e7af63a73c8a7814dd52fd723b0ada Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 26 Nov 2024 12:16:26 +0900 Subject: [PATCH 0099/1275] Add basic xmldoc to results class --- .../Rulesets/Scoring/HitEventExtensions.cs | 25 ++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/osu.Game/Rulesets/Scoring/HitEventExtensions.cs b/osu.Game/Rulesets/Scoring/HitEventExtensions.cs index 7442c6ccc3..269342460f 100644 --- a/osu.Game/Rulesets/Scoring/HitEventExtensions.cs +++ b/osu.Game/Rulesets/Scoring/HitEventExtensions.cs @@ -73,13 +73,36 @@ namespace osu.Game.Rulesets.Scoring public static bool AffectsUnstableRate(HitEvent e) => AffectsUnstableRate(e.HitObject, e.Result); public static bool AffectsUnstableRate(HitObject hitObject, HitResult result) => hitObject.HitWindows != HitWindows.Empty && result.IsHit(); + /// + /// Data type returned by which allows efficient incremental processing. + /// + /// + /// This should be passed back into future calls as a parameter. + /// + /// The optimisations used here rely on hit events being a consecutive sequence from a single gameplay session. + /// When a new gameplay session is started, any existing results should be disposed. + /// public class UnstableRateCalculationResult { + /// + /// Total events processed. For internal incremental calculation use. + /// public int EventCount; + + /// + /// Last sum-of-squares value. For internal incremental calculation use. + /// public double SumOfSquares; + + /// + /// Last mean value. For internal incremental calculation use. + /// public double Mean; - public double Result => 10.0 * Math.Sqrt(SumOfSquares / EventCount); + /// + /// The unstable rate. + /// + public double Result => EventCount == 0 ? 0 : 10.0 * Math.Sqrt(SumOfSquares / EventCount); } } } From f708466a9bc8593f18700426b8b40d23865b3899 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sun, 24 Nov 2024 23:43:31 +0900 Subject: [PATCH 0100/1275] Add test coverage --- .../Visual/Online/TestSceneChatOverlay.cs | 55 +++++++++++++++++++ .../Overlays/Chat/ChannelList/ChannelList.cs | 32 ++++++----- 2 files changed, 72 insertions(+), 15 deletions(-) diff --git a/osu.Game.Tests/Visual/Online/TestSceneChatOverlay.cs b/osu.Game.Tests/Visual/Online/TestSceneChatOverlay.cs index 3d6fe50d34..ab9ee1d8cc 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneChatOverlay.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneChatOverlay.cs @@ -457,6 +457,61 @@ namespace osu.Game.Tests.Visual.Online waitForChannel1Visible(); } + [Test] + public void TestPublicChannelsSortedByName() + { + // Intentionally join back to front. + AddStep("Show overlay with channel 2", () => + { + channelManager.CurrentChannel.Value = channelManager.JoinChannel(testChannel2); + chatOverlay.Show(); + }); + AddUntilStep("second channel is at top of list", () => getFirstVisiblePublicChannel().Channel == testChannel2); + + AddStep("Join channel 1", () => channelManager.JoinChannel(testChannel1)); + AddUntilStep("first channel is at top of list", () => getFirstVisiblePublicChannel().Channel == testChannel1); + + AddStep("message in channel 2", () => + { + testChannel2.AddNewMessages(new Message(1) { Content = "hi!", Sender = new APIUser { Username = "person" } }); + }); + AddUntilStep("first channel still at top of list", () => getFirstVisiblePublicChannel().Channel == testChannel1); + + ChannelListItem getFirstVisiblePublicChannel() => + chatOverlay.ChildrenOfType().Single().PublicChannelGroup.ItemFlow.FlowingChildren.OfType().First(item => item.Channel.Type == ChannelType.Public); + } + + [Test] + public void TestPrivateChannelsSortedByRecent() + { + Channel pmChannel1 = createPrivateChannel(); + Channel pmChannel2 = createPrivateChannel(); + + joinChannel(pmChannel1); + joinChannel(pmChannel2); + + AddStep("Show overlay", () => chatOverlay.Show()); + + AddUntilStep("first channel is at top of list", () => getFirstVisiblePMChannel().Channel == pmChannel1); + + AddStep("message in channel 2", () => + { + pmChannel2.AddNewMessages(new Message(1) { Content = "hi!", Sender = new APIUser { Username = "person" } }); + }); + + AddUntilStep("wait for first channel raised to top of list", () => getFirstVisiblePMChannel().Channel == pmChannel2); + + AddStep("message in channel 1", () => + { + pmChannel1.AddNewMessages(new Message(1) { Content = "hi!", Sender = new APIUser { Username = "person" } }); + }); + + AddUntilStep("wait for first channel raised to top of list", () => getFirstVisiblePMChannel().Channel == pmChannel1); + + ChannelListItem getFirstVisiblePMChannel() => + chatOverlay.ChildrenOfType().Single().PrivateChannelGroup.ItemFlow.FlowingChildren.OfType().First(item => item.Channel.Type == ChannelType.PM); + } + [Test] public void TestKeyboardNewChannel() { diff --git a/osu.Game/Overlays/Chat/ChannelList/ChannelList.cs b/osu.Game/Overlays/Chat/ChannelList/ChannelList.cs index a2ec385a7e..3e8c71e645 100644 --- a/osu.Game/Overlays/Chat/ChannelList/ChannelList.cs +++ b/osu.Game/Overlays/Chat/ChannelList/ChannelList.cs @@ -37,11 +37,13 @@ namespace osu.Game.Overlays.Chat.ChannelList private readonly Dictionary channelMap = new Dictionary(); + public ChannelGroup AnnounceChannelGroup { get; private set; } = null!; + public ChannelGroup PublicChannelGroup { get; private set; } = null!; + public ChannelGroup PrivateChannelGroup { get; private set; } = null!; + private OsuScrollContainer scroll = null!; private SearchContainer groupFlow = null!; - private ChannelGroup announceChannelGroup = null!; - private ChannelGroup publicChannelGroup = null!; - private ChannelGroup privateChannelGroup = null!; + private ChannelListItem selector = null!; private TextBox searchTextBox = null!; @@ -77,10 +79,10 @@ namespace osu.Game.Overlays.Chat.ChannelList RelativeSizeAxes = Axes.X, } }, - announceChannelGroup = new ChannelGroup(ChatStrings.ChannelsListTitleANNOUNCE.ToUpper(), false), - publicChannelGroup = new ChannelGroup(ChatStrings.ChannelsListTitlePUBLIC.ToUpper(), false), + AnnounceChannelGroup = new ChannelGroup(ChatStrings.ChannelsListTitleANNOUNCE.ToUpper(), false), + PublicChannelGroup = new ChannelGroup(ChatStrings.ChannelsListTitlePUBLIC.ToUpper(), false), selector = new ChannelListItem(ChannelListingChannel), - privateChannelGroup = new ChannelGroup(ChatStrings.ChannelsListTitlePM.ToUpper(), true), + PrivateChannelGroup = new ChannelGroup(ChatStrings.ChannelsListTitlePM.ToUpper(), true), }, }, }, @@ -146,28 +148,28 @@ namespace osu.Game.Overlays.Chat.ChannelList switch (channel.Type) { case ChannelType.Public: - return publicChannelGroup; + return PublicChannelGroup; case ChannelType.PM: - return privateChannelGroup; + return PrivateChannelGroup; case ChannelType.Announce: - return announceChannelGroup; + return AnnounceChannelGroup; default: - return publicChannelGroup; + return PublicChannelGroup; } } private void updateVisibility() { - if (announceChannelGroup.ItemFlow.Children.Count == 0) - announceChannelGroup.Hide(); + if (AnnounceChannelGroup.ItemFlow.Children.Count == 0) + AnnounceChannelGroup.Hide(); else - announceChannelGroup.Show(); + AnnounceChannelGroup.Show(); } - private partial class ChannelGroup : FillFlowContainer + public partial class ChannelGroup : FillFlowContainer { public readonly ChannelListItemFlow ItemFlow; @@ -207,7 +209,7 @@ namespace osu.Game.Overlays.Chat.ChannelList public void Reflow() => InvalidateLayout(); public override IEnumerable FlowingChildren => sortByRecent - ? base.FlowingChildren.OfType().OrderByDescending(i => i.Channel.LastMessageId) + ? base.FlowingChildren.OfType().OrderByDescending(i => i.Channel.LastMessageId ?? long.MinValue) : base.FlowingChildren.OfType().OrderBy(i => i.Channel.Name); } From 17347563ee5c139288f50b990a15d630c7681ea4 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 26 Nov 2024 13:06:47 +0900 Subject: [PATCH 0101/1275] Fix incorrect null handling --- osu.Game/Online/Chat/Channel.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Online/Chat/Channel.cs b/osu.Game/Online/Chat/Channel.cs index 15ce926039..9de77237b4 100644 --- a/osu.Game/Online/Chat/Channel.cs +++ b/osu.Game/Online/Chat/Channel.cs @@ -161,7 +161,7 @@ namespace osu.Game.Online.Chat Messages.AddRange(messages); long? maxMessageId = messages.Max(m => m.Id); - if (maxMessageId > LastMessageId) + if (LastMessageId == null || maxMessageId > LastMessageId) LastMessageId = maxMessageId; purgeOldMessages(); From 8585327858a00b6c15612bf29e446ccb733773d9 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 26 Nov 2024 14:08:53 +0900 Subject: [PATCH 0102/1275] Ensure `DrawableMedal` loading doesn't ever block on online resources --- .../Overlays/MedalSplash/DrawableMedal.cs | 37 ++++++++++++++++--- 1 file changed, 31 insertions(+), 6 deletions(-) diff --git a/osu.Game/Overlays/MedalSplash/DrawableMedal.cs b/osu.Game/Overlays/MedalSplash/DrawableMedal.cs index 2beed6645a..adad540c34 100644 --- a/osu.Game/Overlays/MedalSplash/DrawableMedal.cs +++ b/osu.Game/Overlays/MedalSplash/DrawableMedal.cs @@ -12,6 +12,7 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.Textures; +using osu.Framework.Logging; using osu.Game.Graphics; using osu.Game.Graphics.Containers; using osu.Game.Graphics.Sprites; @@ -28,16 +29,14 @@ namespace osu.Game.Overlays.MedalSplash [CanBeNull] public event Action StateChanged; - private readonly Medal medal; private readonly Container medalContainer; - private readonly Sprite medalSprite, medalGlow; + private readonly Sprite medalGlow; private readonly OsuSpriteText unlocked, name; private readonly TextFlowContainer description; private DisplayState state; public DrawableMedal(Medal medal) { - this.medal = medal; Position = new Vector2(0f, MedalAnimation.DISC_SIZE / 2); FillFlowContainer infoFlow; @@ -51,7 +50,7 @@ namespace osu.Game.Overlays.MedalSplash Alpha = 0f, Children = new Drawable[] { - medalSprite = new Sprite + new DelayedLoadWrapper(() => new MedalOnlineSprite(medal), 0) { Anchor = Anchor.Centre, Origin = Anchor.Centre, @@ -122,11 +121,12 @@ namespace osu.Game.Overlays.MedalSplash } [BackgroundDependencyLoader] - private void load(OsuColour colours, TextureStore textures, LargeTextureStore largeTextures) + private void load(OsuColour colours, TextureStore textures) { - medalSprite.Texture = largeTextures.Get(medal.ImageUrl); medalGlow.Texture = textures.Get(@"MedalSplash/medal-glow"); description.Colour = colours.BlueLight; + + Logger.Log("loaded"); } protected override void LoadComplete() @@ -191,6 +191,31 @@ namespace osu.Game.Overlays.MedalSplash break; } } + + private partial class MedalOnlineSprite : Sprite + { + private readonly Medal medal; + + public MedalOnlineSprite(Medal medal) + { + this.medal = medal; + } + + [BackgroundDependencyLoader] + private void load(OsuColour colours, TextureStore textures, LargeTextureStore largeTextures) + { + Texture = largeTextures.Get(medal.ImageUrl); + + Anchor = Anchor.Centre; + Origin = Anchor.Centre; + } + + protected override void LoadComplete() + { + base.LoadComplete(); + this.FadeInFromZero(150, Easing.OutQuint); + } + } } public enum DisplayState From d057dc9a95cf76f6888e6e0d8f8a60dca3705343 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 26 Nov 2024 14:13:07 +0900 Subject: [PATCH 0103/1275] Refactor `MedalOverlay` to be more readable Shouldn't really have any functionality changes, just fixing some old code that I can't easily parse these days. --- osu.Game/Overlays/MedalOverlay.cs | 78 ++++++++++++++++++++----------- 1 file changed, 50 insertions(+), 28 deletions(-) diff --git a/osu.Game/Overlays/MedalOverlay.cs b/osu.Game/Overlays/MedalOverlay.cs index 19f61cb910..7303a57cd0 100644 --- a/osu.Game/Overlays/MedalOverlay.cs +++ b/osu.Game/Overlays/MedalOverlay.cs @@ -35,7 +35,7 @@ namespace osu.Game.Overlays private IAPIProvider api { get; set; } = null!; private Container medalContainer = null!; - private MedalAnimation? lastAnimation; + private MedalAnimation? currentMedalDisplay; [BackgroundDependencyLoader] private void load() @@ -54,11 +54,7 @@ namespace osu.Game.Overlays { base.LoadComplete(); - OverlayActivationMode.BindValueChanged(val => - { - if (val.NewValue == OverlayActivation.All && (queuedMedals.Any() || medalContainer.Any() || lastAnimation?.IsLoaded == false)) - Show(); - }, true); + OverlayActivationMode.BindValueChanged(_ => displayIfReady(), true); } private void handleMedalMessages(SocketMessage obj) @@ -86,31 +82,13 @@ namespace osu.Game.Overlays queuedMedals.Enqueue(medalAnimation); Logger.Log($"Queueing medal unlock for \"{medal.Name}\" ({queuedMedals.Count} to display)"); - if (OverlayActivationMode.Value == OverlayActivation.All) - Scheduler.AddOnce(Show); - } - - protected override void Update() - { - base.Update(); - - if (medalContainer.Any() || lastAnimation?.IsLoaded == false) - return; - - if (!queuedMedals.TryDequeue(out lastAnimation)) - { - Logger.Log("All queued medals have been displayed!"); - Hide(); - return; - } - - Logger.Log($"Preparing to display \"{lastAnimation.Medal.Name}\""); - LoadComponentAsync(lastAnimation, medalContainer.Add); + Schedule(displayIfReady); } protected override bool OnClick(ClickEvent e) { - lastAnimation?.Dismiss(); + dismissDisplayedMedal(); + loadNextMedal(); return true; } @@ -118,13 +96,57 @@ namespace osu.Game.Overlays { if (e.Action == GlobalAction.Back) { - lastAnimation?.Dismiss(); + dismissDisplayedMedal(); + loadNextMedal(); return true; } return base.OnPressed(e); } + private void dismissDisplayedMedal() + { + if (currentMedalDisplay?.IsLoaded == false) + return; + + currentMedalDisplay?.Dismiss(); + currentMedalDisplay = null; + } + + private void displayIfReady() + { + if (OverlayActivationMode.Value != OverlayActivation.All) + return; + + if (currentMedalDisplay != null) + { + Show(); + return; + } + + if (queuedMedals.Any()) + { + Show(); + loadNextMedal(); + } + } + + private void loadNextMedal() + { + if (currentMedalDisplay != null) + return; + + if (!queuedMedals.TryDequeue(out currentMedalDisplay)) + { + Logger.Log("All queued medals have been displayed!"); + Hide(); + return; + } + + Logger.Log($"Preparing to display \"{currentMedalDisplay.Medal.Name}\""); + LoadComponentAsync(currentMedalDisplay, m => medalContainer.Add(m)); + } + protected override void Dispose(bool isDisposing) { base.Dispose(isDisposing); From 672dbe6e03bd95971f46ace7685d91cd75729bb8 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 26 Nov 2024 14:42:30 +0900 Subject: [PATCH 0104/1275] Better control of show/hide of overlay --- osu.Game/Overlays/MedalOverlay.cs | 55 +++++++++++++++++-------------- 1 file changed, 31 insertions(+), 24 deletions(-) diff --git a/osu.Game/Overlays/MedalOverlay.cs b/osu.Game/Overlays/MedalOverlay.cs index 7303a57cd0..b7e68fd557 100644 --- a/osu.Game/Overlays/MedalOverlay.cs +++ b/osu.Game/Overlays/MedalOverlay.cs @@ -57,6 +57,11 @@ namespace osu.Game.Overlays OverlayActivationMode.BindValueChanged(_ => displayIfReady(), true); } + public override void Hide() + { + // don't allow hiding the overlay via any method other than our own. + } + private void handleMedalMessages(SocketMessage obj) { if (obj.Event != @"new") @@ -87,8 +92,7 @@ namespace osu.Game.Overlays protected override bool OnClick(ClickEvent e) { - dismissDisplayedMedal(); - loadNextMedal(); + progressDisplayByUser(); return true; } @@ -96,21 +100,31 @@ namespace osu.Game.Overlays { if (e.Action == GlobalAction.Back) { - dismissDisplayedMedal(); - loadNextMedal(); + progressDisplayByUser(); return true; } return base.OnPressed(e); } - private void dismissDisplayedMedal() + private void progressDisplayByUser() { + // For now, we want to make sure that medals are definitely seen by the user. + // So we block exiting the overlay until the load of the active medal completes. if (currentMedalDisplay?.IsLoaded == false) return; currentMedalDisplay?.Dismiss(); currentMedalDisplay = null; + + if (!queuedMedals.Any()) + { + Logger.Log("All queued medals have been displayed, hiding overlay!"); + base.Hide(); + return; + } + + showNextMedal(); } private void displayIfReady() @@ -118,33 +132,26 @@ namespace osu.Game.Overlays if (OverlayActivationMode.Value != OverlayActivation.All) return; - if (currentMedalDisplay != null) - { - Show(); - return; - } - - if (queuedMedals.Any()) - { - Show(); - loadNextMedal(); - } + if (currentMedalDisplay != null || queuedMedals.Any()) + showNextMedal(); } - private void loadNextMedal() + private void showNextMedal() { + // A medal is already loading / loaded, so just ensure the overlay is visible. if (currentMedalDisplay != null) - return; - - if (!queuedMedals.TryDequeue(out currentMedalDisplay)) { - Logger.Log("All queued medals have been displayed!"); - Hide(); + Show(); return; } - Logger.Log($"Preparing to display \"{currentMedalDisplay.Medal.Name}\""); - LoadComponentAsync(currentMedalDisplay, m => medalContainer.Add(m)); + if (queuedMedals.TryDequeue(out currentMedalDisplay)) + { + Logger.Log($"Preparing to display \"{currentMedalDisplay.Medal.Name}\""); + + Show(); + LoadComponentAsync(currentMedalDisplay, m => medalContainer.Add(m)); + } } protected override void Dispose(bool isDisposing) From e8fae85e8d5b0077b9825a650180b89511c38d87 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 26 Nov 2024 14:45:40 +0900 Subject: [PATCH 0105/1275] Fix hidden dissmissing logic --- osu.Game/Overlays/MedalAnimation.cs | 5 +++-- osu.Game/Overlays/MedalOverlay.cs | 5 ++++- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/osu.Game/Overlays/MedalAnimation.cs b/osu.Game/Overlays/MedalAnimation.cs index daceeedf47..fdca0b2cc7 100644 --- a/osu.Game/Overlays/MedalAnimation.cs +++ b/osu.Game/Overlays/MedalAnimation.cs @@ -245,18 +245,19 @@ namespace osu.Game.Overlays this.FadeOut(200); } - public void Dismiss() + public bool Dismiss() { if (drawableMedal != null && drawableMedal.State != DisplayState.Full) { // if we haven't yet, play out the animation fully drawableMedal.State = DisplayState.Full; FinishTransforms(true); - return; + return false; } Hide(); Expire(); + return true; } private partial class BackgroundStrip : Container diff --git a/osu.Game/Overlays/MedalOverlay.cs b/osu.Game/Overlays/MedalOverlay.cs index b7e68fd557..736f744429 100644 --- a/osu.Game/Overlays/MedalOverlay.cs +++ b/osu.Game/Overlays/MedalOverlay.cs @@ -114,7 +114,10 @@ namespace osu.Game.Overlays if (currentMedalDisplay?.IsLoaded == false) return; - currentMedalDisplay?.Dismiss(); + // Dismissing may sometimes play out the medal animation rather than immediately dismissing. + if (currentMedalDisplay?.Dismiss() == false) + return; + currentMedalDisplay = null; if (!queuedMedals.Any()) From d150aeef2b019ecf1cad5f4e3f5b16ea1473297b Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Tue, 26 Nov 2024 01:01:59 -0500 Subject: [PATCH 0106/1275] Use score-based endpoint everywhere --- .../TestScenePlaylistsResultsScreen.cs | 4 ++-- .../Rooms/ShowPlaylistUserScoreRequest.cs | 23 ------------------- .../PlaylistItemUserResultsScreen.cs | 4 ++-- 3 files changed, 4 insertions(+), 27 deletions(-) delete mode 100644 osu.Game/Online/Rooms/ShowPlaylistUserScoreRequest.cs diff --git a/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsResultsScreen.cs b/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsResultsScreen.cs index 5977e67b0e..6ccbcd2859 100644 --- a/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsResultsScreen.cs +++ b/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsResultsScreen.cs @@ -231,7 +231,7 @@ namespace osu.Game.Tests.Visual.Playlists // pre-check for requests we should be handling (as they are scheduled below). switch (request) { - case ShowPlaylistUserScoreRequest: + case ShowPlaylistScoreRequest: case IndexPlaylistScoresRequest: break; @@ -253,7 +253,7 @@ namespace osu.Game.Tests.Visual.Playlists switch (request) { - case ShowPlaylistUserScoreRequest s: + case ShowPlaylistScoreRequest s: if (userScore == null) triggerFail(s); else diff --git a/osu.Game/Online/Rooms/ShowPlaylistUserScoreRequest.cs b/osu.Game/Online/Rooms/ShowPlaylistUserScoreRequest.cs deleted file mode 100644 index 8e6a1ac7c7..0000000000 --- a/osu.Game/Online/Rooms/ShowPlaylistUserScoreRequest.cs +++ /dev/null @@ -1,23 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -using osu.Game.Online.API; - -namespace osu.Game.Online.Rooms -{ - public class ShowPlaylistUserScoreRequest : APIRequest - { - private readonly long roomId; - private readonly long playlistItemId; - private readonly long userId; - - public ShowPlaylistUserScoreRequest(long roomId, long playlistItemId, long userId) - { - this.roomId = roomId; - this.playlistItemId = playlistItemId; - this.userId = userId; - } - - protected override string Target => $"rooms/{roomId}/playlist/{playlistItemId}/scores/users/{userId}"; - } -} diff --git a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistItemUserResultsScreen.cs b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistItemUserResultsScreen.cs index e038cf3288..988331e213 100644 --- a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistItemUserResultsScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistItemUserResultsScreen.cs @@ -11,7 +11,7 @@ using osu.Game.Scoring; namespace osu.Game.Screens.OnlinePlay.Playlists { /// - /// Shows the user's best score for a given playlist item, with scores around included. + /// Shows the user's submitted score in a given playlist item, with scores around included. /// public partial class PlaylistItemUserResultsScreen : PlaylistItemResultsScreen { @@ -20,7 +20,7 @@ namespace osu.Game.Screens.OnlinePlay.Playlists { } - protected override APIRequest CreateScoreRequest() => new ShowPlaylistUserScoreRequest(RoomId, PlaylistItem.ID, API.LocalUser.Value.Id); + protected override APIRequest CreateScoreRequest() => new ShowPlaylistScoreRequest(RoomId, PlaylistItem.ID, Score?.OnlineID ?? -1); protected override ScoreInfo[] PerformSuccessCallback(Action> callback, List scores, MultiplayerScores? pivot = null) { From c1416f9920e454812bf78e8e9d770dade16de1d9 Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Tue, 26 Nov 2024 01:10:12 -0500 Subject: [PATCH 0107/1275] Bring back user-based endpoint for viewing result screen from playlists lounge --- .../Rooms/ShowPlaylistUserScoreRequest.cs | 23 +++++++++++++++++++ .../PlaylistItemUserResultsScreen.cs | 4 +++- 2 files changed, 26 insertions(+), 1 deletion(-) create mode 100644 osu.Game/Online/Rooms/ShowPlaylistUserScoreRequest.cs diff --git a/osu.Game/Online/Rooms/ShowPlaylistUserScoreRequest.cs b/osu.Game/Online/Rooms/ShowPlaylistUserScoreRequest.cs new file mode 100644 index 0000000000..8e6a1ac7c7 --- /dev/null +++ b/osu.Game/Online/Rooms/ShowPlaylistUserScoreRequest.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.Game.Online.API; + +namespace osu.Game.Online.Rooms +{ + public class ShowPlaylistUserScoreRequest : APIRequest + { + private readonly long roomId; + private readonly long playlistItemId; + private readonly long userId; + + public ShowPlaylistUserScoreRequest(long roomId, long playlistItemId, long userId) + { + this.roomId = roomId; + this.playlistItemId = playlistItemId; + this.userId = userId; + } + + protected override string Target => $"rooms/{roomId}/playlist/{playlistItemId}/scores/users/{userId}"; + } +} diff --git a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistItemUserResultsScreen.cs b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistItemUserResultsScreen.cs index 988331e213..b659a98802 100644 --- a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistItemUserResultsScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistItemUserResultsScreen.cs @@ -20,7 +20,9 @@ namespace osu.Game.Screens.OnlinePlay.Playlists { } - protected override APIRequest CreateScoreRequest() => new ShowPlaylistScoreRequest(RoomId, PlaylistItem.ID, Score?.OnlineID ?? -1); + protected override APIRequest CreateScoreRequest() => Score == null + ? new ShowPlaylistScoreRequest(RoomId, PlaylistItem.ID, Score?.OnlineID ?? -1) + : new ShowPlaylistUserScoreRequest(RoomId, PlaylistItem.ID, API.LocalUser.Value.Id); protected override ScoreInfo[] PerformSuccessCallback(Action> callback, List scores, MultiplayerScores? pivot = null) { From 7201bac60d88b33177a16a596177f431e9b06192 Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Tue, 26 Nov 2024 01:10:19 -0500 Subject: [PATCH 0108/1275] Remove `DailyChallengePlayer` --- .../DailyChallenge/DailyChallenge.cs | 2 +- .../DailyChallenge/DailyChallengePlayer.cs | 41 ------------------- 2 files changed, 1 insertion(+), 42 deletions(-) delete mode 100644 osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallengePlayer.cs diff --git a/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallenge.cs b/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallenge.cs index 6cb8a87a2a..0dc7e7930a 100644 --- a/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallenge.cs +++ b/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallenge.cs @@ -532,7 +532,7 @@ namespace osu.Game.Screens.OnlinePlay.DailyChallenge private void startPlay() { sampleStart?.Play(); - this.Push(new PlayerLoader(() => new DailyChallengePlayer(room, playlistItem) + this.Push(new PlayerLoader(() => new PlaylistsPlayer(room, playlistItem) { Exited = () => Scheduler.AddOnce(() => leaderboard.RefetchScores()) })); diff --git a/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallengePlayer.cs b/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallengePlayer.cs deleted file mode 100644 index cfc0898e5a..0000000000 --- a/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallengePlayer.cs +++ /dev/null @@ -1,41 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -using System.Diagnostics; -using osu.Game.Online.Rooms; -using osu.Game.Scoring; -using osu.Game.Screens.OnlinePlay.Playlists; -using osu.Game.Screens.Play; -using osu.Game.Screens.Ranking; - -namespace osu.Game.Screens.OnlinePlay.DailyChallenge -{ - public partial class DailyChallengePlayer : PlaylistsPlayer - { - public DailyChallengePlayer(Room room, PlaylistItem playlistItem, PlayerConfiguration? configuration = null) - : base(room, playlistItem, configuration) - { - } - - protected override ResultsScreen CreateResults(ScoreInfo score) - { - Debug.Assert(Room.RoomID != null); - - if (score.OnlineID >= 0) - { - return new PlaylistItemScoreResultsScreen(Room.RoomID.Value, PlaylistItem, score.OnlineID) - { - AllowRetry = true, - ShowUserStatistics = true, - }; - } - - // If the score has failed submission, fall back to displaying scores from user's highest. - return new PlaylistItemUserResultsScreen(score, Room.RoomID.Value, PlaylistItem) - { - AllowRetry = true, - ShowUserStatistics = true, - }; - } - } -} From e3ea38a366601df01f045f4f111765c34d041145 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 26 Nov 2024 15:12:38 +0900 Subject: [PATCH 0109/1275] Add setting to allow hold-for-pause to still exist Users have asked for this multiple times since last release. Not sure on the best default value, but I'm going with the stable/classic one, at least for the initial release to avoid needing migrations. In the future we may reconsider this for new users. --- osu.Game/Configuration/OsuConfigManager.cs | 2 ++ osu.Game/Localisation/GameplaySettingsStrings.cs | 5 +++++ .../Overlays/Settings/Sections/Gameplay/HUDSettings.cs | 5 +++++ osu.Game/Screens/Play/HUD/HoldForMenuButton.cs | 10 +++++++--- 4 files changed, 19 insertions(+), 3 deletions(-) diff --git a/osu.Game/Configuration/OsuConfigManager.cs b/osu.Game/Configuration/OsuConfigManager.cs index 362c06849d..33d99e9b0f 100644 --- a/osu.Game/Configuration/OsuConfigManager.cs +++ b/osu.Game/Configuration/OsuConfigManager.cs @@ -214,6 +214,7 @@ namespace osu.Game.Configuration SetDefault(OsuSetting.EditorContractSidebars, false); SetDefault(OsuSetting.AlwaysShowHoldForMenuButton, false); + SetDefault(OsuSetting.AlwaysRequireHoldingForPause, false); } protected override bool CheckLookupContainsPrivateInformation(OsuSetting lookup) @@ -444,5 +445,6 @@ namespace osu.Game.Configuration EditorRotationOrigin, EditorTimelineShowBreaks, EditorAdjustExistingObjectsOnTimingChanges, + AlwaysRequireHoldingForPause } } diff --git a/osu.Game/Localisation/GameplaySettingsStrings.cs b/osu.Game/Localisation/GameplaySettingsStrings.cs index 6de61f7ebe..ff6a6102a7 100644 --- a/osu.Game/Localisation/GameplaySettingsStrings.cs +++ b/osu.Game/Localisation/GameplaySettingsStrings.cs @@ -89,6 +89,11 @@ namespace osu.Game.Localisation /// public static LocalisableString AlwaysShowHoldForMenuButton => new TranslatableString(getKey(@"always_show_hold_for_menu_button"), @"Always show hold for menu button"); + /// + /// "Require holding key to pause gameplay" + /// + public static LocalisableString AlwaysRequireHoldForMenu => new TranslatableString(getKey(@"require_holding_key_to_pause_gameplay"), @"Require holding key to pause gameplay"); + /// /// "Always play first combo break sound" /// diff --git a/osu.Game/Overlays/Settings/Sections/Gameplay/HUDSettings.cs b/osu.Game/Overlays/Settings/Sections/Gameplay/HUDSettings.cs index f4dd319152..b4caaf7983 100644 --- a/osu.Game/Overlays/Settings/Sections/Gameplay/HUDSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/Gameplay/HUDSettings.cs @@ -41,6 +41,11 @@ namespace osu.Game.Overlays.Settings.Sections.Gameplay Current = config.GetBindable(OsuSetting.GameplayLeaderboard), }, new SettingsCheckbox + { + LabelText = GameplaySettingsStrings.AlwaysRequireHoldForMenu, + Current = config.GetBindable(OsuSetting.AlwaysRequireHoldingForPause), + }, + new SettingsCheckbox { LabelText = GameplaySettingsStrings.AlwaysShowHoldForMenuButton, Current = config.GetBindable(OsuSetting.AlwaysShowHoldForMenuButton), diff --git a/osu.Game/Screens/Play/HUD/HoldForMenuButton.cs b/osu.Game/Screens/Play/HUD/HoldForMenuButton.cs index 5d3d5774d0..96e937fda7 100644 --- a/osu.Game/Screens/Play/HUD/HoldForMenuButton.cs +++ b/osu.Game/Screens/Play/HUD/HoldForMenuButton.cs @@ -162,14 +162,18 @@ namespace osu.Game.Screens.Play.HUD private bool pendingAnimation; private ScheduledDelegate shakeOperation; + private Bindable alwaysRequireHold; + public HoldButton(bool isDangerousAction) : base(isDangerousAction) { } [BackgroundDependencyLoader] - private void load(OsuColour colours) + private void load(OsuColour colours, OsuConfigManager config) { + alwaysRequireHold = config.GetBindable(OsuSetting.AlwaysRequireHoldingForPause); + Size = new Vector2(60); Child = new CircularContainer @@ -300,7 +304,7 @@ namespace osu.Game.Screens.Play.HUD case GlobalAction.Back: if (!pendingAnimation) { - if (IsDangerousAction) + if (IsDangerousAction || alwaysRequireHold.Value) BeginConfirm(); else Confirm(); @@ -314,7 +318,7 @@ namespace osu.Game.Screens.Play.HUD if (!pendingAnimation) { - if (IsDangerousAction) + if (IsDangerousAction || alwaysRequireHold.Value) BeginConfirm(); else Confirm(); From b76460f1003a269d2893d2466a71dd8e8a4a3201 Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Tue, 26 Nov 2024 01:26:44 -0500 Subject: [PATCH 0110/1275] Schedule the thing Queuing up requests on change to `api.LocalUser` is bad because the API state is updated after `LocalUser` is updated, therefore we have to schhhhhedullllllllleeeeeeeeeeeeeeee. --- osu.Game/Online/LocalUserStatisticsProvider.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Online/LocalUserStatisticsProvider.cs b/osu.Game/Online/LocalUserStatisticsProvider.cs index 79122b4186..312b80e18a 100644 --- a/osu.Game/Online/LocalUserStatisticsProvider.cs +++ b/osu.Game/Online/LocalUserStatisticsProvider.cs @@ -50,7 +50,7 @@ namespace osu.Game.Online api.LocalUser.BindValueChanged(_ => initialiseStatistics(), true); } - private void initialiseStatistics() + private void initialiseStatistics() => Schedule(() => { statisticsCache.Clear(); @@ -59,7 +59,7 @@ namespace osu.Game.Online foreach (var ruleset in rulesets.AvailableRulesets.Where(r => r.IsLegacyRuleset())) RefetchStatistics(ruleset); - } + }); public void RefetchStatistics(RulesetInfo ruleset, Action? callback = null) { From 42c68ba43ee1f4a0964406425c0497d603d124cc Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Tue, 26 Nov 2024 01:28:58 -0500 Subject: [PATCH 0111/1275] Add inline comment --- osu.Game/Online/LocalUserStatisticsProvider.cs | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/osu.Game/Online/LocalUserStatisticsProvider.cs b/osu.Game/Online/LocalUserStatisticsProvider.cs index 312b80e18a..22d5788c87 100644 --- a/osu.Game/Online/LocalUserStatisticsProvider.cs +++ b/osu.Game/Online/LocalUserStatisticsProvider.cs @@ -47,10 +47,16 @@ namespace osu.Game.Online protected override void LoadComplete() { base.LoadComplete(); - api.LocalUser.BindValueChanged(_ => initialiseStatistics(), true); + + api.LocalUser.BindValueChanged(_ => + { + // queuing up requests directly on user change is unsafe, as the API status may have not been updated yet. + // schedule a frame to allow the API to be in its correct state sending requests. + Schedule(initialiseStatistics); + }, true); } - private void initialiseStatistics() => Schedule(() => + private void initialiseStatistics() { statisticsCache.Clear(); @@ -59,7 +65,7 @@ namespace osu.Game.Online foreach (var ruleset in rulesets.AvailableRulesets.Where(r => r.IsLegacyRuleset())) RefetchStatistics(ruleset); - }); + } public void RefetchStatistics(RulesetInfo ruleset, Action? callback = null) { From 1e6c04e98b092e52a35b20d07e9a5a67e61de1b4 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 26 Nov 2024 16:05:04 +0900 Subject: [PATCH 0112/1275] Remove debug logging --- osu.Game/Overlays/MedalSplash/DrawableMedal.cs | 3 --- 1 file changed, 3 deletions(-) diff --git a/osu.Game/Overlays/MedalSplash/DrawableMedal.cs b/osu.Game/Overlays/MedalSplash/DrawableMedal.cs index adad540c34..460239f620 100644 --- a/osu.Game/Overlays/MedalSplash/DrawableMedal.cs +++ b/osu.Game/Overlays/MedalSplash/DrawableMedal.cs @@ -12,7 +12,6 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.Textures; -using osu.Framework.Logging; using osu.Game.Graphics; using osu.Game.Graphics.Containers; using osu.Game.Graphics.Sprites; @@ -125,8 +124,6 @@ namespace osu.Game.Overlays.MedalSplash { medalGlow.Texture = textures.Get(@"MedalSplash/medal-glow"); description.Colour = colours.BlueLight; - - Logger.Log("loaded"); } protected override void LoadComplete() From e0199386a38ef6581a8e78168e7080b1b34f9b0c Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 26 Nov 2024 17:33:39 +0900 Subject: [PATCH 0113/1275] Add failing test case showing changing selection in editor affects samples --- .../TestSceneHitObjectSampleAdjustments.cs | 101 ++++++++++++------ 1 file changed, 67 insertions(+), 34 deletions(-) diff --git a/osu.Game.Tests/Visual/Editing/TestSceneHitObjectSampleAdjustments.cs b/osu.Game.Tests/Visual/Editing/TestSceneHitObjectSampleAdjustments.cs index 5cc1e64197..ae814173a1 100644 --- a/osu.Game.Tests/Visual/Editing/TestSceneHitObjectSampleAdjustments.cs +++ b/osu.Game.Tests/Visual/Editing/TestSceneHitObjectSampleAdjustments.cs @@ -527,8 +527,11 @@ namespace osu.Game.Tests.Visual.Editing checkPlacementSampleBank(HitSampleInfo.BANK_NORMAL); checkPlacementSampleAdditionBank(HitSampleInfo.BANK_NORMAL); - void checkPlacementSampleBank(string expected) => AddAssert($"Placement sample is {expected}", () => EditorBeatmap.PlacementObject.Value.Samples.First(s => s.Name == HitSampleInfo.HIT_NORMAL).Bank, () => Is.EqualTo(expected)); - void checkPlacementSampleAdditionBank(string expected) => AddAssert($"Placement sample addition is {expected}", () => EditorBeatmap.PlacementObject.Value.Samples.First(s => s.Name != HitSampleInfo.HIT_NORMAL).Bank, () => Is.EqualTo(expected)); + void checkPlacementSampleBank(string expected) => AddAssert($"Placement sample is {expected}", + () => EditorBeatmap.PlacementObject.Value.Samples.First(s => s.Name == HitSampleInfo.HIT_NORMAL).Bank, () => Is.EqualTo(expected)); + + void checkPlacementSampleAdditionBank(string expected) => AddAssert($"Placement sample addition is {expected}", + () => EditorBeatmap.PlacementObject.Value.Samples.First(s => s.Name != HitSampleInfo.HIT_NORMAL).Bank, () => Is.EqualTo(expected)); } [Test] @@ -774,6 +777,7 @@ namespace osu.Game.Tests.Visual.Editing } [Test] + [Solo] public void TestSelectingObjectDoesNotMutateSamples() { clickSamplePiece(0); @@ -781,15 +785,39 @@ namespace osu.Game.Tests.Visual.Editing setAdditionBankViaPopover(HitSampleInfo.BANK_SOFT); dismissPopover(); - hitObjectHasSamples(0, HitSampleInfo.HIT_NORMAL, HitSampleInfo.HIT_FINISH); - hitObjectHasSampleNormalBank(0, HitSampleInfo.BANK_NORMAL); - hitObjectHasSampleAdditionBank(0, HitSampleInfo.BANK_SOFT); + assertNoChanges(); - AddStep("select first object", () => EditorBeatmap.SelectedHitObjects.Add(EditorBeatmap.HitObjects[0])); + AddStep("select first object", () => + { + EditorBeatmap.SelectedHitObjects.Clear(); + EditorBeatmap.SelectedHitObjects.Add(EditorBeatmap.HitObjects[0]); + }); + assertNoChanges(); - hitObjectHasSamples(0, HitSampleInfo.HIT_NORMAL, HitSampleInfo.HIT_FINISH); - hitObjectHasSampleNormalBank(0, HitSampleInfo.BANK_NORMAL); - hitObjectHasSampleAdditionBank(0, HitSampleInfo.BANK_SOFT); + AddStep("select second object", () => + { + EditorBeatmap.SelectedHitObjects.Clear(); + EditorBeatmap.SelectedHitObjects.Add(EditorBeatmap.HitObjects[1]); + }); + assertNoChanges(); + + AddStep("select first object", () => + { + EditorBeatmap.SelectedHitObjects.Clear(); + EditorBeatmap.SelectedHitObjects.Add(EditorBeatmap.HitObjects[0]); + }); + assertNoChanges(); + + void assertNoChanges() + { + hitObjectHasSamples(0, HitSampleInfo.HIT_NORMAL, HitSampleInfo.HIT_FINISH); + hitObjectHasSampleNormalBank(0, HitSampleInfo.BANK_NORMAL); + hitObjectHasSampleAdditionBank(0, HitSampleInfo.BANK_SOFT); + + hitObjectHasSamples(1, HitSampleInfo.HIT_NORMAL); + hitObjectHasSampleNormalBank(1, HitSampleInfo.BANK_SOFT); + hitObjectHasSampleAdditionBank(1, HitSampleInfo.BANK_SOFT); + } } private void clickSamplePiece(int objectIndex) => AddStep($"click {objectIndex.ToOrdinalWords()} sample piece", () => @@ -883,11 +911,12 @@ namespace osu.Game.Tests.Visual.Editing return h.Samples.All(o => o.Volume == volume); }); - private void hitObjectNodeHasSampleVolume(int objectIndex, int nodeIndex, int volume) => AddAssert($"{objectIndex.ToOrdinalWords()} object {nodeIndex.ToOrdinalWords()} node has volume {volume}", () => - { - var h = EditorBeatmap.HitObjects.ElementAt(objectIndex) as IHasRepeats; - return h is not null && h.NodeSamples[nodeIndex].All(o => o.Volume == volume); - }); + private void hitObjectNodeHasSampleVolume(int objectIndex, int nodeIndex, int volume) => AddAssert( + $"{objectIndex.ToOrdinalWords()} object {nodeIndex.ToOrdinalWords()} node has volume {volume}", () => + { + var h = EditorBeatmap.HitObjects.ElementAt(objectIndex) as IHasRepeats; + return h is not null && h.NodeSamples[nodeIndex].All(o => o.Volume == volume); + }); private void setBankViaPopover(string bank) => AddStep($"set bank {bank} via popover", () => { @@ -944,29 +973,33 @@ namespace osu.Game.Tests.Visual.Editing return h.Samples.Where(o => o.Name != HitSampleInfo.HIT_NORMAL).All(o => o.Bank == bank); }); - private void hitObjectNodeHasSamples(int objectIndex, int nodeIndex, params string[] samples) => AddAssert($"{objectIndex.ToOrdinalWords()} object {nodeIndex.ToOrdinalWords()} node has samples {string.Join(',', samples)}", () => - { - var h = EditorBeatmap.HitObjects.ElementAt(objectIndex) as IHasRepeats; - return h is not null && h.NodeSamples[nodeIndex].Select(s => s.Name).SequenceEqual(samples); - }); + private void hitObjectNodeHasSamples(int objectIndex, int nodeIndex, params string[] samples) => AddAssert( + $"{objectIndex.ToOrdinalWords()} object {nodeIndex.ToOrdinalWords()} node has samples {string.Join(',', samples)}", () => + { + var h = EditorBeatmap.HitObjects.ElementAt(objectIndex) as IHasRepeats; + return h is not null && h.NodeSamples[nodeIndex].Select(s => s.Name).SequenceEqual(samples); + }); - private void hitObjectNodeHasSampleBank(int objectIndex, int nodeIndex, string bank) => AddAssert($"{objectIndex.ToOrdinalWords()} object {nodeIndex.ToOrdinalWords()} node has bank {bank}", () => - { - var h = EditorBeatmap.HitObjects.ElementAt(objectIndex) as IHasRepeats; - return h is not null && h.NodeSamples[nodeIndex].All(o => o.Bank == bank); - }); + private void hitObjectNodeHasSampleBank(int objectIndex, int nodeIndex, string bank) => AddAssert($"{objectIndex.ToOrdinalWords()} object {nodeIndex.ToOrdinalWords()} node has bank {bank}", + () => + { + var h = EditorBeatmap.HitObjects.ElementAt(objectIndex) as IHasRepeats; + return h is not null && h.NodeSamples[nodeIndex].All(o => o.Bank == bank); + }); - private void hitObjectNodeHasSampleNormalBank(int objectIndex, int nodeIndex, string bank) => AddAssert($"{objectIndex.ToOrdinalWords()} object {nodeIndex.ToOrdinalWords()} node has normal bank {bank}", () => - { - var h = EditorBeatmap.HitObjects.ElementAt(objectIndex) as IHasRepeats; - return h is not null && h.NodeSamples[nodeIndex].Where(o => o.Name == HitSampleInfo.HIT_NORMAL).All(o => o.Bank == bank); - }); + private void hitObjectNodeHasSampleNormalBank(int objectIndex, int nodeIndex, string bank) => AddAssert( + $"{objectIndex.ToOrdinalWords()} object {nodeIndex.ToOrdinalWords()} node has normal bank {bank}", () => + { + var h = EditorBeatmap.HitObjects.ElementAt(objectIndex) as IHasRepeats; + return h is not null && h.NodeSamples[nodeIndex].Where(o => o.Name == HitSampleInfo.HIT_NORMAL).All(o => o.Bank == bank); + }); - private void hitObjectNodeHasSampleAdditionBank(int objectIndex, int nodeIndex, string bank) => AddAssert($"{objectIndex.ToOrdinalWords()} object {nodeIndex.ToOrdinalWords()} node has addition bank {bank}", () => - { - var h = EditorBeatmap.HitObjects.ElementAt(objectIndex) as IHasRepeats; - return h is not null && h.NodeSamples[nodeIndex].Where(o => o.Name != HitSampleInfo.HIT_NORMAL).All(o => o.Bank == bank); - }); + private void hitObjectNodeHasSampleAdditionBank(int objectIndex, int nodeIndex, string bank) => AddAssert( + $"{objectIndex.ToOrdinalWords()} object {nodeIndex.ToOrdinalWords()} node has addition bank {bank}", () => + { + var h = EditorBeatmap.HitObjects.ElementAt(objectIndex) as IHasRepeats; + return h is not null && h.NodeSamples[nodeIndex].Where(o => o.Name != HitSampleInfo.HIT_NORMAL).All(o => o.Bank == bank); + }); private void editorTimeIs(double time) => AddAssert($"editor time is {time}", () => Precision.AlmostEquals(EditorClock.CurrentTimeAccurate, time, 1)); } From 3ecb3b674d5e519b110546802ff14d6a3d248dc1 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 26 Nov 2024 16:04:37 +0900 Subject: [PATCH 0114/1275] Don't reset state when changing from one selection to another in the editor This was causing state pollution in the new selection. I can't see why this needs to happen when a selection changes to another. This fixes https://github.com/ppy/osu/issues/30839 and also the same issue happening for the new combo toggle. Tests all seem to pass, and I can't immediately find anything broken, but YMMV. --- .../Screens/Edit/Compose/Components/EditorSelectionHandler.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/osu.Game/Screens/Edit/Compose/Components/EditorSelectionHandler.cs b/osu.Game/Screens/Edit/Compose/Components/EditorSelectionHandler.cs index 6724a1dc4d..78cee2c1cf 100644 --- a/osu.Game/Screens/Edit/Compose/Components/EditorSelectionHandler.cs +++ b/osu.Game/Screens/Edit/Compose/Components/EditorSelectionHandler.cs @@ -258,6 +258,9 @@ namespace osu.Game.Screens.Edit.Compose.Components private void resetTernaryStates() { + if (SelectedItems.Count > 0) + return; + SelectionNewComboState.Value = TernaryState.False; AutoSelectionBankEnabled.Value = true; SelectionAdditionBanksEnabled.Value = true; From 41c309fb7220c830ab646c5a2d63c71c063e5cc8 Mon Sep 17 00:00:00 2001 From: "tsrk." Date: Tue, 26 Nov 2024 09:35:18 +0100 Subject: [PATCH 0115/1275] chore(MenuTip): update text according to recent changes Signed-off-by: tsrk. --- osu.Game/Localisation/MenuTipStrings.cs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/osu.Game/Localisation/MenuTipStrings.cs b/osu.Game/Localisation/MenuTipStrings.cs index e955040f37..b8a00a1c17 100644 --- a/osu.Game/Localisation/MenuTipStrings.cs +++ b/osu.Game/Localisation/MenuTipStrings.cs @@ -15,9 +15,9 @@ namespace osu.Game.Localisation public static LocalisableString ToggleToolbarShortcut => new TranslatableString(getKey(@"toggle_toolbar_shortcut"), @"Press Ctrl-T anywhere in the game to toggle the toolbar!"); /// - /// "Press Ctrl-O anywhere in the game to access options!" + /// "Press Ctrl-O anywhere in the game to access settings!" /// - public static LocalisableString GameSettingsShortcut => new TranslatableString(getKey(@"game_settings_shortcut"), @"Press Ctrl-O anywhere in the game to access options!"); + public static LocalisableString GameSettingsShortcut => new TranslatableString(getKey(@"game_settings_shortcut"), @"Press Ctrl-O anywhere in the game to access settings!"); /// /// "All settings are dynamic and take effect in real-time. Try changing the skin while watching autoplay!" @@ -85,9 +85,9 @@ namespace osu.Game.Localisation public static LocalisableString ConfigurableHotkeys => new TranslatableString(getKey(@"configurable_hotkeys"), @"Most of the hotkeys in the game are configurable and can be changed to anything you want. Check the bindings panel under input settings!"); /// - /// "Your gameplay HUD can be customized by using the skin layout editor. Open it at any time via Ctrl-Shift-S!" + /// "Your gameplay HUD can be customised by using the skin layout editor. Open it at any time via Ctrl-Shift-S!" /// - public static LocalisableString SkinEditor => new TranslatableString(getKey(@"skin_editor"), @"Your gameplay HUD can be customized by using the skin layout editor. Open it at any time via Ctrl-Shift-S!"); + public static LocalisableString SkinEditor => new TranslatableString(getKey(@"skin_editor"), @"Your gameplay HUD can be customised by using the skin layout editor. Open it at any time via Ctrl-Shift-S!"); /// /// "You can create mod presets to make toggling your favorite mod combinations easier!" @@ -95,9 +95,9 @@ namespace osu.Game.Localisation public static LocalisableString ModPresets => new TranslatableString(getKey(@"mod_presets"), @"You can create mod presets to make toggling your favorite mod combinations easier!"); /// - /// "Many mods have customisation settings that drastically change how they function. Click the Mod Customisation button in mod select to view settings!" + /// "Many mods have customisation settings that drastically change how they function. Click the Customise button in mod select to view settings!" /// - public static LocalisableString ModCustomisationSettings => new TranslatableString(getKey(@"mod_customisation_settings"), @"Many mods have customisation settings that drastically change how they function. Click the Mod Customisation button in mod select to view settings!"); + public static LocalisableString ModCustomisationSettings => new TranslatableString(getKey(@"mod_customisation_settings"), @"Many mods have customisation settings that drastically change how they function. Click the Customise button in mod select to view settings!"); /// /// "Press Ctrl-Shift-R to switch to a random skin!" From 98044c108e7f7e9e8723c61ff1ca3823a95feaeb Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 26 Nov 2024 17:41:00 +0900 Subject: [PATCH 0116/1275] Revert "Ensure `DrawableMedal` loading doesn't ever block on online resources" This reverts commit 8585327858a00b6c15612bf29e446ccb733773d9. --- .../Overlays/MedalSplash/DrawableMedal.cs | 34 ++++--------------- 1 file changed, 6 insertions(+), 28 deletions(-) diff --git a/osu.Game/Overlays/MedalSplash/DrawableMedal.cs b/osu.Game/Overlays/MedalSplash/DrawableMedal.cs index 460239f620..2beed6645a 100644 --- a/osu.Game/Overlays/MedalSplash/DrawableMedal.cs +++ b/osu.Game/Overlays/MedalSplash/DrawableMedal.cs @@ -28,14 +28,16 @@ namespace osu.Game.Overlays.MedalSplash [CanBeNull] public event Action StateChanged; + private readonly Medal medal; private readonly Container medalContainer; - private readonly Sprite medalGlow; + private readonly Sprite medalSprite, medalGlow; private readonly OsuSpriteText unlocked, name; private readonly TextFlowContainer description; private DisplayState state; public DrawableMedal(Medal medal) { + this.medal = medal; Position = new Vector2(0f, MedalAnimation.DISC_SIZE / 2); FillFlowContainer infoFlow; @@ -49,7 +51,7 @@ namespace osu.Game.Overlays.MedalSplash Alpha = 0f, Children = new Drawable[] { - new DelayedLoadWrapper(() => new MedalOnlineSprite(medal), 0) + medalSprite = new Sprite { Anchor = Anchor.Centre, Origin = Anchor.Centre, @@ -120,8 +122,9 @@ namespace osu.Game.Overlays.MedalSplash } [BackgroundDependencyLoader] - private void load(OsuColour colours, TextureStore textures) + private void load(OsuColour colours, TextureStore textures, LargeTextureStore largeTextures) { + medalSprite.Texture = largeTextures.Get(medal.ImageUrl); medalGlow.Texture = textures.Get(@"MedalSplash/medal-glow"); description.Colour = colours.BlueLight; } @@ -188,31 +191,6 @@ namespace osu.Game.Overlays.MedalSplash break; } } - - private partial class MedalOnlineSprite : Sprite - { - private readonly Medal medal; - - public MedalOnlineSprite(Medal medal) - { - this.medal = medal; - } - - [BackgroundDependencyLoader] - private void load(OsuColour colours, TextureStore textures, LargeTextureStore largeTextures) - { - Texture = largeTextures.Get(medal.ImageUrl); - - Anchor = Anchor.Centre; - Origin = Anchor.Centre; - } - - protected override void LoadComplete() - { - base.LoadComplete(); - this.FadeInFromZero(150, Easing.OutQuint); - } - } } public enum DisplayState From 71294c312b1a29d2ca73c1f335140e4f350754d4 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 26 Nov 2024 17:58:50 +0900 Subject: [PATCH 0117/1275] Change point of queueing to avoid loading-from-in-queue --- osu.Game/Overlays/MedalOverlay.cs | 32 +++++++++++++------------------ 1 file changed, 13 insertions(+), 19 deletions(-) diff --git a/osu.Game/Overlays/MedalOverlay.cs b/osu.Game/Overlays/MedalOverlay.cs index 736f744429..c24b209b3a 100644 --- a/osu.Game/Overlays/MedalOverlay.cs +++ b/osu.Game/Overlays/MedalOverlay.cs @@ -54,7 +54,7 @@ namespace osu.Game.Overlays { base.LoadComplete(); - OverlayActivationMode.BindValueChanged(_ => displayIfReady(), true); + OverlayActivationMode.BindValueChanged(_ => showNextMedal(), true); } public override void Hide() @@ -84,10 +84,13 @@ namespace osu.Game.Overlays var medalAnimation = new MedalAnimation(medal); - queuedMedals.Enqueue(medalAnimation); Logger.Log($"Queueing medal unlock for \"{medal.Name}\" ({queuedMedals.Count} to display)"); - Schedule(displayIfReady); + LoadComponentAsync(medalAnimation, m => + { + queuedMedals.Enqueue(m); + showNextMedal(); + }); } protected override bool OnClick(ClickEvent e) @@ -130,30 +133,21 @@ namespace osu.Game.Overlays showNextMedal(); } - private void displayIfReady() - { - if (OverlayActivationMode.Value != OverlayActivation.All) - return; - - if (currentMedalDisplay != null || queuedMedals.Any()) - showNextMedal(); - } - private void showNextMedal() { - // A medal is already loading / loaded, so just ensure the overlay is visible. + // If already displayed, keep displaying medals regardless of activation mode changes. + if (OverlayActivationMode.Value != OverlayActivation.All && State.Value == Visibility.Hidden) + return; + + // A medal is already displaying. if (currentMedalDisplay != null) - { - Show(); return; - } if (queuedMedals.TryDequeue(out currentMedalDisplay)) { - Logger.Log($"Preparing to display \"{currentMedalDisplay.Medal.Name}\""); - + Logger.Log($"Displaying \"{currentMedalDisplay.Medal.Name}\""); + medalContainer.Add(currentMedalDisplay); Show(); - LoadComponentAsync(currentMedalDisplay, m => medalContainer.Add(m)); } } From bf29e3ae718373f16ff1edc5e35c412fa89e9902 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 26 Nov 2024 18:00:32 +0900 Subject: [PATCH 0118/1275] Simplify hide code by moving to common method --- osu.Game/Overlays/MedalOverlay.cs | 19 +++++-------------- 1 file changed, 5 insertions(+), 14 deletions(-) diff --git a/osu.Game/Overlays/MedalOverlay.cs b/osu.Game/Overlays/MedalOverlay.cs index c24b209b3a..512cb697dd 100644 --- a/osu.Game/Overlays/MedalOverlay.cs +++ b/osu.Game/Overlays/MedalOverlay.cs @@ -2,7 +2,6 @@ // See the LICENCE file in the repository root for full licence text. using System.Collections.Generic; -using System.Linq; using osu.Framework.Allocation; using osu.Framework.Extensions.ObjectExtensions; using osu.Framework.Graphics; @@ -112,24 +111,11 @@ namespace osu.Game.Overlays private void progressDisplayByUser() { - // For now, we want to make sure that medals are definitely seen by the user. - // So we block exiting the overlay until the load of the active medal completes. - if (currentMedalDisplay?.IsLoaded == false) - return; - // Dismissing may sometimes play out the medal animation rather than immediately dismissing. if (currentMedalDisplay?.Dismiss() == false) return; currentMedalDisplay = null; - - if (!queuedMedals.Any()) - { - Logger.Log("All queued medals have been displayed, hiding overlay!"); - base.Hide(); - return; - } - showNextMedal(); } @@ -149,6 +135,11 @@ namespace osu.Game.Overlays medalContainer.Add(currentMedalDisplay); Show(); } + else if (State.Value == Visibility.Visible) + { + Logger.Log("All queued medals have been displayed, hiding overlay!"); + base.Hide(); + } } protected override void Dispose(bool isDisposing) From 312336de24e730247b64af3b601bbe114d2d563f Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 26 Nov 2024 18:12:26 +0900 Subject: [PATCH 0119/1275] Fix classic skin spinner's middle pieces displaying in the wrong order Closes https://github.com/ppy/osu/issues/30873. See [stable reference](https://github.com/peppy/osu-stable-reference/blob/3ea48705eb67172c430371dcfc8a16a002ed0d3d/osu!/GameplayElements/HitObjects/Osu/SpinnerOsu.cs#L148-L158). --- .../Skinning/Legacy/LegacyNewStyleSpinner.cs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyNewStyleSpinner.cs b/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyNewStyleSpinner.cs index d4a0f243e4..5d09267c21 100644 --- a/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyNewStyleSpinner.cs +++ b/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyNewStyleSpinner.cs @@ -63,18 +63,18 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy Origin = Anchor.Centre, Texture = source.GetTexture("spinner-top"), }, - fixedMiddle = new Sprite - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Texture = source.GetTexture("spinner-middle"), - }, spinningMiddle = new Sprite { Anchor = Anchor.Centre, Origin = Anchor.Centre, Texture = source.GetTexture("spinner-middle2"), }, + fixedMiddle = new Sprite + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Texture = source.GetTexture("spinner-middle"), + }, } }); From 46d1f005907f83c769840974eda24630136e9701 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 26 Nov 2024 11:39:03 +0100 Subject: [PATCH 0120/1275] Fix `Beatmap.Countdown` not being copied on conversion --- osu.Game/Beatmaps/BeatmapConverter.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/osu.Game/Beatmaps/BeatmapConverter.cs b/osu.Game/Beatmaps/BeatmapConverter.cs index c0066cc637..82b40c0318 100644 --- a/osu.Game/Beatmaps/BeatmapConverter.cs +++ b/osu.Game/Beatmaps/BeatmapConverter.cs @@ -83,6 +83,7 @@ namespace osu.Game.Beatmaps beatmap.DistanceSpacing = original.DistanceSpacing; beatmap.GridSize = original.GridSize; beatmap.TimelineZoom = original.TimelineZoom; + beatmap.Countdown = original.Countdown; beatmap.CountdownOffset = original.CountdownOffset; return beatmap; From c69d36dc96fc39e13fe8980837c94dc31f633dbf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 26 Nov 2024 12:40:49 +0100 Subject: [PATCH 0121/1275] Remove leftover `[Solo]` attribute --- .../Visual/Editing/TestSceneHitObjectSampleAdjustments.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/osu.Game.Tests/Visual/Editing/TestSceneHitObjectSampleAdjustments.cs b/osu.Game.Tests/Visual/Editing/TestSceneHitObjectSampleAdjustments.cs index ae814173a1..765fe1ecf6 100644 --- a/osu.Game.Tests/Visual/Editing/TestSceneHitObjectSampleAdjustments.cs +++ b/osu.Game.Tests/Visual/Editing/TestSceneHitObjectSampleAdjustments.cs @@ -777,7 +777,6 @@ namespace osu.Game.Tests.Visual.Editing } [Test] - [Solo] public void TestSelectingObjectDoesNotMutateSamples() { clickSamplePiece(0); From af0c6fc51b7f5faf9f5ff9ba01e692c8b03a5808 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Tue, 26 Nov 2024 21:06:08 +0900 Subject: [PATCH 0122/1275] Add `Room.HasEnded` helper method --- osu.Game/Graphics/OsuColour.cs | 2 +- osu.Game/Online/Rooms/Room.cs | 9 +++++++++ .../OnlinePlay/Lounge/Components/RoomStatusPill.cs | 2 +- .../OnlinePlay/Multiplayer/MultiplayerRoomManager.cs | 2 +- 4 files changed, 12 insertions(+), 3 deletions(-) diff --git a/osu.Game/Graphics/OsuColour.cs b/osu.Game/Graphics/OsuColour.cs index 20e65323f8..2c43876fb2 100644 --- a/osu.Game/Graphics/OsuColour.cs +++ b/osu.Game/Graphics/OsuColour.cs @@ -200,7 +200,7 @@ namespace osu.Game.Graphics /// public Color4 ForRoomStatus(Room room) { - if (DateTimeOffset.Now >= room.EndDate) + if (room.HasEnded) return YellowDarker; switch (room.Status) diff --git a/osu.Game/Online/Rooms/Room.cs b/osu.Game/Online/Rooms/Room.cs index 6e073bdcd7..897ba6bd70 100644 --- a/osu.Game/Online/Rooms/Room.cs +++ b/osu.Game/Online/Rooms/Room.cs @@ -374,6 +374,15 @@ namespace osu.Game.Online.Rooms RecentParticipants = other.RecentParticipants; } + /// + /// Whether the room is no longer available. + /// + /// + /// This property does not update in real-time and needs to be queried periodically. + /// Subscribe to to be notified of any immediate changes. + /// + public bool HasEnded => DateTimeOffset.Now >= EndDate; + [JsonObject(MemberSerialization.OptIn)] public class RoomPlaylistItemStats { diff --git a/osu.Game/Screens/OnlinePlay/Lounge/Components/RoomStatusPill.cs b/osu.Game/Screens/OnlinePlay/Lounge/Components/RoomStatusPill.cs index 5d2c4b28e6..32d0add5fd 100644 --- a/osu.Game/Screens/OnlinePlay/Lounge/Components/RoomStatusPill.cs +++ b/osu.Game/Screens/OnlinePlay/Lounge/Components/RoomStatusPill.cs @@ -59,7 +59,7 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components { Pill.Background.FadeColour(colours.ForRoomStatus(room), 100); - if (DateTimeOffset.Now >= room.EndDate) + if (room.HasEnded) TextFlow.Text = RoomStatusPillStrings.Ended; else { diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerRoomManager.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerRoomManager.cs index b6f4b0e8d9..7f09c9cbe9 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerRoomManager.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerRoomManager.cs @@ -30,7 +30,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer // this is done here as a pre-check to avoid clicking on already closed rooms in the lounge from triggering a server join. // should probably be done at a higher level, but due to the current structure of things this is the easiest place for now. - if (DateTimeOffset.Now >= room.EndDate) + if (room.HasEnded) { onError?.Invoke("Cannot join an ended room."); return; From 4c7976bb9305c1e7b7b1f075ff194449d6e4b3f0 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Tue, 26 Nov 2024 21:11:48 +0900 Subject: [PATCH 0123/1275] Remove unused using --- osu.Game/Screens/OnlinePlay/Lounge/Components/RoomStatusPill.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/osu.Game/Screens/OnlinePlay/Lounge/Components/RoomStatusPill.cs b/osu.Game/Screens/OnlinePlay/Lounge/Components/RoomStatusPill.cs index 32d0add5fd..6da8f3ecbd 100644 --- a/osu.Game/Screens/OnlinePlay/Lounge/Components/RoomStatusPill.cs +++ b/osu.Game/Screens/OnlinePlay/Lounge/Components/RoomStatusPill.cs @@ -1,7 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using System; using System.ComponentModel; using osu.Framework.Allocation; using osu.Framework.Graphics; From bd1f978138c2dc6d02f084f49be91c99b3902366 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Tue, 26 Nov 2024 21:35:10 +0900 Subject: [PATCH 0124/1275] Empty commit to fix CI From f04862ea7417349a8dd32895cfa80d4477d0cadd Mon Sep 17 00:00:00 2001 From: Joseph Madamba Date: Tue, 26 Nov 2024 12:11:29 -0800 Subject: [PATCH 0125/1275] Edit one more word not using british 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 b8a00a1c17..f97ad5fa2c 100644 --- a/osu.Game/Localisation/MenuTipStrings.cs +++ b/osu.Game/Localisation/MenuTipStrings.cs @@ -90,9 +90,9 @@ namespace osu.Game.Localisation public static LocalisableString SkinEditor => new TranslatableString(getKey(@"skin_editor"), @"Your gameplay HUD can be customised by using the skin layout editor. Open it at any time via Ctrl-Shift-S!"); /// - /// "You can create mod presets to make toggling your favorite mod combinations easier!" + /// "You can create mod presets to make toggling your favourite mod combinations easier!" /// - public static LocalisableString ModPresets => new TranslatableString(getKey(@"mod_presets"), @"You can create mod presets to make toggling your favorite mod combinations easier!"); + public static LocalisableString ModPresets => new TranslatableString(getKey(@"mod_presets"), @"You can create mod presets to make toggling your favourite mod combinations easier!"); /// /// "Many mods have customisation settings that drastically change how they function. Click the Customise button in mod select to view settings!" From df74a177ae8ce195d79e20b6903b7477d201db10 Mon Sep 17 00:00:00 2001 From: HenintsoaSky Date: Wed, 27 Nov 2024 00:13:32 +0300 Subject: [PATCH 0126/1275] Add option to disable star fountain in gameplay --- .../Visual/Menus/TestSceneStarFountain.cs | 54 +++++++++++++++++++ osu.Game/Configuration/OsuConfigManager.cs | 2 + .../Localisation/GameplaySettingsStrings.cs | 5 ++ .../Sections/Gameplay/GeneralSettings.cs | 5 ++ .../Screens/Play/KiaiGameplayFountains.cs | 16 +++++- 5 files changed, 81 insertions(+), 1 deletion(-) diff --git a/osu.Game.Tests/Visual/Menus/TestSceneStarFountain.cs b/osu.Game.Tests/Visual/Menus/TestSceneStarFountain.cs index 29fa7287d2..64a0f1f821 100644 --- a/osu.Game.Tests/Visual/Menus/TestSceneStarFountain.cs +++ b/osu.Game.Tests/Visual/Menus/TestSceneStarFountain.cs @@ -3,8 +3,10 @@ using System.Linq; using NUnit.Framework; +using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Utils; +using osu.Game.Configuration; using osu.Game.Screens.Menu; using osu.Game.Screens.Play; @@ -73,5 +75,57 @@ namespace osu.Game.Tests.Visual.Menus ((StarFountain)Children[1]).Shoot(-1); }); } + + [Test] + public void TestGameplayKiaiStarToggle() + { + Bindable kiaiStarEffectsEnabled = null!; + + AddStep("load configuration", () => + { + var config = new OsuConfigManager(LocalStorage); + kiaiStarEffectsEnabled = config.GetBindable(OsuSetting.KiaiStarFountain); + }); + + AddStep("make fountains", () => + { + Children = new Drawable[] + { + new KiaiGameplayFountains.GameplayStarFountain + { + Anchor = Anchor.BottomLeft, + Origin = Anchor.BottomLeft, + X = 75, + }, + new KiaiGameplayFountains.GameplayStarFountain + { + Anchor = Anchor.BottomRight, + Origin = Anchor.BottomRight, + X = -75, + }, + }; + }); + + AddStep("enable KiaiStarEffects", () => kiaiStarEffectsEnabled.Value = true); + AddRepeatStep("activate fountains (enabled)", () => + { + ((KiaiGameplayFountains.GameplayStarFountain)Children[0]).Shoot(1); + ((KiaiGameplayFountains.GameplayStarFountain)Children[1]).Shoot(-1); + }, 100); + + AddStep("disable KiaiStarEffects", () => kiaiStarEffectsEnabled.Value = false); + AddRepeatStep("attempt to activate fountains (disabled)", () => + { + ((KiaiGameplayFountains.GameplayStarFountain)Children[0]).Shoot(1); + ((KiaiGameplayFountains.GameplayStarFountain)Children[1]).Shoot(-1); + }, 100); + + AddStep("re-enable KiaiStarEffects", () => kiaiStarEffectsEnabled.Value = true); + AddRepeatStep("activate fountains (re-enabled)", () => + { + ((KiaiGameplayFountains.GameplayStarFountain)Children[0]).Shoot(1); + ((KiaiGameplayFountains.GameplayStarFountain)Children[1]).Shoot(-1); + }, 100); + } } } diff --git a/osu.Game/Configuration/OsuConfigManager.cs b/osu.Game/Configuration/OsuConfigManager.cs index 33d99e9b0f..36a5328756 100644 --- a/osu.Game/Configuration/OsuConfigManager.cs +++ b/osu.Game/Configuration/OsuConfigManager.cs @@ -138,6 +138,7 @@ namespace osu.Game.Configuration SetDefault(OsuSetting.LightenDuringBreaks, true); SetDefault(OsuSetting.HitLighting, true); + SetDefault(OsuSetting.KiaiStarFountain, true); SetDefault(OsuSetting.HUDVisibilityMode, HUDVisibilityMode.Always); SetDefault(OsuSetting.ShowHealthDisplayWhenCantFail, true); @@ -414,6 +415,7 @@ namespace osu.Game.Configuration NotifyOnPrivateMessage, UIHoldActivationDelay, HitLighting, + KiaiStarFountain, MenuBackgroundSource, GameplayDisableWinKey, SeasonalBackgroundMode, diff --git a/osu.Game/Localisation/GameplaySettingsStrings.cs b/osu.Game/Localisation/GameplaySettingsStrings.cs index ff6a6102a7..3d18eacf9d 100644 --- a/osu.Game/Localisation/GameplaySettingsStrings.cs +++ b/osu.Game/Localisation/GameplaySettingsStrings.cs @@ -74,6 +74,11 @@ namespace osu.Game.Localisation /// public static LocalisableString FadePlayfieldWhenHealthLow => new TranslatableString(getKey(@"fade_playfield_when_health_low"), @"Fade playfield to red when health is low"); + /// + /// "Star fountain during kiai time" + /// + public static LocalisableString KiaiStarFountain => new TranslatableString(getKey(@"star_fountain_during_kiai_time"), @"Star fountain during kiai time"); + /// /// "Always show key overlay" /// diff --git a/osu.Game/Overlays/Settings/Sections/Gameplay/GeneralSettings.cs b/osu.Game/Overlays/Settings/Sections/Gameplay/GeneralSettings.cs index 83e9140b33..136832a75b 100644 --- a/osu.Game/Overlays/Settings/Sections/Gameplay/GeneralSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/Gameplay/GeneralSettings.cs @@ -31,6 +31,11 @@ namespace osu.Game.Overlays.Settings.Sections.Gameplay LabelText = GraphicsSettingsStrings.HitLighting, Current = config.GetBindable(OsuSetting.HitLighting) }, + new SettingsCheckbox + { + LabelText = GameplaySettingsStrings.KiaiStarFountain, + Current = config.GetBindable(OsuSetting.KiaiStarFountain) + }, }; } } diff --git a/osu.Game/Screens/Play/KiaiGameplayFountains.cs b/osu.Game/Screens/Play/KiaiGameplayFountains.cs index 7659c61123..4d1d247f87 100644 --- a/osu.Game/Screens/Play/KiaiGameplayFountains.cs +++ b/osu.Game/Screens/Play/KiaiGameplayFountains.cs @@ -5,8 +5,10 @@ using System; using osu.Framework.Allocation; using osu.Framework.Audio.Track; +using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Utils; +using osu.Game.Configuration; using osu.Game.Beatmaps.ControlPoints; using osu.Game.Graphics.Containers; using osu.Game.Screens.Menu; @@ -18,9 +20,13 @@ namespace osu.Game.Screens.Play private StarFountain leftFountain = null!; private StarFountain rightFountain = null!; + private Bindable kiaiStarEffectsEnabled = null!; + [BackgroundDependencyLoader] - private void load() + private void load(OsuConfigManager config) { + kiaiStarEffectsEnabled = config.GetBindable(OsuSetting.KiaiStarFountain); + RelativeSizeAxes = Axes.Both; Children = new[] @@ -48,6 +54,12 @@ namespace osu.Game.Screens.Play { base.OnNewBeat(beatIndex, timingPoint, effectPoint, amplitudes); + if (!kiaiStarEffectsEnabled.Value) + return; + + if (!kiaiStarEffectsEnabled.Value) + return; + if (effectPoint.KiaiMode && !isTriggered) { bool isNearEffectPoint = Math.Abs(BeatSyncSource.Clock.CurrentTime - effectPoint.Time) < 500; @@ -76,6 +88,8 @@ namespace osu.Game.Screens.Play { protected override double ShootDuration => 400; + private readonly Bindable kiaiStarEffectsEnabled = new Bindable(); + public GameplayStarFountainSpewer() : base(perSecond: 180) { From 460471e73fc17c50cd67073de1e6eb05d0e75179 Mon Sep 17 00:00:00 2001 From: HenintsoaSky Date: Wed, 27 Nov 2024 00:27:22 +0300 Subject: [PATCH 0127/1275] Rename of the setting --- osu.Game.Tests/Visual/Menus/TestSceneStarFountain.cs | 2 +- osu.Game/Configuration/OsuConfigManager.cs | 4 ++-- osu.Game/Localisation/GameplaySettingsStrings.cs | 4 ++-- .../Overlays/Settings/Sections/Gameplay/GeneralSettings.cs | 4 ++-- osu.Game/Screens/Play/KiaiGameplayFountains.cs | 2 +- 5 files changed, 8 insertions(+), 8 deletions(-) diff --git a/osu.Game.Tests/Visual/Menus/TestSceneStarFountain.cs b/osu.Game.Tests/Visual/Menus/TestSceneStarFountain.cs index 64a0f1f821..6f73979e58 100644 --- a/osu.Game.Tests/Visual/Menus/TestSceneStarFountain.cs +++ b/osu.Game.Tests/Visual/Menus/TestSceneStarFountain.cs @@ -84,7 +84,7 @@ namespace osu.Game.Tests.Visual.Menus AddStep("load configuration", () => { var config = new OsuConfigManager(LocalStorage); - kiaiStarEffectsEnabled = config.GetBindable(OsuSetting.KiaiStarFountain); + kiaiStarEffectsEnabled = config.GetBindable(OsuSetting.StarFountains); }); AddStep("make fountains", () => diff --git a/osu.Game/Configuration/OsuConfigManager.cs b/osu.Game/Configuration/OsuConfigManager.cs index 36a5328756..4f62db8cf7 100644 --- a/osu.Game/Configuration/OsuConfigManager.cs +++ b/osu.Game/Configuration/OsuConfigManager.cs @@ -138,7 +138,7 @@ namespace osu.Game.Configuration SetDefault(OsuSetting.LightenDuringBreaks, true); SetDefault(OsuSetting.HitLighting, true); - SetDefault(OsuSetting.KiaiStarFountain, true); + SetDefault(OsuSetting.StarFountains, true); SetDefault(OsuSetting.HUDVisibilityMode, HUDVisibilityMode.Always); SetDefault(OsuSetting.ShowHealthDisplayWhenCantFail, true); @@ -415,7 +415,7 @@ namespace osu.Game.Configuration NotifyOnPrivateMessage, UIHoldActivationDelay, HitLighting, - KiaiStarFountain, + StarFountains, MenuBackgroundSource, GameplayDisableWinKey, SeasonalBackgroundMode, diff --git a/osu.Game/Localisation/GameplaySettingsStrings.cs b/osu.Game/Localisation/GameplaySettingsStrings.cs index 3d18eacf9d..2715f0b8cf 100644 --- a/osu.Game/Localisation/GameplaySettingsStrings.cs +++ b/osu.Game/Localisation/GameplaySettingsStrings.cs @@ -75,9 +75,9 @@ namespace osu.Game.Localisation public static LocalisableString FadePlayfieldWhenHealthLow => new TranslatableString(getKey(@"fade_playfield_when_health_low"), @"Fade playfield to red when health is low"); /// - /// "Star fountain during kiai time" + /// "Star fountains" /// - public static LocalisableString KiaiStarFountain => new TranslatableString(getKey(@"star_fountain_during_kiai_time"), @"Star fountain during kiai time"); + public static LocalisableString StarFountains => new TranslatableString(getKey(@"star_fountains"), @"Star fountains"); /// /// "Always show key overlay" diff --git a/osu.Game/Overlays/Settings/Sections/Gameplay/GeneralSettings.cs b/osu.Game/Overlays/Settings/Sections/Gameplay/GeneralSettings.cs index 136832a75b..779d5cdf00 100644 --- a/osu.Game/Overlays/Settings/Sections/Gameplay/GeneralSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/Gameplay/GeneralSettings.cs @@ -33,8 +33,8 @@ namespace osu.Game.Overlays.Settings.Sections.Gameplay }, new SettingsCheckbox { - LabelText = GameplaySettingsStrings.KiaiStarFountain, - Current = config.GetBindable(OsuSetting.KiaiStarFountain) + LabelText = GameplaySettingsStrings.StarFountains, + Current = config.GetBindable(OsuSetting.StarFountains) }, }; } diff --git a/osu.Game/Screens/Play/KiaiGameplayFountains.cs b/osu.Game/Screens/Play/KiaiGameplayFountains.cs index 4d1d247f87..011de52b2a 100644 --- a/osu.Game/Screens/Play/KiaiGameplayFountains.cs +++ b/osu.Game/Screens/Play/KiaiGameplayFountains.cs @@ -25,7 +25,7 @@ namespace osu.Game.Screens.Play [BackgroundDependencyLoader] private void load(OsuConfigManager config) { - kiaiStarEffectsEnabled = config.GetBindable(OsuSetting.KiaiStarFountain); + kiaiStarEffectsEnabled = config.GetBindable(OsuSetting.StarFountains); RelativeSizeAxes = Axes.Both; From 80a66085a9497b97e6c7312a34ca52abe8e632a4 Mon Sep 17 00:00:00 2001 From: HenintsoaSky Date: Wed, 27 Nov 2024 00:41:02 +0300 Subject: [PATCH 0128/1275] rename and remove again --- osu.Game.Tests/Visual/Menus/TestSceneStarFountain.cs | 12 ++++++------ osu.Game/Screens/Play/KiaiGameplayFountains.cs | 10 ++++------ 2 files changed, 10 insertions(+), 12 deletions(-) diff --git a/osu.Game.Tests/Visual/Menus/TestSceneStarFountain.cs b/osu.Game.Tests/Visual/Menus/TestSceneStarFountain.cs index 6f73979e58..0d981014b8 100644 --- a/osu.Game.Tests/Visual/Menus/TestSceneStarFountain.cs +++ b/osu.Game.Tests/Visual/Menus/TestSceneStarFountain.cs @@ -77,14 +77,14 @@ namespace osu.Game.Tests.Visual.Menus } [Test] - public void TestGameplayKiaiStarToggle() + public void TestGameplayStarFountainsSetting() { - Bindable kiaiStarEffectsEnabled = null!; + Bindable starFountainsEnabled = null!; AddStep("load configuration", () => { var config = new OsuConfigManager(LocalStorage); - kiaiStarEffectsEnabled = config.GetBindable(OsuSetting.StarFountains); + starFountainsEnabled = config.GetBindable(OsuSetting.StarFountains); }); AddStep("make fountains", () => @@ -106,21 +106,21 @@ namespace osu.Game.Tests.Visual.Menus }; }); - AddStep("enable KiaiStarEffects", () => kiaiStarEffectsEnabled.Value = true); + AddStep("enable KiaiStarEffects", () => starFountainsEnabled.Value = true); AddRepeatStep("activate fountains (enabled)", () => { ((KiaiGameplayFountains.GameplayStarFountain)Children[0]).Shoot(1); ((KiaiGameplayFountains.GameplayStarFountain)Children[1]).Shoot(-1); }, 100); - AddStep("disable KiaiStarEffects", () => kiaiStarEffectsEnabled.Value = false); + AddStep("disable KiaiStarEffects", () => starFountainsEnabled.Value = false); AddRepeatStep("attempt to activate fountains (disabled)", () => { ((KiaiGameplayFountains.GameplayStarFountain)Children[0]).Shoot(1); ((KiaiGameplayFountains.GameplayStarFountain)Children[1]).Shoot(-1); }, 100); - AddStep("re-enable KiaiStarEffects", () => kiaiStarEffectsEnabled.Value = true); + AddStep("re-enable KiaiStarEffects", () => starFountainsEnabled.Value = true); AddRepeatStep("activate fountains (re-enabled)", () => { ((KiaiGameplayFountains.GameplayStarFountain)Children[0]).Shoot(1); diff --git a/osu.Game/Screens/Play/KiaiGameplayFountains.cs b/osu.Game/Screens/Play/KiaiGameplayFountains.cs index 011de52b2a..a6b2cd6fdb 100644 --- a/osu.Game/Screens/Play/KiaiGameplayFountains.cs +++ b/osu.Game/Screens/Play/KiaiGameplayFountains.cs @@ -20,12 +20,12 @@ namespace osu.Game.Screens.Play private StarFountain leftFountain = null!; private StarFountain rightFountain = null!; - private Bindable kiaiStarEffectsEnabled = null!; + private Bindable kiaiStarFountainsEnabled = null!; [BackgroundDependencyLoader] private void load(OsuConfigManager config) { - kiaiStarEffectsEnabled = config.GetBindable(OsuSetting.StarFountains); + kiaiStarFountainsEnabled = config.GetBindable(OsuSetting.StarFountains); RelativeSizeAxes = Axes.Both; @@ -54,10 +54,10 @@ namespace osu.Game.Screens.Play { base.OnNewBeat(beatIndex, timingPoint, effectPoint, amplitudes); - if (!kiaiStarEffectsEnabled.Value) + if (!kiaiStarFountainsEnabled.Value) return; - if (!kiaiStarEffectsEnabled.Value) + if (!kiaiStarFountainsEnabled.Value) return; if (effectPoint.KiaiMode && !isTriggered) @@ -88,8 +88,6 @@ namespace osu.Game.Screens.Play { protected override double ShootDuration => 400; - private readonly Bindable kiaiStarEffectsEnabled = new Bindable(); - public GameplayStarFountainSpewer() : base(perSecond: 180) { From 3e1b4f4ac564a1b69b2d8111b19d3908f99980e4 Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Tue, 26 Nov 2024 16:50:23 -0500 Subject: [PATCH 0129/1275] Rename `AllowBackButton` to `AllowUserExit` and rewrite visibility flow structure Co-authored-by: Dean Herbert --- osu.Game/OsuGame.cs | 25 ++++++++++++------- .../Maintenance/MigrationRunScreen.cs | 2 +- osu.Game/Screens/Edit/Editor.cs | 2 +- osu.Game/Screens/Edit/EditorLoader.cs | 2 +- osu.Game/Screens/IOsuScreen.cs | 21 ++++++++-------- osu.Game/Screens/Menu/MainMenu.cs | 2 +- .../Screens/OnlinePlay/OnlinePlayScreen.cs | 2 +- osu.Game/Screens/OsuScreen.cs | 13 +++++++--- osu.Game/Screens/Play/Player.cs | 2 +- osu.Game/Screens/Play/PlayerLoader.cs | 4 +-- osu.Game/Screens/StartupScreen.cs | 2 +- 11 files changed, 44 insertions(+), 33 deletions(-) diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs index 4d6bc4fd14..514209524e 100644 --- a/osu.Game/OsuGame.cs +++ b/osu.Game/OsuGame.cs @@ -175,6 +175,11 @@ namespace osu.Game /// public readonly IBindable OverlayActivationMode = new Bindable(); + /// + /// Whether the back button is currently displayed. + /// + public readonly IBindable BackButtonVisibility = new Bindable(); + IBindable ILocalUserPlayInfo.PlayingState => playingState; private readonly Bindable playingState = new Bindable(); @@ -1019,7 +1024,7 @@ namespace osu.Game if (!(ScreenStack.CurrentScreen is IOsuScreen currentScreen)) return; - if (!((Drawable)currentScreen).IsLoaded || (currentScreen.AllowBackButton && !currentScreen.OnBackButton())) + if (!((Drawable)currentScreen).IsLoaded || (currentScreen.AllowUserExit && !currentScreen.OnBackButton())) ScreenStack.Exit(); } }, @@ -1189,6 +1194,14 @@ namespace osu.Game if (mode.NewValue != OverlayActivation.All) CloseAllOverlays(); }; + BackButtonVisibility.ValueChanged += visible => + { + if (visible.NewValue) + BackButton.Show(); + else + BackButton.Hide(); + }; + // Importantly, this should be run after binding PostNotification to the import handlers so they can present the import after game startup. handleStartupImport(); } @@ -1581,20 +1594,14 @@ namespace osu.Game if (current is IOsuScreen currentOsuScreen) { - if (currentOsuScreen.AllowBackButton) - BackButton.State.UnbindFrom(currentOsuScreen.BackButtonState); - + BackButtonVisibility.UnbindFrom(currentOsuScreen.BackButtonVisibility); OverlayActivationMode.UnbindFrom(currentOsuScreen.OverlayActivationMode); API.Activity.UnbindFrom(currentOsuScreen.Activity); } if (newScreen is IOsuScreen newOsuScreen) { - if (newOsuScreen.AllowBackButton) - ((IBindable)BackButton.State).BindTo(newOsuScreen.BackButtonState); - else - BackButton.Hide(); - + BackButtonVisibility.BindTo(newOsuScreen.BackButtonVisibility); OverlayActivationMode.BindTo(newOsuScreen.OverlayActivationMode); API.Activity.BindTo(newOsuScreen.Activity); diff --git a/osu.Game/Overlays/Settings/Sections/Maintenance/MigrationRunScreen.cs b/osu.Game/Overlays/Settings/Sections/Maintenance/MigrationRunScreen.cs index 3bba480aaa..c0363851ef 100644 --- a/osu.Game/Overlays/Settings/Sections/Maintenance/MigrationRunScreen.cs +++ b/osu.Game/Overlays/Settings/Sections/Maintenance/MigrationRunScreen.cs @@ -26,7 +26,7 @@ namespace osu.Game.Overlays.Settings.Sections.Maintenance [Resolved(canBeNull: true)] private OsuGame game { get; set; } - public override bool AllowBackButton => false; + public override bool AllowUserExit => false; public override bool AllowExternalScreenChange => false; diff --git a/osu.Game/Screens/Edit/Editor.cs b/osu.Game/Screens/Edit/Editor.cs index 644e1afb3b..13e5791605 100644 --- a/osu.Game/Screens/Edit/Editor.cs +++ b/osu.Game/Screens/Edit/Editor.cs @@ -80,7 +80,7 @@ namespace osu.Game.Screens.Edit public override float BackgroundParallaxAmount => 0.1f; - public override bool AllowBackButton => false; + public override bool AllowUserExit => false; public override bool HideOverlaysOnEnter => true; diff --git a/osu.Game/Screens/Edit/EditorLoader.cs b/osu.Game/Screens/Edit/EditorLoader.cs index 0e0fb9f795..7c6ee10840 100644 --- a/osu.Game/Screens/Edit/EditorLoader.cs +++ b/osu.Game/Screens/Edit/EditorLoader.cs @@ -36,7 +36,7 @@ namespace osu.Game.Screens.Edit public override float BackgroundParallaxAmount => 0.1f; - public override bool AllowBackButton => false; + public override bool AllowUserExit => false; public override bool HideOverlaysOnEnter => true; diff --git a/osu.Game/Screens/IOsuScreen.cs b/osu.Game/Screens/IOsuScreen.cs index 7025460daa..46dfbfb1ac 100644 --- a/osu.Game/Screens/IOsuScreen.cs +++ b/osu.Game/Screens/IOsuScreen.cs @@ -3,10 +3,8 @@ using System.Collections.Generic; using osu.Framework.Bindables; -using osu.Framework.Graphics.Containers; using osu.Framework.Screens; using osu.Game.Beatmaps; -using osu.Game.Graphics.UserInterface; using osu.Game.Input.Bindings; using osu.Game.Overlays; using osu.Game.Rulesets; @@ -24,15 +22,20 @@ namespace osu.Game.Screens bool DisallowExternalBeatmapRulesetChanges { get; } /// - /// Whether the user can exit this by pressing the back button. + /// Whether the user can exit this . /// - bool AllowBackButton { get; } + /// + /// When overriden to false, + /// the user is blocked from exiting the screen via the action, + /// and the back button is hidden from this screen by the initial state of being set to hidden. + /// + bool AllowUserExit { get; } /// /// Whether a footer (and a back button) should be displayed underneath the screen. /// /// - /// Temporarily, the back button is shown regardless of whether is true. + /// Temporarily, the back button is shown regardless of whether is true. /// bool ShowFooter { get; } @@ -63,13 +66,9 @@ namespace osu.Game.Screens IBindable OverlayActivationMode { get; } /// - /// Controls the visibility state of to better work with screen-specific transitions (i.e. quick restart in player). - /// The back button can still be triggered by the action even while hidden. + /// Whether the back button should be displayed in this screen. /// - /// - /// This is ignored when is set to false. - /// - IBindable BackButtonState { get; } + IBindable BackButtonVisibility { get; } /// /// The current for this screen. diff --git a/osu.Game/Screens/Menu/MainMenu.cs b/osu.Game/Screens/Menu/MainMenu.cs index 35c6bab81b..6b94d4bdfb 100644 --- a/osu.Game/Screens/Menu/MainMenu.cs +++ b/osu.Game/Screens/Menu/MainMenu.cs @@ -48,7 +48,7 @@ namespace osu.Game.Screens.Menu public override bool HideOverlaysOnEnter => Buttons == null || Buttons.State == ButtonSystemState.Initial; - public override bool AllowBackButton => false; + public override bool AllowUserExit => false; public override bool AllowExternalScreenChange => true; diff --git a/osu.Game/Screens/OnlinePlay/OnlinePlayScreen.cs b/osu.Game/Screens/OnlinePlay/OnlinePlayScreen.cs index cc6a4e09e1..17fb667e14 100644 --- a/osu.Game/Screens/OnlinePlay/OnlinePlayScreen.cs +++ b/osu.Game/Screens/OnlinePlay/OnlinePlayScreen.cs @@ -180,7 +180,7 @@ namespace osu.Game.Screens.OnlinePlay if (!(screenStack.CurrentScreen is IOnlinePlaySubScreen onlineSubScreen)) return false; - if (((Drawable)onlineSubScreen).IsLoaded && onlineSubScreen.AllowBackButton && onlineSubScreen.OnBackButton()) + if (((Drawable)onlineSubScreen).IsLoaded && onlineSubScreen.AllowUserExit && onlineSubScreen.OnBackButton()) return true; if (screenStack.CurrentScreen != null && !(screenStack.CurrentScreen is LoungeSubScreen)) diff --git a/osu.Game/Screens/OsuScreen.cs b/osu.Game/Screens/OsuScreen.cs index 2c5c889154..ab66241a77 100644 --- a/osu.Game/Screens/OsuScreen.cs +++ b/osu.Game/Screens/OsuScreen.cs @@ -11,7 +11,6 @@ using osu.Framework.Audio; using osu.Framework.Audio.Sample; using osu.Framework.Bindables; using osu.Framework.Graphics; -using osu.Framework.Graphics.Containers; using osu.Framework.Screens; using osu.Game.Beatmaps; using osu.Game.Overlays; @@ -38,7 +37,7 @@ namespace osu.Game.Screens public string Description => Title; - public virtual bool AllowBackButton => true; + public virtual bool AllowUserExit => true; public virtual bool ShowFooter => false; @@ -57,9 +56,14 @@ namespace osu.Game.Screens IBindable IOsuScreen.OverlayActivationMode => OverlayActivationMode; - public readonly Bindable BackButtonState = new Bindable(Visibility.Visible); + /// + /// The initial visibility state of the back button when this screen is entered for the first time. + /// + protected virtual bool InitialBackButtonVisibility => AllowUserExit; - IBindable IOsuScreen.BackButtonState => BackButtonState; + public readonly Bindable BackButtonVisibility; + + IBindable IOsuScreen.BackButtonVisibility => BackButtonVisibility; public virtual bool CursorVisible => true; @@ -159,6 +163,7 @@ namespace osu.Game.Screens Origin = Anchor.Centre; OverlayActivationMode = new Bindable(InitialOverlayActivationMode); + BackButtonVisibility = new Bindable(InitialBackButtonVisibility); } [BackgroundDependencyLoader(true)] diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs index e9722350bd..f4e3e6f434 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -57,7 +57,7 @@ namespace osu.Game.Screens.Play /// public event Action OnGameplayStarted; - public override bool AllowBackButton => false; // handled by HoldForMenuButton + public override bool AllowUserExit => false; // handled by HoldForMenuButton protected override bool PlayExitSound => !isRestarting; diff --git a/osu.Game/Screens/Play/PlayerLoader.cs b/osu.Game/Screens/Play/PlayerLoader.cs index a6e171ba02..49db0f05bd 100644 --- a/osu.Game/Screens/Play/PlayerLoader.cs +++ b/osu.Game/Screens/Play/PlayerLoader.cs @@ -478,7 +478,7 @@ namespace osu.Game.Screens.Play if (quickRestart) { - BackButtonState.Value = Visibility.Hidden; + BackButtonVisibility.Value = false; // A quick restart starts by triggering a fade to black AddInternal(quickRestartBlackLayer = new Box @@ -499,7 +499,7 @@ namespace osu.Game.Screens.Play .ScaleTo(1) .FadeInFromZero(500, Easing.OutQuint); - this.Delay(quick_restart_initial_delay).Schedule(() => BackButtonState.Value = Visibility.Visible); + this.Delay(quick_restart_initial_delay).Schedule(() => BackButtonVisibility.Value = true); } else { diff --git a/osu.Game/Screens/StartupScreen.cs b/osu.Game/Screens/StartupScreen.cs index 9e04a238eb..0724327a9f 100644 --- a/osu.Game/Screens/StartupScreen.cs +++ b/osu.Game/Screens/StartupScreen.cs @@ -10,7 +10,7 @@ namespace osu.Game.Screens /// public abstract partial class StartupScreen : OsuScreen { - public override bool AllowBackButton => false; + public override bool AllowUserExit => false; public override bool HideOverlaysOnEnter => true; From 16d8b1138562d4350726ea0b3ed3053d73f805f0 Mon Sep 17 00:00:00 2001 From: HenintsoaSky Date: Wed, 27 Nov 2024 00:53:22 +0300 Subject: [PATCH 0130/1275] A toggle for star fountains --- changes.patch | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 changes.patch diff --git a/changes.patch b/changes.patch new file mode 100644 index 0000000000..e69de29bb2 From 9083daf3630ae230cccab5f469369906d9cc5d48 Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Tue, 26 Nov 2024 20:04:35 -0500 Subject: [PATCH 0131/1275] Fix epic code failure I wasn't feeling well last night. --- .../OnlinePlay/Playlists/PlaylistItemUserResultsScreen.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistItemUserResultsScreen.cs b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistItemUserResultsScreen.cs index b659a98802..22bab7eb93 100644 --- a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistItemUserResultsScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistItemUserResultsScreen.cs @@ -20,8 +20,8 @@ namespace osu.Game.Screens.OnlinePlay.Playlists { } - protected override APIRequest CreateScoreRequest() => Score == null - ? new ShowPlaylistScoreRequest(RoomId, PlaylistItem.ID, Score?.OnlineID ?? -1) + protected override APIRequest CreateScoreRequest() => Score != null + ? new ShowPlaylistScoreRequest(RoomId, PlaylistItem.ID, Score.OnlineID) : new ShowPlaylistUserScoreRequest(RoomId, PlaylistItem.ID, API.LocalUser.Value.Id); protected override ScoreInfo[] PerformSuccessCallback(Action> callback, List scores, MultiplayerScores? pivot = null) From a477bb7bfec1422b18c17c88f8012807838dc7e2 Mon Sep 17 00:00:00 2001 From: HenintsoaSky Date: Wed, 27 Nov 2024 07:38:33 +0300 Subject: [PATCH 0132/1275] Renaming of 'StarFountainEnabled' --- osu.Game/Screens/Play/KiaiGameplayFountains.cs | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/osu.Game/Screens/Play/KiaiGameplayFountains.cs b/osu.Game/Screens/Play/KiaiGameplayFountains.cs index a6b2cd6fdb..fd9596c838 100644 --- a/osu.Game/Screens/Play/KiaiGameplayFountains.cs +++ b/osu.Game/Screens/Play/KiaiGameplayFountains.cs @@ -20,12 +20,12 @@ namespace osu.Game.Screens.Play private StarFountain leftFountain = null!; private StarFountain rightFountain = null!; - private Bindable kiaiStarFountainsEnabled = null!; + private Bindable kiaiStarFountains = null!; [BackgroundDependencyLoader] private void load(OsuConfigManager config) { - kiaiStarFountainsEnabled = config.GetBindable(OsuSetting.StarFountains); + kiaiStarFountains = config.GetBindable(OsuSetting.StarFountains); RelativeSizeAxes = Axes.Both; @@ -54,10 +54,7 @@ namespace osu.Game.Screens.Play { base.OnNewBeat(beatIndex, timingPoint, effectPoint, amplitudes); - if (!kiaiStarFountainsEnabled.Value) - return; - - if (!kiaiStarFountainsEnabled.Value) + if (!kiaiStarFountains.Value) return; if (effectPoint.KiaiMode && !isTriggered) From aa3d3a6344dd9428840236119132aba023874d63 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 27 Nov 2024 14:24:57 +0900 Subject: [PATCH 0133/1275] Remove unnecessary local subscription in `BeatmapCarousel` Not sure why I left this around during the refactor. This is 100% handled by the `DetachedBeatmapStore`. Removing this subscription reduces overheads by a huge amount for users with large beatmap databases. My hypothesis is that subscriptions are more expensive based on **the number of results matching**. This one matches almost every beatmap so removing it is a large win. --- osu.Game/Screens/Select/BeatmapCarousel.cs | 46 ---------------------- 1 file changed, 46 deletions(-) diff --git a/osu.Game/Screens/Select/BeatmapCarousel.cs b/osu.Game/Screens/Select/BeatmapCarousel.cs index 5e1e0ce615..fc7c7989e2 100644 --- a/osu.Game/Screens/Select/BeatmapCarousel.cs +++ b/osu.Game/Screens/Select/BeatmapCarousel.cs @@ -29,7 +29,6 @@ using osu.Game.Input.Bindings; using osu.Game.Screens.Select.Carousel; using osuTK; using osuTK.Input; -using Realms; namespace osu.Game.Screens.Select { @@ -207,8 +206,6 @@ namespace osu.Game.Screens.Select private CarouselRoot root; - private IDisposable? subscriptionBeatmaps; - private readonly DrawablePool setPool = new DrawablePool(100); private Sample? spinSample; @@ -258,13 +255,6 @@ namespace osu.Game.Screens.Select } } - protected override void LoadComplete() - { - base.LoadComplete(); - - subscriptionBeatmaps = realm.RegisterForNotifications(r => r.All().Where(b => !b.Hidden), beatmapsChanged); - } - private readonly HashSet setsRequiringUpdate = new HashSet(); private readonly HashSet setsRequiringRemoval = new HashSet(); @@ -366,35 +356,6 @@ namespace osu.Game.Screens.Select BeatmapSetInfo? fetchFromID(Guid id) => realm.Realm.Find(id); } - private void beatmapsChanged(IRealmCollection sender, ChangeSet? changes) - { - // we only care about actual changes in hidden status. - if (changes == null) - return; - - bool changed = false; - - foreach (int i in changes.InsertedIndices) - { - var beatmapInfo = sender[i]; - var beatmapSet = beatmapInfo.BeatmapSet; - - Debug.Assert(beatmapSet != null); - - // Only require to action here if the beatmap is missing. - // This avoids processing these events unnecessarily when new beatmaps are imported, for example. - if (root.BeatmapSetsByID.TryGetValue(beatmapSet.ID, out var existingSets) - && existingSets.SelectMany(s => s.Beatmaps).All(b => b.BeatmapInfo.ID != beatmapInfo.ID)) - { - updateBeatmapSet(beatmapSet.Detach()); - changed = true; - } - } - - if (changed) - invalidateAfterChange(); - } - public void RemoveBeatmapSet(BeatmapSetInfo beatmapSet) => Schedule(() => { removeBeatmapSet(beatmapSet.ID); @@ -1292,12 +1253,5 @@ namespace osu.Game.Screens.Select return ScrollableExtent * ((scrollbarPosition - top_padding) / (ScrollbarMovementExtent - (top_padding + bottom_padding))); } } - - protected override void Dispose(bool isDisposing) - { - base.Dispose(isDisposing); - - subscriptionBeatmaps?.Dispose(); - } } } From dfbccc2144cfe509d1e49bdaaeaa0e3f4e62d334 Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Wed, 27 Nov 2024 00:55:02 -0500 Subject: [PATCH 0134/1275] Knock some sense into the playlists results screen implementation As we're moving towards using the `/playlist//scores/` endpoint, the existing playlists results screen classes needed some restructuring. --- .../TestScenePlaylistsResultsScreen.cs | 136 +++++++++++++----- .../DailyChallenge/DailyChallenge.cs | 2 +- .../Multiplayer/MultiplayerResultsScreen.cs | 2 +- .../Playlists/PlaylistItemResultsScreen.cs | 4 +- .../PlaylistItemScoreResultsScreen.cs | 14 +- .../PlaylistItemUserBestResultsScreen.cs | 41 ++++++ .../PlaylistItemUserResultsScreen.cs | 48 ------- .../OnlinePlay/Playlists/PlaylistsPlayer.cs | 2 +- .../Playlists/PlaylistsRoomSubScreen.cs | 6 +- 9 files changed, 161 insertions(+), 94 deletions(-) create mode 100644 osu.Game/Screens/OnlinePlay/Playlists/PlaylistItemUserBestResultsScreen.cs delete mode 100644 osu.Game/Screens/OnlinePlay/Playlists/PlaylistItemUserResultsScreen.cs diff --git a/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsResultsScreen.cs b/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsResultsScreen.cs index 6ccbcd2859..c288b04da2 100644 --- a/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsResultsScreen.cs +++ b/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsResultsScreen.cs @@ -7,6 +7,7 @@ using System.Linq; using System.Net; using Newtonsoft.Json.Linq; using NUnit.Framework; +using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Testing; using osu.Game.Graphics.Containers; @@ -31,7 +32,7 @@ namespace osu.Game.Tests.Visual.Playlists private const int scores_per_result = 10; private const int real_user_position = 200; - private TestResultsScreen resultsScreen = null!; + private ResultsScreen resultsScreen = null!; private int lowestScoreId; // Score ID of the lowest score in the list. private int highestScoreId; // Score ID of the highest score in the list. @@ -68,11 +69,11 @@ namespace osu.Game.Tests.Visual.Playlists } [Test] - public void TestShowWithUserScore() + public void TestShowUserScore() { AddStep("bind user score info handler", () => bindHandler(userScore: userScore)); - createResults(() => userScore); + createResultsWithScore(() => userScore); waitForDisplay(); AddAssert("user score selected", () => this.ChildrenOfType().Single(p => p.Score.OnlineID == userScore.OnlineID).State == PanelState.Expanded); @@ -81,11 +82,24 @@ namespace osu.Game.Tests.Visual.Playlists } [Test] - public void TestShowNullUserScore() + public void TestShowUserBest() + { + AddStep("bind user score info handler", () => bindHandler(userScore: userScore)); + + createUserBestResults(); + waitForDisplay(); + + AddAssert("user score selected", () => this.ChildrenOfType().Single(p => p.Score.UserID == userScore.UserID).State == PanelState.Expanded); + AddAssert($"score panel position is {real_user_position}", + () => this.ChildrenOfType().Single(p => p.Score.UserID == userScore.UserID).ScorePosition.Value == real_user_position); + } + + [Test] + public void TestShowNonUserScores() { AddStep("bind user score info handler", () => bindHandler()); - createResults(); + createUserBestResults(); waitForDisplay(); AddAssert("top score selected", () => this.ChildrenOfType().OrderByDescending(p => p.Score.TotalScore).First().State == PanelState.Expanded); @@ -96,7 +110,7 @@ namespace osu.Game.Tests.Visual.Playlists { AddStep("bind user score info handler", () => bindHandler(true, userScore)); - createResults(() => userScore); + createResultsWithScore(() => userScore); waitForDisplay(); AddAssert("more than 1 panel displayed", () => this.ChildrenOfType().Count() > 1); @@ -104,11 +118,11 @@ namespace osu.Game.Tests.Visual.Playlists } [Test] - public void TestShowNullUserScoreWithDelay() + public void TestShowNonUserScoresWithDelay() { AddStep("bind delayed handler", () => bindHandler(true)); - createResults(); + createUserBestResults(); waitForDisplay(); AddAssert("top score selected", () => this.ChildrenOfType().OrderByDescending(p => p.Score.TotalScore).First().State == PanelState.Expanded); @@ -119,7 +133,7 @@ namespace osu.Game.Tests.Visual.Playlists { AddStep("bind delayed handler", () => bindHandler(true)); - createResults(); + createUserBestResults(); waitForDisplay(); for (int i = 0; i < 2; i++) @@ -127,13 +141,16 @@ namespace osu.Game.Tests.Visual.Playlists int beforePanelCount = 0; AddStep("get panel count", () => beforePanelCount = this.ChildrenOfType().Count()); - AddStep("scroll to right", () => resultsScreen.ScorePanelList.ChildrenOfType().Single().ScrollToEnd(false)); + AddStep("scroll to right", () => resultsScreen.ChildrenOfType().Single().ChildrenOfType().Single().ScrollToEnd(false)); + + AddAssert("right loading spinner shown", () => + resultsScreen.ChildrenOfType().Single(l => l.Anchor == Anchor.CentreRight).State.Value == Visibility.Visible); - AddAssert("right loading spinner shown", () => resultsScreen.RightSpinner.State.Value == Visibility.Visible); waitForDisplay(); AddAssert($"count increased by {scores_per_result}", () => this.ChildrenOfType().Count() == beforePanelCount + scores_per_result); - AddAssert("right loading spinner hidden", () => resultsScreen.RightSpinner.State.Value == Visibility.Hidden); + AddAssert("right loading spinner hidden", () => + resultsScreen.ChildrenOfType().Single(l => l.Anchor == Anchor.CentreRight).State.Value == Visibility.Hidden); } } @@ -142,29 +159,36 @@ namespace osu.Game.Tests.Visual.Playlists { AddStep("bind delayed handler with scores", () => bindHandler(delayed: true)); - createResults(); + createUserBestResults(); waitForDisplay(); int beforePanelCount = 0; AddStep("get panel count", () => beforePanelCount = this.ChildrenOfType().Count()); - AddStep("scroll to right", () => resultsScreen.ScorePanelList.ChildrenOfType().Single().ScrollToEnd(false)); + AddStep("scroll to right", () => resultsScreen.ChildrenOfType().Single().ChildrenOfType().Single().ScrollToEnd(false)); + + AddAssert("right loading spinner shown", () => + resultsScreen.ChildrenOfType().Single(l => l.Anchor == Anchor.CentreRight).State.Value == Visibility.Visible); - AddAssert("right loading spinner shown", () => resultsScreen.RightSpinner.State.Value == Visibility.Visible); waitForDisplay(); AddAssert($"count increased by {scores_per_result}", () => this.ChildrenOfType().Count() == beforePanelCount + scores_per_result); - AddAssert("right loading spinner hidden", () => resultsScreen.RightSpinner.State.Value == Visibility.Hidden); + AddAssert("right loading spinner hidden", () => + resultsScreen.ChildrenOfType().Single(l => l.Anchor == Anchor.CentreRight).State.Value == Visibility.Hidden); AddStep("get panel count", () => beforePanelCount = this.ChildrenOfType().Count()); AddStep("bind delayed handler with no scores", () => bindHandler(delayed: true, noScores: true)); - AddStep("scroll to right", () => resultsScreen.ScorePanelList.ChildrenOfType().Single().ScrollToEnd(false)); + AddStep("scroll to right", () => resultsScreen.ChildrenOfType().Single().ChildrenOfType().Single().ScrollToEnd(false)); + + AddAssert("right loading spinner shown", () => + resultsScreen.ChildrenOfType().Single(l => l.Anchor == Anchor.CentreRight).State.Value == Visibility.Visible); - AddAssert("right loading spinner shown", () => resultsScreen.RightSpinner.State.Value == Visibility.Visible); waitForDisplay(); AddAssert("count not increased", () => this.ChildrenOfType().Count() == beforePanelCount); - AddAssert("right loading spinner hidden", () => resultsScreen.RightSpinner.State.Value == Visibility.Hidden); + AddAssert("right loading spinner hidden", () => + resultsScreen.ChildrenOfType().Single(l => l.Anchor == Anchor.CentreRight).State.Value == Visibility.Hidden); + AddAssert("no placeholders shown", () => this.ChildrenOfType().Count(), () => Is.Zero); } @@ -173,7 +197,7 @@ namespace osu.Game.Tests.Visual.Playlists { AddStep("bind user score info handler", () => bindHandler(userScore: userScore)); - createResults(() => userScore); + createResultsWithScore(() => userScore); waitForDisplay(); AddStep("bind delayed handler", () => bindHandler(true)); @@ -183,30 +207,36 @@ namespace osu.Game.Tests.Visual.Playlists int beforePanelCount = 0; AddStep("get panel count", () => beforePanelCount = this.ChildrenOfType().Count()); - AddStep("scroll to left", () => resultsScreen.ScorePanelList.ChildrenOfType().Single().ScrollToStart(false)); + AddStep("scroll to left", () => resultsScreen.ChildrenOfType().Single().ChildrenOfType().Single().ScrollToStart(false)); + + AddAssert("left loading spinner shown", () => + resultsScreen.ChildrenOfType().Single(l => l.Anchor == Anchor.CentreLeft).State.Value == Visibility.Visible); - AddAssert("left loading spinner shown", () => resultsScreen.LeftSpinner.State.Value == Visibility.Visible); waitForDisplay(); AddAssert($"count increased by {scores_per_result}", () => this.ChildrenOfType().Count() == beforePanelCount + scores_per_result); - AddAssert("left loading spinner hidden", () => resultsScreen.LeftSpinner.State.Value == Visibility.Hidden); + AddAssert("left loading spinner hidden", () => + resultsScreen.ChildrenOfType().Single(l => l.Anchor == Anchor.CentreLeft).State.Value == Visibility.Hidden); } } + /// + /// Shows the with no scores provided by the API. + /// [Test] - public void TestShowWithNoScores() + public void TestShowUserBestWithNoScoresPresent() { AddStep("bind user score info handler", () => bindHandler(noScores: true)); - createResults(); - AddAssert("no scores visible", () => !resultsScreen.ScorePanelList.GetScorePanels().Any()); + createUserBestResults(); + AddAssert("no scores visible", () => !resultsScreen.ChildrenOfType().Single().GetScorePanels().Any()); AddAssert("placeholder shown", () => this.ChildrenOfType().Count(), () => Is.EqualTo(1)); } - private void createResults(Func? getScore = null) + private void createResultsWithScore(Func getScore) { AddStep("load results", () => { - LoadScreen(resultsScreen = new TestResultsScreen(getScore?.Invoke(), 1, new PlaylistItem(new TestBeatmap(new OsuRuleset().RulesetInfo).BeatmapInfo) + LoadScreen(resultsScreen = new TestScoreResultsScreen(getScore(), 1, new PlaylistItem(new TestBeatmap(new OsuRuleset().RulesetInfo).BeatmapInfo) { RulesetID = new OsuRuleset().RulesetInfo.OnlineID })); @@ -215,14 +245,27 @@ namespace osu.Game.Tests.Visual.Playlists AddUntilStep("wait for screen to load", () => resultsScreen.IsLoaded); } + private void createUserBestResults() + { + AddStep("load results", () => + { + LoadScreen(resultsScreen = new TestUserBestResultsScreen(1, new PlaylistItem(new TestBeatmap(new OsuRuleset().RulesetInfo).BeatmapInfo) + { + RulesetID = new OsuRuleset().RulesetInfo.OnlineID + }, 2)); + }); + + AddUntilStep("wait for screen to load", () => resultsScreen.IsLoaded); + } + private void waitForDisplay() { AddUntilStep("wait for scores loaded", () => requestComplete // request handler may need to fire more than once to get scores. && totalCount > 0 - && resultsScreen.ScorePanelList.GetScorePanels().Count() == totalCount - && resultsScreen.ScorePanelList.AllPanelsVisible); + && resultsScreen.ChildrenOfType().Single().GetScorePanels().Count() == totalCount + && resultsScreen.ChildrenOfType().Single().AllPanelsVisible); AddWaitStep("wait for display", 5); } @@ -232,6 +275,7 @@ namespace osu.Game.Tests.Visual.Playlists switch (request) { case ShowPlaylistScoreRequest: + case ShowPlaylistUserScoreRequest: case IndexPlaylistScoresRequest: break; @@ -261,6 +305,14 @@ namespace osu.Game.Tests.Visual.Playlists break; + case ShowPlaylistUserScoreRequest u: + if (userScore == null) + triggerFail(u); + else + triggerSuccess(u, createUserResponse(userScore)); + + break; + case IndexPlaylistScoresRequest i: triggerSuccess(i, createIndexResponse(i, noScores)); break; @@ -314,7 +366,7 @@ namespace osu.Game.Tests.Visual.Playlists MaxCombo = userScore.MaxCombo, User = new APIUser { - Id = 2, + Id = 2 + i, Username = $"peppy{i}", CoverUrl = "https://osu.ppy.sh/images/headers/profile-covers/c3.jpg", }, @@ -329,7 +381,7 @@ namespace osu.Game.Tests.Visual.Playlists MaxCombo = userScore.MaxCombo, User = new APIUser { - Id = 2, + Id = 2 + i, Username = $"peppy{i}", CoverUrl = "https://osu.ppy.sh/images/headers/profile-covers/c3.jpg", }, @@ -363,7 +415,7 @@ namespace osu.Game.Tests.Visual.Playlists MaxCombo = 1000, User = new APIUser { - Id = 2, + Id = 2 + i, Username = $"peppy{i}", CoverUrl = "https://osu.ppy.sh/images/headers/profile-covers/c3.jpg", }, @@ -410,18 +462,32 @@ namespace osu.Game.Tests.Visual.Playlists }; } - private partial class TestResultsScreen : PlaylistItemUserResultsScreen + private partial class TestScoreResultsScreen : PlaylistItemScoreResultsScreen { public new LoadingSpinner LeftSpinner => base.LeftSpinner; public new LoadingSpinner CentreSpinner => base.CentreSpinner; public new LoadingSpinner RightSpinner => base.RightSpinner; public new ScorePanelList ScorePanelList => base.ScorePanelList; - public TestResultsScreen(ScoreInfo? score, int roomId, PlaylistItem playlistItem) + public TestScoreResultsScreen(ScoreInfo score, int roomId, PlaylistItem playlistItem) : base(score, roomId, playlistItem) { AllowRetry = true; } } + + private partial class TestUserBestResultsScreen : PlaylistItemUserBestResultsScreen + { + public new LoadingSpinner LeftSpinner => base.LeftSpinner; + public new LoadingSpinner CentreSpinner => base.CentreSpinner; + public new LoadingSpinner RightSpinner => base.RightSpinner; + public new ScorePanelList ScorePanelList => base.ScorePanelList; + + public TestUserBestResultsScreen(int roomId, PlaylistItem playlistItem, int userId) + : base(roomId, playlistItem, userId) + { + AllowRetry = true; + } + } } } diff --git a/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallenge.cs b/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallenge.cs index 0dc7e7930a..13a282dd52 100644 --- a/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallenge.cs +++ b/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallenge.cs @@ -345,7 +345,7 @@ namespace osu.Game.Screens.OnlinePlay.DailyChallenge private void presentScore(long id) { if (this.IsCurrentScreen()) - this.Push(new PlaylistItemScoreResultsScreen(room.RoomID!.Value, playlistItem, id)); + this.Push(new PlaylistItemScoreResultsScreen(id, room.RoomID!.Value, playlistItem)); } private void onRoomScoreSet(MultiplayerRoomScoreSetEvent e) diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerResultsScreen.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerResultsScreen.cs index c439df82a6..6b3e8fea46 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerResultsScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerResultsScreen.cs @@ -7,7 +7,7 @@ using osu.Game.Screens.OnlinePlay.Playlists; namespace osu.Game.Screens.OnlinePlay.Multiplayer { - public partial class MultiplayerResultsScreen : PlaylistItemUserResultsScreen + public partial class MultiplayerResultsScreen : PlaylistItemScoreResultsScreen { public MultiplayerResultsScreen(ScoreInfo score, long roomId, PlaylistItem playlistItem) : base(score, roomId, playlistItem) diff --git a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistItemResultsScreen.cs b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistItemResultsScreen.cs index dc06b88823..81ae51bd1b 100644 --- a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistItemResultsScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistItemResultsScreen.cs @@ -191,8 +191,8 @@ namespace osu.Game.Screens.OnlinePlay.Playlists { var scoreInfos = scores.Select(s => s.CreateScoreInfo(ScoreManager, Rulesets, PlaylistItem, Beatmap.Value.BeatmapInfo)).OrderByTotalScore().ToArray(); - // Invoke callback to add the scores. - callback.Invoke(scoreInfos); + // Invoke callback to add the scores. Exclude the score provided to this screen since it's added already. + callback.Invoke(scoreInfos.Where(s => s.OnlineID != Score?.OnlineID)); return scoreInfos; } diff --git a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistItemScoreResultsScreen.cs b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistItemScoreResultsScreen.cs index 32be7f21b0..05c03a4b28 100644 --- a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistItemScoreResultsScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistItemScoreResultsScreen.cs @@ -11,13 +11,19 @@ using osu.Game.Scoring; namespace osu.Game.Screens.OnlinePlay.Playlists { /// - /// Shows a selected arbitrary score for a playlist item, with scores around included. + /// Shows a given score in a playlist item, with scores around included. /// public partial class PlaylistItemScoreResultsScreen : PlaylistItemResultsScreen { private readonly long scoreId; - public PlaylistItemScoreResultsScreen(long roomId, PlaylistItem playlistItem, long scoreId) + public PlaylistItemScoreResultsScreen(ScoreInfo score, long roomId, PlaylistItem playlistItem) + : base(score, roomId, playlistItem) + { + scoreId = score.OnlineID; + } + + public PlaylistItemScoreResultsScreen(long scoreId, long roomId, PlaylistItem playlistItem) : base(null, roomId, playlistItem) { this.scoreId = scoreId; @@ -28,9 +34,7 @@ namespace osu.Game.Screens.OnlinePlay.Playlists protected override ScoreInfo[] PerformSuccessCallback(Action> callback, List scores, MultiplayerScores? pivot = null) { var scoreInfos = base.PerformSuccessCallback(callback, scores, pivot); - - Schedule(() => SelectedScore.Value ??= scoreInfos.SingleOrDefault(score => score.OnlineID == scoreId)); - + Schedule(() => SelectedScore.Value ??= scoreInfos.SingleOrDefault(s => s.OnlineID == scoreId)); return scoreInfos; } } diff --git a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistItemUserBestResultsScreen.cs b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistItemUserBestResultsScreen.cs new file mode 100644 index 0000000000..5b20496dba --- /dev/null +++ b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistItemUserBestResultsScreen.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; +using System.Collections.Generic; +using System.Linq; +using osu.Game.Online.API; +using osu.Game.Online.Rooms; +using osu.Game.Scoring; + +namespace osu.Game.Screens.OnlinePlay.Playlists +{ + /// + /// Shows a user's best score in a playlist item, with scores around included. + /// + public partial class PlaylistItemUserBestResultsScreen : PlaylistItemResultsScreen + { + private readonly int userId; + + public PlaylistItemUserBestResultsScreen(long roomId, PlaylistItem playlistItem, int userId) + : base(null, roomId, playlistItem) + { + this.userId = userId; + } + + protected override APIRequest CreateScoreRequest() => new ShowPlaylistUserScoreRequest(RoomId, PlaylistItem.ID, userId); + + protected override ScoreInfo[] PerformSuccessCallback(Action> callback, List scores, MultiplayerScores? pivot = null) + { + var scoreInfos = base.PerformSuccessCallback(callback, scores, pivot); + + Schedule(() => + { + // Prefer selecting the local user's score, or otherwise default to the first visible score. + SelectedScore.Value ??= scoreInfos.FirstOrDefault(s => s.UserID == userId) ?? scoreInfos.FirstOrDefault(); + }); + + return scoreInfos; + } + } +} diff --git a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistItemUserResultsScreen.cs b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistItemUserResultsScreen.cs deleted file mode 100644 index 22bab7eb93..0000000000 --- a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistItemUserResultsScreen.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 System.Collections.Generic; -using System.Linq; -using osu.Game.Online.API; -using osu.Game.Online.Rooms; -using osu.Game.Scoring; - -namespace osu.Game.Screens.OnlinePlay.Playlists -{ - /// - /// Shows the user's submitted score in a given playlist item, with scores around included. - /// - public partial class PlaylistItemUserResultsScreen : PlaylistItemResultsScreen - { - public PlaylistItemUserResultsScreen(ScoreInfo? score, long roomId, PlaylistItem playlistItem) - : base(score, roomId, playlistItem) - { - } - - protected override APIRequest CreateScoreRequest() => Score != null - ? new ShowPlaylistScoreRequest(RoomId, PlaylistItem.ID, Score.OnlineID) - : new ShowPlaylistUserScoreRequest(RoomId, PlaylistItem.ID, API.LocalUser.Value.Id); - - protected override ScoreInfo[] PerformSuccessCallback(Action> callback, List scores, MultiplayerScores? pivot = null) - { - var scoreInfos = scores.Select(s => s.CreateScoreInfo(ScoreManager, Rulesets, PlaylistItem, Beatmap.Value.BeatmapInfo)).OrderByTotalScore().ToArray(); - - // Select a score if we don't already have one selected. - // Note: This is done before the callback so that the panel list centres on the selected score before panels are added (eliminating initial scroll). - if (SelectedScore.Value == null) - { - Schedule(() => - { - // Prefer selecting the local user's score, or otherwise default to the first visible score. - SelectedScore.Value = scoreInfos.FirstOrDefault(s => s.User.OnlineID == API.LocalUser.Value.Id) ?? scoreInfos.FirstOrDefault(); - }); - } - - // Invoke callback to add the scores. Exclude the user's current score which was added previously. - callback.Invoke(scoreInfos.Where(s => s.OnlineID != Score?.OnlineID)); - - return scoreInfos; - } - } -} diff --git a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsPlayer.cs b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsPlayer.cs index 7ca09b5563..b82c2404ab 100644 --- a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsPlayer.cs +++ b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsPlayer.cs @@ -56,7 +56,7 @@ namespace osu.Game.Screens.OnlinePlay.Playlists protected override ResultsScreen CreateResults(ScoreInfo score) { Debug.Assert(Room.RoomID != null); - return new PlaylistItemUserResultsScreen(score, Room.RoomID.Value, PlaylistItem) + return new PlaylistItemScoreResultsScreen(score, Room.RoomID.Value, PlaylistItem) { AllowRetry = true, ShowUserStatistics = true, diff --git a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs index 44d1841fb8..1aaae60195 100644 --- a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs @@ -12,6 +12,7 @@ using osu.Framework.Logging; using osu.Framework.Screens; using osu.Game.Graphics.Cursor; using osu.Game.Input; +using osu.Game.Online.API; using osu.Game.Online.Rooms; using osu.Game.Screens.OnlinePlay.Components; using osu.Game.Screens.OnlinePlay.Match; @@ -32,6 +33,9 @@ namespace osu.Game.Screens.OnlinePlay.Playlists private readonly IBindable isIdle = new BindableBool(); + [Resolved] + private IAPIProvider api { get; set; } = null!; + [Resolved(CanBeNull = true)] private IdleTracker? idleTracker { get; set; } @@ -143,7 +147,7 @@ namespace osu.Game.Screens.OnlinePlay.Playlists RequestResults = item => { Debug.Assert(Room.RoomID != null); - ParentScreen?.Push(new PlaylistItemUserResultsScreen(null, Room.RoomID.Value, item)); + ParentScreen?.Push(new PlaylistItemUserBestResultsScreen(Room.RoomID.Value, item, api.LocalUser.Value.Id)); } } }, From 5260a401d4a96241f4ea21cc88e7a8b840193c61 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 27 Nov 2024 15:09:54 +0900 Subject: [PATCH 0135/1275] Use `RealmLive` in `SaveFailedScoreButton` This also optimises the manager classes to better support `Live` usage where the managed object is already in a good state (ie. doesn't require re-fetching). --- osu.Game/Beatmaps/BeatmapManager.cs | 6 +++++- osu.Game/Scoring/ScoreManager.cs | 12 +++++++++--- osu.Game/Screens/Play/SaveFailedScoreButton.cs | 10 +++++----- 3 files changed, 19 insertions(+), 9 deletions(-) diff --git a/osu.Game/Beatmaps/BeatmapManager.cs b/osu.Game/Beatmaps/BeatmapManager.cs index 4191771116..f1ce977d96 100644 --- a/osu.Game/Beatmaps/BeatmapManager.cs +++ b/osu.Game/Beatmaps/BeatmapManager.cs @@ -559,7 +559,11 @@ namespace osu.Game.Beatmaps // If we seem to be missing files, now is a good time to re-fetch. bool missingFiles = beatmapInfo.BeatmapSet?.Files.Count == 0; - if (refetch || beatmapInfo.IsManaged || missingFiles) + if (beatmapInfo.IsManaged) + { + beatmapInfo = beatmapInfo.Detach(); + } + else if (refetch || missingFiles) { Guid id = beatmapInfo.ID; beatmapInfo = Realm.Run(r => r.Find(id)?.Detach()) ?? beatmapInfo; diff --git a/osu.Game/Scoring/ScoreManager.cs b/osu.Game/Scoring/ScoreManager.cs index e3601fe91e..3177873182 100644 --- a/osu.Game/Scoring/ScoreManager.cs +++ b/osu.Game/Scoring/ScoreManager.cs @@ -78,7 +78,7 @@ namespace osu.Game.Scoring /// Perform a lookup query on available s. /// /// The query. - /// The first result for the provided query, or null if no results were found. + /// The first result for the provided query in its detached form, or null if no results were found. public ScoreInfo? Query(Expression> query) { return Realm.Run(r => r.All().FirstOrDefault(query)?.Detach()); @@ -88,8 +88,14 @@ namespace osu.Game.Scoring { ScoreInfo? databasedScoreInfo = null; - if (originalScoreInfo is ScoreInfo scoreInfo && !string.IsNullOrEmpty(scoreInfo.Hash)) - databasedScoreInfo = Query(s => s.Hash == scoreInfo.Hash); + if (originalScoreInfo is ScoreInfo scoreInfo) + { + if (scoreInfo.IsManaged) + return scoreInfo.Detach(); + + if (!string.IsNullOrEmpty(scoreInfo.Hash)) + databasedScoreInfo = Query(s => s.Hash == scoreInfo.Hash); + } if (originalScoreInfo.OnlineID > 0) databasedScoreInfo ??= Query(s => s.OnlineID == originalScoreInfo.OnlineID); diff --git a/osu.Game/Screens/Play/SaveFailedScoreButton.cs b/osu.Game/Screens/Play/SaveFailedScoreButton.cs index 4f665b87e8..e5c9e115d1 100644 --- a/osu.Game/Screens/Play/SaveFailedScoreButton.cs +++ b/osu.Game/Screens/Play/SaveFailedScoreButton.cs @@ -32,7 +32,7 @@ namespace osu.Game.Screens.Play private readonly Func>? importFailedScore; - private ScoreInfo? importedScore; + private Live? importedScore; private DownloadButton button = null!; @@ -55,7 +55,7 @@ namespace osu.Game.Screens.Play switch (state.Value) { case DownloadState.LocallyAvailable: - game?.PresentScore(importedScore, ScorePresentType.Gameplay); + game?.PresentScore(importedScore?.Value, ScorePresentType.Gameplay); break; case DownloadState.NotDownloaded: @@ -65,7 +65,7 @@ namespace osu.Game.Screens.Play { Task.Run(importFailedScore).ContinueWith(t => { - importedScore = realm.Run(r => r.Find(t.GetResultSafely().ID)?.Detach()); + importedScore = realm.Run?>(r => r.Find(t.GetResultSafely().ID)?.ToLive(realm)); Schedule(() => state.Value = importedScore != null ? DownloadState.LocallyAvailable : DownloadState.NotDownloaded); }).FireAndForget(); } @@ -77,7 +77,7 @@ namespace osu.Game.Screens.Play if (player != null) { - importedScore = realm.Run(r => r.Find(player.Score.ScoreInfo.ID)?.Detach()); + importedScore = realm.Run(r => r.Find(player.Score.ScoreInfo.ID)?.ToLive(realm)); state.Value = importedScore != null ? DownloadState.LocallyAvailable : DownloadState.NotDownloaded; } @@ -137,7 +137,7 @@ namespace osu.Game.Screens.Play { if (state.NewValue != DownloadState.LocallyAvailable) return; - if (importedScore != null) scoreManager.Export(importedScore); + if (importedScore != null) scoreManager.Export(importedScore.Value); this.state.ValueChanged -= exportWhenReady; } From 4fcc76270a276421c998f3e9b668b110bd69e207 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 27 Nov 2024 15:46:55 +0900 Subject: [PATCH 0136/1275] Ensure events are unbound on disposal as a safety --- .../Overlays/Chat/ChannelList/ChannelList.cs | 28 ++++++++++++++++--- 1 file changed, 24 insertions(+), 4 deletions(-) diff --git a/osu.Game/Overlays/Chat/ChannelList/ChannelList.cs b/osu.Game/Overlays/Chat/ChannelList/ChannelList.cs index 3e8c71e645..f027888962 100644 --- a/osu.Game/Overlays/Chat/ChannelList/ChannelList.cs +++ b/osu.Game/Overlays/Chat/ChannelList/ChannelList.cs @@ -171,10 +171,12 @@ namespace osu.Game.Overlays.Chat.ChannelList public partial class ChannelGroup : FillFlowContainer { + private readonly bool sortByRecent; public readonly ChannelListItemFlow ItemFlow; public ChannelGroup(LocalisableString label, bool sortByRecent) { + this.sortByRecent = sortByRecent; Direction = FillDirection.Vertical; RelativeSizeAxes = Axes.X; AutoSizeAxes = Axes.Y; @@ -217,21 +219,39 @@ namespace osu.Game.Overlays.Chat.ChannelList { ItemFlow.Add(item); - item.Channel.NewMessagesArrived += newMessagesArrived; - item.Channel.PendingMessageResolved += pendingMessageResolved; + if (sortByRecent) + { + item.Channel.NewMessagesArrived += newMessagesArrived; + item.Channel.PendingMessageResolved += pendingMessageResolved; + } ItemFlow.Reflow(); } public void RemoveChannel(ChannelListItem item) { - item.Channel.NewMessagesArrived -= newMessagesArrived; - item.Channel.PendingMessageResolved -= pendingMessageResolved; + if (sortByRecent) + { + item.Channel.NewMessagesArrived -= newMessagesArrived; + item.Channel.PendingMessageResolved -= pendingMessageResolved; + } + ItemFlow.Remove(item, true); } private void pendingMessageResolved(LocalEchoMessage _, Message __) => ItemFlow.Reflow(); private void newMessagesArrived(IEnumerable _) => ItemFlow.Reflow(); + + protected override void Dispose(bool isDisposing) + { + base.Dispose(isDisposing); + + foreach (var item in ItemFlow) + { + item.Channel.NewMessagesArrived -= newMessagesArrived; + item.Channel.PendingMessageResolved -= pendingMessageResolved; + } + } } private partial class ChannelSearchTextBox : BasicSearchTextBox From c3ac6d7fe5a89888e62ebe88a55ac5c21f0efa33 Mon Sep 17 00:00:00 2001 From: HenintsoaSky Date: Wed, 27 Nov 2024 10:22:30 +0300 Subject: [PATCH 0137/1275] Delete changes.patch oops --- changes.patch | 0 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 changes.patch diff --git a/changes.patch b/changes.patch deleted file mode 100644 index e69de29bb2..0000000000 From 9c707ed3418b5bde88dd76d5d5f790fb69decb1b Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 27 Nov 2024 16:47:54 +0900 Subject: [PATCH 0138/1275] Rename class and fix padding considerations --- ...ings.cs => ExpandingPlayerSettingsOverlay.cs} | 16 ++++++++++++---- .../Multiplayer/Spectate/MultiSpectatorScreen.cs | 2 +- 2 files changed, 13 insertions(+), 5 deletions(-) rename osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/{MultiSpectatorSettings.cs => ExpandingPlayerSettingsOverlay.cs} (75%) diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorSettings.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/ExpandingPlayerSettingsOverlay.cs similarity index 75% rename from osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorSettings.cs rename to osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/ExpandingPlayerSettingsOverlay.cs index dfb26d104a..6ad53d4aeb 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorSettings.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/ExpandingPlayerSettingsOverlay.cs @@ -8,15 +8,21 @@ using osu.Framework.Input.Events; using osu.Game.Graphics.Containers; using osu.Game.Graphics.UserInterface; using osu.Game.Screens.Play.HUD; +using osuTK; namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate { - public partial class MultiSpectatorSettings : ExpandingContainer + public partial class ExpandingPlayerSettingsOverlay : ExpandingContainer { - public const float CONTRACTED_WIDTH = 30; - public const int EXPANDED_WIDTH = 300; + private const float padding = 10; - public MultiSpectatorSettings() + public const float CONTRACTED_WIDTH = button_size + padding * 2; + public const float EXPANDED_WIDTH = player_settings_width + button_size + padding * 3; + + private const float player_settings_width = 270; + private const float button_size = IconButton.DEFAULT_BUTTON_SIZE; + + public ExpandingPlayerSettingsOverlay() : base(CONTRACTED_WIDTH, EXPANDED_WIDTH) { Origin = Anchor.TopRight; @@ -30,6 +36,8 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate Anchor = Anchor.TopLeft, Direction = FillDirection.Horizontal, AutoSizeAxes = Axes.Both, + Margin = new MarginPadding(padding), + Spacing = new Vector2(padding), Children = new Drawable[] { new IconButton diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorScreen.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorScreen.cs index 841aaf7a45..44d26a6dd0 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorScreen.cs @@ -127,7 +127,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate { ReadyToStart = performInitialSeek, }, - new MultiSpectatorSettings() + new ExpandingPlayerSettingsOverlay() }; for (int i = 0; i < Users.Count; i++) From 782ce24ca67c08525ff8c4f3d4382c5627b6af8e Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 27 Nov 2024 17:09:13 +0900 Subject: [PATCH 0139/1275] Move player settings out of right flow --- osu.Game/Screens/Play/HUDOverlay.cs | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/osu.Game/Screens/Play/HUDOverlay.cs b/osu.Game/Screens/Play/HUDOverlay.cs index a6c2405eb6..62d9686aad 100644 --- a/osu.Game/Screens/Play/HUDOverlay.cs +++ b/osu.Game/Screens/Play/HUDOverlay.cs @@ -146,7 +146,6 @@ namespace osu.Game.Screens.Play Children = new Drawable[] { ModDisplay = CreateModsContainer(), - PlayerSettingsOverlay = CreatePlayerSettingsOverlay(), } }, bottomRightElements = new FillFlowContainer @@ -164,6 +163,7 @@ namespace osu.Game.Screens.Play HoldToQuit = CreateHoldForMenuButton(), } }, + PlayerSettingsOverlay = new PlayerSettingsOverlay(), LeaderboardFlow = new FillFlowContainer { AutoSizeAxes = Axes.Both, @@ -173,7 +173,7 @@ namespace osu.Game.Screens.Play }, }; - hideTargets = new List { mainComponents, topRightElements }; + hideTargets = new List { mainComponents, topRightElements, PlayerSettingsOverlay }; if (rulesetComponents != null) hideTargets.Add(rulesetComponents); @@ -389,8 +389,6 @@ namespace osu.Game.Screens.Play Origin = Anchor.TopRight, }; - protected PlayerSettingsOverlay CreatePlayerSettingsOverlay() => new PlayerSettingsOverlay(); - public bool OnPressed(KeyBindingPressEvent e) { if (e.Repeat) From 7fdf13911b7500a4cc9b08259655aabc49142142 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 27 Nov 2024 17:47:27 +0900 Subject: [PATCH 0140/1275] Adjust the colour of non-pinned settings groups' headers to be more legible --- osu.Game/Overlays/SettingsToolboxGroup.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Overlays/SettingsToolboxGroup.cs b/osu.Game/Overlays/SettingsToolboxGroup.cs index 53849fa53c..f8cf218564 100644 --- a/osu.Game/Overlays/SettingsToolboxGroup.cs +++ b/osu.Game/Overlays/SettingsToolboxGroup.cs @@ -184,7 +184,7 @@ namespace osu.Game.Overlays content.ResizeHeightTo(0, animate ? transition_duration : 0, Easing.OutQuint); } - headerContent.FadeColour(Expanded.Value ? Color4.White : OsuColour.Gray(0.5f), 200, Easing.OutQuint); + headerContent.FadeColour(Expanded.Value ? Color4.White : OsuColour.Gray(0.7f), 200, Easing.OutQuint); } private void updateFadeState() From 0f739418084d99925a0e91691ed459122aec23d1 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 27 Nov 2024 17:47:42 +0900 Subject: [PATCH 0141/1275] Combine new implementation back into the old one and use everywhere --- .../ExpandingPlayerSettingsOverlay.cs | 68 -------------- .../Spectate/MultiSpectatorScreen.cs | 2 +- .../Screens/Play/HUD/PlayerSettingsOverlay.cs | 88 +++++++++++++++---- 3 files changed, 74 insertions(+), 84 deletions(-) delete mode 100644 osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/ExpandingPlayerSettingsOverlay.cs diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/ExpandingPlayerSettingsOverlay.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/ExpandingPlayerSettingsOverlay.cs deleted file mode 100644 index 6ad53d4aeb..0000000000 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/ExpandingPlayerSettingsOverlay.cs +++ /dev/null @@ -1,68 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -using osu.Framework.Graphics; -using osu.Framework.Graphics.Containers; -using osu.Framework.Graphics.Sprites; -using osu.Framework.Input.Events; -using osu.Game.Graphics.Containers; -using osu.Game.Graphics.UserInterface; -using osu.Game.Screens.Play.HUD; -using osuTK; - -namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate -{ - public partial class ExpandingPlayerSettingsOverlay : ExpandingContainer - { - private const float padding = 10; - - public const float CONTRACTED_WIDTH = button_size + padding * 2; - public const float EXPANDED_WIDTH = player_settings_width + button_size + padding * 3; - - private const float player_settings_width = 270; - private const float button_size = IconButton.DEFAULT_BUTTON_SIZE; - - public ExpandingPlayerSettingsOverlay() - : base(CONTRACTED_WIDTH, EXPANDED_WIDTH) - { - Origin = Anchor.TopRight; - Anchor = Anchor.TopRight; - - PlayerSettingsOverlay playerSettingsOverlay; - - InternalChild = new FillFlowContainer - { - Origin = Anchor.TopLeft, - Anchor = Anchor.TopLeft, - Direction = FillDirection.Horizontal, - AutoSizeAxes = Axes.Both, - Margin = new MarginPadding(padding), - Spacing = new Vector2(padding), - Children = new Drawable[] - { - new IconButton - { - Icon = FontAwesome.Solid.Cog, - Origin = Anchor.TopLeft, - Anchor = Anchor.TopLeft, - Action = () => Expanded.Toggle() - }, - playerSettingsOverlay = new PlayerSettingsOverlay - { - Anchor = Anchor.TopLeft, - Origin = Anchor.TopLeft - } - } - }; - - playerSettingsOverlay.Show(); - } - - protected override void OnHoverLost(HoverLostEvent e) - { - // Prevent unexpanding when hovering player settings - if (!Contains(e.ScreenSpaceMousePosition)) - base.OnHoverLost(e); - } - } -} diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorScreen.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorScreen.cs index 44d26a6dd0..33c3c60ed3 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorScreen.cs @@ -127,7 +127,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate { ReadyToStart = performInitialSeek, }, - new ExpandingPlayerSettingsOverlay() + new PlayerSettingsOverlay() }; for (int i = 0; i < Users.Count; i++) diff --git a/osu.Game/Screens/Play/HUD/PlayerSettingsOverlay.cs b/osu.Game/Screens/Play/HUD/PlayerSettingsOverlay.cs index a2b49f6302..e68ca4da7a 100644 --- a/osu.Game/Screens/Play/HUD/PlayerSettingsOverlay.cs +++ b/osu.Game/Screens/Play/HUD/PlayerSettingsOverlay.cs @@ -1,46 +1,104 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; +using osu.Framework.Graphics.Colour; using osu.Framework.Graphics.Containers; -using osuTK; +using osu.Framework.Graphics.Shapes; +using osu.Framework.Graphics.Sprites; +using osu.Framework.Input; +using osu.Framework.Input.Events; +using osu.Game.Graphics.Containers; +using osu.Game.Graphics.UserInterface; using osu.Game.Screens.Play.PlayerSettings; +using osuTK; +using osuTK.Graphics; namespace osu.Game.Screens.Play.HUD { - public partial class PlayerSettingsOverlay : VisibilityContainer + public partial class PlayerSettingsOverlay : ExpandingContainer { + public VisualSettings VisualSettings { get; private set; } + + private const float padding = 10; + + public const float CONTRACTED_WIDTH = button_size + padding * 2; + public const float EXPANDED_WIDTH = player_settings_width + padding * 2; + + private const float player_settings_width = 270; + private const float button_size = IconButton.DEFAULT_BUTTON_SIZE; + + public override void Show() => this.FadeIn(fade_duration); + public override void Hide() => this.FadeOut(fade_duration); + private const int fade_duration = 200; - public readonly VisualSettings VisualSettings; + // we'll handle this ourselves because we have slightly custom logic. + protected override bool ExpandOnHover => false; protected override Container Content => content; private readonly FillFlowContainer content; - public PlayerSettingsOverlay() - { - Anchor = Anchor.TopRight; - Origin = Anchor.TopRight; - AutoSizeAxes = Axes.Both; + private readonly IconButton button; - InternalChild = content = new FillFlowContainer + private InputManager inputManager = null!; + + public PlayerSettingsOverlay() + : base(0, EXPANDED_WIDTH) + { + Origin = Anchor.TopRight; + Anchor = Anchor.TopRight; + + base.Content.Add(content = new FillFlowContainer { - Anchor = Anchor.TopRight, - Origin = Anchor.TopRight, AutoSizeAxes = Axes.Both, Direction = FillDirection.Vertical, Spacing = new Vector2(0, 20), + Margin = new MarginPadding(padding), Children = new PlayerSettingsGroup[] { VisualSettings = new VisualSettings { Expanded = { Value = false } }, new AudioSettings { Expanded = { Value = false } } } - }; + }); + + AddInternal(button = new IconButton + { + Icon = FontAwesome.Solid.Cog, + Origin = Anchor.TopRight, + Anchor = Anchor.TopLeft, + Margin = new MarginPadding(5), + Action = () => Expanded.Toggle() + }); + + AddInternal(new Box + { + Colour = ColourInfo.GradientHorizontal(Color4.Black.Opacity(0), Color4.Black.Opacity(0.8f)), + Depth = float.MaxValue, + RelativeSizeAxes = Axes.Both, + }); } - protected override void PopIn() => this.FadeIn(fade_duration); - protected override void PopOut() => this.FadeOut(fade_duration); + protected override void LoadComplete() + { + base.LoadComplete(); + + inputManager = GetContainingInputManager()!; + } + + protected override void Update() + { + base.Update(); + + Expanded.Value = inputManager.CurrentState.Mouse.Position.X >= button.ScreenSpaceDrawQuad.TopLeft.X; + } + + protected override void OnHoverLost(HoverLostEvent e) + { + // handle un-expanding manually because our children do weird hover blocking stuff. + } public void AddAtStart(PlayerSettingsGroup drawable) => content.Insert(-1, drawable); } From b70fb4b0fe747977871c04a49feaad1a9b175654 Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Wed, 27 Nov 2024 05:52:43 -0500 Subject: [PATCH 0142/1275] Add `FormBeatmapFileSelector` for intermediate user-choice step --- .../UserInterfaceV2/FormFileSelector.cs | 30 +++- .../Edit/Setup/FormBeatmapFileSelector.cs | 161 ++++++++++++++++++ 2 files changed, 186 insertions(+), 5 deletions(-) create mode 100644 osu.Game/Screens/Edit/Setup/FormBeatmapFileSelector.cs diff --git a/osu.Game/Graphics/UserInterfaceV2/FormFileSelector.cs b/osu.Game/Graphics/UserInterfaceV2/FormFileSelector.cs index 81023417a5..5fdf453fc4 100644 --- a/osu.Game/Graphics/UserInterfaceV2/FormFileSelector.cs +++ b/osu.Game/Graphics/UserInterfaceV2/FormFileSelector.cs @@ -242,20 +242,26 @@ namespace osu.Game.Graphics.UserInterfaceV2 Task ICanAcceptFiles.Import(ImportTask[] tasks, ImportParameters parameters) => throw new NotImplementedException(); + protected virtual FileChooserPopover CreatePopover(string[] handledExtensions, Bindable current, string? chooserPath) => new FileChooserPopover(handledExtensions, current, chooserPath); + public Popover GetPopover() { - var popover = new FileChooserPopover(handledExtensions, Current, initialChooserPath); + var popover = CreatePopover(handledExtensions, Current, initialChooserPath); popoverState.UnbindBindings(); popoverState.BindTo(popover.State); return popover; } - private partial class FileChooserPopover : OsuPopover + protected partial class FileChooserPopover : OsuPopover { protected override string PopInSampleName => "UI/overlay-big-pop-in"; protected override string PopOutSampleName => "UI/overlay-big-pop-out"; - public FileChooserPopover(string[] handledExtensions, Bindable currentFile, string? chooserPath) + private readonly Bindable current = new Bindable(); + + protected OsuFileSelector FileSelector; + + public FileChooserPopover(string[] handledExtensions, Bindable current, string? chooserPath) : base(false) { Child = new Container @@ -264,12 +270,13 @@ namespace osu.Game.Graphics.UserInterfaceV2 // simplest solution to avoid underlying text to bleed through the bottom border // https://github.com/ppy/osu/pull/30005#issuecomment-2378884430 Padding = new MarginPadding { Bottom = 1 }, - Child = new OsuFileSelector(chooserPath, handledExtensions) + Child = FileSelector = new OsuFileSelector(chooserPath, handledExtensions) { RelativeSizeAxes = Axes.Both, - CurrentFile = { BindTarget = currentFile } }, }; + + this.current.BindTo(current); } [BackgroundDependencyLoader] @@ -292,6 +299,19 @@ namespace osu.Game.Graphics.UserInterfaceV2 } }); } + + protected override void LoadComplete() + { + base.LoadComplete(); + + FileSelector.CurrentFile.ValueChanged += f => + { + if (f.NewValue != null) + OnFileSelected(f.NewValue); + }; + } + + protected virtual void OnFileSelected(FileInfo file) => current.Value = file; } } } diff --git a/osu.Game/Screens/Edit/Setup/FormBeatmapFileSelector.cs b/osu.Game/Screens/Edit/Setup/FormBeatmapFileSelector.cs new file mode 100644 index 0000000000..317ed1b903 --- /dev/null +++ b/osu.Game/Screens/Edit/Setup/FormBeatmapFileSelector.cs @@ -0,0 +1,161 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Diagnostics; +using System.IO; +using osu.Framework.Allocation; +using osu.Framework.Bindables; +using osu.Framework.Extensions.Color4Extensions; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; +using osu.Game.Graphics; +using osu.Game.Graphics.Sprites; +using osu.Game.Graphics.UserInterfaceV2; +using osu.Game.Overlays; +using osuTK; + +namespace osu.Game.Screens.Edit.Setup +{ + /// + /// A type of dedicated to beatmap resources. + /// + /// + /// This expands on by adding an intermediate step before finalisation + /// to choose whether the selected file should be applied to the current difficulty or all difficulties in the set, + /// the user's choice is saved in before the file selection is finalised and propagated to . + /// + public partial class FormBeatmapFileSelector : FormFileSelector + { + private readonly bool multipleDifficulties; + + public readonly Bindable ApplyToAllDifficulties = new Bindable(true); + + public FormBeatmapFileSelector(bool multipleDifficulties, params string[] handledExtensions) + : base(handledExtensions) + { + this.multipleDifficulties = multipleDifficulties; + } + + protected override FileChooserPopover CreatePopover(string[] handledExtensions, Bindable current, string? chooserPath) + { + var popover = new BeatmapFileChooserPopover(handledExtensions, current, chooserPath, multipleDifficulties); + + popover.ApplyToAllDifficulties.ValueChanged += v => + { + Debug.Assert(v.NewValue != null); + ApplyToAllDifficulties.Value = v.NewValue.Value; + }; + + return popover; + } + + private partial class BeatmapFileChooserPopover : FileChooserPopover + { + private readonly bool multipleDifficulties; + + public readonly Bindable ApplyToAllDifficulties = new Bindable(); + + private Container changeScopeContainer = null!; + + public BeatmapFileChooserPopover(string[] handledExtensions, Bindable current, string? chooserPath, bool multipleDifficulties) + : base(handledExtensions, current, chooserPath) + { + this.multipleDifficulties = multipleDifficulties; + } + + [BackgroundDependencyLoader] + private void load(OverlayColourProvider colourProvider, OsuColour colours) + { + Add(changeScopeContainer = new InputBlockingContainer + { + Alpha = 0f, + RelativeSizeAxes = Axes.Both, + Children = new Drawable[] + { + new Box + { + Colour = colourProvider.Background6.Opacity(0.9f), + RelativeSizeAxes = Axes.Both, + }, + new Container + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Masking = true, + CornerRadius = 10f, + AutoSizeAxes = Axes.Both, + Children = new Drawable[] + { + new Box + { + Colour = colourProvider.Background5, + RelativeSizeAxes = Axes.Both, + }, + new FillFlowContainer + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + AutoSizeAxes = Axes.Both, + Direction = FillDirection.Vertical, + Spacing = new Vector2(0f, 10f), + Margin = new MarginPadding(30), + Children = new Drawable[] + { + new OsuSpriteText + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Text = "Apply this change to all difficulties?", + Margin = new MarginPadding { Bottom = 20f }, + }, + new RoundedButton + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Width = 300f, + Text = "Apply to all difficulties", + Action = () => ApplyToAllDifficulties.Value = true, + BackgroundColour = colours.Red2, + }, + new RoundedButton + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Width = 300f, + Text = "Only apply to this difficulty", + Action = () => ApplyToAllDifficulties.Value = false, + }, + } + } + } + }, + } + }); + } + + protected override void LoadComplete() + { + base.LoadComplete(); + ApplyToAllDifficulties.ValueChanged += onChangeScopeSelected; + } + + protected override void OnFileSelected(FileInfo file) + { + if (multipleDifficulties) + changeScopeContainer.FadeIn(200, Easing.InQuint); + else + base.OnFileSelected(file); + } + + private void onChangeScopeSelected(ValueChangedEvent c) + { + if (c.NewValue == null) + return; + + Debug.Assert(FileSelector.CurrentFile.Value != null); + base.OnFileSelected(FileSelector.CurrentFile.Value); + } + } + } +} From efb68e423268a289898c1a5967d20fe73a58b78d Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Wed, 27 Nov 2024 05:53:22 -0500 Subject: [PATCH 0143/1275] Refactor `ResourcesSection` to support new form of selection --- .../Screens/Edit/Setup/ResourcesSection.cs | 188 ++++++++---------- 1 file changed, 88 insertions(+), 100 deletions(-) diff --git a/osu.Game/Screens/Edit/Setup/ResourcesSection.cs b/osu.Game/Screens/Edit/Setup/ResourcesSection.cs index 90603a6366..70282878e0 100644 --- a/osu.Game/Screens/Edit/Setup/ResourcesSection.cs +++ b/osu.Game/Screens/Edit/Setup/ResourcesSection.cs @@ -9,7 +9,6 @@ using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Localisation; using osu.Game.Beatmaps; -using osu.Game.Graphics.UserInterfaceV2; using osu.Game.Overlays; using osu.Game.Localisation; using osu.Game.Models; @@ -19,8 +18,8 @@ namespace osu.Game.Screens.Edit.Setup { public partial class ResourcesSection : SetupSection { - private FormFileSelector audioTrackChooser = null!; - private FormFileSelector backgroundChooser = null!; + private FormBeatmapFileSelector audioTrackChooser = null!; + private FormBeatmapFileSelector backgroundChooser = null!; public override LocalisableString Title => EditorSetupStrings.ResourcesHeader; @@ -40,7 +39,6 @@ namespace osu.Game.Screens.Edit.Setup private Editor? editor { get; set; } private SetupScreenHeaderBackground headerBackground = null!; - private RoundedButton syncResourcesButton = null!; [BackgroundDependencyLoader] private void load() @@ -51,25 +49,20 @@ namespace osu.Game.Screens.Edit.Setup Height = 110, }; + bool multipleDifficulties = working.Value.BeatmapSetInfo.Beatmaps.Count > 1; + Children = new Drawable[] { - backgroundChooser = new FormFileSelector(".jpg", ".jpeg", ".png") + backgroundChooser = new FormBeatmapFileSelector(multipleDifficulties, ".jpg", ".jpeg", ".png") { Caption = GameplaySettingsStrings.BackgroundHeader, PlaceholderText = EditorSetupStrings.ClickToSelectBackground, }, - audioTrackChooser = new FormFileSelector(".mp3", ".ogg") + audioTrackChooser = new FormBeatmapFileSelector(multipleDifficulties, ".mp3", ".ogg") { Caption = EditorSetupStrings.AudioTrack, PlaceholderText = EditorSetupStrings.ClickToSelectTrack, }, - syncResourcesButton = new RoundedButton - { - RelativeSizeAxes = Axes.X, - Text = EditorSetupStrings.ResourcesUpdateAllDifficulties, - Action = syncResources, - Enabled = { Value = false }, - } }; backgroundChooser.PreviewContainer.Add(headerBackground); @@ -84,39 +77,56 @@ namespace osu.Game.Screens.Edit.Setup audioTrackChooser.Current.BindValueChanged(audioTrackChanged); } - private string? newBackgroundFile; - private string? newAudioFile; - - public bool ChangeBackgroundImage(FileInfo source) + public bool ChangeBackgroundImage(FileInfo source, bool applyToAllDifficulties) { if (!source.Exists) return false; - var beatmap = working.Value.BeatmapInfo; var set = working.Value.BeatmapSetInfo; - string[] filenames = set.Files.Select(f => f.Filename).Where(f => - f.StartsWith(@"bg", StringComparison.OrdinalIgnoreCase) && - f.EndsWith(source.Extension, StringComparison.OrdinalIgnoreCase)).ToArray(); - - string currentFilename = working.Value.Metadata.BackgroundFile; - string? newFilename = null; - - var oldFile = set.GetFile(currentFilename); - - if (oldFile != null && set.Beatmaps.Where(b => !b.Equals(beatmap)).All(b => b.Metadata.BackgroundFile != currentFilename)) + if (applyToAllDifficulties) { - beatmaps.DeleteFile(set, oldFile); - newFilename = currentFilename; + string newFilename = $@"bg{source.Extension}"; + + foreach (var beatmapInSet in set.Beatmaps) + { + if (set.GetFile(beatmapInSet.Metadata.BackgroundFile) is RealmNamedFileUsage existingFile) + beatmaps.DeleteFile(set, existingFile); + + if (beatmapInSet.Metadata.BackgroundFile != newFilename) + { + beatmapInSet.Metadata.BackgroundFile = newFilename; + + if (!beatmapInSet.Equals(working.Value.BeatmapInfo)) + beatmaps.Save(beatmapInSet, beatmaps.GetWorkingBeatmap(beatmapInSet).Beatmap); + } + } + } + else + { + var beatmap = working.Value.BeatmapInfo; + + string[] filenames = set.Files.Select(f => f.Filename).Where(f => + f.StartsWith(@"bg", StringComparison.OrdinalIgnoreCase) && + f.EndsWith(source.Extension, StringComparison.OrdinalIgnoreCase)).ToArray(); + + string currentFilename = working.Value.Metadata.BackgroundFile; + + var oldFile = set.GetFile(currentFilename); + string? newFilename = null; + + if (oldFile != null && set.Beatmaps.Where(b => !b.Equals(beatmap)).All(b => b.Metadata.BackgroundFile != currentFilename)) + { + beatmaps.DeleteFile(set, oldFile); + newFilename = currentFilename; + } + + newFilename ??= NamingUtils.GetNextBestFilename(filenames, $@"bg{source.Extension}"); + working.Value.Metadata.BackgroundFile = newFilename; } - newFilename ??= NamingUtils.GetNextBestFilename(filenames, $@"bg{source.Extension}"); - using (var stream = source.OpenRead()) - beatmaps.AddFile(set, stream, newFilename); - - working.Value.Metadata.BackgroundFile = newBackgroundFile = newFilename; - syncResourcesButton.Enabled.Value = set.Beatmaps.Count > 1; + beatmaps.AddFile(set, stream, working.Value.Metadata.BackgroundFile); editorBeatmap.SaveState(); @@ -126,36 +136,56 @@ namespace osu.Game.Screens.Edit.Setup return true; } - public bool ChangeAudioTrack(FileInfo source) + public bool ChangeAudioTrack(FileInfo source, bool applyToAllDifficulties) { if (!source.Exists) return false; - var beatmap = working.Value.BeatmapInfo; var set = working.Value.BeatmapSetInfo; - string[] filenames = set.Files.Select(f => f.Filename).Where(f => - f.StartsWith(@"audio", StringComparison.OrdinalIgnoreCase) && - f.EndsWith(source.Extension, StringComparison.OrdinalIgnoreCase)).ToArray(); - - string currentFilename = working.Value.Metadata.AudioFile; - string? newFilename = null; - - var oldFile = set.GetFile(currentFilename); - - if (oldFile != null && set.Beatmaps.Where(b => !b.Equals(beatmap)).All(b => b.Metadata.AudioFile != currentFilename)) + if (applyToAllDifficulties) { - beatmaps.DeleteFile(set, oldFile); - newFilename = currentFilename; + string newFilename = $@"audio{source.Extension}"; + + foreach (var beatmapInSet in set.Beatmaps) + { + if (set.GetFile(beatmapInSet.Metadata.AudioFile) is RealmNamedFileUsage existingFile) + beatmaps.DeleteFile(set, existingFile); + + if (beatmapInSet.Metadata.AudioFile != newFilename) + { + beatmapInSet.Metadata.AudioFile = newFilename; + + if (!beatmapInSet.Equals(working.Value.BeatmapInfo)) + beatmaps.Save(beatmapInSet, beatmaps.GetWorkingBeatmap(beatmapInSet).Beatmap); + } + } + } + else + { + var beatmap = working.Value.BeatmapInfo; + + string[] filenames = set.Files.Select(f => f.Filename).Where(f => + f.StartsWith(@"audio", StringComparison.OrdinalIgnoreCase) && + f.EndsWith(source.Extension, StringComparison.OrdinalIgnoreCase)).ToArray(); + + string currentFilename = working.Value.Metadata.AudioFile; + + var oldFile = set.GetFile(currentFilename); + string? newFilename = null; + + if (oldFile != null && set.Beatmaps.Where(b => !b.Equals(beatmap)).All(b => b.Metadata.AudioFile != currentFilename)) + { + beatmaps.DeleteFile(set, oldFile); + newFilename = currentFilename; + } + + newFilename ??= NamingUtils.GetNextBestFilename(filenames, $@"audio{source.Extension}"); + working.Value.Metadata.AudioFile = newFilename; } - newFilename ??= NamingUtils.GetNextBestFilename(filenames, $@"audio{source.Extension}"); - using (var stream = source.OpenRead()) - beatmaps.AddFile(set, stream, newFilename); - - working.Value.Metadata.AudioFile = newAudioFile = newFilename; - updateSyncResourcesButton(); + beatmaps.AddFile(set, stream, working.Value.Metadata.AudioFile); editorBeatmap.SaveState(); music.ReloadCurrentTrack(); @@ -163,57 +193,15 @@ namespace osu.Game.Screens.Edit.Setup return true; } - private void updateSyncResourcesButton() - { - var set = working.Value.BeatmapSetInfo; - - syncResourcesButton.Enabled.Value = - (newBackgroundFile != null && set.Beatmaps.DistinctBy(b => b.Metadata.BackgroundFile, StringComparer.OrdinalIgnoreCase).Count() > 1) || - (newAudioFile != null && set.Beatmaps.DistinctBy(b => b.Metadata.AudioFile, StringComparer.OrdinalIgnoreCase).Count() > 1); - } - - private void syncResources() - { - var beatmap = working.Value.BeatmapInfo; - var set = working.Value.BeatmapSetInfo; - - foreach (var otherBeatmap in set.Beatmaps.Where(b => !b.Equals(beatmap))) - { - var otherWorking = beatmaps.GetWorkingBeatmap(otherBeatmap); - - if (newBackgroundFile != null && !string.Equals(otherBeatmap.Metadata.BackgroundFile, newBackgroundFile, StringComparison.OrdinalIgnoreCase)) - { - if (set.GetFile(otherBeatmap.Metadata.BackgroundFile) is RealmNamedFileUsage file) - beatmaps.DeleteFile(set, file); - - otherBeatmap.Metadata.BackgroundFile = newBackgroundFile; - } - - if (newAudioFile != null && !string.Equals(otherBeatmap.Metadata.AudioFile, newAudioFile, StringComparison.OrdinalIgnoreCase)) - { - if (set.GetFile(otherBeatmap.Metadata.AudioFile) is RealmNamedFileUsage file) - beatmaps.DeleteFile(set, file); - - otherBeatmap.Metadata.AudioFile = newAudioFile; - } - - beatmaps.Save(otherBeatmap, otherWorking.Beatmap); - } - - newAudioFile = null; - newBackgroundFile = null; - syncResourcesButton.Enabled.Value = false; - } - private void backgroundChanged(ValueChangedEvent file) { - if (file.NewValue == null || !ChangeBackgroundImage(file.NewValue)) + if (file.NewValue == null || !ChangeBackgroundImage(file.NewValue, backgroundChooser.ApplyToAllDifficulties.Value)) backgroundChooser.Current.Value = file.OldValue; } private void audioTrackChanged(ValueChangedEvent file) { - if (file.NewValue == null || !ChangeAudioTrack(file.NewValue)) + if (file.NewValue == null || !ChangeAudioTrack(file.NewValue, audioTrackChooser.ApplyToAllDifficulties.Value)) audioTrackChooser.Current.Value = file.OldValue; } } From 4b8094d0dbc5fd4d9e63511b0f59d62d7e7257d9 Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Wed, 27 Nov 2024 05:53:30 -0500 Subject: [PATCH 0144/1275] Update test coverage --- .../Editing/TestSceneEditorBeatmapCreation.cs | 208 ++++++++++-------- .../UserInterface/TestSceneFormControls.cs | 10 +- 2 files changed, 129 insertions(+), 89 deletions(-) diff --git a/osu.Game.Tests/Visual/Editing/TestSceneEditorBeatmapCreation.cs b/osu.Game.Tests/Visual/Editing/TestSceneEditorBeatmapCreation.cs index 9fabed346b..2817225f2b 100644 --- a/osu.Game.Tests/Visual/Editing/TestSceneEditorBeatmapCreation.cs +++ b/osu.Game.Tests/Visual/Editing/TestSceneEditorBeatmapCreation.cs @@ -15,8 +15,6 @@ using osu.Game.Beatmaps; using osu.Game.Beatmaps.ControlPoints; using osu.Game.Collections; using osu.Game.Database; -using osu.Game.Graphics.UserInterfaceV2; -using osu.Game.Localisation; using osu.Game.Overlays.Dialog; using osu.Game.Rulesets; using osu.Game.Rulesets.Catch; @@ -102,17 +100,7 @@ namespace osu.Game.Tests.Visual.Editing AddStep("enter setup mode", () => InputManager.Key(Key.F4)); AddAssert("track is virtual", () => Beatmap.Value.Track is TrackVirtual); - AddAssert("switch track to real track", () => - { - var setup = Editor.ChildrenOfType().First(); - - return setFile(TestResources.GetTestBeatmapForImport(), extractedFolder => - { - bool success = setup.ChildrenOfType().First().ChangeAudioTrack(new FileInfo(Path.Combine(extractedFolder, "03. Renatus - Soleily 192kbps.mp3"))); - Assert.That(Beatmap.Value.Metadata.AudioFile == "audio.mp3"); - return success; - }); - }); + AddAssert("switch track to real track", () => setAudio(applyToAllDifficulties: true, expected: "audio.mp3")); AddAssert("track is not virtual", () => Beatmap.Value.Track is not TrackVirtual); AddUntilStep("track length changed", () => Beatmap.Value.Track.Length > 60000); @@ -517,11 +505,105 @@ namespace osu.Game.Tests.Visual.Editing }); } + [Test] + public void TestSingleBackgroundFile() + { + AddStep("enter setup mode", () => InputManager.Key(Key.F4)); + AddAssert("set background", () => setBackground(applyToAllDifficulties: true, expected: "bg.jpg")); + + AddStep("save", () => Editor.Save()); + AddStep("create new difficulty", () => Editor.CreateNewDifficulty(new OsuRuleset().RulesetInfo)); + AddUntilStep("wait for dialog", () => DialogOverlay.CurrentDialog is CreateNewDifficultyDialog); + AddStep("confirm creation with no objects", () => DialogOverlay.CurrentDialog!.PerformOkAction()); + AddUntilStep("wait for created", () => + { + string? difficultyName = Editor.ChildrenOfType().SingleOrDefault()?.BeatmapInfo.DifficultyName; + return difficultyName != null && difficultyName == "New Difficulty"; + }); + + AddStep("create new difficulty", () => Editor.CreateNewDifficulty(new OsuRuleset().RulesetInfo)); + AddUntilStep("wait for dialog", () => DialogOverlay.CurrentDialog is CreateNewDifficultyDialog); + AddStep("confirm creation with no objects", () => DialogOverlay.CurrentDialog!.PerformOkAction()); + AddUntilStep("wait for created", () => + { + string? difficultyName = Editor.ChildrenOfType().SingleOrDefault()?.BeatmapInfo.DifficultyName; + return difficultyName != null && difficultyName == "New Difficulty (1)"; + }); + + AddStep("switch to second difficulty", () => Editor.SwitchToDifficulty(Beatmap.Value.BeatmapSetInfo.Beatmaps.ElementAt(1))); + AddUntilStep("wait for editor load", () => Editor.IsLoaded); + AddStep("enter setup mode", () => InputManager.Key(Key.F4)); + AddUntilStep("wait for load", () => Editor.ChildrenOfType().Any()); + AddAssert("set background on second diff only", () => setBackground(applyToAllDifficulties: false, expected: "bg (1).jpg")); + AddAssert("file added", () => Beatmap.Value.BeatmapSetInfo.Files.Any(f => f.Filename == "bg (1).jpg")); + AddStep("save", () => Editor.Save()); + + AddStep("switch to first difficulty", () => Editor.SwitchToDifficulty(Beatmap.Value.BeatmapSetInfo.Beatmaps.First())); + AddUntilStep("wait for editor load", () => Editor.IsLoaded); + AddStep("enter setup mode", () => InputManager.Key(Key.F4)); + AddUntilStep("wait for load", () => Editor.ChildrenOfType().Any()); + AddAssert("set background on first diff only", () => setBackground(applyToAllDifficulties: false, expected: "bg (2).jpg")); + AddAssert("file added", () => Beatmap.Value.BeatmapSetInfo.Files.Any(f => f.Filename == "bg (2).jpg")); + AddStep("save", () => Editor.Save()); + + AddAssert("set background on all diff", () => setBackground(applyToAllDifficulties: true, expected: "bg.jpg")); + AddAssert("all diff uses one background", () => Beatmap.Value.BeatmapSetInfo.Beatmaps.All(b => b.Metadata.BackgroundFile == "bg.jpg")); + AddAssert("file added", () => Beatmap.Value.BeatmapSetInfo.Files.Any(f => f.Filename == "bg.jpg")); + AddAssert("other files removed", () => !Beatmap.Value.BeatmapSetInfo.Files.Any(f => f.Filename == "bg (1).jpg" || f.Filename == "bg (2).jpg")); + } + + [Test] + public void TestSingleAudioFile() + { + AddStep("enter setup mode", () => InputManager.Key(Key.F4)); + AddAssert("set audio", () => setAudio(applyToAllDifficulties: true, expected: "audio.mp3")); + + AddStep("save", () => Editor.Save()); + AddStep("create new difficulty", () => Editor.CreateNewDifficulty(new OsuRuleset().RulesetInfo)); + AddUntilStep("wait for dialog", () => DialogOverlay.CurrentDialog is CreateNewDifficultyDialog); + AddStep("confirm creation with no objects", () => DialogOverlay.CurrentDialog!.PerformOkAction()); + AddUntilStep("wait for created", () => + { + string? difficultyName = Editor.ChildrenOfType().SingleOrDefault()?.BeatmapInfo.DifficultyName; + return difficultyName != null && difficultyName == "New Difficulty"; + }); + + AddStep("create new difficulty", () => Editor.CreateNewDifficulty(new OsuRuleset().RulesetInfo)); + AddUntilStep("wait for dialog", () => DialogOverlay.CurrentDialog is CreateNewDifficultyDialog); + AddStep("confirm creation with no objects", () => DialogOverlay.CurrentDialog!.PerformOkAction()); + AddUntilStep("wait for created", () => + { + string? difficultyName = Editor.ChildrenOfType().SingleOrDefault()?.BeatmapInfo.DifficultyName; + return difficultyName != null && difficultyName == "New Difficulty (1)"; + }); + + AddStep("switch to second difficulty", () => Editor.SwitchToDifficulty(Beatmap.Value.BeatmapSetInfo.Beatmaps.ElementAt(1))); + AddUntilStep("wait for editor load", () => Editor.IsLoaded); + AddStep("enter setup mode", () => InputManager.Key(Key.F4)); + AddUntilStep("wait for load", () => Editor.ChildrenOfType().Any()); + AddAssert("set audio on second diff only", () => setAudio(applyToAllDifficulties: false, expected: "audio (1).mp3")); + AddAssert("file added", () => Beatmap.Value.BeatmapSetInfo.Files.Any(f => f.Filename == "audio (1).mp3")); + AddStep("save", () => Editor.Save()); + + AddStep("switch to first difficulty", () => Editor.SwitchToDifficulty(Beatmap.Value.BeatmapSetInfo.Beatmaps.First())); + AddUntilStep("wait for editor load", () => Editor.IsLoaded); + AddStep("enter setup mode", () => InputManager.Key(Key.F4)); + AddUntilStep("wait for load", () => Editor.ChildrenOfType().Any()); + AddAssert("set audio on first diff only", () => setAudio(applyToAllDifficulties: false, expected: "audio (2).mp3")); + AddAssert("file added", () => Beatmap.Value.BeatmapSetInfo.Files.Any(f => f.Filename == "audio (2).mp3")); + AddStep("save", () => Editor.Save()); + + AddAssert("set audio on all diff", () => setAudio(applyToAllDifficulties: true, expected: "audio.mp3")); + AddAssert("all diff uses one audio", () => Beatmap.Value.BeatmapSetInfo.Beatmaps.All(b => b.Metadata.AudioFile == "audio.mp3")); + AddAssert("file added", () => Beatmap.Value.BeatmapSetInfo.Files.Any(f => f.Filename == "audio.mp3")); + AddAssert("other files removed", () => !Beatmap.Value.BeatmapSetInfo.Files.Any(f => f.Filename == "audio (1).mp3" || f.Filename == "audio (2).mp3")); + } + [Test] public void TestMultipleBackgroundFiles() { AddStep("enter setup mode", () => InputManager.Key(Key.F4)); - AddAssert("set background", () => setBackground(expected: "bg.jpg")); + AddAssert("set background", () => setBackground(applyToAllDifficulties: false, expected: "bg.jpg")); AddStep("save", () => Editor.Save()); AddStep("create new difficulty", () => Editor.CreateNewDifficulty(new OsuRuleset().RulesetInfo)); @@ -536,7 +618,7 @@ namespace osu.Game.Tests.Visual.Editing AddAssert("new difficulty uses same background", () => Beatmap.Value.Metadata.BackgroundFile == "bg.jpg"); AddStep("enter setup mode", () => InputManager.Key(Key.F4)); AddUntilStep("wait for load", () => Editor.ChildrenOfType().Any()); - AddAssert("set background", () => setBackground(expected: "bg (1).jpg")); + AddAssert("set background", () => setBackground(applyToAllDifficulties: false, expected: "bg (1).jpg")); AddAssert("new difficulty uses new background", () => Beatmap.Value.Metadata.BackgroundFile == "bg (1).jpg"); AddStep("save", () => Editor.Save()); @@ -546,27 +628,15 @@ namespace osu.Game.Tests.Visual.Editing AddStep("enter setup mode", () => InputManager.Key(Key.F4)); AddUntilStep("wait for load", () => Editor.ChildrenOfType().Any()); - AddStep("set background", () => setBackground(expected: "bg.jpg")); + AddStep("set background", () => setBackground(applyToAllDifficulties: false, expected: "bg.jpg")); AddAssert("other background not removed", () => Beatmap.Value.BeatmapSetInfo.Files.Any(f => f.Filename == "bg (1).jpg")); - - bool setBackground(string expected) - { - var setup = Editor.ChildrenOfType().First(); - - return setFile(TestResources.GetQuickTestBeatmapForImport(), extractedFolder => - { - bool success = setup.ChildrenOfType().First().ChangeBackgroundImage(new FileInfo(Path.Combine(extractedFolder, "machinetop_background.jpg"))); - Assert.That(Beatmap.Value.Metadata.BackgroundFile, Is.EqualTo(expected)); - return success; - }); - } } [Test] public void TestMultipleAudioFiles() { AddStep("enter setup mode", () => InputManager.Key(Key.F4)); - AddAssert("set audio", () => setAudio(expected: "audio.mp3")); + AddAssert("set audio", () => setAudio(applyToAllDifficulties: false, expected: "audio.mp3")); AddStep("save", () => Editor.Save()); AddStep("create new difficulty", () => Editor.CreateNewDifficulty(new OsuRuleset().RulesetInfo)); @@ -581,7 +651,7 @@ namespace osu.Game.Tests.Visual.Editing AddAssert("new difficulty uses same audio", () => Beatmap.Value.Metadata.AudioFile == "audio.mp3"); AddStep("enter setup mode", () => InputManager.Key(Key.F4)); AddUntilStep("wait for load", () => Editor.ChildrenOfType().Any()); - AddAssert("set audio", () => setAudio(expected: "audio (1).mp3")); + AddAssert("set audio", () => setAudio(applyToAllDifficulties: false, expected: "audio (1).mp3")); AddAssert("new difficulty uses new audio", () => Beatmap.Value.Metadata.AudioFile == "audio (1).mp3"); AddStep("save", () => Editor.Save()); @@ -591,74 +661,38 @@ namespace osu.Game.Tests.Visual.Editing AddStep("enter setup mode", () => InputManager.Key(Key.F4)); AddUntilStep("wait for load", () => Editor.ChildrenOfType().Any()); - AddStep("set audio", () => setAudio(expected: "audio.mp3")); + AddStep("set audio", () => setAudio(applyToAllDifficulties: false, expected: "audio.mp3")); AddAssert("other audio not removed", () => Beatmap.Value.BeatmapSetInfo.Files.Any(f => f.Filename == "audio (1).mp3")); - - bool setAudio(string expected) - { - var setup = Editor.ChildrenOfType().First(); - - return setFile(TestResources.GetTestBeatmapForImport(), extractedFolder => - { - bool success = setup.ChildrenOfType().First().ChangeAudioTrack(new FileInfo(Path.Combine(extractedFolder, "03. Renatus - Soleily 192kbps.mp3"))); - Assert.That(Beatmap.Value.Metadata.AudioFile, Is.EqualTo(expected)); - return success; - }); - } } - [Test] - public void TestUpdateBackgroundOnAllDifficulties() + private bool setBackground(bool applyToAllDifficulties, string expected) { - AddStep("enter setup mode", () => InputManager.Key(Key.F4)); - AddAssert("button disabled", () => !getButton().Enabled.Value); - AddAssert("set background", () => setBackground(expected: "bg.jpg")); + var setup = Editor.ChildrenOfType().First(); - // there is only one diff so this should still be disabled. - AddAssert("button still disabled", () => !getButton().Enabled.Value); - - AddStep("save", () => Editor.Save()); - AddStep("create new difficulty", () => Editor.CreateNewDifficulty(new OsuRuleset().RulesetInfo)); - AddUntilStep("wait for dialog", () => DialogOverlay.CurrentDialog is CreateNewDifficultyDialog); - AddStep("confirm creation with no objects", () => DialogOverlay.CurrentDialog!.PerformOkAction()); - AddUntilStep("wait for created", () => + return setFile(TestResources.GetQuickTestBeatmapForImport(), extractedFolder => { - string? difficultyName = Editor.ChildrenOfType().SingleOrDefault()?.BeatmapInfo.DifficultyName; - return difficultyName != null && difficultyName == "New Difficulty"; + bool success = setup.ChildrenOfType().First().ChangeBackgroundImage( + new FileInfo(Path.Combine(extractedFolder, @"machinetop_background.jpg")), + applyToAllDifficulties); + + Assert.That(Beatmap.Value.Metadata.BackgroundFile, Is.EqualTo(expected)); + return success; }); + } - AddAssert("new difficulty uses same background", () => Beatmap.Value.Metadata.BackgroundFile == "bg.jpg"); - AddStep("enter setup mode", () => InputManager.Key(Key.F4)); - AddUntilStep("wait for load", () => Editor.ChildrenOfType().Any()); - AddAssert("button disabled", () => !getButton().Enabled.Value); - AddAssert("set background", () => setBackground(expected: "bg (1).jpg")); - AddAssert("new background added", () => Beatmap.Value.BeatmapSetInfo.Files.Any(f => f.Filename == "bg (1).jpg")); - AddAssert("new difficulty uses new background", () => Beatmap.Value.Metadata.BackgroundFile == "bg (1).jpg"); + private bool setAudio(bool applyToAllDifficulties, string expected) + { + var setup = Editor.ChildrenOfType().First(); - AddAssert("button enabled", () => getButton().Enabled.Value); - AddStep("press button", () => getButton().TriggerClick()); - - AddAssert("new difficulty still uses new background", () => Beatmap.Value.BeatmapSetInfo.Beatmaps[1].Metadata.BackgroundFile == "bg (1).jpg"); - AddAssert("old difficulty uses new background", () => Beatmap.Value.BeatmapSetInfo.Beatmaps[0].Metadata.BackgroundFile == "bg (1).jpg"); - AddAssert("old background removed", () => Beatmap.Value.BeatmapSetInfo.Files.All(f => f.Filename != "bg.jpg")); - - AddStep("save", () => Editor.Save()); - AddStep("switch to previous difficulty", () => Editor.SwitchToDifficulty(Beatmap.Value.BeatmapSetInfo.Beatmaps.First())); - AddAssert("old difficulty still uses new background", () => Beatmap.Value.Metadata.BackgroundFile == "bg (1).jpg"); - - bool setBackground(string expected) + return setFile(TestResources.GetTestBeatmapForImport(), extractedFolder => { - var setup = Editor.ChildrenOfType().First(); + bool success = setup.ChildrenOfType().First().ChangeAudioTrack( + new FileInfo(Path.Combine(extractedFolder, "03. Renatus - Soleily 192kbps.mp3")), + applyToAllDifficulties); - return setFile(TestResources.GetQuickTestBeatmapForImport(), extractedFolder => - { - bool success = setup.ChildrenOfType().First().ChangeBackgroundImage(new FileInfo(Path.Combine(extractedFolder, "machinetop_background.jpg"))); - Assert.That(Beatmap.Value.Metadata.BackgroundFile, Is.EqualTo(expected)); - return success; - }); - } - - RoundedButton getButton() => Editor.ChildrenOfType().Single(b => b.Text == EditorSetupStrings.ResourcesUpdateAllDifficulties); + Assert.That(Beatmap.Value.Metadata.AudioFile, Is.EqualTo(expected)); + return success; + }); } private bool setFile(string archivePath, Func func) diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneFormControls.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneFormControls.cs index c6fd65b973..b9ff78b49f 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneFormControls.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneFormControls.cs @@ -9,6 +9,7 @@ using osu.Game.Beatmaps; using osu.Game.Graphics.Cursor; using osu.Game.Graphics.UserInterfaceV2; using osu.Game.Localisation; +using osu.Game.Screens.Edit.Setup; using osuTK; namespace osu.Game.Tests.Visual.UserInterface @@ -89,8 +90,13 @@ namespace osu.Game.Tests.Visual.UserInterface }, new FormFileSelector { - Caption = "Audio file", - PlaceholderText = "Select an audio file", + Caption = "File selector", + PlaceholderText = "Select a file", + }, + new FormBeatmapFileSelector(true) + { + Caption = "File selector with intermediate choice dialog", + PlaceholderText = "Select a file", }, new FormColourPalette { From 4ae3ccfe480bb40ffa3b44bb1fc0879bfb129f98 Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Wed, 27 Nov 2024 06:05:02 -0500 Subject: [PATCH 0145/1275] Make `BackButtonVisibility` in game class private --- osu.Game/OsuGame.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs index 514209524e..c52755197b 100644 --- a/osu.Game/OsuGame.cs +++ b/osu.Game/OsuGame.cs @@ -178,7 +178,7 @@ namespace osu.Game /// /// Whether the back button is currently displayed. /// - public readonly IBindable BackButtonVisibility = new Bindable(); + private readonly IBindable backButtonVisibility = new Bindable(); IBindable ILocalUserPlayInfo.PlayingState => playingState; @@ -1194,7 +1194,7 @@ namespace osu.Game if (mode.NewValue != OverlayActivation.All) CloseAllOverlays(); }; - BackButtonVisibility.ValueChanged += visible => + backButtonVisibility.ValueChanged += visible => { if (visible.NewValue) BackButton.Show(); @@ -1594,14 +1594,14 @@ namespace osu.Game if (current is IOsuScreen currentOsuScreen) { - BackButtonVisibility.UnbindFrom(currentOsuScreen.BackButtonVisibility); + backButtonVisibility.UnbindFrom(currentOsuScreen.BackButtonVisibility); OverlayActivationMode.UnbindFrom(currentOsuScreen.OverlayActivationMode); API.Activity.UnbindFrom(currentOsuScreen.Activity); } if (newScreen is IOsuScreen newOsuScreen) { - BackButtonVisibility.BindTo(newOsuScreen.BackButtonVisibility); + backButtonVisibility.BindTo(newOsuScreen.BackButtonVisibility); OverlayActivationMode.BindTo(newOsuScreen.OverlayActivationMode); API.Activity.BindTo(newOsuScreen.Activity); From f792b6de002f1e82e004ba2c4c7b3d8de5360a27 Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Wed, 27 Nov 2024 06:07:10 -0500 Subject: [PATCH 0146/1275] Fix comment --- osu.Game/Screens/IOsuScreen.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/osu.Game/Screens/IOsuScreen.cs b/osu.Game/Screens/IOsuScreen.cs index 46dfbfb1ac..9e474ed0c6 100644 --- a/osu.Game/Screens/IOsuScreen.cs +++ b/osu.Game/Screens/IOsuScreen.cs @@ -35,7 +35,8 @@ namespace osu.Game.Screens /// Whether a footer (and a back button) should be displayed underneath the screen. /// /// - /// Temporarily, the back button is shown regardless of whether is true. + /// Temporarily, the footer's own back button is shown regardless of whether is set to hidden. + /// This will be corrected as the footer becomes used more commonly. /// bool ShowFooter { get; } From 238a1ce284ce0b77fda9823c2255f3525fe6c8e9 Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Wed, 27 Nov 2024 15:27:18 -0500 Subject: [PATCH 0147/1275] Fix tests reliability and improve code Shaved off lots of copypasta so the test actually shows what it's testing. --- .../Editing/TestSceneEditorBeatmapCreation.cs | 141 +++++++----------- 1 file changed, 54 insertions(+), 87 deletions(-) diff --git a/osu.Game.Tests/Visual/Editing/TestSceneEditorBeatmapCreation.cs b/osu.Game.Tests/Visual/Editing/TestSceneEditorBeatmapCreation.cs index 2817225f2b..c7d745b6e0 100644 --- a/osu.Game.Tests/Visual/Editing/TestSceneEditorBeatmapCreation.cs +++ b/osu.Game.Tests/Visual/Editing/TestSceneEditorBeatmapCreation.cs @@ -98,7 +98,7 @@ namespace osu.Game.Tests.Visual.Editing AddStep("enter compose mode", () => InputManager.Key(Key.F1)); AddUntilStep("wait for timeline load", () => Editor.ChildrenOfType().FirstOrDefault()?.IsLoaded == true); - AddStep("enter setup mode", () => InputManager.Key(Key.F4)); + AddStep("enter setup mode", () => Editor.Mode.Value = EditorScreenMode.SongSetup); AddAssert("track is virtual", () => Beatmap.Value.Track is TrackVirtual); AddAssert("switch track to real track", () => setAudio(applyToAllDifficulties: true, expected: "audio.mp3")); @@ -508,43 +508,21 @@ namespace osu.Game.Tests.Visual.Editing [Test] public void TestSingleBackgroundFile() { - AddStep("enter setup mode", () => InputManager.Key(Key.F4)); + AddStep("enter setup mode", () => Editor.Mode.Value = EditorScreenMode.SongSetup); AddAssert("set background", () => setBackground(applyToAllDifficulties: true, expected: "bg.jpg")); - AddStep("save", () => Editor.Save()); - AddStep("create new difficulty", () => Editor.CreateNewDifficulty(new OsuRuleset().RulesetInfo)); - AddUntilStep("wait for dialog", () => DialogOverlay.CurrentDialog is CreateNewDifficultyDialog); - AddStep("confirm creation with no objects", () => DialogOverlay.CurrentDialog!.PerformOkAction()); - AddUntilStep("wait for created", () => - { - string? difficultyName = Editor.ChildrenOfType().SingleOrDefault()?.BeatmapInfo.DifficultyName; - return difficultyName != null && difficultyName == "New Difficulty"; - }); + createNewDifficulty(); + createNewDifficulty(); - AddStep("create new difficulty", () => Editor.CreateNewDifficulty(new OsuRuleset().RulesetInfo)); - AddUntilStep("wait for dialog", () => DialogOverlay.CurrentDialog is CreateNewDifficultyDialog); - AddStep("confirm creation with no objects", () => DialogOverlay.CurrentDialog!.PerformOkAction()); - AddUntilStep("wait for created", () => - { - string? difficultyName = Editor.ChildrenOfType().SingleOrDefault()?.BeatmapInfo.DifficultyName; - return difficultyName != null && difficultyName == "New Difficulty (1)"; - }); + switchToDifficulty(1); - AddStep("switch to second difficulty", () => Editor.SwitchToDifficulty(Beatmap.Value.BeatmapSetInfo.Beatmaps.ElementAt(1))); - AddUntilStep("wait for editor load", () => Editor.IsLoaded); - AddStep("enter setup mode", () => InputManager.Key(Key.F4)); - AddUntilStep("wait for load", () => Editor.ChildrenOfType().Any()); AddAssert("set background on second diff only", () => setBackground(applyToAllDifficulties: false, expected: "bg (1).jpg")); AddAssert("file added", () => Beatmap.Value.BeatmapSetInfo.Files.Any(f => f.Filename == "bg (1).jpg")); - AddStep("save", () => Editor.Save()); - AddStep("switch to first difficulty", () => Editor.SwitchToDifficulty(Beatmap.Value.BeatmapSetInfo.Beatmaps.First())); - AddUntilStep("wait for editor load", () => Editor.IsLoaded); - AddStep("enter setup mode", () => InputManager.Key(Key.F4)); - AddUntilStep("wait for load", () => Editor.ChildrenOfType().Any()); + switchToDifficulty(0); + AddAssert("set background on first diff only", () => setBackground(applyToAllDifficulties: false, expected: "bg (2).jpg")); AddAssert("file added", () => Beatmap.Value.BeatmapSetInfo.Files.Any(f => f.Filename == "bg (2).jpg")); - AddStep("save", () => Editor.Save()); AddAssert("set background on all diff", () => setBackground(applyToAllDifficulties: true, expected: "bg.jpg")); AddAssert("all diff uses one background", () => Beatmap.Value.BeatmapSetInfo.Beatmaps.All(b => b.Metadata.BackgroundFile == "bg.jpg")); @@ -555,43 +533,21 @@ namespace osu.Game.Tests.Visual.Editing [Test] public void TestSingleAudioFile() { - AddStep("enter setup mode", () => InputManager.Key(Key.F4)); + AddStep("enter setup mode", () => Editor.Mode.Value = EditorScreenMode.SongSetup); AddAssert("set audio", () => setAudio(applyToAllDifficulties: true, expected: "audio.mp3")); - AddStep("save", () => Editor.Save()); - AddStep("create new difficulty", () => Editor.CreateNewDifficulty(new OsuRuleset().RulesetInfo)); - AddUntilStep("wait for dialog", () => DialogOverlay.CurrentDialog is CreateNewDifficultyDialog); - AddStep("confirm creation with no objects", () => DialogOverlay.CurrentDialog!.PerformOkAction()); - AddUntilStep("wait for created", () => - { - string? difficultyName = Editor.ChildrenOfType().SingleOrDefault()?.BeatmapInfo.DifficultyName; - return difficultyName != null && difficultyName == "New Difficulty"; - }); + createNewDifficulty(); + createNewDifficulty(); - AddStep("create new difficulty", () => Editor.CreateNewDifficulty(new OsuRuleset().RulesetInfo)); - AddUntilStep("wait for dialog", () => DialogOverlay.CurrentDialog is CreateNewDifficultyDialog); - AddStep("confirm creation with no objects", () => DialogOverlay.CurrentDialog!.PerformOkAction()); - AddUntilStep("wait for created", () => - { - string? difficultyName = Editor.ChildrenOfType().SingleOrDefault()?.BeatmapInfo.DifficultyName; - return difficultyName != null && difficultyName == "New Difficulty (1)"; - }); + switchToDifficulty(1); - AddStep("switch to second difficulty", () => Editor.SwitchToDifficulty(Beatmap.Value.BeatmapSetInfo.Beatmaps.ElementAt(1))); - AddUntilStep("wait for editor load", () => Editor.IsLoaded); - AddStep("enter setup mode", () => InputManager.Key(Key.F4)); - AddUntilStep("wait for load", () => Editor.ChildrenOfType().Any()); AddAssert("set audio on second diff only", () => setAudio(applyToAllDifficulties: false, expected: "audio (1).mp3")); AddAssert("file added", () => Beatmap.Value.BeatmapSetInfo.Files.Any(f => f.Filename == "audio (1).mp3")); - AddStep("save", () => Editor.Save()); - AddStep("switch to first difficulty", () => Editor.SwitchToDifficulty(Beatmap.Value.BeatmapSetInfo.Beatmaps.First())); - AddUntilStep("wait for editor load", () => Editor.IsLoaded); - AddStep("enter setup mode", () => InputManager.Key(Key.F4)); - AddUntilStep("wait for load", () => Editor.ChildrenOfType().Any()); + switchToDifficulty(0); + AddAssert("set audio on first diff only", () => setAudio(applyToAllDifficulties: false, expected: "audio (2).mp3")); AddAssert("file added", () => Beatmap.Value.BeatmapSetInfo.Files.Any(f => f.Filename == "audio (2).mp3")); - AddStep("save", () => Editor.Save()); AddAssert("set audio on all diff", () => setAudio(applyToAllDifficulties: true, expected: "audio.mp3")); AddAssert("all diff uses one audio", () => Beatmap.Value.BeatmapSetInfo.Beatmaps.All(b => b.Metadata.AudioFile == "audio.mp3")); @@ -602,32 +558,19 @@ namespace osu.Game.Tests.Visual.Editing [Test] public void TestMultipleBackgroundFiles() { - AddStep("enter setup mode", () => InputManager.Key(Key.F4)); + AddStep("enter setup mode", () => Editor.Mode.Value = EditorScreenMode.SongSetup); AddAssert("set background", () => setBackground(applyToAllDifficulties: false, expected: "bg.jpg")); - AddStep("save", () => Editor.Save()); - AddStep("create new difficulty", () => Editor.CreateNewDifficulty(new OsuRuleset().RulesetInfo)); - AddUntilStep("wait for dialog", () => DialogOverlay.CurrentDialog is CreateNewDifficultyDialog); - AddStep("confirm creation with no objects", () => DialogOverlay.CurrentDialog!.PerformOkAction()); - AddUntilStep("wait for created", () => - { - string? difficultyName = Editor.ChildrenOfType().SingleOrDefault()?.BeatmapInfo.DifficultyName; - return difficultyName != null && difficultyName == "New Difficulty"; - }); + createNewDifficulty(); AddAssert("new difficulty uses same background", () => Beatmap.Value.Metadata.BackgroundFile == "bg.jpg"); - AddStep("enter setup mode", () => InputManager.Key(Key.F4)); - AddUntilStep("wait for load", () => Editor.ChildrenOfType().Any()); AddAssert("set background", () => setBackground(applyToAllDifficulties: false, expected: "bg (1).jpg")); AddAssert("new difficulty uses new background", () => Beatmap.Value.Metadata.BackgroundFile == "bg (1).jpg"); - AddStep("save", () => Editor.Save()); - AddStep("switch to previous difficulty", () => Editor.SwitchToDifficulty(Beatmap.Value.BeatmapSetInfo.Beatmaps.First())); + switchToDifficulty(0); + AddAssert("old difficulty uses old background", () => Beatmap.Value.Metadata.BackgroundFile == "bg.jpg"); AddAssert("old background not removed", () => Beatmap.Value.BeatmapSetInfo.Files.Any(f => f.Filename == "bg.jpg")); - - AddStep("enter setup mode", () => InputManager.Key(Key.F4)); - AddUntilStep("wait for load", () => Editor.ChildrenOfType().Any()); AddStep("set background", () => setBackground(applyToAllDifficulties: false, expected: "bg.jpg")); AddAssert("other background not removed", () => Beatmap.Value.BeatmapSetInfo.Files.Any(f => f.Filename == "bg (1).jpg")); } @@ -635,34 +578,58 @@ namespace osu.Game.Tests.Visual.Editing [Test] public void TestMultipleAudioFiles() { - AddStep("enter setup mode", () => InputManager.Key(Key.F4)); + AddStep("enter setup mode", () => Editor.Mode.Value = EditorScreenMode.SongSetup); AddAssert("set audio", () => setAudio(applyToAllDifficulties: false, expected: "audio.mp3")); + createNewDifficulty(); + + AddAssert("new difficulty uses same audio", () => Beatmap.Value.Metadata.AudioFile == "audio.mp3"); + AddStep("enter setup mode", () => Editor.Mode.Value = EditorScreenMode.SongSetup); + AddUntilStep("wait for load", () => Editor.ChildrenOfType().Any()); + AddAssert("set audio", () => setAudio(applyToAllDifficulties: false, expected: "audio (1).mp3")); + AddAssert("new difficulty uses new audio", () => Beatmap.Value.Metadata.AudioFile == "audio (1).mp3"); + + switchToDifficulty(0); + + AddAssert("old difficulty uses old audio", () => Beatmap.Value.Metadata.AudioFile == "audio.mp3"); + AddAssert("old audio not removed", () => Beatmap.Value.BeatmapSetInfo.Files.Any(f => f.Filename == "audio.mp3")); + AddStep("set audio", () => setAudio(applyToAllDifficulties: false, expected: "audio.mp3")); + AddAssert("other audio not removed", () => Beatmap.Value.BeatmapSetInfo.Files.Any(f => f.Filename == "audio (1).mp3")); + } + + private void createNewDifficulty() + { + string? currentDifficulty = null; + AddStep("save", () => Editor.Save()); - AddStep("create new difficulty", () => Editor.CreateNewDifficulty(new OsuRuleset().RulesetInfo)); + AddStep("create new difficulty", () => + { + currentDifficulty = EditorBeatmap.BeatmapInfo.DifficultyName; + Editor.CreateNewDifficulty(new OsuRuleset().RulesetInfo); + }); + AddUntilStep("wait for dialog", () => DialogOverlay.CurrentDialog is CreateNewDifficultyDialog); AddStep("confirm creation with no objects", () => DialogOverlay.CurrentDialog!.PerformOkAction()); AddUntilStep("wait for created", () => { string? difficultyName = Editor.ChildrenOfType().SingleOrDefault()?.BeatmapInfo.DifficultyName; - return difficultyName != null && difficultyName == "New Difficulty"; + return difficultyName != null && difficultyName != currentDifficulty; }); - AddAssert("new difficulty uses same audio", () => Beatmap.Value.Metadata.AudioFile == "audio.mp3"); - AddStep("enter setup mode", () => InputManager.Key(Key.F4)); + AddUntilStep("wait for editor load", () => Editor.IsLoaded); + AddStep("enter setup mode", () => Editor.Mode.Value = EditorScreenMode.SongSetup); AddUntilStep("wait for load", () => Editor.ChildrenOfType().Any()); - AddAssert("set audio", () => setAudio(applyToAllDifficulties: false, expected: "audio (1).mp3")); - AddAssert("new difficulty uses new audio", () => Beatmap.Value.Metadata.AudioFile == "audio (1).mp3"); + } + private void switchToDifficulty(int index) + { AddStep("save", () => Editor.Save()); - AddStep("switch to previous difficulty", () => Editor.SwitchToDifficulty(Beatmap.Value.BeatmapSetInfo.Beatmaps.First())); - AddAssert("old difficulty uses old audio", () => Beatmap.Value.Metadata.AudioFile == "audio.mp3"); - AddAssert("old audio not removed", () => Beatmap.Value.BeatmapSetInfo.Files.Any(f => f.Filename == "audio.mp3")); + AddStep($"switch to difficulty #{index + 1}", () => + Editor.SwitchToDifficulty(Beatmap.Value.BeatmapSetInfo.Beatmaps.ElementAt(index))); - AddStep("enter setup mode", () => InputManager.Key(Key.F4)); + AddUntilStep("wait for editor load", () => Editor.IsLoaded); + AddStep("enter setup mode", () => Editor.Mode.Value = EditorScreenMode.SongSetup); AddUntilStep("wait for load", () => Editor.ChildrenOfType().Any()); - AddStep("set audio", () => setAudio(applyToAllDifficulties: false, expected: "audio.mp3")); - AddAssert("other audio not removed", () => Beatmap.Value.BeatmapSetInfo.Files.Any(f => f.Filename == "audio (1).mp3")); } private bool setBackground(bool applyToAllDifficulties, string expected) From 24c0799680c30223bdc1326bc3735807da950178 Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Wed, 27 Nov 2024 16:54:51 -0500 Subject: [PATCH 0148/1275] Move beatmap ID lookup to `UesrActivity` --- osu.Desktop/DiscordRichPresence.cs | 18 +----------------- osu.Game/Users/UserActivity.cs | 12 ++++++++++++ 2 files changed, 13 insertions(+), 17 deletions(-) diff --git a/osu.Desktop/DiscordRichPresence.cs b/osu.Desktop/DiscordRichPresence.cs index ba61f4be34..1fa964d8bc 100644 --- a/osu.Desktop/DiscordRichPresence.cs +++ b/osu.Desktop/DiscordRichPresence.cs @@ -167,9 +167,7 @@ namespace osu.Desktop presence.State = clampLength(activity.Value.GetStatus(hideIdentifiableInformation)); presence.Details = clampLength(activity.Value.GetDetails(hideIdentifiableInformation) ?? string.Empty); - if (getBeatmapID(activity.Value) is int beatmapId - && beatmapId > 0 - && !(activity.Value is UserActivity.EditingBeatmap && hideIdentifiableInformation)) + if (activity.Value.GetBeatmapID(hideIdentifiableInformation) is int beatmapId && beatmapId > 0) { presence.Buttons = new[] { @@ -329,20 +327,6 @@ namespace osu.Desktop return true; } - private static int? getBeatmapID(UserActivity activity) - { - switch (activity) - { - case UserActivity.InGame game: - return game.BeatmapID; - - case UserActivity.EditingBeatmap edit: - return edit.BeatmapID; - } - - return null; - } - protected override void Dispose(bool isDisposing) { if (multiplayerClient.IsNotNull()) diff --git a/osu.Game/Users/UserActivity.cs b/osu.Game/Users/UserActivity.cs index 93812e3f6b..a8e0fc9030 100644 --- a/osu.Game/Users/UserActivity.cs +++ b/osu.Game/Users/UserActivity.cs @@ -41,6 +41,12 @@ namespace osu.Game.Users public virtual Color4 GetAppropriateColour(OsuColour colours) => colours.GreenDarker; + /// + /// Returns the ID of the beatmap involved in this activity, if applicable and/or available. + /// + /// + public virtual int? GetBeatmapID(bool hideIdentifiableInformation = false) => null; + [MessagePackObject] public class ChoosingBeatmap : UserActivity { @@ -76,6 +82,7 @@ namespace osu.Game.Users public override string GetStatus(bool hideIdentifiableInformation = false) => RulesetPlayingVerb; public override string GetDetails(bool hideIdentifiableInformation = false) => BeatmapDisplayTitle; + public override int? GetBeatmapID(bool hideIdentifiableInformation = false) => BeatmapID; } [MessagePackObject] @@ -156,6 +163,11 @@ namespace osu.Game.Users // For now let's assume that showing the beatmap a user is editing could reveal unwanted information. ? string.Empty : BeatmapDisplayTitle; + + public override int? GetBeatmapID(bool hideIdentifiableInformation = false) => hideIdentifiableInformation + // For now let's assume that showing the beatmap a user is editing could reveal unwanted information. + ? null + : BeatmapID; } [MessagePackObject] From 19e396f87886836f41a14b0e739a625e6e663e80 Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Wed, 27 Nov 2024 23:46:19 -0500 Subject: [PATCH 0149/1275] Fix android workflow not installing .NET 8 version --- .github/workflows/ci.yml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d75f09f184..d8645d728e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -114,7 +114,10 @@ jobs: dotnet-version: "8.0.x" - name: Install .NET workloads - run: dotnet workload install android + # since windows image 20241113.3.0, not specifying a version here + # installs the .NET 7 version of android workload for very unknown reasons. + # revisit once we upgrade to .NET 9, it's probably fixed there. + run: dotnet workload install android --version (dotnet --version) - name: Compile run: dotnet build -c Debug osu.Android.slnf From 4d9d5adbf441ecc8286ca4dc512f96063ddf19bf Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 28 Nov 2024 15:13:32 +0900 Subject: [PATCH 0150/1275] Rename parameter to be more clear --- .../Edit/Setup/FormBeatmapFileSelector.cs | 16 ++++++++-------- osu.Game/Screens/Edit/Setup/ResourcesSection.cs | 6 +++--- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/osu.Game/Screens/Edit/Setup/FormBeatmapFileSelector.cs b/osu.Game/Screens/Edit/Setup/FormBeatmapFileSelector.cs index 317ed1b903..ae368a7b7e 100644 --- a/osu.Game/Screens/Edit/Setup/FormBeatmapFileSelector.cs +++ b/osu.Game/Screens/Edit/Setup/FormBeatmapFileSelector.cs @@ -27,19 +27,19 @@ namespace osu.Game.Screens.Edit.Setup /// public partial class FormBeatmapFileSelector : FormFileSelector { - private readonly bool multipleDifficulties; + private readonly bool beatmapHasMultipleDifficulties; public readonly Bindable ApplyToAllDifficulties = new Bindable(true); - public FormBeatmapFileSelector(bool multipleDifficulties, params string[] handledExtensions) + public FormBeatmapFileSelector(bool beatmapHasMultipleDifficulties, params string[] handledExtensions) : base(handledExtensions) { - this.multipleDifficulties = multipleDifficulties; + this.beatmapHasMultipleDifficulties = beatmapHasMultipleDifficulties; } protected override FileChooserPopover CreatePopover(string[] handledExtensions, Bindable current, string? chooserPath) { - var popover = new BeatmapFileChooserPopover(handledExtensions, current, chooserPath, multipleDifficulties); + var popover = new BeatmapFileChooserPopover(handledExtensions, current, chooserPath, beatmapHasMultipleDifficulties); popover.ApplyToAllDifficulties.ValueChanged += v => { @@ -52,16 +52,16 @@ namespace osu.Game.Screens.Edit.Setup private partial class BeatmapFileChooserPopover : FileChooserPopover { - private readonly bool multipleDifficulties; + private readonly bool beatmapHasMultipleDifficulties; public readonly Bindable ApplyToAllDifficulties = new Bindable(); private Container changeScopeContainer = null!; - public BeatmapFileChooserPopover(string[] handledExtensions, Bindable current, string? chooserPath, bool multipleDifficulties) + public BeatmapFileChooserPopover(string[] handledExtensions, Bindable current, string? chooserPath, bool beatmapHasMultipleDifficulties) : base(handledExtensions, current, chooserPath) { - this.multipleDifficulties = multipleDifficulties; + this.beatmapHasMultipleDifficulties = beatmapHasMultipleDifficulties; } [BackgroundDependencyLoader] @@ -142,7 +142,7 @@ namespace osu.Game.Screens.Edit.Setup protected override void OnFileSelected(FileInfo file) { - if (multipleDifficulties) + if (beatmapHasMultipleDifficulties) changeScopeContainer.FadeIn(200, Easing.InQuint); else base.OnFileSelected(file); diff --git a/osu.Game/Screens/Edit/Setup/ResourcesSection.cs b/osu.Game/Screens/Edit/Setup/ResourcesSection.cs index 70282878e0..1ce944b5a4 100644 --- a/osu.Game/Screens/Edit/Setup/ResourcesSection.cs +++ b/osu.Game/Screens/Edit/Setup/ResourcesSection.cs @@ -49,16 +49,16 @@ namespace osu.Game.Screens.Edit.Setup Height = 110, }; - bool multipleDifficulties = working.Value.BeatmapSetInfo.Beatmaps.Count > 1; + bool beatmapHasMultipleDifficulties = working.Value.BeatmapSetInfo.Beatmaps.Count > 1; Children = new Drawable[] { - backgroundChooser = new FormBeatmapFileSelector(multipleDifficulties, ".jpg", ".jpeg", ".png") + backgroundChooser = new FormBeatmapFileSelector(beatmapHasMultipleDifficulties, ".jpg", ".jpeg", ".png") { Caption = GameplaySettingsStrings.BackgroundHeader, PlaceholderText = EditorSetupStrings.ClickToSelectBackground, }, - audioTrackChooser = new FormBeatmapFileSelector(multipleDifficulties, ".mp3", ".ogg") + audioTrackChooser = new FormBeatmapFileSelector(beatmapHasMultipleDifficulties, ".mp3", ".ogg") { Caption = EditorSetupStrings.AudioTrack, PlaceholderText = EditorSetupStrings.ClickToSelectTrack, From 32b34c1967172bab39c5b2f05975e23dee76cdcd Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 28 Nov 2024 15:20:51 +0900 Subject: [PATCH 0151/1275] Rename container to make more sense --- osu.Game/Screens/Edit/Setup/FormBeatmapFileSelector.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Game/Screens/Edit/Setup/FormBeatmapFileSelector.cs b/osu.Game/Screens/Edit/Setup/FormBeatmapFileSelector.cs index ae368a7b7e..6af78f24f8 100644 --- a/osu.Game/Screens/Edit/Setup/FormBeatmapFileSelector.cs +++ b/osu.Game/Screens/Edit/Setup/FormBeatmapFileSelector.cs @@ -56,7 +56,7 @@ namespace osu.Game.Screens.Edit.Setup public readonly Bindable ApplyToAllDifficulties = new Bindable(); - private Container changeScopeContainer = null!; + private Container selectApplicationScopeContainer = null!; public BeatmapFileChooserPopover(string[] handledExtensions, Bindable current, string? chooserPath, bool beatmapHasMultipleDifficulties) : base(handledExtensions, current, chooserPath) @@ -67,7 +67,7 @@ namespace osu.Game.Screens.Edit.Setup [BackgroundDependencyLoader] private void load(OverlayColourProvider colourProvider, OsuColour colours) { - Add(changeScopeContainer = new InputBlockingContainer + Add(selectApplicationScopeContainer = new InputBlockingContainer { Alpha = 0f, RelativeSizeAxes = Axes.Both, @@ -143,7 +143,7 @@ namespace osu.Game.Screens.Edit.Setup protected override void OnFileSelected(FileInfo file) { if (beatmapHasMultipleDifficulties) - changeScopeContainer.FadeIn(200, Easing.InQuint); + selectApplicationScopeContainer.FadeIn(200, Easing.InQuint); else base.OnFileSelected(file); } From 70eee8882a0ed4df045c0ea8f30764cd93cee88c Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 28 Nov 2024 15:42:37 +0900 Subject: [PATCH 0152/1275] Remove unnecessary null check --- osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomFooter.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomFooter.cs b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomFooter.cs index 7838bd2fc8..c8e5f0859d 100644 --- a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomFooter.cs +++ b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomFooter.cs @@ -77,7 +77,7 @@ namespace osu.Game.Screens.OnlinePlay.Playlists private void hideCloseButton() { - closeButton?.ResizeWidthTo(0, 100, Easing.OutQuint) + closeButton.ResizeWidthTo(0, 100, Easing.OutQuint) .Then().FadeOut().Expire(); } From 4a1401a33df7c3b489c6c0db05b68f6f1fe31079 Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Thu, 28 Nov 2024 02:37:27 -0500 Subject: [PATCH 0153/1275] Rewrite bindable flow to make more sense --- .../Edit/Setup/FormBeatmapFileSelector.cs | 33 ++++++++----------- 1 file changed, 13 insertions(+), 20 deletions(-) diff --git a/osu.Game/Screens/Edit/Setup/FormBeatmapFileSelector.cs b/osu.Game/Screens/Edit/Setup/FormBeatmapFileSelector.cs index 6af78f24f8..3e5f0f4306 100644 --- a/osu.Game/Screens/Edit/Setup/FormBeatmapFileSelector.cs +++ b/osu.Game/Screens/Edit/Setup/FormBeatmapFileSelector.cs @@ -40,13 +40,7 @@ namespace osu.Game.Screens.Edit.Setup protected override FileChooserPopover CreatePopover(string[] handledExtensions, Bindable current, string? chooserPath) { var popover = new BeatmapFileChooserPopover(handledExtensions, current, chooserPath, beatmapHasMultipleDifficulties); - - popover.ApplyToAllDifficulties.ValueChanged += v => - { - Debug.Assert(v.NewValue != null); - ApplyToAllDifficulties.Value = v.NewValue.Value; - }; - + popover.ApplyToAllDifficulties.BindTo(ApplyToAllDifficulties); return popover; } @@ -54,7 +48,7 @@ namespace osu.Game.Screens.Edit.Setup { private readonly bool beatmapHasMultipleDifficulties; - public readonly Bindable ApplyToAllDifficulties = new Bindable(); + public readonly Bindable ApplyToAllDifficulties = new Bindable(true); private Container selectApplicationScopeContainer = null!; @@ -115,7 +109,11 @@ namespace osu.Game.Screens.Edit.Setup Origin = Anchor.Centre, Width = 300f, Text = "Apply to all difficulties", - Action = () => ApplyToAllDifficulties.Value = true, + Action = () => + { + ApplyToAllDifficulties.Value = true; + updateFileSelection(); + }, BackgroundColour = colours.Red2, }, new RoundedButton @@ -124,7 +122,11 @@ namespace osu.Game.Screens.Edit.Setup Origin = Anchor.Centre, Width = 300f, Text = "Only apply to this difficulty", - Action = () => ApplyToAllDifficulties.Value = false, + Action = () => + { + ApplyToAllDifficulties.Value = false; + updateFileSelection(); + }, }, } } @@ -134,12 +136,6 @@ namespace osu.Game.Screens.Edit.Setup }); } - protected override void LoadComplete() - { - base.LoadComplete(); - ApplyToAllDifficulties.ValueChanged += onChangeScopeSelected; - } - protected override void OnFileSelected(FileInfo file) { if (beatmapHasMultipleDifficulties) @@ -148,11 +144,8 @@ namespace osu.Game.Screens.Edit.Setup base.OnFileSelected(file); } - private void onChangeScopeSelected(ValueChangedEvent c) + private void updateFileSelection() { - if (c.NewValue == null) - return; - Debug.Assert(FileSelector.CurrentFile.Value != null); base.OnFileSelected(FileSelector.CurrentFile.Value); } From b1d0939142f62ea2d43401bb7bd4bc0d32191479 Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Thu, 28 Nov 2024 02:37:31 -0500 Subject: [PATCH 0154/1275] Add localisation support --- osu.Game/Localisation/EditorSetupStrings.cs | 20 ++++++++++++++----- .../Edit/Setup/FormBeatmapFileSelector.cs | 7 ++++--- 2 files changed, 19 insertions(+), 8 deletions(-) diff --git a/osu.Game/Localisation/EditorSetupStrings.cs b/osu.Game/Localisation/EditorSetupStrings.cs index 60e677757e..8597b7d9a1 100644 --- a/osu.Game/Localisation/EditorSetupStrings.cs +++ b/osu.Game/Localisation/EditorSetupStrings.cs @@ -188,11 +188,6 @@ namespace osu.Game.Localisation /// public static LocalisableString AudioTrack => new TranslatableString(getKey(@"audio_track"), @"Audio Track"); - /// - /// "Update all difficulties" - /// - public static LocalisableString ResourcesUpdateAllDifficulties => new TranslatableString(getKey(@"resources_update_all_difficulties"), @"Update all difficulties"); - /// /// "Click to select a track" /// @@ -203,6 +198,21 @@ namespace osu.Game.Localisation /// public static LocalisableString ClickToSelectBackground => new TranslatableString(getKey(@"click_to_select_background"), @"Click to select a background image"); + /// + /// "Apply this change to all difficulties?" + /// + public static LocalisableString ApplicationScopeSelectionTitle => new TranslatableString(getKey(@"application_scope_selection_title"), @"Apply this change to all difficulties?"); + + /// + /// "Apply to all difficulties" + /// + public static LocalisableString ApplyToAllDifficulties => new TranslatableString(getKey(@"apply_to_all_difficulties"), @"Apply to all difficulties"); + + /// + /// "Only apply to this difficulty" + /// + public static LocalisableString ApplyToThisDifficulty => new TranslatableString(getKey(@"apply_to_this_difficulty"), @"Only apply to this difficulty"); + /// /// "Ruleset ({0})" /// diff --git a/osu.Game/Screens/Edit/Setup/FormBeatmapFileSelector.cs b/osu.Game/Screens/Edit/Setup/FormBeatmapFileSelector.cs index 3e5f0f4306..53287383ec 100644 --- a/osu.Game/Screens/Edit/Setup/FormBeatmapFileSelector.cs +++ b/osu.Game/Screens/Edit/Setup/FormBeatmapFileSelector.cs @@ -14,6 +14,7 @@ using osu.Game.Graphics.Sprites; using osu.Game.Graphics.UserInterfaceV2; using osu.Game.Overlays; using osuTK; +using osu.Game.Localisation; namespace osu.Game.Screens.Edit.Setup { @@ -100,7 +101,7 @@ namespace osu.Game.Screens.Edit.Setup { Anchor = Anchor.Centre, Origin = Anchor.Centre, - Text = "Apply this change to all difficulties?", + Text = EditorSetupStrings.ApplicationScopeSelectionTitle, Margin = new MarginPadding { Bottom = 20f }, }, new RoundedButton @@ -108,7 +109,7 @@ namespace osu.Game.Screens.Edit.Setup Anchor = Anchor.Centre, Origin = Anchor.Centre, Width = 300f, - Text = "Apply to all difficulties", + Text = EditorSetupStrings.ApplyToAllDifficulties, Action = () => { ApplyToAllDifficulties.Value = true; @@ -121,7 +122,7 @@ namespace osu.Game.Screens.Edit.Setup Anchor = Anchor.Centre, Origin = Anchor.Centre, Width = 300f, - Text = "Only apply to this difficulty", + Text = EditorSetupStrings.ApplyToThisDifficulty, Action = () => { ApplyToAllDifficulties.Value = false; From 4314f9c0a92ca15635cc317d38b3a56c1bcce9d6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 28 Nov 2024 09:22:08 +0100 Subject: [PATCH 0155/1275] Remove unused accessors --- .../Playlists/TestScenePlaylistsResultsScreen.cs | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsResultsScreen.cs b/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsResultsScreen.cs index c288b04da2..33bd573617 100644 --- a/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsResultsScreen.cs +++ b/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsResultsScreen.cs @@ -464,11 +464,6 @@ namespace osu.Game.Tests.Visual.Playlists private partial class TestScoreResultsScreen : PlaylistItemScoreResultsScreen { - public new LoadingSpinner LeftSpinner => base.LeftSpinner; - public new LoadingSpinner CentreSpinner => base.CentreSpinner; - public new LoadingSpinner RightSpinner => base.RightSpinner; - public new ScorePanelList ScorePanelList => base.ScorePanelList; - public TestScoreResultsScreen(ScoreInfo score, int roomId, PlaylistItem playlistItem) : base(score, roomId, playlistItem) { @@ -478,11 +473,6 @@ namespace osu.Game.Tests.Visual.Playlists private partial class TestUserBestResultsScreen : PlaylistItemUserBestResultsScreen { - public new LoadingSpinner LeftSpinner => base.LeftSpinner; - public new LoadingSpinner CentreSpinner => base.CentreSpinner; - public new LoadingSpinner RightSpinner => base.RightSpinner; - public new ScorePanelList ScorePanelList => base.ScorePanelList; - public TestUserBestResultsScreen(int roomId, PlaylistItem playlistItem, int userId) : base(roomId, playlistItem, userId) { From ced8dda1a29da0697bf5e47c7ab0734f473b6892 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 28 Nov 2024 17:30:50 +0900 Subject: [PATCH 0156/1275] Clear previous `LastLocalUserScore` when returning to song select This seems like the lowest friction way of fixing https://github.com/ppy/osu/issues/30885. We could also only null this on application, but this feels worse because - It would require local handling (potentially complex) in `BeatmapOffsetControl` if we want to continue displaying the graph and button after clicking it. - It would make the session static very specific in usage and potentially make future usage not possible due to being nulled in only a very specific scenario. One might argue that it would be nice to have this non-null until the next play, but if such a usage comes up I'd propose we rename this session static and add a new one with that purpose. --- osu.Game/Configuration/SessionStatics.cs | 4 +++- osu.Game/Screens/Play/PlayerLoader.cs | 7 +++++++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/osu.Game/Configuration/SessionStatics.cs b/osu.Game/Configuration/SessionStatics.cs index 225f209380..18631f5d00 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.Screens.Play; namespace osu.Game.Configuration { @@ -77,7 +78,8 @@ namespace osu.Game.Configuration TouchInputActive, /// - /// Stores the local user's last score (can be completed or aborted). + /// Contains the local user's last score (can be completed or aborted) after exiting . + /// Will be cleared to null when leaving . /// LastLocalUserScore, diff --git a/osu.Game/Screens/Play/PlayerLoader.cs b/osu.Game/Screens/Play/PlayerLoader.cs index 3e36c630db..0db96b71ad 100644 --- a/osu.Game/Screens/Play/PlayerLoader.cs +++ b/osu.Game/Screens/Play/PlayerLoader.cs @@ -28,6 +28,7 @@ using osu.Game.Localisation; using osu.Game.Overlays; using osu.Game.Overlays.Notifications; using osu.Game.Performance; +using osu.Game.Scoring; using osu.Game.Screens.Menu; using osu.Game.Screens.Play.PlayerSettings; using osu.Game.Skinning; @@ -78,6 +79,8 @@ namespace osu.Game.Screens.Play private FillFlowContainer disclaimers = null!; private OsuScrollContainer settingsScroll = null!; + private Bindable lastScore = null!; + private Bindable showStoryboards = null!; private bool backgroundBrightnessReduction; @@ -179,6 +182,8 @@ 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; @@ -347,6 +352,8 @@ namespace osu.Game.Screens.Play highPerformanceSession?.Dispose(); highPerformanceSession = null; + lastScore.Value = null; + return base.OnExiting(e); } From c26c84ba4519ade44fb3196a0e8187dde35605ab Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 28 Nov 2024 18:03:19 +0900 Subject: [PATCH 0157/1275] Add test coverage governing new behaviour --- .../Navigation/TestSceneScreenNavigation.cs | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs b/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs index eda7ce925a..5646649d33 100644 --- a/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs +++ b/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs @@ -354,6 +354,23 @@ namespace osu.Game.Tests.Visual.Navigation AddAssert("retry count is 1", () => player.RestartCount == 1); } + [Test] + public void TestLastScoreNullAfterExitingPlayer() + { + AddUntilStep("wait for 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("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); + + ScoreInfo getLastPlay() => Game.Dependencies.Get().Get(Static.LastLocalUserScore); + } + [Test] public void TestRetryImmediatelyAfterCompletion() { From b0958c8d418db28022fe2d12dd0ca2722ddad14c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 28 Nov 2024 10:24:56 +0100 Subject: [PATCH 0158/1275] Attempt to fix test failures --- osu.Game/Overlays/MedalOverlay.cs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/osu.Game/Overlays/MedalOverlay.cs b/osu.Game/Overlays/MedalOverlay.cs index 512cb697dd..e102feb3e2 100644 --- a/osu.Game/Overlays/MedalOverlay.cs +++ b/osu.Game/Overlays/MedalOverlay.cs @@ -144,10 +144,12 @@ namespace osu.Game.Overlays protected override void Dispose(bool isDisposing) { - base.Dispose(isDisposing); - + // this event subscription fires async loads, which hard-fail if `CompositeDrawable.disposalCancellationSource` is canceled, which happens in the base call. + // therefore, unsubscribe from this event early to reduce the chances of a stray event firing at an inconvenient spot. if (api.IsNotNull()) api.NotificationsClient.MessageReceived -= handleMedalMessages; + + base.Dispose(isDisposing); } } } From c14fe21219acc24741b7d6b31106763fdd488796 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 28 Nov 2024 11:19:00 +0100 Subject: [PATCH 0159/1275] Fix LCA call crashing in actual usage It's not allowed to call `LoadComponentsAsync()` on a background thread: https://github.com/ppy/osu-framework/blob/fd64f2f0d47f0ee1aaa596bde1e83e527d610340/osu.Framework/Graphics/Containers/CompositeDrawable.cs#L147 and in this case the event that causes the LCA call is dispatched from a websocket client, which is not on the update thread, so scheduling is required. --- osu.Game/Overlays/MedalOverlay.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Overlays/MedalOverlay.cs b/osu.Game/Overlays/MedalOverlay.cs index e102feb3e2..25e22ffbda 100644 --- a/osu.Game/Overlays/MedalOverlay.cs +++ b/osu.Game/Overlays/MedalOverlay.cs @@ -85,11 +85,11 @@ namespace osu.Game.Overlays Logger.Log($"Queueing medal unlock for \"{medal.Name}\" ({queuedMedals.Count} to display)"); - LoadComponentAsync(medalAnimation, m => + Schedule(() => LoadComponentAsync(medalAnimation, m => { queuedMedals.Enqueue(m); showNextMedal(); - }); + })); } protected override bool OnClick(ClickEvent e) From 8f6e5c475465d826204f5308f4b431c4a52db253 Mon Sep 17 00:00:00 2001 From: Huo Yaoyuan Date: Thu, 28 Nov 2024 19:14:18 +0800 Subject: [PATCH 0160/1275] Convert legacy ruleset to globalconfig --- .globalconfig | 8 +++ CodeAnalysis/osu.ruleset | 58 ------------------- Directory.Build.props | 1 - .../CodeAnalysis.tests.globalconfig | 7 +++ osu.Game.Tests/osu.Game.Tests.csproj | 6 +- osu.Game.Tests/tests.ruleset | 6 -- osu.sln | 2 +- 7 files changed, 19 insertions(+), 69 deletions(-) delete mode 100644 CodeAnalysis/osu.ruleset create mode 100644 osu.Game.Tests/CodeAnalysis.tests.globalconfig delete mode 100644 osu.Game.Tests/tests.ruleset diff --git a/.globalconfig b/.globalconfig index a4d4707f9b..c5723daed6 100644 --- a/.globalconfig +++ b/.globalconfig @@ -46,6 +46,14 @@ dotnet_diagnostic.IDE0130.severity = warning # IDE1006: Naming style dotnet_diagnostic.IDE1006.severity = warning +dotnet_diagnostic.CA1309.severity = warning + +# CA2007: Consider calling ConfigureAwait on the awaited task +dotnet_diagnostic.CA2007.severity = warning + +# CA2201: Do not raise reserved exception types +dotnet_diagnostic.CA2201.severity = warning + #Disable operator overloads requiring alternate named methods dotnet_diagnostic.CA2225.severity = none diff --git a/CodeAnalysis/osu.ruleset b/CodeAnalysis/osu.ruleset deleted file mode 100644 index 6a99e230d1..0000000000 --- a/CodeAnalysis/osu.ruleset +++ /dev/null @@ -1,58 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/Directory.Build.props b/Directory.Build.props index 5ba12b845b..e7e5e4e831 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -20,7 +20,6 @@ - $(MSBuildThisFileDirectory)CodeAnalysis\osu.ruleset true diff --git a/osu.Game.Tests/CodeAnalysis.tests.globalconfig b/osu.Game.Tests/CodeAnalysis.tests.globalconfig new file mode 100644 index 0000000000..3f039736ec --- /dev/null +++ b/osu.Game.Tests/CodeAnalysis.tests.globalconfig @@ -0,0 +1,7 @@ +# Higher global_level has higher priority, the default global_level +# is 100 for root .globalconfig and 0 for others +# https://learn.microsoft.com/dotnet/fundamentals/code-analysis/configuration-files#precedence +is_global = true +global_level = 101 + +dotnet_diagnostic.CA2007.severity = none diff --git a/osu.Game.Tests/osu.Game.Tests.csproj b/osu.Game.Tests/osu.Game.Tests.csproj index 28a1d4d021..01d2241650 100644 --- a/osu.Game.Tests/osu.Game.Tests.csproj +++ b/osu.Game.Tests/osu.Game.Tests.csproj @@ -12,9 +12,9 @@ WinExe net8.0 - - tests.ruleset - + + + diff --git a/osu.Game.Tests/tests.ruleset b/osu.Game.Tests/tests.ruleset deleted file mode 100644 index a0abb781d3..0000000000 --- a/osu.Game.Tests/tests.ruleset +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - diff --git a/osu.sln b/osu.sln index 829e43fc65..2d9a4e86d0 100644 --- a/osu.sln +++ b/osu.sln @@ -60,7 +60,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Directory.Build.props = Directory.Build.props osu.Android.props = osu.Android.props osu.iOS.props = osu.iOS.props - CodeAnalysis\osu.ruleset = CodeAnalysis\osu.ruleset + global.json = global.json osu.sln.DotSettings = osu.sln.DotSettings osu.TestProject.props = osu.TestProject.props EndProjectSection From 66093872e85d8d08dba3860393fabbe87cf9a660 Mon Sep 17 00:00:00 2001 From: Hiviexd Date: Thu, 28 Nov 2024 12:49:30 +0100 Subject: [PATCH 0161/1275] Adjust daily challenge tier thresholds to match expectations --- .../Components/DailyChallengeStatsTooltip.cs | 22 +++++++++---------- 1 file changed, 10 insertions(+), 12 deletions(-) diff --git a/osu.Game/Overlays/Profile/Header/Components/DailyChallengeStatsTooltip.cs b/osu.Game/Overlays/Profile/Header/Components/DailyChallengeStatsTooltip.cs index 24e531bd87..ea49f9d784 100644 --- a/osu.Game/Overlays/Profile/Header/Components/DailyChallengeStatsTooltip.cs +++ b/osu.Game/Overlays/Profile/Header/Components/DailyChallengeStatsTooltip.cs @@ -138,34 +138,32 @@ namespace osu.Game.Overlays.Profile.Header.Components topFifty.ValueColour = colourProvider.Content2; } - // reference: https://github.com/ppy/osu-web/blob/adf1e94754ba9625b85eba795f4a310caf169eec/resources/js/profile-page/daily-challenge.tsx#L13-L47 + // reference: https://github.com/ppy/osu-web/blob/a97f156014e00ea1aa315140da60542e798a9f06/resources/js/profile-page/daily-challenge.tsx#L13-L47 - // Rounding up is needed here to ensure the overlay shows the same colour as osu-web for the play count. - // This is because, for example, 31 / 3 > 10 in JavaScript because floats are used, while here it would - // get truncated to 10 with an integer division and show a lower tier. - public static RankingTier TierForPlayCount(int playCount) => TierForDaily((int)Math.Ceiling(playCount / 3.0d)); + // Rounding down is needed here to ensure the overlay shows the same colour as osu-web for the play count. + public static RankingTier TierForPlayCount(int playCount) => TierForDaily((int)Math.Floor(playCount / 3.0d)); public static RankingTier TierForDaily(int daily) { - if (daily > 360) + if (daily >= 360) return RankingTier.Lustrous; - if (daily > 240) + if (daily >= 240) return RankingTier.Radiant; - if (daily > 120) + if (daily >= 120) return RankingTier.Rhodium; - if (daily > 60) + if (daily >= 60) return RankingTier.Platinum; - if (daily > 30) + if (daily >= 30) return RankingTier.Gold; - if (daily > 10) + if (daily >= 10) return RankingTier.Silver; - if (daily > 5) + if (daily >= 5) return RankingTier.Bronze; return RankingTier.Iron; From 6ed21229b7c5c95f7125a1c2f534cbce3b1931b3 Mon Sep 17 00:00:00 2001 From: Hiviexd Date: Thu, 28 Nov 2024 12:49:48 +0100 Subject: [PATCH 0162/1275] update test --- .../Visual/Online/TestSceneUserProfileDailyChallenge.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Game.Tests/Visual/Online/TestSceneUserProfileDailyChallenge.cs b/osu.Game.Tests/Visual/Online/TestSceneUserProfileDailyChallenge.cs index 9db30380f6..3222e16412 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneUserProfileDailyChallenge.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneUserProfileDailyChallenge.cs @@ -37,7 +37,7 @@ namespace osu.Game.Tests.Visual.Online 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, 999, 0, v => update(s => s.PlayCount = v)); + AddSliderStep("playcount", 0, 1500, 0, v => update(s => s.PlayCount = v)); AddStep("create", () => { Clear(); @@ -66,8 +66,8 @@ namespace osu.Game.Tests.Visual.Online [Test] public void TestPlayCountRankingTier() { - AddAssert("1 before silver", () => DailyChallengeStatsTooltip.TierForPlayCount(30) == RankingTier.Bronze); - AddAssert("first silver", () => DailyChallengeStatsTooltip.TierForPlayCount(31) == RankingTier.Silver); + AddAssert("1 before silver", () => DailyChallengeStatsTooltip.TierForPlayCount(29) == RankingTier.Bronze); + AddAssert("first silver", () => DailyChallengeStatsTooltip.TierForPlayCount(30) == RankingTier.Silver); } } } From c57ace0b5f184491890ab566084e4a5b9ec9d790 Mon Sep 17 00:00:00 2001 From: Huo Yaoyuan Date: Thu, 28 Nov 2024 19:40:09 +0800 Subject: [PATCH 0163/1275] Enable recommended rules for documentation --- Directory.Build.props | 3 +++ osu.Game/Database/RealmObjectExtensions.cs | 22 ++++++++++++---------- 2 files changed, 15 insertions(+), 10 deletions(-) diff --git a/Directory.Build.props b/Directory.Build.props index e7e5e4e831..60e0334f97 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -20,6 +20,9 @@ + Default + Default + Recommended true diff --git a/osu.Game/Database/RealmObjectExtensions.cs b/osu.Game/Database/RealmObjectExtensions.cs index 2fa3b8a880..df725505fc 100644 --- a/osu.Game/Database/RealmObjectExtensions.cs +++ b/osu.Game/Database/RealmObjectExtensions.cs @@ -248,29 +248,30 @@ namespace osu.Game.Database return new RealmLive(realmObject, realm); } +#pragma warning disable RS0030 // mentioning banned symbols in documentation /// - /// Register a callback to be invoked each time this changes. + /// Register a callback to be invoked each time this changes. /// /// /// /// This adds osu! specific thread and managed state safety checks on top of . /// /// - /// The first callback will be invoked with the initial after the asynchronous query completes, + /// The first callback will be invoked with the initial after the asynchronous query completes, /// and then called again after each write transaction which changes either any of the objects in the collection, or /// which objects are in the collection. The changes parameter will /// be null the first time the callback is invoked with the initial results. For each call after that, /// it will contain information about which rows in the results were added, removed or modified. /// /// - /// If a write transaction did not modify any objects in this , the callback is not invoked at all. + /// 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 + /// At the time when the block is called, the object will be fully evaluated /// and up-to-date, and as long as you do not perform a write transaction on the same thread - /// or explicitly call , accessing it will never perform blocking work. + /// or explicitly call , accessing it will never perform blocking work. /// /// /// Notifications are delivered via the standard event loop, and so can't be delivered while the event loop is blocked by other activity. @@ -279,13 +280,14 @@ namespace osu.Game.Database /// /// /// The to observe for changes. - /// The callback to be invoked with the updated . + /// The callback to be invoked with the updated . /// /// A subscription token. It must be kept alive for as long as you want to receive change notifications. - /// To stop receiving notifications, call . + /// To stop receiving notifications, call . /// - /// - /// + /// + /// +#pragma warning restore RS0030 public static IDisposable QueryAsyncWithNotifications(this IRealmCollection collection, NotificationCallbackDelegate callback) where T : RealmObjectBase { From cd9b5927eba9b19b4b66382aaedd25298a91a4df Mon Sep 17 00:00:00 2001 From: Huo Yaoyuan Date: Thu, 28 Nov 2024 19:46:25 +0800 Subject: [PATCH 0164/1275] Enable recommended rules for globalization --- .globalconfig | 8 +++++++- CodeAnalysis/BannedSymbols.txt | 4 ---- Directory.Build.props | 2 ++ 3 files changed, 9 insertions(+), 5 deletions(-) diff --git a/.globalconfig b/.globalconfig index c5723daed6..d6c657bad0 100644 --- a/.globalconfig +++ b/.globalconfig @@ -46,11 +46,17 @@ dotnet_diagnostic.IDE0130.severity = warning # IDE1006: Naming style dotnet_diagnostic.IDE1006.severity = warning -dotnet_diagnostic.CA1309.severity = warning +# CA1305: Specify IFormatProvider +# Too many noisy warnings for parsing/formatting numbers +dotnet_diagnostic.CA1305.severity = none # CA2007: Consider calling ConfigureAwait on the awaited task dotnet_diagnostic.CA2007.severity = warning +# CA2101: Specify marshaling for P/Invoke string arguments +# Reports warning for all non-UTF16 usages on DllImport; consider migrating to LibraryImport +dotnet_diagnostic.CA2101.severity = none + # CA2201: Do not raise reserved exception types dotnet_diagnostic.CA2201.severity = warning diff --git a/CodeAnalysis/BannedSymbols.txt b/CodeAnalysis/BannedSymbols.txt index 3c60b28765..550f7c8e11 100644 --- a/CodeAnalysis/BannedSymbols.txt +++ b/CodeAnalysis/BannedSymbols.txt @@ -14,10 +14,6 @@ M:Realms.CollectionExtensions.SubscribeForNotifications`1(System.Collections.Gen M:System.Threading.Tasks.Task.Wait();Don't use Task.Wait. Use Task.WaitSafely() to ensure we avoid deadlocks. P:System.Threading.Tasks.Task`1.Result;Don't use Task.Result. Use Task.GetResultSafely() to ensure we avoid deadlocks. M:System.Threading.ManualResetEventSlim.Wait();Specify a timeout to avoid waiting forever. -M:System.Char.ToLower(System.Char);char.ToLower() changes behaviour depending on CultureInfo.CurrentCulture. Use char.ToLowerInvariant() instead. If wanting culture-sensitive behaviour, explicitly provide CultureInfo.CurrentCulture. -M:System.Char.ToUpper(System.Char);char.ToUpper() changes behaviour depending on CultureInfo.CurrentCulture. Use char.ToUpperInvariant() instead. If wanting culture-sensitive behaviour, explicitly provide CultureInfo.CurrentCulture. -M:System.String.ToLower();string.ToLower() changes behaviour depending on CultureInfo.CurrentCulture. Use string.ToLowerInvariant() instead. If wanting culture-sensitive behaviour, explicitly provide CultureInfo.CurrentCulture or use LocalisableString. -M:System.String.ToUpper();string.ToUpper() changes behaviour depending on CultureInfo.CurrentCulture. Use string.ToUpperInvariant() instead. If wanting culture-sensitive behaviour, explicitly provide CultureInfo.CurrentCulture or use LocalisableString. M:Humanizer.InflectorExtensions.Pascalize(System.String);Humanizer's .Pascalize() extension method changes behaviour depending on CultureInfo.CurrentCulture. Use StringDehumanizeExtensions.ToPascalCase() instead. M:Humanizer.InflectorExtensions.Camelize(System.String);Humanizer's .Camelize() extension method changes behaviour depending on CultureInfo.CurrentCulture. Use StringDehumanizeExtensions.ToCamelCase() instead. M:Humanizer.InflectorExtensions.Underscore(System.String);Humanizer's .Underscore() extension method changes behaviour depending on CultureInfo.CurrentCulture. Use StringDehumanizeExtensions.ToSnakeCase() instead. diff --git a/Directory.Build.props b/Directory.Build.props index 60e0334f97..f89696d867 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -23,6 +23,8 @@ Default Default Recommended + Recommended + Recommended true From 13d7c6a2d863b9bae4ae024e4fd59ac2f895c292 Mon Sep 17 00:00:00 2001 From: Huo Yaoyuan Date: Thu, 28 Nov 2024 19:55:03 +0800 Subject: [PATCH 0165/1275] Enable recommended rules for maintainability --- Directory.Build.props | 2 ++ osu.Game/Online/API/Requests/Responses/APIKudosuHistory.cs | 2 ++ .../API/Requests/Responses/APIUserMostPlayedBeatmap.cs | 2 ++ .../Notifications/WebSocket/Events/NewChatMessageData.cs | 2 ++ osu.Game/Utils/SpecialFunctions.cs | 5 +---- 5 files changed, 9 insertions(+), 4 deletions(-) diff --git a/Directory.Build.props b/Directory.Build.props index f89696d867..7cd02a72d4 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -25,6 +25,8 @@ Recommended Recommended Recommended + Recommended + Default true diff --git a/osu.Game/Online/API/Requests/Responses/APIKudosuHistory.cs b/osu.Game/Online/API/Requests/Responses/APIKudosuHistory.cs index 583def8eda..8e4cc387ed 100644 --- a/osu.Game/Online/API/Requests/Responses/APIKudosuHistory.cs +++ b/osu.Game/Online/API/Requests/Responses/APIKudosuHistory.cs @@ -45,7 +45,9 @@ namespace osu.Game.Online.API.Requests.Responses public KudosuAction Action; +#pragma warning disable CA1507 // Happens to name the same because of casing preference [JsonProperty("action")] +#pragma warning restore CA1507 private string action { set diff --git a/osu.Game/Online/API/Requests/Responses/APIUserMostPlayedBeatmap.cs b/osu.Game/Online/API/Requests/Responses/APIUserMostPlayedBeatmap.cs index 6d5fd59f9c..38ad2bd02d 100644 --- a/osu.Game/Online/API/Requests/Responses/APIUserMostPlayedBeatmap.cs +++ b/osu.Game/Online/API/Requests/Responses/APIUserMostPlayedBeatmap.cs @@ -15,7 +15,9 @@ namespace osu.Game.Online.API.Requests.Responses [JsonProperty("count")] public int PlayCount { get; set; } +#pragma warning disable CA1507 // Happens to name the same because of casing preference [JsonProperty("beatmap")] +#pragma warning restore CA1507 private APIBeatmap beatmap { get; set; } public APIBeatmap BeatmapInfo diff --git a/osu.Game/Online/Notifications/WebSocket/Events/NewChatMessageData.cs b/osu.Game/Online/Notifications/WebSocket/Events/NewChatMessageData.cs index ff9f5ee9f7..677286bb8a 100644 --- a/osu.Game/Online/Notifications/WebSocket/Events/NewChatMessageData.cs +++ b/osu.Game/Online/Notifications/WebSocket/Events/NewChatMessageData.cs @@ -19,7 +19,9 @@ namespace osu.Game.Online.Notifications.WebSocket.Events [JsonProperty(@"messages")] public List Messages { get; set; } = null!; +#pragma warning disable CA1507 // Happens to name the same because of casing preference [JsonProperty(@"users")] +#pragma warning restore CA1507 private List users { get; set; } = null!; [OnDeserialized] diff --git a/osu.Game/Utils/SpecialFunctions.cs b/osu.Game/Utils/SpecialFunctions.cs index 0b0f0598bb..795a84a973 100644 --- a/osu.Game/Utils/SpecialFunctions.cs +++ b/osu.Game/Utils/SpecialFunctions.cs @@ -666,10 +666,7 @@ namespace osu.Game.Utils { // 2020-10-07 jbialogrodzki #730 Since this is public API we should probably // handle null arguments? It doesn't seem to have been done consistently in this class though. - if (coefficients == null) - { - throw new ArgumentNullException(nameof(coefficients)); - } + ArgumentNullException.ThrowIfNull(coefficients); // 2020-10-07 jbialogrodzki #730 Zero polynomials need explicit handling. // Without this check, we attempted to peek coefficients at negative indices! From f5a77165096fd395a2df7d8e3656bf794a7db2aa Mon Sep 17 00:00:00 2001 From: Huo Yaoyuan Date: Thu, 28 Nov 2024 20:37:53 +0800 Subject: [PATCH 0166/1275] Apply minor performance rules --- .globalconfig | 16 ++++++++++++++++ Directory.Build.props | 1 + .../TestSceneMultiplayerParticipantsList.cs | 2 +- .../UserInterface/TestSceneDeleteLocalScore.cs | 2 +- .../Extensions/StringDehumanizeExtensions.cs | 2 +- .../Overlays/Chat/ChannelList/ChannelList.cs | 11 ++--------- osu.Game/Overlays/ChatOverlay.cs | 3 +-- osu.Game/Overlays/Comments/CommentsContainer.cs | 2 +- osu.Game/Rulesets/Mods/ModAdaptiveSpeed.cs | 4 ++-- .../Objects/Legacy/ConvertHitObjectParser.cs | 2 +- .../Ranking/Statistics/User/OverallRanking.cs | 4 ++-- osu.Game/Skinning/SkinImporter.cs | 2 +- osu.Game/Storyboards/Storyboard.cs | 3 ++- .../Visual/Spectator/TestSpectatorClient.cs | 4 ++-- 14 files changed, 34 insertions(+), 24 deletions(-) diff --git a/.globalconfig b/.globalconfig index d6c657bad0..e218f46cd2 100644 --- a/.globalconfig +++ b/.globalconfig @@ -50,6 +50,22 @@ dotnet_diagnostic.IDE1006.severity = warning # Too many noisy warnings for parsing/formatting numbers dotnet_diagnostic.CA1305.severity = none +# CA1806: Do not ignore method results +# The usages for numeric parsing are explicitly optional +dotnet_diagnostic.CA1806.severity = suggestion + +# CA1822: Mark members as static +# Potential false positive around reflection/too much noise +dotnet_diagnostic.CA1822.severity = none + +# CA1859: Use concrete types when possible for improved performance +# Involves design considerations +dotnet_diagnostic.CA1859.severity = none + +# CA1861: Avoid constant arrays as arguments +# Outdated with collection expressions +dotnet_diagnostic.CA1861.severity = none + # CA2007: Consider calling ConfigureAwait on the awaited task dotnet_diagnostic.CA2007.severity = warning diff --git a/Directory.Build.props b/Directory.Build.props index 7cd02a72d4..0f982d496e 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -27,6 +27,7 @@ Recommended Recommended Default + Minimum true diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerParticipantsList.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerParticipantsList.cs index 95ae4c5e80..d88741ec0c 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerParticipantsList.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerParticipantsList.cs @@ -198,7 +198,7 @@ namespace osu.Game.Tests.Visual.Multiplayer AddStep("make second user host", () => MultiplayerClient.TransferHost(3)); - AddUntilStep("kick buttons not visible", () => this.ChildrenOfType().Count(d => d.IsPresent) == 0); + AddUntilStep("kick buttons not visible", () => !this.ChildrenOfType().Any(d => d.IsPresent)); AddStep("make local user host again", () => MultiplayerClient.TransferHost(API.LocalUser.Value.Id)); diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneDeleteLocalScore.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneDeleteLocalScore.cs index e2fe10fa74..f7bdda6b57 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneDeleteLocalScore.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneDeleteLocalScore.cs @@ -151,7 +151,7 @@ namespace osu.Game.Tests.Visual.UserInterface AddStep("click delete option", () => { - InputManager.MoveMouseTo(contextMenuContainer.ChildrenOfType().First(i => i.Item.Text.Value.ToString().ToLowerInvariant() == "delete")); + InputManager.MoveMouseTo(contextMenuContainer.ChildrenOfType().First(i => string.Equals(i.Item.Text.Value.ToString(), "delete", System.StringComparison.OrdinalIgnoreCase))); InputManager.Click(MouseButton.Left); }); diff --git a/osu.Game/Extensions/StringDehumanizeExtensions.cs b/osu.Game/Extensions/StringDehumanizeExtensions.cs index 6f0d7622d3..5993f83b55 100644 --- a/osu.Game/Extensions/StringDehumanizeExtensions.cs +++ b/osu.Game/Extensions/StringDehumanizeExtensions.cs @@ -60,7 +60,7 @@ namespace osu.Game.Extensions public static string ToCamelCase(this string input) { string word = input.ToPascalCase(); - return word.Length > 0 ? word.Substring(0, 1).ToLowerInvariant() + word.Substring(1) : word; + return word.Length > 0 ? char.ToLowerInvariant(word[0]) + word.Substring(1) : word; } /// diff --git a/osu.Game/Overlays/Chat/ChannelList/ChannelList.cs b/osu.Game/Overlays/Chat/ChannelList/ChannelList.cs index fc0060d86a..39860b5e03 100644 --- a/osu.Game/Overlays/Chat/ChannelList/ChannelList.cs +++ b/osu.Game/Overlays/Chat/ChannelList/ChannelList.cs @@ -120,10 +120,9 @@ namespace osu.Game.Overlays.Chat.ChannelList public void RemoveChannel(Channel channel) { - if (!channelMap.ContainsKey(channel)) + if (!channelMap.TryGetValue(channel, out var item)) return; - ChannelListItem item = channelMap[channel]; FillFlowContainer flow = getFlowForChannel(channel); channelMap.Remove(channel); @@ -132,13 +131,7 @@ namespace osu.Game.Overlays.Chat.ChannelList updateVisibility(); } - public ChannelListItem GetItem(Channel channel) - { - if (!channelMap.ContainsKey(channel)) - throw new ArgumentOutOfRangeException(); - - return channelMap[channel]; - } + public ChannelListItem GetItem(Channel channel) => channelMap[channel]; public void ScrollChannelIntoView(Channel channel) => scroll.ScrollIntoView(GetItem(channel)); diff --git a/osu.Game/Overlays/ChatOverlay.cs b/osu.Game/Overlays/ChatOverlay.cs index b11483e678..a00414522d 100644 --- a/osu.Game/Overlays/ChatOverlay.cs +++ b/osu.Game/Overlays/ChatOverlay.cs @@ -386,9 +386,8 @@ namespace osu.Game.Overlays { channelList.RemoveChannel(channel); - if (loadedChannels.ContainsKey(channel)) + if (loadedChannels.TryGetValue(channel, out var loaded)) { - DrawableChannel loaded = loadedChannels[channel]; loadedChannels.Remove(channel); // DrawableChannel removed from cache must be manually disposed loaded.Dispose(); diff --git a/osu.Game/Overlays/Comments/CommentsContainer.cs b/osu.Game/Overlays/Comments/CommentsContainer.cs index 921c1682f5..5e277357a9 100644 --- a/osu.Game/Overlays/Comments/CommentsContainer.cs +++ b/osu.Game/Overlays/Comments/CommentsContainer.cs @@ -244,7 +244,7 @@ namespace osu.Game.Overlays.Comments protected void OnSuccess(CommentBundle response) { commentCounter.Current.Value = response.Total; - newCommentEditor.CommentableMeta.Value = response.CommentableMeta.SingleOrDefault(m => m.Id == id.Value && m.Type == type.Value.ToString().ToSnakeCase().ToLowerInvariant()); + newCommentEditor.CommentableMeta.Value = response.CommentableMeta.SingleOrDefault(m => m.Id == id.Value && string.Equals(m.Type, type.Value.ToString().ToSnakeCase(), StringComparison.OrdinalIgnoreCase)); if (!response.Comments.Any()) { diff --git a/osu.Game/Rulesets/Mods/ModAdaptiveSpeed.cs b/osu.Game/Rulesets/Mods/ModAdaptiveSpeed.cs index 4ca937bf86..fb056b457b 100644 --- a/osu.Game/Rulesets/Mods/ModAdaptiveSpeed.cs +++ b/osu.Game/Rulesets/Mods/ModAdaptiveSpeed.cs @@ -173,10 +173,10 @@ namespace osu.Game.Rulesets.Mods }; drawable.OnRevertResult += (_, result) => { - if (!ratesForRewinding.ContainsKey(result.HitObject)) return; + if (!ratesForRewinding.TryGetValue(result.HitObject, out double rewindValue)) return; if (!shouldProcessResult(result)) return; - recentRates.Insert(0, ratesForRewinding[result.HitObject]); + recentRates.Insert(0, rewindValue); ratesForRewinding.Remove(result.HitObject); recentRates.RemoveAt(recentRates.Count - 1); diff --git a/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs b/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs index f8bc0ce112..0162c8017b 100644 --- a/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs +++ b/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs @@ -243,7 +243,7 @@ namespace osu.Game.Rulesets.Objects.Legacy return PathType.CATMULL; case 'B': - if (input.Length > 1 && int.TryParse(input.Substring(1), out int degree) && degree > 0) + if (input.Length > 1 && int.TryParse(input.AsSpan(1), out int degree) && degree > 0) return PathType.BSpline(degree); return PathType.BEZIER; diff --git a/osu.Game/Screens/Ranking/Statistics/User/OverallRanking.cs b/osu.Game/Screens/Ranking/Statistics/User/OverallRanking.cs index 171a3f0f65..9f5afea6f0 100644 --- a/osu.Game/Screens/Ranking/Statistics/User/OverallRanking.cs +++ b/osu.Game/Screens/Ranking/Statistics/User/OverallRanking.cs @@ -59,14 +59,14 @@ namespace osu.Game.Screens.Ranking.Statistics.User new SimpleStatisticTable.Spacer(), new PerformancePointsChangeRow { StatisticsUpdate = { BindTarget = StatisticsUpdate } }, }, - new Drawable[] { }, + [], new Drawable[] { new MaximumComboChangeRow { StatisticsUpdate = { BindTarget = StatisticsUpdate } }, new SimpleStatisticTable.Spacer(), new AccuracyChangeRow { StatisticsUpdate = { BindTarget = StatisticsUpdate } }, }, - new Drawable[] { }, + [], new Drawable[] { new RankedScoreChangeRow { StatisticsUpdate = { BindTarget = StatisticsUpdate } }, diff --git a/osu.Game/Skinning/SkinImporter.cs b/osu.Game/Skinning/SkinImporter.cs index 59c7f0ba26..70d3195ecd 100644 --- a/osu.Game/Skinning/SkinImporter.cs +++ b/osu.Game/Skinning/SkinImporter.cs @@ -37,7 +37,7 @@ namespace osu.Game.Skinning protected override string[] HashableFileTypes => new[] { ".ini", ".json" }; - protected override bool ShouldDeleteArchive(string path) => Path.GetExtension(path).ToLowerInvariant() == @".osk"; + protected override bool ShouldDeleteArchive(string path) => string.Equals(Path.GetExtension(path), @".osk", StringComparison.OrdinalIgnoreCase); protected override SkinInfo CreateModel(ArchiveReader archive, ImportParameters parameters) => new SkinInfo { Name = archive.Name ?? @"No name" }; diff --git a/osu.Game/Storyboards/Storyboard.cs b/osu.Game/Storyboards/Storyboard.cs index 8c43b99702..5120757f3d 100644 --- a/osu.Game/Storyboards/Storyboard.cs +++ b/osu.Game/Storyboards/Storyboard.cs @@ -1,6 +1,7 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System; using System.Collections.Generic; using System.IO; using System.Linq; @@ -89,7 +90,7 @@ namespace osu.Game.Storyboards // Importantly, do this after the NullOrEmpty because EF may have stored the non-nullable value as null to the database, bypassing compile-time constraints. backgroundPath = backgroundPath.ToLowerInvariant(); - return GetLayer("Background").Elements.Any(e => e.Path.ToLowerInvariant() == backgroundPath); + return GetLayer("Background").Elements.Any(e => string.Equals(e.Path, backgroundPath, StringComparison.OrdinalIgnoreCase)); } } diff --git a/osu.Game/Tests/Visual/Spectator/TestSpectatorClient.cs b/osu.Game/Tests/Visual/Spectator/TestSpectatorClient.cs index c27e7f15ca..5d33afd288 100644 --- a/osu.Game/Tests/Visual/Spectator/TestSpectatorClient.cs +++ b/osu.Game/Tests/Visual/Spectator/TestSpectatorClient.cs @@ -78,12 +78,12 @@ namespace osu.Game.Tests.Visual.Spectator /// The spectator state to end play with. public void SendEndPlay(int userId, SpectatedUserState state = SpectatedUserState.Quit) { - if (!userBeatmapDictionary.ContainsKey(userId)) + if (!userBeatmapDictionary.TryGetValue(userId, out int beatmapId)) return; ((ISpectatorClient)this).UserFinishedPlaying(userId, new SpectatorState { - BeatmapID = userBeatmapDictionary[userId], + BeatmapID = beatmapId, RulesetID = 0, Mods = userModsDictionary[userId], State = state From ac2c4e81c77fcee81462adf5b2c8d60dd21036a8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 28 Nov 2024 14:04:39 +0100 Subject: [PATCH 0167/1275] Use switch --- .../OnlinePlay/Playlists/PlaylistsRoomFooter.cs | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomFooter.cs b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomFooter.cs index c8e5f0859d..9ccc8f3cab 100644 --- a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomFooter.cs +++ b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomFooter.cs @@ -83,8 +83,14 @@ namespace osu.Game.Screens.OnlinePlay.Playlists private void onRoomChanged(object? sender, PropertyChangedEventArgs e) { - if (e.PropertyName == nameof(Room.Status) || e.PropertyName == nameof(Room.Host) || e.PropertyName == nameof(Room.StartDate)) - updateState(); + switch (e.PropertyName) + { + case nameof(Room.Status): + case nameof(Room.Host): + case nameof(Room.StartDate): + updateState(); + break; + } } private void updateState() From 9926ffd32627b6d50442dabcea30bb58c8f2ca6f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 28 Nov 2024 14:06:12 +0100 Subject: [PATCH 0168/1275] Make button a little narrower --- osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomFooter.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomFooter.cs b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomFooter.cs index 9ccc8f3cab..6089b4734e 100644 --- a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomFooter.cs +++ b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomFooter.cs @@ -59,7 +59,7 @@ namespace osu.Game.Screens.OnlinePlay.Playlists Action = () => OnClose?.Invoke(), Anchor = Anchor.Centre, Origin = Anchor.Centre, - Size = new Vector2(200, 1), + Size = new Vector2(120, 1), Alpha = 0, RelativeSizeAxes = Axes.Y, } From c93c549b054a7e4ec5935a05e9bb71be807a5182 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 28 Nov 2024 14:17:31 +0100 Subject: [PATCH 0169/1275] Fix ready button not disabling on playlist close --- .../Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs index 61df331c48..9573155f5a 100644 --- a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.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.ComponentModel; using System.Diagnostics; using System.Linq; @@ -285,7 +286,11 @@ namespace osu.Game.Screens.OnlinePlay.Playlists DialogOverlay?.Push(new ClosePlaylistDialog(Room, () => { var request = new ClosePlaylistRequest(Room.RoomID!.Value); - request.Success += () => Room.Status = new RoomStatusEnded(); + request.Success += () => + { + Room.Status = new RoomStatusEnded(); + Room.EndDate = DateTimeOffset.UtcNow; + }; API.Queue(request); })); } From 5b63d725c507b7e12cf111e3b3db9faf20cd0725 Mon Sep 17 00:00:00 2001 From: Huo Yaoyuan Date: Thu, 28 Nov 2024 22:13:59 +0800 Subject: [PATCH 0170/1275] Mark CA1826/CA1860 as suggestion --- .globalconfig | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/.globalconfig b/.globalconfig index e218f46cd2..7673e5afb0 100644 --- a/.globalconfig +++ b/.globalconfig @@ -58,13 +58,19 @@ dotnet_diagnostic.CA1806.severity = suggestion # Potential false positive around reflection/too much noise dotnet_diagnostic.CA1822.severity = none +# CA1826: Do not use Enumerable method on indexable collections +dotnet_diagnostic.CA1826.severity = suggestion + # CA1859: Use concrete types when possible for improved performance # Involves design considerations -dotnet_diagnostic.CA1859.severity = none +dotnet_diagnostic.CA1859.severity = suggestion + +# CA1860: Avoid using 'Enumerable.Any()' extension method +dotnet_diagnostic.CA1860.severity = suggestion # CA1861: Avoid constant arrays as arguments # Outdated with collection expressions -dotnet_diagnostic.CA1861.severity = none +dotnet_diagnostic.CA1861.severity = suggestion # CA2007: Consider calling ConfigureAwait on the awaited task dotnet_diagnostic.CA2007.severity = warning From 0a8ec4db2b6fc80cdcf6c9d5dc190b4e31a140d1 Mon Sep 17 00:00:00 2001 From: Huo Yaoyuan Date: Thu, 28 Nov 2024 22:20:36 +0800 Subject: [PATCH 0171/1275] Enable recommended rules for reliability --- .globalconfig | 8 ++++++++ Directory.Build.props | 1 + 2 files changed, 9 insertions(+) diff --git a/.globalconfig b/.globalconfig index 7673e5afb0..7eec612775 100644 --- a/.globalconfig +++ b/.globalconfig @@ -75,6 +75,14 @@ dotnet_diagnostic.CA1861.severity = suggestion # CA2007: Consider calling ConfigureAwait on the awaited task dotnet_diagnostic.CA2007.severity = warning +# CA2016: Forward the 'CancellationToken' parameter to methods +# Some overloads are having special handling for debugger +dotnet_diagnostic.CA2016.severity = suggestion + +# CA2021: Do not call Enumerable.Cast or Enumerable.OfType with incompatible types +# Causing a lot of false positives with generics +dotnet_diagnostic.CA2021.severity = none + # CA2101: Specify marshaling for P/Invoke string arguments # Reports warning for all non-UTF16 usages on DllImport; consider migrating to LibraryImport dotnet_diagnostic.CA2101.severity = none diff --git a/Directory.Build.props b/Directory.Build.props index 0f982d496e..0dcbffd0dc 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -28,6 +28,7 @@ Recommended Default Minimum + Recommended true From fced2545944f401fe6cf7a8429eb97bc774824fc Mon Sep 17 00:00:00 2001 From: Huo Yaoyuan Date: Thu, 28 Nov 2024 22:32:55 +0800 Subject: [PATCH 0172/1275] Enable selected rules for usage --- .globalconfig | 7 +++++-- Directory.Build.props | 2 ++ .../Argon/ManiaArgonSkinTransformer.cs | 20 +++++++++---------- osu.Game/Online/Leaderboards/Leaderboard.cs | 2 +- osu.Game/Online/OnlineViewContainer.cs | 2 +- osu.Game/Overlays/AccountCreationOverlay.cs | 2 +- .../Overlays/Toolbar/ToolbarUserButton.cs | 2 +- 7 files changed, 21 insertions(+), 16 deletions(-) diff --git a/.globalconfig b/.globalconfig index 7eec612775..e2cd1926f0 100644 --- a/.globalconfig +++ b/.globalconfig @@ -90,8 +90,11 @@ dotnet_diagnostic.CA2101.severity = none # CA2201: Do not raise reserved exception types dotnet_diagnostic.CA2201.severity = warning -#Disable operator overloads requiring alternate named methods -dotnet_diagnostic.CA2225.severity = none +# CA2208: Instantiate argument exceptions correctly +dotnet_diagnostic.CA2208.severity = suggestion + +# CA2242: Test for NaN correctly +dotnet_diagnostic.CA2242.severity = warning # Banned APIs dotnet_diagnostic.RS0030.severity = error diff --git a/Directory.Build.props b/Directory.Build.props index 0dcbffd0dc..0ab41d27a0 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -29,6 +29,8 @@ Default Minimum Recommended + Default + Default true diff --git a/osu.Game.Rulesets.Mania/Skinning/Argon/ManiaArgonSkinTransformer.cs b/osu.Game.Rulesets.Mania/Skinning/Argon/ManiaArgonSkinTransformer.cs index afccb2e568..c37c18081a 100644 --- a/osu.Game.Rulesets.Mania/Skinning/Argon/ManiaArgonSkinTransformer.cs +++ b/osu.Game.Rulesets.Mania/Skinning/Argon/ManiaArgonSkinTransformer.cs @@ -164,7 +164,7 @@ namespace osu.Game.Rulesets.Mania.Skinning.Argon case 1: return colour_cyan; - default: throw new ArgumentOutOfRangeException(); + default: throw new ArgumentOutOfRangeException(nameof(columnIndex)); } case 3: @@ -176,7 +176,7 @@ namespace osu.Game.Rulesets.Mania.Skinning.Argon case 2: return colour_cyan; - default: throw new ArgumentOutOfRangeException(); + default: throw new ArgumentOutOfRangeException(nameof(columnIndex)); } case 4: @@ -190,7 +190,7 @@ namespace osu.Game.Rulesets.Mania.Skinning.Argon case 3: return colour_purple; - default: throw new ArgumentOutOfRangeException(); + default: throw new ArgumentOutOfRangeException(nameof(columnIndex)); } case 5: @@ -206,7 +206,7 @@ namespace osu.Game.Rulesets.Mania.Skinning.Argon case 4: return colour_cyan; - default: throw new ArgumentOutOfRangeException(); + default: throw new ArgumentOutOfRangeException(nameof(columnIndex)); } case 6: @@ -224,7 +224,7 @@ namespace osu.Game.Rulesets.Mania.Skinning.Argon case 5: return colour_pink; - default: throw new ArgumentOutOfRangeException(); + default: throw new ArgumentOutOfRangeException(nameof(columnIndex)); } case 7: @@ -244,7 +244,7 @@ namespace osu.Game.Rulesets.Mania.Skinning.Argon case 6: return colour_pink; - default: throw new ArgumentOutOfRangeException(); + default: throw new ArgumentOutOfRangeException(nameof(columnIndex)); } case 8: @@ -266,7 +266,7 @@ namespace osu.Game.Rulesets.Mania.Skinning.Argon case 7: return colour_purple; - default: throw new ArgumentOutOfRangeException(); + default: throw new ArgumentOutOfRangeException(nameof(columnIndex)); } case 9: @@ -290,7 +290,7 @@ namespace osu.Game.Rulesets.Mania.Skinning.Argon case 8: return colour_purple; - default: throw new ArgumentOutOfRangeException(); + default: throw new ArgumentOutOfRangeException(nameof(columnIndex)); } case 10: @@ -316,7 +316,7 @@ namespace osu.Game.Rulesets.Mania.Skinning.Argon case 9: return colour_purple; - default: throw new ArgumentOutOfRangeException(); + default: throw new ArgumentOutOfRangeException(nameof(columnIndex)); } } @@ -339,7 +339,7 @@ namespace osu.Game.Rulesets.Mania.Skinning.Argon case 5: return colour_green; - default: throw new ArgumentOutOfRangeException(); + default: throw new ArgumentOutOfRangeException(nameof(columnIndex)); } } } diff --git a/osu.Game/Online/Leaderboards/Leaderboard.cs b/osu.Game/Online/Leaderboards/Leaderboard.cs index 0fd9597ac0..d76da54adf 100644 --- a/osu.Game/Online/Leaderboards/Leaderboard.cs +++ b/osu.Game/Online/Leaderboards/Leaderboard.cs @@ -363,7 +363,7 @@ namespace osu.Game.Online.Leaderboards return null; default: - throw new ArgumentOutOfRangeException(); + throw new ArgumentOutOfRangeException(nameof(state)); } } diff --git a/osu.Game/Online/OnlineViewContainer.cs b/osu.Game/Online/OnlineViewContainer.cs index 824da152b2..ce55b50d94 100644 --- a/osu.Game/Online/OnlineViewContainer.cs +++ b/osu.Game/Online/OnlineViewContainer.cs @@ -87,7 +87,7 @@ namespace osu.Game.Online break; default: - throw new ArgumentOutOfRangeException(); + throw new ArgumentOutOfRangeException(nameof(state.NewValue)); } }); diff --git a/osu.Game/Overlays/AccountCreationOverlay.cs b/osu.Game/Overlays/AccountCreationOverlay.cs index 82fc5508f1..55cba33153 100644 --- a/osu.Game/Overlays/AccountCreationOverlay.cs +++ b/osu.Game/Overlays/AccountCreationOverlay.cs @@ -126,7 +126,7 @@ namespace osu.Game.Overlays break; default: - throw new ArgumentOutOfRangeException(); + throw new ArgumentOutOfRangeException(nameof(state.NewValue)); } } } diff --git a/osu.Game/Overlays/Toolbar/ToolbarUserButton.cs b/osu.Game/Overlays/Toolbar/ToolbarUserButton.cs index 96c0b15c44..787c525566 100644 --- a/osu.Game/Overlays/Toolbar/ToolbarUserButton.cs +++ b/osu.Game/Overlays/Toolbar/ToolbarUserButton.cs @@ -130,7 +130,7 @@ namespace osu.Game.Overlays.Toolbar break; default: - throw new ArgumentOutOfRangeException(); + throw new ArgumentOutOfRangeException(nameof(state.NewValue)); } }); } From 58cf1c11e4721c85b554ccd08bf3b64ca62b2395 Mon Sep 17 00:00:00 2001 From: Jamie Taylor Date: Thu, 28 Nov 2024 06:41:43 +0900 Subject: [PATCH 0173/1275] Improve menu/context-menu sample playback --- .../UserInterface/TestSceneContextMenu.cs | 11 ++- .../Cursor/OsuContextMenuContainer.cs | 8 --- .../Graphics/UserInterface/HoverSampleSet.cs | 5 +- .../Graphics/UserInterface/OsuContextMenu.cs | 15 ++-- .../UserInterface/OsuContextMenuSamples.cs | 37 ---------- osu.Game/Graphics/UserInterface/OsuMenu.cs | 19 ++--- .../Graphics/UserInterface/OsuMenuSamples.cs | 70 +++++++++++++++++++ osu.Game/OsuGameBase.cs | 5 ++ .../Edit/Components/Menus/EditorMenuBar.cs | 2 +- 9 files changed, 103 insertions(+), 69 deletions(-) delete mode 100644 osu.Game/Graphics/UserInterface/OsuContextMenuSamples.cs create mode 100644 osu.Game/Graphics/UserInterface/OsuMenuSamples.cs diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneContextMenu.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneContextMenu.cs index 2a2f267fc8..118e37dab4 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneContextMenu.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneContextMenu.cs @@ -101,7 +101,16 @@ namespace osu.Game.Tests.Visual.UserInterface } } } - } + }, + } + }, + new OsuMenuItem(@"Another nested option") + { + Items = new MenuItem[] + { + new OsuMenuItem(@"Sub-One"), + new OsuMenuItem(@"Sub-Two"), + new OsuMenuItem(@"Sub-Three"), } }, new OsuMenuItem(@"Choose me please"), diff --git a/osu.Game/Graphics/Cursor/OsuContextMenuContainer.cs b/osu.Game/Graphics/Cursor/OsuContextMenuContainer.cs index 7b21a413f7..3180661b0c 100644 --- a/osu.Game/Graphics/Cursor/OsuContextMenuContainer.cs +++ b/osu.Game/Graphics/Cursor/OsuContextMenuContainer.cs @@ -11,16 +11,8 @@ namespace osu.Game.Graphics.Cursor [Cached(typeof(OsuContextMenuContainer))] public partial class OsuContextMenuContainer : ContextMenuContainer { - [Cached] - private OsuContextMenuSamples samples = new OsuContextMenuSamples(); - private OsuContextMenu menu = null!; - public OsuContextMenuContainer() - { - AddInternal(samples); - } - protected override Menu CreateMenu() => menu = new OsuContextMenu(true); public void CloseMenu() diff --git a/osu.Game/Graphics/UserInterface/HoverSampleSet.cs b/osu.Game/Graphics/UserInterface/HoverSampleSet.cs index 5b0fbc693e..62eb765cc8 100644 --- a/osu.Game/Graphics/UserInterface/HoverSampleSet.cs +++ b/osu.Game/Graphics/UserInterface/HoverSampleSet.cs @@ -23,6 +23,9 @@ namespace osu.Game.Graphics.UserInterface DialogCancel, [Description("dialog-ok")] - DialogOk + DialogOk, + + [Description("menu-open")] + MenuOpen, } } diff --git a/osu.Game/Graphics/UserInterface/OsuContextMenu.cs b/osu.Game/Graphics/UserInterface/OsuContextMenu.cs index 96797e5d01..7a5d2c369b 100644 --- a/osu.Game/Graphics/UserInterface/OsuContextMenu.cs +++ b/osu.Game/Graphics/UserInterface/OsuContextMenu.cs @@ -15,7 +15,7 @@ namespace osu.Game.Graphics.UserInterface private const int fade_duration = 250; [Resolved] - private OsuContextMenuSamples samples { get; set; } = null!; + private OsuMenuSamples menuSamples { get; set; } = null!; // todo: this shouldn't be required after https://github.com/ppy/osu-framework/issues/4519 is fixed. private bool wasOpened; @@ -47,15 +47,14 @@ namespace osu.Game.Graphics.UserInterface protected override void AnimateOpen() { + wasOpened = true; this.FadeIn(fade_duration, Easing.OutQuint); - if (playClickSample) - samples.PlayClickSample(); + if (!playClickSample) + return; - if (!wasOpened) - samples.PlayOpenSample(); - - wasOpened = true; + menuSamples?.PlayClickSample(); + menuSamples?.PlayOpenSample(); } protected override void AnimateClose() @@ -63,7 +62,7 @@ namespace osu.Game.Graphics.UserInterface this.FadeOut(fade_duration, Easing.OutQuint); if (wasOpened) - samples.PlayCloseSample(); + menuSamples?.PlayCloseSample(); wasOpened = false; } diff --git a/osu.Game/Graphics/UserInterface/OsuContextMenuSamples.cs b/osu.Game/Graphics/UserInterface/OsuContextMenuSamples.cs deleted file mode 100644 index 6d7543c472..0000000000 --- a/osu.Game/Graphics/UserInterface/OsuContextMenuSamples.cs +++ /dev/null @@ -1,37 +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 osu.Framework.Allocation; -using osu.Framework.Audio; -using osu.Framework.Audio.Sample; -using osu.Framework.Extensions; -using osu.Framework.Graphics; - -namespace osu.Game.Graphics.UserInterface -{ - public partial class OsuContextMenuSamples : Component - { - private Sample sampleClick; - private Sample sampleOpen; - private Sample sampleClose; - - [BackgroundDependencyLoader] - private void load(AudioManager audio) - { - sampleClick = audio.Samples.Get($@"UI/{HoverSampleSet.Default.GetDescription()}-select"); - sampleOpen = audio.Samples.Get(@"UI/dropdown-open"); - sampleClose = audio.Samples.Get(@"UI/dropdown-close"); - } - - public void PlayClickSample() => Scheduler.AddOnce(playClickSample); - private void playClickSample() => sampleClick.Play(); - - public void PlayOpenSample() => Scheduler.AddOnce(playOpenSample); - private void playOpenSample() => sampleOpen.Play(); - - public void PlayCloseSample() => Scheduler.AddOnce(playCloseSample); - private void playCloseSample() => sampleClose.Play(); - } -} diff --git a/osu.Game/Graphics/UserInterface/OsuMenu.cs b/osu.Game/Graphics/UserInterface/OsuMenu.cs index 6e7dad2b5f..7cc1bab25f 100644 --- a/osu.Game/Graphics/UserInterface/OsuMenu.cs +++ b/osu.Game/Graphics/UserInterface/OsuMenu.cs @@ -4,8 +4,6 @@ #nullable disable using osu.Framework.Allocation; -using osu.Framework.Audio; -using osu.Framework.Audio.Sample; using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; @@ -20,12 +18,12 @@ namespace osu.Game.Graphics.UserInterface { public partial class OsuMenu : Menu { - private Sample sampleOpen; - private Sample sampleClose; - // todo: this shouldn't be required after https://github.com/ppy/osu-framework/issues/4519 is fixed. private bool wasOpened; + [Resolved] + private OsuMenuSamples menuSamples { get; set; } = null!; + public OsuMenu(Direction direction, bool topLevelMenu = false) : base(direction, topLevelMenu) { @@ -33,13 +31,8 @@ namespace osu.Game.Graphics.UserInterface MaskingContainer.CornerRadius = 4; ItemsContainer.Padding = new MarginPadding(5); - } - [BackgroundDependencyLoader] - private void load(AudioManager audio) - { - sampleOpen = audio.Samples.Get(@"UI/dropdown-open"); - sampleClose = audio.Samples.Get(@"UI/dropdown-close"); + OnSubmenuOpen += _ => { menuSamples?.PlaySubOpenSample(); }; } protected override void Update() @@ -64,7 +57,7 @@ namespace osu.Game.Graphics.UserInterface protected override void AnimateOpen() { if (!TopLevelMenu && !wasOpened) - sampleOpen?.Play(); + menuSamples?.PlayOpenSample(); this.FadeIn(300, Easing.OutQuint); wasOpened = true; @@ -73,7 +66,7 @@ namespace osu.Game.Graphics.UserInterface protected override void AnimateClose() { if (!TopLevelMenu && wasOpened) - sampleClose?.Play(); + menuSamples?.PlayCloseSample(); this.FadeOut(300, Easing.OutQuint); wasOpened = false; diff --git a/osu.Game/Graphics/UserInterface/OsuMenuSamples.cs b/osu.Game/Graphics/UserInterface/OsuMenuSamples.cs new file mode 100644 index 0000000000..779671b6ad --- /dev/null +++ b/osu.Game/Graphics/UserInterface/OsuMenuSamples.cs @@ -0,0 +1,70 @@ +// 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.Audio; +using osu.Framework.Audio.Sample; +using osu.Framework.Graphics; + +namespace osu.Game.Graphics.UserInterface +{ + public partial class OsuMenuSamples : Component + { + private Sample sampleClick; + private Sample sampleOpen; + private Sample sampleSubOpen; + private Sample sampleClose; + + private bool triggerOpen; + private bool triggerSubOpen; + private bool triggerClose; + + [BackgroundDependencyLoader] + private void load(AudioManager audio) + { + sampleClick = audio.Samples.Get(@"UI/menu-open-select"); + sampleOpen = audio.Samples.Get(@"UI/menu-open"); + sampleSubOpen = audio.Samples.Get(@"UI/menu-sub-open"); + sampleClose = audio.Samples.Get(@"UI/menu-close"); + } + + public void PlayClickSample() + { + Scheduler.AddOnce(playClickSample); + } + + public void PlayOpenSample() + { + triggerOpen = true; + Scheduler.AddOnce(resolvePlayback); + } + + public void PlaySubOpenSample() + { + triggerSubOpen = true; + Scheduler.AddOnce(resolvePlayback); + } + + public void PlayCloseSample() + { + triggerClose = true; + Scheduler.AddOnce(resolvePlayback); + } + + private void playClickSample() => sampleClick.Play(); + + private void resolvePlayback() + { + if (triggerSubOpen) + sampleSubOpen?.Play(); + else if (triggerOpen) + sampleOpen?.Play(); + else if (triggerClose) + sampleClose?.Play(); + + triggerOpen = triggerSubOpen = triggerClose = false; + } + } +} diff --git a/osu.Game/OsuGameBase.cs b/osu.Game/OsuGameBase.cs index dc13924b4f..0f9848cacc 100644 --- a/osu.Game/OsuGameBase.cs +++ b/osu.Game/OsuGameBase.cs @@ -41,6 +41,7 @@ using osu.Game.Database; using osu.Game.Extensions; using osu.Game.Graphics; using osu.Game.Graphics.Cursor; +using osu.Game.Graphics.UserInterface; using osu.Game.Input; using osu.Game.Input.Bindings; using osu.Game.IO; @@ -385,6 +386,10 @@ namespace osu.Game GlobalActionContainer globalBindings; + OsuMenuSamples menuSamples; + dependencies.Cache(menuSamples = new OsuMenuSamples()); + base.Content.Add(menuSamples); + base.Content.Add(SafeAreaContainer = new SafeAreaContainer { SafeAreaOverrideEdges = SafeAreaOverrideEdges, diff --git a/osu.Game/Screens/Edit/Components/Menus/EditorMenuBar.cs b/osu.Game/Screens/Edit/Components/Menus/EditorMenuBar.cs index 47a13dcfba..76b8811b89 100644 --- a/osu.Game/Screens/Edit/Components/Menus/EditorMenuBar.cs +++ b/osu.Game/Screens/Edit/Components/Menus/EditorMenuBar.cs @@ -99,7 +99,7 @@ namespace osu.Game.Screens.Edit.Components.Menus ForegroundColourHover = colourProvider.Content1; BackgroundColourHover = colourProvider.Background1; - AddInternal(hoverClickSounds = new HoverClickSounds()); + AddInternal(hoverClickSounds = new HoverClickSounds(HoverSampleSet.MenuOpen)); } protected override void LoadComplete() From 932afcde01469543084467b4699d9774123b8363 Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Thu, 28 Nov 2024 17:43:32 -0500 Subject: [PATCH 0174/1275] Make editor make sense --- osu.Game/Screens/Edit/Editor.cs | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/osu.Game/Screens/Edit/Editor.cs b/osu.Game/Screens/Edit/Editor.cs index 13e5791605..0e4807dc78 100644 --- a/osu.Game/Screens/Edit/Editor.cs +++ b/osu.Game/Screens/Edit/Editor.cs @@ -80,8 +80,6 @@ namespace osu.Game.Screens.Edit public override float BackgroundParallaxAmount => 0.1f; - public override bool AllowUserExit => false; - public override bool HideOverlaysOnEnter => true; public override bool DisallowExternalBeatmapRulesetChanges => true; @@ -194,6 +192,8 @@ namespace osu.Game.Screens.Edit } } + protected override bool InitialBackButtonVisibility => false; + protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent) => dependencies = new DependencyContainer(base.CreateChildDependencies(parent)); @@ -760,11 +760,6 @@ namespace osu.Game.Screens.Edit switch (e.Action) { - case GlobalAction.Back: - // as we don't want to display the back button, manual handling of exit action is required. - this.Exit(); - return true; - case GlobalAction.EditorCloneSelection: Clone(); return true; From 078d62fe093fc1ba9587cb6f3cdd4e4fec02e1f7 Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Thu, 28 Nov 2024 17:54:03 -0500 Subject: [PATCH 0175/1275] Fix weird default in test scene --- .../Visual/Online/TestSceneUserProfileDailyChallenge.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Tests/Visual/Online/TestSceneUserProfileDailyChallenge.cs b/osu.Game.Tests/Visual/Online/TestSceneUserProfileDailyChallenge.cs index 3222e16412..0477d39193 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneUserProfileDailyChallenge.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneUserProfileDailyChallenge.cs @@ -37,7 +37,7 @@ namespace osu.Game.Tests.Visual.Online 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, 0, v => update(s => s.PlayCount = v)); + AddSliderStep("playcount", 0, 1500, 1, v => update(s => s.PlayCount = v)); AddStep("create", () => { Clear(); From 51bcde67aae51cf23250109b4256a931dcf074f3 Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Thu, 28 Nov 2024 17:55:15 -0500 Subject: [PATCH 0176/1275] Remove no longer required comment --- .../Profile/Header/Components/DailyChallengeStatsTooltip.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/osu.Game/Overlays/Profile/Header/Components/DailyChallengeStatsTooltip.cs b/osu.Game/Overlays/Profile/Header/Components/DailyChallengeStatsTooltip.cs index ea49f9d784..826b40d70c 100644 --- a/osu.Game/Overlays/Profile/Header/Components/DailyChallengeStatsTooltip.cs +++ b/osu.Game/Overlays/Profile/Header/Components/DailyChallengeStatsTooltip.cs @@ -140,7 +140,6 @@ namespace osu.Game.Overlays.Profile.Header.Components // reference: https://github.com/ppy/osu-web/blob/a97f156014e00ea1aa315140da60542e798a9f06/resources/js/profile-page/daily-challenge.tsx#L13-L47 - // Rounding down is needed here to ensure the overlay shows the same colour as osu-web for the play count. public static RankingTier TierForPlayCount(int playCount) => TierForDaily((int)Math.Floor(playCount / 3.0d)); public static RankingTier TierForDaily(int daily) From 311f0947e41b44aaf5a08397138a8b3d57bc59d7 Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Thu, 28 Nov 2024 17:57:47 -0500 Subject: [PATCH 0177/1275] Abstractify resource change logic and share between background and audio --- .../Screens/Edit/Setup/ResourcesSection.cs | 91 ++++++------------- 1 file changed, 29 insertions(+), 62 deletions(-) diff --git a/osu.Game/Screens/Edit/Setup/ResourcesSection.cs b/osu.Game/Screens/Edit/Setup/ResourcesSection.cs index 1ce944b5a4..a02900a204 100644 --- a/osu.Game/Screens/Edit/Setup/ResourcesSection.cs +++ b/osu.Game/Screens/Edit/Setup/ResourcesSection.cs @@ -82,57 +82,12 @@ namespace osu.Game.Screens.Edit.Setup if (!source.Exists) return false; - var set = working.Value.BeatmapSetInfo; - - if (applyToAllDifficulties) - { - string newFilename = $@"bg{source.Extension}"; - - foreach (var beatmapInSet in set.Beatmaps) - { - if (set.GetFile(beatmapInSet.Metadata.BackgroundFile) is RealmNamedFileUsage existingFile) - beatmaps.DeleteFile(set, existingFile); - - if (beatmapInSet.Metadata.BackgroundFile != newFilename) - { - beatmapInSet.Metadata.BackgroundFile = newFilename; - - if (!beatmapInSet.Equals(working.Value.BeatmapInfo)) - beatmaps.Save(beatmapInSet, beatmaps.GetWorkingBeatmap(beatmapInSet).Beatmap); - } - } - } - else - { - var beatmap = working.Value.BeatmapInfo; - - string[] filenames = set.Files.Select(f => f.Filename).Where(f => - f.StartsWith(@"bg", StringComparison.OrdinalIgnoreCase) && - f.EndsWith(source.Extension, StringComparison.OrdinalIgnoreCase)).ToArray(); - - string currentFilename = working.Value.Metadata.BackgroundFile; - - var oldFile = set.GetFile(currentFilename); - string? newFilename = null; - - if (oldFile != null && set.Beatmaps.Where(b => !b.Equals(beatmap)).All(b => b.Metadata.BackgroundFile != currentFilename)) - { - beatmaps.DeleteFile(set, oldFile); - newFilename = currentFilename; - } - - newFilename ??= NamingUtils.GetNextBestFilename(filenames, $@"bg{source.Extension}"); - working.Value.Metadata.BackgroundFile = newFilename; - } - - using (var stream = source.OpenRead()) - beatmaps.AddFile(set, stream, working.Value.Metadata.BackgroundFile); - - editorBeatmap.SaveState(); + changeResource(source, applyToAllDifficulties, @"bg", + metadata => metadata.BackgroundFile, + (metadata, name) => metadata.BackgroundFile = name); headerBackground.UpdateBackground(); editor?.ApplyToBackground(bg => bg.RefreshBackground()); - return true; } @@ -141,20 +96,34 @@ namespace osu.Game.Screens.Edit.Setup if (!source.Exists) return false; + changeResource(source, applyToAllDifficulties, @"audio", + metadata => metadata.AudioFile, + (metadata, name) => metadata.AudioFile = name); + + music.ReloadCurrentTrack(); + return true; + } + + private void changeResource(FileInfo source, bool applyToAllDifficulties, string baseFilename, Func readFilename, Action writeFilename) + { var set = working.Value.BeatmapSetInfo; + string newFilename = string.Empty; + if (applyToAllDifficulties) { - string newFilename = $@"audio{source.Extension}"; + newFilename = $"{baseFilename}{source.Extension}"; foreach (var beatmapInSet in set.Beatmaps) { - if (set.GetFile(beatmapInSet.Metadata.AudioFile) is RealmNamedFileUsage existingFile) + string filenameInBeatmap = readFilename(beatmapInSet.Metadata); + + if (set.GetFile(filenameInBeatmap) is RealmNamedFileUsage existingFile) beatmaps.DeleteFile(set, existingFile); - if (beatmapInSet.Metadata.AudioFile != newFilename) + if (filenameInBeatmap != newFilename) { - beatmapInSet.Metadata.AudioFile = newFilename; + writeFilename(beatmapInSet.Metadata, newFilename); if (!beatmapInSet.Equals(working.Value.BeatmapInfo)) beatmaps.Save(beatmapInSet, beatmaps.GetWorkingBeatmap(beatmapInSet).Beatmap); @@ -166,31 +135,29 @@ namespace osu.Game.Screens.Edit.Setup var beatmap = working.Value.BeatmapInfo; string[] filenames = set.Files.Select(f => f.Filename).Where(f => - f.StartsWith(@"audio", StringComparison.OrdinalIgnoreCase) && + f.StartsWith(baseFilename, StringComparison.OrdinalIgnoreCase) && f.EndsWith(source.Extension, StringComparison.OrdinalIgnoreCase)).ToArray(); - string currentFilename = working.Value.Metadata.AudioFile; + string currentFilename = readFilename(working.Value.Metadata); var oldFile = set.GetFile(currentFilename); - string? newFilename = null; - if (oldFile != null && set.Beatmaps.Where(b => !b.Equals(beatmap)).All(b => b.Metadata.AudioFile != currentFilename)) + if (oldFile != null && set.Beatmaps.Where(b => !b.Equals(beatmap)).All(b => readFilename(b.Metadata) != currentFilename)) { beatmaps.DeleteFile(set, oldFile); newFilename = currentFilename; } - newFilename ??= NamingUtils.GetNextBestFilename(filenames, $@"audio{source.Extension}"); - working.Value.Metadata.AudioFile = newFilename; + if (string.IsNullOrEmpty(newFilename)) + newFilename = NamingUtils.GetNextBestFilename(filenames, $@"{baseFilename}{source.Extension}"); + + writeFilename(working.Value.Metadata, newFilename); } using (var stream = source.OpenRead()) - beatmaps.AddFile(set, stream, working.Value.Metadata.AudioFile); + beatmaps.AddFile(set, stream, newFilename); editorBeatmap.SaveState(); - music.ReloadCurrentTrack(); - - return true; } private void backgroundChanged(ValueChangedEvent file) From 489d7a30ec093152cd838cfba9b64c6f235bfe66 Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Thu, 28 Nov 2024 18:32:03 -0500 Subject: [PATCH 0178/1275] Perform a single `Save` call rather than doing it in each difficulty --- .../Editing/TestSceneEditorBeatmapCreation.cs | 3 --- .../Screens/Edit/Setup/ResourcesSection.cs | 27 +++++++------------ 2 files changed, 9 insertions(+), 21 deletions(-) diff --git a/osu.Game.Tests/Visual/Editing/TestSceneEditorBeatmapCreation.cs b/osu.Game.Tests/Visual/Editing/TestSceneEditorBeatmapCreation.cs index c7d745b6e0..7a390ac131 100644 --- a/osu.Game.Tests/Visual/Editing/TestSceneEditorBeatmapCreation.cs +++ b/osu.Game.Tests/Visual/Editing/TestSceneEditorBeatmapCreation.cs @@ -107,9 +107,6 @@ namespace osu.Game.Tests.Visual.Editing AddStep("test play", () => Editor.TestGameplay()); - AddUntilStep("wait for dialog", () => DialogOverlay.CurrentDialog != null); - AddStep("confirm save", () => InputManager.Key(Key.Number1)); - AddUntilStep("wait for return to editor", () => Editor.IsCurrentScreen()); AddAssert("track is still not virtual", () => Beatmap.Value.Track is not TrackVirtual); diff --git a/osu.Game/Screens/Edit/Setup/ResourcesSection.cs b/osu.Game/Screens/Edit/Setup/ResourcesSection.cs index a02900a204..4d2bbb035e 100644 --- a/osu.Game/Screens/Edit/Setup/ResourcesSection.cs +++ b/osu.Game/Screens/Edit/Setup/ResourcesSection.cs @@ -32,9 +32,6 @@ namespace osu.Game.Screens.Edit.Setup [Resolved] private IBindable working { get; set; } = null!; - [Resolved] - private EditorBeatmap editorBeatmap { get; set; } = null!; - [Resolved] private Editor? editor { get; set; } @@ -114,25 +111,17 @@ namespace osu.Game.Screens.Edit.Setup { newFilename = $"{baseFilename}{source.Extension}"; - foreach (var beatmapInSet in set.Beatmaps) + foreach (var beatmap in set.Beatmaps) { - string filenameInBeatmap = readFilename(beatmapInSet.Metadata); + if (set.GetFile(readFilename(beatmap.Metadata)) is RealmNamedFileUsage otherExistingFile) + beatmaps.DeleteFile(set, otherExistingFile); - if (set.GetFile(filenameInBeatmap) is RealmNamedFileUsage existingFile) - beatmaps.DeleteFile(set, existingFile); - - if (filenameInBeatmap != newFilename) - { - writeFilename(beatmapInSet.Metadata, newFilename); - - if (!beatmapInSet.Equals(working.Value.BeatmapInfo)) - beatmaps.Save(beatmapInSet, beatmaps.GetWorkingBeatmap(beatmapInSet).Beatmap); - } + writeFilename(beatmap.Metadata, newFilename); } } else { - var beatmap = working.Value.BeatmapInfo; + var thisBeatmap = working.Value.BeatmapInfo; string[] filenames = set.Files.Select(f => f.Filename).Where(f => f.StartsWith(baseFilename, StringComparison.OrdinalIgnoreCase) && @@ -142,7 +131,7 @@ namespace osu.Game.Screens.Edit.Setup var oldFile = set.GetFile(currentFilename); - if (oldFile != null && set.Beatmaps.Where(b => !b.Equals(beatmap)).All(b => readFilename(b.Metadata) != currentFilename)) + if (oldFile != null && set.Beatmaps.Where(b => !b.Equals(thisBeatmap)).All(b => readFilename(b.Metadata) != currentFilename)) { beatmaps.DeleteFile(set, oldFile); newFilename = currentFilename; @@ -157,7 +146,9 @@ namespace osu.Game.Screens.Edit.Setup using (var stream = source.OpenRead()) beatmaps.AddFile(set, stream, newFilename); - editorBeatmap.SaveState(); + // 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. + editor?.Save(); } private void backgroundChanged(ValueChangedEvent file) From 276c37bcf77b19762c84b9d4c89251597a492ea8 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 29 Nov 2024 13:47:56 +0900 Subject: [PATCH 0179/1275] 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 4699beeac0..02898623a9 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 ccae4a15ee..80e695e5d1 100644 --- a/osu.iOS.props +++ b/osu.iOS.props @@ -17,6 +17,6 @@ -all - + From b697ddc6db70de3ff8a7f07a9f734de66ea7f694 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 29 Nov 2024 15:32:35 +0900 Subject: [PATCH 0180/1275] Simplify the dev footer display --- osu.Game/OsuGame.cs | 14 ++--- osu.Game/Overlays/DevBuildBanner.cs | 58 ++++++++++++++++++ osu.Game/Overlays/VersionManager.cs | 95 ----------------------------- osu.Game/Screens/Menu/MainMenu.cs | 13 ---- 4 files changed, 64 insertions(+), 116 deletions(-) create mode 100644 osu.Game/Overlays/DevBuildBanner.cs delete mode 100644 osu.Game/Overlays/VersionManager.cs diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs index a92b1f4d36..2e3b989c4e 100644 --- a/osu.Game/OsuGame.cs +++ b/osu.Game/OsuGame.cs @@ -195,7 +195,8 @@ namespace osu.Game private MainMenu menuScreen; - private VersionManager versionManager; + [CanBeNull] + private DevBuildBanner devBuildBanner; [CanBeNull] private IntroScreen introScreen; @@ -1055,10 +1056,7 @@ namespace osu.Game }, topMostOverlayContent.Add); if (!IsDeployedBuild) - { - dependencies.Cache(versionManager = new VersionManager()); - loadComponentSingleFile(versionManager, ScreenContainer.Add); - } + loadComponentSingleFile(devBuildBanner = new DevBuildBanner(), ScreenContainer.Add); loadComponentSingleFile(osuLogo, _ => { @@ -1564,12 +1562,12 @@ namespace osu.Game { case IntroScreen intro: introScreen = intro; - versionManager?.Show(); + devBuildBanner?.Show(); break; case MainMenu menu: menuScreen = menu; - versionManager?.Show(); + devBuildBanner?.Show(); break; case Player player: @@ -1577,7 +1575,7 @@ namespace osu.Game break; default: - versionManager?.Hide(); + devBuildBanner?.Hide(); break; } diff --git a/osu.Game/Overlays/DevBuildBanner.cs b/osu.Game/Overlays/DevBuildBanner.cs new file mode 100644 index 0000000000..f514483e76 --- /dev/null +++ b/osu.Game/Overlays/DevBuildBanner.cs @@ -0,0 +1,58 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Allocation; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Sprites; +using osu.Framework.Graphics.Textures; +using osu.Game.Graphics; +using osu.Game.Graphics.Sprites; +using osuTK; + +namespace osu.Game.Overlays +{ + public partial class DevBuildBanner : VisibilityContainer + { + [BackgroundDependencyLoader] + private void load(OsuColour colours, TextureStore textures, OsuGameBase game) + { + AutoSizeAxes = Axes.Both; + + Anchor = Anchor.BottomCentre; + Origin = Anchor.BottomCentre; + + Alpha = 0; + + AddRange(new Drawable[] + { + new OsuSpriteText + { + Anchor = Anchor.BottomCentre, + Origin = Anchor.BottomCentre, + Font = OsuFont.Numeric.With(weight: FontWeight.Bold, size: 12), + Colour = colours.YellowDark, + Text = @"DEVELOPER BUILD", + }, + new Sprite + { + Anchor = Anchor.BottomCentre, + Origin = Anchor.BottomCentre, + Texture = textures.Get(@"Menu/dev-build-footer"), + Scale = new Vector2(0.4f, 1), + Y = 2, + }, + }); + } + + protected override void PopIn() + { + this.FadeIn(1400, Easing.OutQuint); + } + + protected override void PopOut() + { + this.FadeOut(500, Easing.OutQuint); + } + } +} diff --git a/osu.Game/Overlays/VersionManager.cs b/osu.Game/Overlays/VersionManager.cs deleted file mode 100644 index 71f8fc05aa..0000000000 --- a/osu.Game/Overlays/VersionManager.cs +++ /dev/null @@ -1,95 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -using osu.Framework.Allocation; -using osu.Framework.Development; -using osu.Framework.Graphics; -using osu.Framework.Graphics.Containers; -using osu.Framework.Graphics.Sprites; -using osu.Framework.Graphics.Textures; -using osu.Game.Graphics; -using osu.Game.Graphics.Sprites; -using osuTK; -using osuTK.Graphics; - -namespace osu.Game.Overlays -{ - public partial class VersionManager : VisibilityContainer - { - [BackgroundDependencyLoader] - private void load(OsuColour colours, TextureStore textures, OsuGameBase game) - { - AutoSizeAxes = Axes.Both; - Anchor = Anchor.BottomCentre; - Origin = Anchor.BottomCentre; - - Alpha = 0; - - FillFlowContainer mainFill; - - Children = new Drawable[] - { - mainFill = new FillFlowContainer - { - AutoSizeAxes = Axes.Both, - Direction = FillDirection.Vertical, - Children = new Drawable[] - { - new FillFlowContainer - { - AutoSizeAxes = Axes.Both, - Direction = FillDirection.Horizontal, - Spacing = new Vector2(5), - Anchor = Anchor.TopCentre, - Origin = Anchor.TopCentre, - Children = new Drawable[] - { - new OsuSpriteText - { - Font = OsuFont.GetFont(weight: FontWeight.Bold), - Text = game.Name - }, - new OsuSpriteText - { - Colour = DebugUtils.IsDebugBuild ? colours.Red : Color4.White, - Text = game.Version - }, - } - }, - } - } - }; - - if (!game.IsDeployedBuild) - { - mainFill.AddRange(new Drawable[] - { - new OsuSpriteText - { - Anchor = Anchor.TopCentre, - Origin = Anchor.TopCentre, - Font = OsuFont.Numeric.With(size: 12), - Colour = colours.Yellow, - Text = @"Development Build" - }, - new Sprite - { - Anchor = Anchor.TopCentre, - Origin = Anchor.TopCentre, - Texture = textures.Get(@"Menu/dev-build-footer"), - }, - }); - } - } - - protected override void PopIn() - { - this.FadeIn(1400, Easing.OutQuint); - } - - protected override void PopOut() - { - this.FadeOut(500, Easing.OutQuint); - } - } -} diff --git a/osu.Game/Screens/Menu/MainMenu.cs b/osu.Game/Screens/Menu/MainMenu.cs index 35c6bab81b..c753a52657 100644 --- a/osu.Game/Screens/Menu/MainMenu.cs +++ b/osu.Game/Screens/Menu/MainMenu.cs @@ -79,9 +79,6 @@ namespace osu.Game.Screens.Menu [Resolved(canBeNull: true)] private IDialogOverlay dialogOverlay { get; set; } - [Resolved(canBeNull: true)] - private VersionManager versionManager { get; set; } - protected override BackgroundScreen CreateBackground() => new BackgroundScreenDefault(); protected override bool PlayExitSound => false; @@ -294,16 +291,6 @@ namespace osu.Game.Screens.Menu } } - protected override void Update() - { - base.Update(); - - bottomElementsFlow.Margin = new MarginPadding - { - Bottom = (versionManager?.DrawHeight + 5) ?? 0 - }; - } - protected override void LogoSuspending(OsuLogo logo) { var seq = logo.FadeOut(300, Easing.InSine) From 110e4fbb30503114779e18348e098d062a9ea378 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 28 Nov 2024 15:37:27 +0100 Subject: [PATCH 0181/1275] Centralise supported file extensions to one helper class As proposed in https://github.com/ppy/osu-server-beatmap-submission/pull/5#discussion_r1861680837. --- osu.Game/Beatmaps/BeatmapManager.cs | 2 +- .../Beatmaps/Formats/LegacyBeatmapDecoder.cs | 3 ++- .../Formats/LegacyStoryboardDecoder.cs | 3 ++- .../UserInterfaceV2/OsuFileSelector.cs | 23 ++++++++----------- osu.Game/OsuGameBase.cs | 2 -- osu.Game/Overlays/SkinEditor/SkinEditor.cs | 3 ++- .../Edit/Checks/Components/AudioCheckUtils.cs | 5 ++-- .../Screens/Edit/Setup/ResourcesSection.cs | 7 ++++-- osu.Game/Skinning/SkinnableSprite.cs | 11 +++++---- osu.Game/Storyboards/Storyboard.cs | 5 ++-- osu.Game/Utils/SupportedExtensions.cs | 19 +++++++++++++++ 11 files changed, 50 insertions(+), 33 deletions(-) create mode 100644 osu.Game/Utils/SupportedExtensions.cs diff --git a/osu.Game/Beatmaps/BeatmapManager.cs b/osu.Game/Beatmaps/BeatmapManager.cs index f1ce977d96..07fcdb9d62 100644 --- a/osu.Game/Beatmaps/BeatmapManager.cs +++ b/osu.Game/Beatmaps/BeatmapManager.cs @@ -408,7 +408,7 @@ namespace osu.Game.Beatmaps // user requested abort return; - var video = b.Files.FirstOrDefault(f => OsuGameBase.VIDEO_EXTENSIONS.Any(ex => f.Filename.EndsWith(ex, StringComparison.OrdinalIgnoreCase))); + var video = b.Files.FirstOrDefault(f => SupportedExtensions.VIDEO_EXTENSIONS.Any(ex => f.Filename.EndsWith(ex, StringComparison.OrdinalIgnoreCase))); if (video != null) { diff --git a/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs b/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs index 4d7ac355e0..d6c658f824 100644 --- a/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs +++ b/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs @@ -17,6 +17,7 @@ using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects.Legacy; using osu.Game.Rulesets.Objects.Types; using osu.Game.Screens.Edit; +using osu.Game.Utils; namespace osu.Game.Beatmaps.Formats { @@ -446,7 +447,7 @@ namespace osu.Game.Beatmaps.Formats // Some very old beatmaps had incorrect type specifications for their backgrounds (ie. using 1 for VIDEO // instead of 0 for BACKGROUND). To handle this gracefully, check the file extension against known supported // video extensions and handle similar to a background if it doesn't match. - if (!OsuGameBase.VIDEO_EXTENSIONS.Contains(Path.GetExtension(filename).ToLowerInvariant())) + if (!SupportedExtensions.VIDEO_EXTENSIONS.Contains(Path.GetExtension(filename).ToLowerInvariant())) { beatmap.BeatmapInfo.Metadata.BackgroundFile = filename; lineSupportedByEncoder = true; diff --git a/osu.Game/Beatmaps/Formats/LegacyStoryboardDecoder.cs b/osu.Game/Beatmaps/Formats/LegacyStoryboardDecoder.cs index 2f9a256d31..fe9a852faf 100644 --- a/osu.Game/Beatmaps/Formats/LegacyStoryboardDecoder.cs +++ b/osu.Game/Beatmaps/Formats/LegacyStoryboardDecoder.cs @@ -10,6 +10,7 @@ using osu.Game.Beatmaps.Legacy; using osu.Game.IO; using osu.Game.Storyboards; using osu.Game.Storyboards.Commands; +using osu.Game.Utils; using osuTK; using osuTK.Graphics; @@ -112,7 +113,7 @@ namespace osu.Game.Beatmaps.Formats // // This avoids potential weird crashes when ffmpeg attempts to parse an image file as a video // (see https://github.com/ppy/osu/issues/22829#issuecomment-1465552451). - if (!OsuGameBase.VIDEO_EXTENSIONS.Contains(Path.GetExtension(path).ToLowerInvariant())) + if (!SupportedExtensions.VIDEO_EXTENSIONS.Contains(Path.GetExtension(path).ToLowerInvariant())) break; storyboard.GetLayer("Video").Add(storyboardSprite = new StoryboardVideo(path, offset)); diff --git a/osu.Game/Graphics/UserInterfaceV2/OsuFileSelector.cs b/osu.Game/Graphics/UserInterfaceV2/OsuFileSelector.cs index c7b559d9ed..addea5c4a9 100644 --- a/osu.Game/Graphics/UserInterfaceV2/OsuFileSelector.cs +++ b/osu.Game/Graphics/UserInterfaceV2/OsuFileSelector.cs @@ -13,6 +13,7 @@ using osu.Game.Graphics.Containers; using osu.Game.Graphics.Sprites; using osu.Game.Graphics.UserInterfaceV2.FileSelection; using osu.Game.Overlays; +using osu.Game.Utils; namespace osu.Game.Graphics.UserInterfaceV2 { @@ -96,24 +97,18 @@ namespace osu.Game.Graphics.UserInterfaceV2 { get { - if (OsuGameBase.VIDEO_EXTENSIONS.Contains(File.Extension.ToLowerInvariant())) + string extension = File.Extension.ToLowerInvariant(); + + if (SupportedExtensions.VIDEO_EXTENSIONS.Contains(extension)) return FontAwesome.Regular.FileVideo; - switch (File.Extension) - { - case @".ogg": - case @".mp3": - case @".wav": - return FontAwesome.Regular.FileAudio; + if (SupportedExtensions.AUDIO_EXTENSIONS.Contains(extension)) + return FontAwesome.Regular.FileAudio; - case @".jpg": - case @".jpeg": - case @".png": - return FontAwesome.Regular.FileImage; + if (SupportedExtensions.IMAGE_EXTENSIONS.Contains(extension)) + return FontAwesome.Regular.FileImage; - default: - return FontAwesome.Regular.File; - } + return FontAwesome.Regular.File; } } diff --git a/osu.Game/OsuGameBase.cs b/osu.Game/OsuGameBase.cs index dc13924b4f..b028280774 100644 --- a/osu.Game/OsuGameBase.cs +++ b/osu.Game/OsuGameBase.cs @@ -73,8 +73,6 @@ namespace osu.Game [Cached(typeof(OsuGameBase))] public partial class OsuGameBase : Framework.Game, ICanAcceptFiles, IBeatSyncProvider { - public static readonly string[] VIDEO_EXTENSIONS = { ".mp4", ".mov", ".avi", ".flv", ".mpg", ".wmv", ".m4v" }; - #if DEBUG public const string GAME_NAME = "osu! (development)"; #else diff --git a/osu.Game/Overlays/SkinEditor/SkinEditor.cs b/osu.Game/Overlays/SkinEditor/SkinEditor.cs index d0ee2ccd71..18e01e2490 100644 --- a/osu.Game/Overlays/SkinEditor/SkinEditor.cs +++ b/osu.Game/Overlays/SkinEditor/SkinEditor.cs @@ -35,6 +35,7 @@ using osu.Game.Screens.Edit.Components.Menus; using osu.Game.Skinning; using osu.Framework.Graphics.Cursor; using osu.Game.Input.Bindings; +using osu.Game.Utils; namespace osu.Game.Overlays.SkinEditor { @@ -709,7 +710,7 @@ namespace osu.Game.Overlays.SkinEditor Task ICanAcceptFiles.Import(ImportTask[] tasks, ImportParameters parameters) => throw new NotImplementedException(); - public IEnumerable HandledExtensions => new[] { ".jpg", ".jpeg", ".png" }; + public IEnumerable HandledExtensions => SupportedExtensions.IMAGE_EXTENSIONS; #endregion diff --git a/osu.Game/Rulesets/Edit/Checks/Components/AudioCheckUtils.cs b/osu.Game/Rulesets/Edit/Checks/Components/AudioCheckUtils.cs index b8cbe63c1e..8a35b84170 100644 --- a/osu.Game/Rulesets/Edit/Checks/Components/AudioCheckUtils.cs +++ b/osu.Game/Rulesets/Edit/Checks/Components/AudioCheckUtils.cs @@ -3,13 +3,12 @@ using System.IO; using System.Linq; +using osu.Game.Utils; namespace osu.Game.Rulesets.Edit.Checks.Components { public static class AudioCheckUtils { - public static readonly string[] AUDIO_EXTENSIONS = { "mp3", "ogg", "wav" }; - - public static bool HasAudioExtension(string filename) => AUDIO_EXTENSIONS.Any(Path.GetExtension(filename).ToLowerInvariant().EndsWith); + public static bool HasAudioExtension(string filename) => SupportedExtensions.AUDIO_EXTENSIONS.Contains(Path.GetExtension(filename).ToLowerInvariant()); } } diff --git a/osu.Game/Screens/Edit/Setup/ResourcesSection.cs b/osu.Game/Screens/Edit/Setup/ResourcesSection.cs index 845c21b598..daed658e3b 100644 --- a/osu.Game/Screens/Edit/Setup/ResourcesSection.cs +++ b/osu.Game/Screens/Edit/Setup/ResourcesSection.cs @@ -10,6 +10,7 @@ using osu.Game.Beatmaps; using osu.Game.Graphics.UserInterfaceV2; using osu.Game.Overlays; using osu.Game.Localisation; +using osu.Game.Utils; namespace osu.Game.Screens.Edit.Setup { @@ -48,12 +49,14 @@ namespace osu.Game.Screens.Edit.Setup Children = new Drawable[] { - backgroundChooser = new FormFileSelector(".jpg", ".jpeg", ".png") + backgroundChooser = new FormFileSelector(SupportedExtensions.IMAGE_EXTENSIONS) { Caption = GameplaySettingsStrings.BackgroundHeader, PlaceholderText = EditorSetupStrings.ClickToSelectBackground, }, - audioTrackChooser = new FormFileSelector(".mp3", ".ogg") + // `SupportedExtensions.AUDIO_EXTENSIONS` not used here specifically it includes `.wav` for samples, which is strictly disallowed by ranking criteria + // (https://osu.ppy.sh/wiki/en/Ranking_criteria#audio) + audioTrackChooser = new FormFileSelector(@".mp3", @".ogg") { Caption = EditorSetupStrings.AudioTrack, PlaceholderText = EditorSetupStrings.ClickToSelectTrack, diff --git a/osu.Game/Skinning/SkinnableSprite.cs b/osu.Game/Skinning/SkinnableSprite.cs index 9effb483c4..47618f6296 100644 --- a/osu.Game/Skinning/SkinnableSprite.cs +++ b/osu.Game/Skinning/SkinnableSprite.cs @@ -1,8 +1,8 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using System; using System.Collections.Generic; +using System.IO; using System.Linq; using osu.Framework.Allocation; using osu.Framework.Bindables; @@ -14,6 +14,7 @@ using osu.Game.Configuration; using osu.Game.Graphics.Sprites; using osu.Game.Localisation.SkinComponents; using osu.Game.Overlays.Settings; +using osu.Game.Utils; using osuTK; namespace osu.Game.Skinning @@ -93,10 +94,10 @@ namespace osu.Game.Skinning // but that requires further thought. var highestPrioritySkin = getHighestPriorityUserSkin(((SkinnableSprite)SettingSourceObject).source.AllSources) as Skin; - string[]? availableFiles = highestPrioritySkin?.SkinInfo.PerformRead(s => s.Files - .Where(f => f.Filename.EndsWith(".png", StringComparison.Ordinal) - || f.Filename.EndsWith(".jpg", StringComparison.Ordinal)) - .Select(f => f.Filename).Distinct()).ToArray(); + string[]? availableFiles = highestPrioritySkin?.SkinInfo.PerformRead( + s => s.Files + .Where(f => SupportedExtensions.IMAGE_EXTENSIONS.Contains(Path.GetExtension(f.Filename).ToLowerInvariant())) + .Select(f => f.Filename).Distinct()).ToArray(); if (availableFiles?.Length > 0) Items = availableFiles; diff --git a/osu.Game/Storyboards/Storyboard.cs b/osu.Game/Storyboards/Storyboard.cs index 8c43b99702..4d456f7360 100644 --- a/osu.Game/Storyboards/Storyboard.cs +++ b/osu.Game/Storyboards/Storyboard.cs @@ -7,6 +7,7 @@ using System.Linq; using osu.Game.Beatmaps; using osu.Game.Rulesets.Mods; using osu.Game.Storyboards.Drawables; +using osu.Game.Utils; namespace osu.Game.Storyboards { @@ -96,8 +97,6 @@ namespace osu.Game.Storyboards public virtual DrawableStoryboard CreateDrawable(IReadOnlyList? mods = null) => new DrawableStoryboard(this, mods); - private static readonly string[] image_extensions = { @".png", @".jpg" }; - public virtual string? GetStoragePathFromStoryboardPath(string path) { string? resolvedPath = null; @@ -109,7 +108,7 @@ namespace osu.Game.Storyboards else { // Some old storyboards don't include a file extension, so let's best guess at one. - foreach (string ext in image_extensions) + foreach (string ext in SupportedExtensions.IMAGE_EXTENSIONS) { if ((resolvedPath = BeatmapInfo.BeatmapSet?.GetPathForFile($"{path}{ext}")) != null) break; diff --git a/osu.Game/Utils/SupportedExtensions.cs b/osu.Game/Utils/SupportedExtensions.cs new file mode 100644 index 0000000000..ec1538a041 --- /dev/null +++ b/osu.Game/Utils/SupportedExtensions.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. + +namespace osu.Game.Utils +{ + public static class SupportedExtensions + { + public static readonly string[] VIDEO_EXTENSIONS = [@".mp4", @".mov", @".avi", @".flv", @".mpg", @".wmv", @".m4v"]; + public static readonly string[] AUDIO_EXTENSIONS = [@".mp3", @".ogg", @".wav"]; + public static readonly string[] IMAGE_EXTENSIONS = [@".jpg", @".jpeg", @".png"]; + + public static readonly string[] ALL_EXTENSIONS = + [ + ..VIDEO_EXTENSIONS, + ..AUDIO_EXTENSIONS, + ..IMAGE_EXTENSIONS + ]; + } +} From 5a9127dfc6568d537e453259bac841b251c448de Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 29 Nov 2024 08:46:08 +0100 Subject: [PATCH 0182/1275] Accidentally a word --- osu.Game/Screens/Edit/Setup/ResourcesSection.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/Edit/Setup/ResourcesSection.cs b/osu.Game/Screens/Edit/Setup/ResourcesSection.cs index daed658e3b..f02e4bbb28 100644 --- a/osu.Game/Screens/Edit/Setup/ResourcesSection.cs +++ b/osu.Game/Screens/Edit/Setup/ResourcesSection.cs @@ -54,7 +54,7 @@ namespace osu.Game.Screens.Edit.Setup Caption = GameplaySettingsStrings.BackgroundHeader, PlaceholderText = EditorSetupStrings.ClickToSelectBackground, }, - // `SupportedExtensions.AUDIO_EXTENSIONS` not used here specifically it includes `.wav` for samples, which is strictly disallowed by ranking criteria + // `SupportedExtensions.AUDIO_EXTENSIONS` not used here specifically because it includes `.wav` for samples, which is strictly disallowed by ranking criteria // (https://osu.ppy.sh/wiki/en/Ranking_criteria#audio) audioTrackChooser = new FormFileSelector(@".mp3", @".ogg") { From 5f092811cb4d984a84d2bcc5cc1a7a7d43765d55 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 29 Nov 2024 09:22:29 +0100 Subject: [PATCH 0183/1275] Use helper in one more place --- osu.Game/Screens/Edit/Setup/ResourcesSection.cs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/osu.Game/Screens/Edit/Setup/ResourcesSection.cs b/osu.Game/Screens/Edit/Setup/ResourcesSection.cs index f02e4bbb28..59a0520a52 100644 --- a/osu.Game/Screens/Edit/Setup/ResourcesSection.cs +++ b/osu.Game/Screens/Edit/Setup/ResourcesSection.cs @@ -54,9 +54,7 @@ namespace osu.Game.Screens.Edit.Setup Caption = GameplaySettingsStrings.BackgroundHeader, PlaceholderText = EditorSetupStrings.ClickToSelectBackground, }, - // `SupportedExtensions.AUDIO_EXTENSIONS` not used here specifically because it includes `.wav` for samples, which is strictly disallowed by ranking criteria - // (https://osu.ppy.sh/wiki/en/Ranking_criteria#audio) - audioTrackChooser = new FormFileSelector(@".mp3", @".ogg") + audioTrackChooser = new FormFileSelector(SupportedExtensions.AUDIO_EXTENSIONS) { Caption = EditorSetupStrings.AudioTrack, PlaceholderText = EditorSetupStrings.ClickToSelectTrack, From 3cfa455369c432b69f11a8a7f121abb0d8fac476 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 29 Nov 2024 10:54:32 +0100 Subject: [PATCH 0184/1275] Fix strong drum rolls being counted for double the combo in legacy scoring attributes --- .../Difficulty/TaikoLegacyScoreSimulator.cs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/osu.Game.Rulesets.Taiko/Difficulty/TaikoLegacyScoreSimulator.cs b/osu.Game.Rulesets.Taiko/Difficulty/TaikoLegacyScoreSimulator.cs index 9839d94277..416a11c2a8 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/TaikoLegacyScoreSimulator.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/TaikoLegacyScoreSimulator.cs @@ -144,6 +144,13 @@ namespace osu.Game.Rulesets.Taiko.Difficulty foreach (var nested in hitObject.NestedHitObjects) simulateHit(nested, ref attributes); return; + + case StrongNestedHitObject: + // we never need to deal with these directly. + // the only thing strong hits do in terms of scoring is double their object's score increase, + // which is already handled at the parent object level via the `strongable.IsStrong` check lower down in this method. + // not handling these here can lead to them falsely being counted as combo-increasing when handling strong drum rolls! + return; } if (hitObject is DrumRollTick tick) From 0e1b62ef8521d3cec72b0c60fbc557d25e94762a Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 29 Nov 2024 21:10:27 +0900 Subject: [PATCH 0185/1275] Expose more migration helper methods For use in https://github.com/ppy/osu-queue-score-statistics/pull/305. Some of these might look a bit odd, but I personally still prefer having them all in one central location. --- .../StandardisedScoreMigrationTools.cs | 31 +++++++++++-------- 1 file changed, 18 insertions(+), 13 deletions(-) diff --git a/osu.Game/Database/StandardisedScoreMigrationTools.cs b/osu.Game/Database/StandardisedScoreMigrationTools.cs index db44731bed..8181c56876 100644 --- a/osu.Game/Database/StandardisedScoreMigrationTools.cs +++ b/osu.Game/Database/StandardisedScoreMigrationTools.cs @@ -245,7 +245,7 @@ namespace osu.Game.Database var scoreProcessor = ruleset.CreateScoreProcessor(); // warning: ordering is important here - both total score and ranks are dependent on accuracy! - score.Accuracy = computeAccuracy(score, scoreProcessor); + score.Accuracy = ComputeAccuracy(score, scoreProcessor); score.Rank = computeRank(score, scoreProcessor); (score.TotalScoreWithoutMods, score.TotalScore) = convertFromLegacyTotalScore(score, ruleset, beatmap); } @@ -269,7 +269,7 @@ namespace osu.Game.Database var scoreProcessor = ruleset.CreateScoreProcessor(); // warning: ordering is important here - both total score and ranks are dependent on accuracy! - score.Accuracy = computeAccuracy(score, scoreProcessor); + score.Accuracy = ComputeAccuracy(score, scoreProcessor); score.Rank = computeRank(score, scoreProcessor); (score.TotalScoreWithoutMods, score.TotalScore) = convertFromLegacyTotalScore(score, ruleset, difficulty, attributes); } @@ -313,7 +313,8 @@ namespace osu.Game.Database /// The beatmap difficulty. /// The legacy scoring attributes for the beatmap which the score was set on. /// The standardised total score. - private static (long withoutMods, long withMods) convertFromLegacyTotalScore(ScoreInfo score, Ruleset ruleset, LegacyBeatmapConversionDifficultyInfo difficulty, LegacyScoreAttributes attributes) + private static (long withoutMods, long withMods) convertFromLegacyTotalScore(ScoreInfo score, Ruleset ruleset, LegacyBeatmapConversionDifficultyInfo difficulty, + LegacyScoreAttributes attributes) { if (!score.IsLegacyScore) return (score.TotalScoreWithoutMods, score.TotalScore); @@ -620,24 +621,28 @@ namespace osu.Game.Database } } - private static double computeAccuracy(ScoreInfo scoreInfo, ScoreProcessor scoreProcessor) + public static double ComputeAccuracy(ScoreInfo scoreInfo, ScoreProcessor scoreProcessor) + => ComputeAccuracy(scoreInfo.Statistics, scoreInfo.MaximumStatistics, scoreProcessor); + + public static double ComputeAccuracy(IReadOnlyDictionary statistics, IReadOnlyDictionary maximumStatistics, ScoreProcessor scoreProcessor) { - int baseScore = scoreInfo.Statistics.Where(kvp => kvp.Key.AffectsAccuracy()) - .Sum(kvp => kvp.Value * scoreProcessor.GetBaseScoreForResult(kvp.Key)); - int maxBaseScore = scoreInfo.MaximumStatistics.Where(kvp => kvp.Key.AffectsAccuracy()) - .Sum(kvp => kvp.Value * scoreProcessor.GetBaseScoreForResult(kvp.Key)); + int baseScore = statistics.Where(kvp => kvp.Key.AffectsAccuracy()) + .Sum(kvp => kvp.Value * scoreProcessor.GetBaseScoreForResult(kvp.Key)); + int maxBaseScore = maximumStatistics.Where(kvp => kvp.Key.AffectsAccuracy()) + .Sum(kvp => kvp.Value * scoreProcessor.GetBaseScoreForResult(kvp.Key)); return maxBaseScore == 0 ? 1 : baseScore / (double)maxBaseScore; } - public static ScoreRank ComputeRank(ScoreInfo scoreInfo) => computeRank(scoreInfo, scoreInfo.Ruleset.CreateInstance().CreateScoreProcessor()); + public static ScoreRank ComputeRank(ScoreInfo scoreInfo) => + ComputeRank(scoreInfo.Accuracy, scoreInfo.Statistics, scoreInfo.Mods, scoreInfo.Ruleset.CreateInstance().CreateScoreProcessor()); - private static ScoreRank computeRank(ScoreInfo scoreInfo, ScoreProcessor scoreProcessor) + public static ScoreRank ComputeRank(double accuracy, IReadOnlyDictionary statistics, IList mods, ScoreProcessor scoreProcessor) { - var rank = scoreProcessor.RankFromScore(scoreInfo.Accuracy, scoreInfo.Statistics); + var rank = scoreProcessor.RankFromScore(accuracy, statistics); - foreach (var mod in scoreInfo.Mods.OfType()) - rank = mod.AdjustRank(rank, scoreInfo.Accuracy); + foreach (var mod in mods.OfType()) + rank = mod.AdjustRank(rank, accuracy); return rank; } From a719693d10cb72cb2a098b9d40f968e6578985aa Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 29 Nov 2024 21:21:05 +0900 Subject: [PATCH 0186/1275] Fix one remaining method call --- osu.Game/Database/StandardisedScoreMigrationTools.cs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/osu.Game/Database/StandardisedScoreMigrationTools.cs b/osu.Game/Database/StandardisedScoreMigrationTools.cs index 8181c56876..15e3da3c19 100644 --- a/osu.Game/Database/StandardisedScoreMigrationTools.cs +++ b/osu.Game/Database/StandardisedScoreMigrationTools.cs @@ -246,7 +246,7 @@ namespace osu.Game.Database // warning: ordering is important here - both total score and ranks are dependent on accuracy! score.Accuracy = ComputeAccuracy(score, scoreProcessor); - score.Rank = computeRank(score, scoreProcessor); + score.Rank = ComputeRank(score, scoreProcessor); (score.TotalScoreWithoutMods, score.TotalScore) = convertFromLegacyTotalScore(score, ruleset, beatmap); } @@ -270,7 +270,7 @@ namespace osu.Game.Database // warning: ordering is important here - both total score and ranks are dependent on accuracy! score.Accuracy = ComputeAccuracy(score, scoreProcessor); - score.Rank = computeRank(score, scoreProcessor); + score.Rank = ComputeRank(score, scoreProcessor); (score.TotalScoreWithoutMods, score.TotalScore) = convertFromLegacyTotalScore(score, ruleset, difficulty, attributes); } @@ -637,6 +637,9 @@ namespace osu.Game.Database public static ScoreRank ComputeRank(ScoreInfo scoreInfo) => ComputeRank(scoreInfo.Accuracy, scoreInfo.Statistics, scoreInfo.Mods, scoreInfo.Ruleset.CreateInstance().CreateScoreProcessor()); + public static ScoreRank ComputeRank(ScoreInfo scoreInfo, ScoreProcessor processor) => + ComputeRank(scoreInfo.Accuracy, scoreInfo.Statistics, scoreInfo.Mods, processor); + public static ScoreRank ComputeRank(double accuracy, IReadOnlyDictionary statistics, IList mods, ScoreProcessor scoreProcessor) { var rank = scoreProcessor.RankFromScore(accuracy, statistics); From 68f21709a8c314488d5c50e1502cc122ac8c756f Mon Sep 17 00:00:00 2001 From: Huo Yaoyuan Date: Sat, 30 Nov 2024 02:32:09 +0800 Subject: [PATCH 0187/1275] Fix CA1865 --- .../Visual/Gameplay/TestScenePlayerLocalScoreImport.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestScenePlayerLocalScoreImport.cs b/osu.Game.Tests/Visual/Gameplay/TestScenePlayerLocalScoreImport.cs index 1660f93384..046ae6d953 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestScenePlayerLocalScoreImport.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestScenePlayerLocalScoreImport.cs @@ -221,7 +221,7 @@ namespace osu.Game.Tests.Visual.Gameplay string? filePath = null; // Files starting with _ are temporary, created by CreateFileSafely call. - AddUntilStep("wait for export file", () => filePath = LocalStorage.GetFiles("exports").SingleOrDefault(f => !Path.GetFileName(f).StartsWith("_", StringComparison.Ordinal)), () => Is.Not.Null); + AddUntilStep("wait for export file", () => filePath = LocalStorage.GetFiles("exports").SingleOrDefault(f => !Path.GetFileName(f).StartsWith('_')), () => Is.Not.Null); AddUntilStep("filesize is non-zero", () => { try From 1e2e364cd3d74281ffb921c7d9542ba82f02d6b4 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sat, 30 Nov 2024 21:01:22 +0900 Subject: [PATCH 0188/1275] Stop loudly logging backwards seek bug to sentry Several users have reported stutters when this happens. It's potentially from the error report overhead. We now know that this is a BASS level issue anyway, so having this logging is not helpful. --- osu.Game/Rulesets/UI/FrameStabilityContainer.cs | 12 +----------- 1 file changed, 1 insertion(+), 11 deletions(-) diff --git a/osu.Game/Rulesets/UI/FrameStabilityContainer.cs b/osu.Game/Rulesets/UI/FrameStabilityContainer.cs index c4feb249f4..92258f3fc9 100644 --- a/osu.Game/Rulesets/UI/FrameStabilityContainer.cs +++ b/osu.Game/Rulesets/UI/FrameStabilityContainer.cs @@ -3,19 +3,15 @@ using System; using System.Diagnostics; -using System.Linq; using osu.Framework.Allocation; using osu.Framework.Audio; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Logging; -using osu.Framework.Testing; using osu.Framework.Timing; -using osu.Game.Beatmaps; using osu.Game.Input.Handlers; using osu.Game.Screens.Play; -using osu.Game.Utils; namespace osu.Game.Rulesets.UI { @@ -168,13 +164,7 @@ namespace osu.Game.Rulesets.UI if (lastBackwardsSeekLogTime == null || Math.Abs(Clock.CurrentTime - lastBackwardsSeekLogTime.Value) > 1000) { lastBackwardsSeekLogTime = Clock.CurrentTime; - - string loggableContent = $"Denying backwards seek during gameplay (reference: {referenceClock.CurrentTime:N2} stable: {proposedTime:N2})"; - - if (parentGameplayClock is GameplayClockContainer gcc) - loggableContent += $"\n{gcc.ChildrenOfType().Single().GetSnapshot()}"; - - Logger.Error(new SentryOnlyDiagnosticsException("backwards seek"), loggableContent); + Logger.Log($"Denying backwards seek during gameplay (reference: {referenceClock.CurrentTime:N2} stable: {proposedTime:N2})"); } state = PlaybackState.NotValid; From f4e155bfa6a6f8158a578540e9e01621e5b46553 Mon Sep 17 00:00:00 2001 From: Tim Schumacher Date: Sat, 30 Nov 2024 15:19:35 +0100 Subject: [PATCH 0189/1275] Account for rate changing mods when disabling the "Ready" button --- .../Playlists/PlaylistsReadyButton.cs | 21 +++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsReadyButton.cs b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsReadyButton.cs index a460779ea6..3c7808356c 100644 --- a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsReadyButton.cs +++ b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsReadyButton.cs @@ -2,6 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using System; +using System.Collections.Generic; using System.ComponentModel; using System.Linq; using osu.Framework.Allocation; @@ -10,7 +11,9 @@ using osu.Framework.Localisation; using osu.Game.Beatmaps; using osu.Game.Graphics; using osu.Game.Online.Rooms; +using osu.Game.Rulesets.Mods; using osu.Game.Screens.OnlinePlay.Components; +using osu.Game.Utils; namespace osu.Game.Screens.OnlinePlay.Playlists { @@ -19,6 +22,9 @@ namespace osu.Game.Screens.OnlinePlay.Playlists [Resolved] private IBindable gameBeatmap { get; set; } = null!; + [Resolved] + private IBindable> mods { get; set; } = null!; + private readonly Room room; public PlaylistsReadyButton(Room room) @@ -63,14 +69,14 @@ namespace osu.Game.Screens.OnlinePlay.Playlists { base.Update(); - Enabled.Value = hasRemainingAttempts && enoughTimeLeft; + Enabled.Value = hasRemainingAttempts && enoughTimeLeft(); } public override LocalisableString TooltipText { get { - if (!enoughTimeLeft) + if (!enoughTimeLeft()) return "No time left!"; if (!hasRemainingAttempts) @@ -80,9 +86,16 @@ namespace osu.Game.Screens.OnlinePlay.Playlists } } - private bool enoughTimeLeft => + private bool enoughTimeLeft() + { + // this doesn't consider mods which apply variable rates, yet. + double rate = ModUtils.CalculateRateWithMods(mods.Value); + + double hitLength = Math.Round(gameBeatmap.Value.Track.Length / rate); + // This should probably consider the length of the currently selected item, rather than a constant 30 seconds. - room.EndDate != null && DateTimeOffset.UtcNow.AddSeconds(30).AddMilliseconds(gameBeatmap.Value.Track.Length) < room.EndDate; + return room.EndDate != null && DateTimeOffset.UtcNow.AddSeconds(30).AddMilliseconds(hitLength) < room.EndDate; + } protected override void Dispose(bool isDisposing) { From 164b809c8911262c5f8f775b5c8c4c3ce843afc7 Mon Sep 17 00:00:00 2001 From: Tim Schumacher Date: Sat, 30 Nov 2024 23:02:22 +0100 Subject: [PATCH 0190/1275] Document ready button enable state with some comments --- .../OnlinePlay/Playlists/PlaylistsReadyButton.cs | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsReadyButton.cs b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsReadyButton.cs index 3c7808356c..0a4b504749 100644 --- a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsReadyButton.cs +++ b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsReadyButton.cs @@ -88,13 +88,15 @@ namespace osu.Game.Screens.OnlinePlay.Playlists private bool enoughTimeLeft() { - // this doesn't consider mods which apply variable rates, yet. + // TODO: This doesn't consider mods which apply variable rates, yet. double rate = ModUtils.CalculateRateWithMods(mods.Value); - double hitLength = Math.Round(gameBeatmap.Value.Track.Length / rate); + // We want to avoid users not being able to submit scores if they chose to not skip, + // so track length is chosen over playable length. + double trackLength = Math.Round(gameBeatmap.Value.Track.Length / rate); - // This should probably consider the length of the currently selected item, rather than a constant 30 seconds. - return room.EndDate != null && DateTimeOffset.UtcNow.AddSeconds(30).AddMilliseconds(hitLength) < room.EndDate; + // Additional 30 second delay added to account for load and/or submit time. + return room.EndDate != null && DateTimeOffset.UtcNow.AddSeconds(30).AddMilliseconds(trackLength) < room.EndDate; } protected override void Dispose(bool isDisposing) From 9140893249037130c9a2bae7bd67be20f8be098a Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Sat, 30 Nov 2024 23:36:02 -0500 Subject: [PATCH 0191/1275] Fix score no longer being saved when quick-restarting after pass --- .../Visual/Mods/TestSceneModFailCondition.cs | 2 +- osu.Game/Screens/Play/Player.cs | 10 +++------- osu.Game/Screens/Play/PlayerLoader.cs | 6 ++---- 3 files changed, 6 insertions(+), 12 deletions(-) diff --git a/osu.Game.Tests/Visual/Mods/TestSceneModFailCondition.cs b/osu.Game.Tests/Visual/Mods/TestSceneModFailCondition.cs index f4732234a7..a7447a92cd 100644 --- a/osu.Game.Tests/Visual/Mods/TestSceneModFailCondition.cs +++ b/osu.Game.Tests/Visual/Mods/TestSceneModFailCondition.cs @@ -21,7 +21,7 @@ namespace osu.Game.Tests.Visual.Mods protected override TestPlayer CreateModPlayer(Ruleset ruleset) { var player = base.CreateModPlayer(ruleset); - player.RestartRequested = _ => restartRequested = true; + player.PrepareLoaderForRestart = _ => restartRequested = true; return player; } diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs index 2d1f602832..cb24b99ce2 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -83,7 +83,7 @@ namespace osu.Game.Screens.Play /// protected virtual bool PauseOnFocusLost => true; - public Action RestartRequested; + public Action PrepareLoaderForRestart; private bool isRestarting; private bool skipExitTransition; @@ -719,12 +719,8 @@ namespace osu.Game.Screens.Play // stopping here is to ensure music doesn't become audible after exiting back to PlayerLoader. musicController.Stop(); - if (RestartRequested != null) - { - skipExitTransition = quickRestart; - RestartRequested?.Invoke(quickRestart); - return true; - } + skipExitTransition = quickRestart; + PrepareLoaderForRestart?.Invoke(quickRestart); return PerformExit(quickRestart); } diff --git a/osu.Game/Screens/Play/PlayerLoader.cs b/osu.Game/Screens/Play/PlayerLoader.cs index 0db96b71ad..d2ba5398e4 100644 --- a/osu.Game/Screens/Play/PlayerLoader.cs +++ b/osu.Game/Screens/Play/PlayerLoader.cs @@ -457,7 +457,7 @@ namespace osu.Game.Screens.Play CurrentPlayer = createPlayer(); CurrentPlayer.Configuration.AutomaticallySkipIntro |= quickRestart; CurrentPlayer.RestartCount = restartCount++; - CurrentPlayer.RestartRequested = restartRequested; + CurrentPlayer.PrepareLoaderForRestart = prepareForRestart; LoadTask = LoadComponentAsync(CurrentPlayer, _ => { @@ -470,13 +470,11 @@ namespace osu.Game.Screens.Play { } - private void restartRequested(bool quickRestartRequested) + private void prepareForRestart(bool quickRestartRequested) { quickRestart = quickRestartRequested; hideOverlays = true; ValidForResume = true; - - this.MakeCurrent(); } private void contentIn(double delayBeforeSideDisplays = 0) From 53dce83b56b9c67657c5a8033016150e20c8e939 Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Sun, 1 Dec 2024 02:11:53 -0500 Subject: [PATCH 0192/1275] Fix restarting no longer working from results screen Thanks to tests for pointing that out :blobsweat: --- osu.Game/Screens/Play/Player.cs | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs index cb24b99ce2..3a0a0613f3 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -722,6 +722,16 @@ namespace osu.Game.Screens.Play skipExitTransition = quickRestart; PrepareLoaderForRestart?.Invoke(quickRestart); + if (!this.IsCurrentScreen()) + { + // if we're called externally (i.e. from results screen), + // use MakeCurrent to exit results screen as well as this player screen + // since ValidForResume = false in here + Debug.Assert(!ValidForResume); + this.MakeCurrent(); + return true; + } + return PerformExit(quickRestart); } From 6afe083ec96f218fbad7638630a6069a761404c9 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sun, 1 Dec 2024 18:44:26 +0900 Subject: [PATCH 0193/1275] Fix settings showing up during gameplay --- osu.Game/Screens/Play/HUD/PlayerSettingsOverlay.cs | 6 ++---- osu.Game/Screens/Play/HUDOverlay.cs | 12 ++++++++++-- 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/osu.Game/Screens/Play/HUD/PlayerSettingsOverlay.cs b/osu.Game/Screens/Play/HUD/PlayerSettingsOverlay.cs index e68ca4da7a..18d7f6a503 100644 --- a/osu.Game/Screens/Play/HUD/PlayerSettingsOverlay.cs +++ b/osu.Game/Screens/Play/HUD/PlayerSettingsOverlay.cs @@ -23,17 +23,15 @@ namespace osu.Game.Screens.Play.HUD private const float padding = 10; - public const float CONTRACTED_WIDTH = button_size + padding * 2; public const float EXPANDED_WIDTH = player_settings_width + padding * 2; private const float player_settings_width = 270; - private const float button_size = IconButton.DEFAULT_BUTTON_SIZE; + + private const int fade_duration = 200; public override void Show() => this.FadeIn(fade_duration); public override void Hide() => this.FadeOut(fade_duration); - private const int fade_duration = 200; - // we'll handle this ourselves because we have slightly custom logic. protected override bool ExpandOnHover => false; diff --git a/osu.Game/Screens/Play/HUDOverlay.cs b/osu.Game/Screens/Play/HUDOverlay.cs index 62d9686aad..1c5277a8d9 100644 --- a/osu.Game/Screens/Play/HUDOverlay.cs +++ b/osu.Game/Screens/Play/HUDOverlay.cs @@ -88,6 +88,7 @@ namespace osu.Game.Screens.Play private readonly FillFlowContainer bottomRightElements; private readonly FillFlowContainer topRightElements; + private readonly Container rightSettings; internal readonly IBindable IsPlaying = new Bindable(); @@ -163,7 +164,14 @@ namespace osu.Game.Screens.Play HoldToQuit = CreateHoldForMenuButton(), } }, - PlayerSettingsOverlay = new PlayerSettingsOverlay(), + rightSettings = new Container + { + RelativeSizeAxes = Axes.Both, + Children = new Drawable[] + { + PlayerSettingsOverlay = new PlayerSettingsOverlay(), + } + }, LeaderboardFlow = new FillFlowContainer { AutoSizeAxes = Axes.Both, @@ -173,7 +181,7 @@ namespace osu.Game.Screens.Play }, }; - hideTargets = new List { mainComponents, topRightElements, PlayerSettingsOverlay }; + hideTargets = new List { mainComponents, topRightElements, rightSettings }; if (rulesetComponents != null) hideTargets.Add(rulesetComponents); From 23522b02d899a1a73b2ad56452dd932d7ff6ee3e Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sun, 1 Dec 2024 19:53:57 +0900 Subject: [PATCH 0194/1275] Use local instead of field for local only usage --- osu.Game/Screens/Play/HUDOverlay.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/osu.Game/Screens/Play/HUDOverlay.cs b/osu.Game/Screens/Play/HUDOverlay.cs index 1c5277a8d9..fca871e42f 100644 --- a/osu.Game/Screens/Play/HUDOverlay.cs +++ b/osu.Game/Screens/Play/HUDOverlay.cs @@ -88,7 +88,6 @@ namespace osu.Game.Screens.Play private readonly FillFlowContainer bottomRightElements; private readonly FillFlowContainer topRightElements; - private readonly Container rightSettings; internal readonly IBindable IsPlaying = new Bindable(); @@ -116,6 +115,8 @@ namespace osu.Game.Screens.Play public HUDOverlay([CanBeNull] DrawableRuleset drawableRuleset, IReadOnlyList mods, bool alwaysShowLeaderboard = true) { + Container rightSettings; + this.drawableRuleset = drawableRuleset; this.mods = mods; From b14dde937ddc8132cb47cdfca6c3c2ce8426c002 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 2 Dec 2024 13:51:41 +0100 Subject: [PATCH 0195/1275] 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 b97fe5c5a8..345965b912 100644 --- a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneOsuComposerSelection.cs +++ b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneOsuComposerSelection.cs @@ -231,6 +231,36 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor AddAssert("slider still has 2 anchors", () => secondSlider.Path.ControlPoints.Count, () => Is.EqualTo(2)); } + [Test] + public void TestControlClickDoesNotDiscardExistingSelectionEvenIfNothingHit() + { + var firstSlider = new Slider + { + StartTime = 0, + Position = new Vector2(0, 0), + Path = new SliderPath + { + ControlPoints = + { + new PathControlPoint(), + new PathControlPoint(new Vector2(100)) + } + } + }; + + AddStep("add object", () => EditorBeatmap.AddRange([firstSlider])); + AddStep("select first slider", () => EditorBeatmap.SelectedHitObjects.AddRange([firstSlider])); + + AddStep("move mouse to middle of playfield", () => InputManager.MoveMouseTo(blueprintContainer.ScreenSpaceDrawQuad.Centre)); + AddStep("control-click left mouse", () => + { + InputManager.PressKey(Key.ControlLeft); + InputManager.Click(MouseButton.Left); + InputManager.ReleaseKey(Key.ControlLeft); + }); + AddAssert("selection preserved", () => EditorBeatmap.SelectedHitObjects.Count, () => Is.EqualTo(1)); + } + private ComposeBlueprintContainer blueprintContainer => Editor.ChildrenOfType().First(); From b505ecc7ba8e44afd38e0ba3cb77edf277d3593c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 2 Dec 2024 13:51:43 +0100 Subject: [PATCH 0196/1275] Do not deselect objects when control-clicking without hitting anything As per feedback in https://discord.com/channels/90072389919997952/1259818301517725707/1310270647187935284. --- .../Edit/Compose/Components/BlueprintContainer.cs | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs b/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs index e12574f7ee..4a321f4a81 100644 --- a/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs +++ b/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs @@ -433,7 +433,10 @@ namespace osu.Game.Screens.Edit.Compose.Components /// Finishes the current blueprint selection. /// /// The mouse event which triggered end of selection. - /// Whether a click selection was active. + /// + /// Whether the mouse event is considered to be fully handled. + /// If the return value is , the standard click / mouse up action will follow. + /// private bool endClickSelection(MouseButtonEvent e) { // If already handled a selection, double-click, or drag, we don't want to perform a mouse up / click action. @@ -443,14 +446,16 @@ namespace osu.Game.Screens.Edit.Compose.Components if (e.ControlPressed) { - // if a selection didn't occur, we may want to trigger a deselection. - // Iterate from the top of the input stack (blueprints closest to the front of the screen first). // Priority is given to already-selected blueprints. foreach (SelectionBlueprint blueprint in SelectionBlueprints.AliveChildren.Where(b => b.IsHovered).OrderByDescending(b => b.IsSelected)) return clickSelectionHandled = SelectionHandler.MouseUpSelectionRequested(blueprint, e); - return false; + // can only be reached if there are no hovered blueprints. + // in that case, we still want to suppress mouse up / click handling, because when control is pressed, + // it is presumed we want to add to existing selection, not remove from it + // (unless explicitly control-clicking a selected object, which is handled above). + return true; } if (selectedBlueprintAlreadySelectedOnMouseDown && SelectedItems.Count == 1) From 68f4fa5a5781d1f6d2344cf6041d39c51ef9bf05 Mon Sep 17 00:00:00 2001 From: Huo Yaoyuan Date: Tue, 3 Dec 2024 00:00:31 +0800 Subject: [PATCH 0197/1275] Turn off CA1507 --- .globalconfig | 4 ++++ osu.Game/Online/API/Requests/Responses/APIKudosuHistory.cs | 2 -- .../Online/API/Requests/Responses/APIUserMostPlayedBeatmap.cs | 2 -- .../Notifications/WebSocket/Events/NewChatMessageData.cs | 2 -- 4 files changed, 4 insertions(+), 6 deletions(-) diff --git a/.globalconfig b/.globalconfig index e2cd1926f0..ca7b86c778 100644 --- a/.globalconfig +++ b/.globalconfig @@ -50,6 +50,10 @@ dotnet_diagnostic.IDE1006.severity = warning # Too many noisy warnings for parsing/formatting numbers dotnet_diagnostic.CA1305.severity = none +# CA1507: Use nameof to express symbol names +# Flaggs serialization name attributes +dotnet_diagnostic.CA1507.severity = suggestion + # CA1806: Do not ignore method results # The usages for numeric parsing are explicitly optional dotnet_diagnostic.CA1806.severity = suggestion diff --git a/osu.Game/Online/API/Requests/Responses/APIKudosuHistory.cs b/osu.Game/Online/API/Requests/Responses/APIKudosuHistory.cs index 8e4cc387ed..583def8eda 100644 --- a/osu.Game/Online/API/Requests/Responses/APIKudosuHistory.cs +++ b/osu.Game/Online/API/Requests/Responses/APIKudosuHistory.cs @@ -45,9 +45,7 @@ namespace osu.Game.Online.API.Requests.Responses public KudosuAction Action; -#pragma warning disable CA1507 // Happens to name the same because of casing preference [JsonProperty("action")] -#pragma warning restore CA1507 private string action { set diff --git a/osu.Game/Online/API/Requests/Responses/APIUserMostPlayedBeatmap.cs b/osu.Game/Online/API/Requests/Responses/APIUserMostPlayedBeatmap.cs index 38ad2bd02d..6d5fd59f9c 100644 --- a/osu.Game/Online/API/Requests/Responses/APIUserMostPlayedBeatmap.cs +++ b/osu.Game/Online/API/Requests/Responses/APIUserMostPlayedBeatmap.cs @@ -15,9 +15,7 @@ namespace osu.Game.Online.API.Requests.Responses [JsonProperty("count")] public int PlayCount { get; set; } -#pragma warning disable CA1507 // Happens to name the same because of casing preference [JsonProperty("beatmap")] -#pragma warning restore CA1507 private APIBeatmap beatmap { get; set; } public APIBeatmap BeatmapInfo diff --git a/osu.Game/Online/Notifications/WebSocket/Events/NewChatMessageData.cs b/osu.Game/Online/Notifications/WebSocket/Events/NewChatMessageData.cs index 677286bb8a..ff9f5ee9f7 100644 --- a/osu.Game/Online/Notifications/WebSocket/Events/NewChatMessageData.cs +++ b/osu.Game/Online/Notifications/WebSocket/Events/NewChatMessageData.cs @@ -19,9 +19,7 @@ namespace osu.Game.Online.Notifications.WebSocket.Events [JsonProperty(@"messages")] public List Messages { get; set; } = null!; -#pragma warning disable CA1507 // Happens to name the same because of casing preference [JsonProperty(@"users")] -#pragma warning restore CA1507 private List users { get; set; } = null!; [OnDeserialized] From 7ece8ec1dc466a5f9676b888746c4c33e3140e0f Mon Sep 17 00:00:00 2001 From: Huo Yaoyuan Date: Tue, 3 Dec 2024 00:03:59 +0800 Subject: [PATCH 0198/1275] Update for suggestions --- osu.Game/Overlays/ChatOverlay.cs | 3 +-- osu.Game/Rulesets/Mods/ModAdaptiveSpeed.cs | 4 ++-- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/osu.Game/Overlays/ChatOverlay.cs b/osu.Game/Overlays/ChatOverlay.cs index a00414522d..c49afa3a66 100644 --- a/osu.Game/Overlays/ChatOverlay.cs +++ b/osu.Game/Overlays/ChatOverlay.cs @@ -386,9 +386,8 @@ namespace osu.Game.Overlays { channelList.RemoveChannel(channel); - if (loadedChannels.TryGetValue(channel, out var loaded)) + if (loadedChannels.Remove(channel, out var loaded)) { - loadedChannels.Remove(channel); // DrawableChannel removed from cache must be manually disposed loaded.Dispose(); } diff --git a/osu.Game/Rulesets/Mods/ModAdaptiveSpeed.cs b/osu.Game/Rulesets/Mods/ModAdaptiveSpeed.cs index fb056b457b..83a48599ca 100644 --- a/osu.Game/Rulesets/Mods/ModAdaptiveSpeed.cs +++ b/osu.Game/Rulesets/Mods/ModAdaptiveSpeed.cs @@ -173,10 +173,10 @@ namespace osu.Game.Rulesets.Mods }; drawable.OnRevertResult += (_, result) => { - if (!ratesForRewinding.TryGetValue(result.HitObject, out double rewindValue)) return; + if (!ratesForRewinding.TryGetValue(result.HitObject, out double rate)) return; if (!shouldProcessResult(result)) return; - recentRates.Insert(0, rewindValue); + recentRates.Insert(0, rate); ratesForRewinding.Remove(result.HitObject); recentRates.RemoveAt(recentRates.Count - 1); From e920cfa1872d233f90df27f4db76ffd0e75da6a8 Mon Sep 17 00:00:00 2001 From: Tim Schumacher Date: Mon, 2 Dec 2024 23:49:26 +0100 Subject: [PATCH 0199/1275] Move rate-changing TODO to a common place in CalculateRateWithMods --- osu.Game/Screens/OnlinePlay/Playlists/PlaylistsReadyButton.cs | 1 - osu.Game/Screens/Select/BeatmapInfoWedge.cs | 1 - osu.Game/Utils/ModUtils.cs | 1 + 3 files changed, 1 insertion(+), 2 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsReadyButton.cs b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsReadyButton.cs index 0a4b504749..e72f8be50a 100644 --- a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsReadyButton.cs +++ b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsReadyButton.cs @@ -88,7 +88,6 @@ namespace osu.Game.Screens.OnlinePlay.Playlists private bool enoughTimeLeft() { - // TODO: This doesn't consider mods which apply variable rates, yet. double rate = ModUtils.CalculateRateWithMods(mods.Value); // We want to avoid users not being able to submit scores if they chose to not skip, diff --git a/osu.Game/Screens/Select/BeatmapInfoWedge.cs b/osu.Game/Screens/Select/BeatmapInfoWedge.cs index 3b0fdc3e47..fd1c944689 100644 --- a/osu.Game/Screens/Select/BeatmapInfoWedge.cs +++ b/osu.Game/Screens/Select/BeatmapInfoWedge.cs @@ -401,7 +401,6 @@ namespace osu.Game.Screens.Select if (beatmap == null || bpmLabelContainer == null) return; - // this doesn't consider mods which apply variable rates, yet. double rate = ModUtils.CalculateRateWithMods(mods.Value); int bpmMax = FormatUtils.RoundBPM(beatmap.ControlPointInfo.BPMMaximum, rate); diff --git a/osu.Game/Utils/ModUtils.cs b/osu.Game/Utils/ModUtils.cs index f901f15388..15fc34b468 100644 --- a/osu.Game/Utils/ModUtils.cs +++ b/osu.Game/Utils/ModUtils.cs @@ -286,6 +286,7 @@ namespace osu.Game.Utils { double rate = 1; + // TODO: This doesn't consider mods which apply variable rates, yet. foreach (var mod in mods.OfType()) rate = mod.ApplyToRate(0, rate); From 2ceb3f6f85e2d592e7b10794b7949de61ff84d6c Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 3 Dec 2024 13:43:20 +0900 Subject: [PATCH 0200/1275] Show an ongoing operation when checking for updates Addresses https://github.com/ppy/osu/discussions/30950. --- osu.Game/Localisation/GeneralSettingsStrings.cs | 5 +++++ .../Overlays/Settings/Sections/General/UpdateSettings.cs | 8 ++++++++ 2 files changed, 13 insertions(+) diff --git a/osu.Game/Localisation/GeneralSettingsStrings.cs b/osu.Game/Localisation/GeneralSettingsStrings.cs index 42623f4632..83a3af574c 100644 --- a/osu.Game/Localisation/GeneralSettingsStrings.cs +++ b/osu.Game/Localisation/GeneralSettingsStrings.cs @@ -44,6 +44,11 @@ namespace osu.Game.Localisation /// public static LocalisableString CheckUpdate => new TranslatableString(getKey(@"check_update"), @"Check for updates"); + /// + /// "Checking for updates" + /// + public static LocalisableString CheckingForUpdates => new TranslatableString(getKey(@"checking_for_updates"), @"Checking for updates"); + /// /// "Open osu! folder" /// diff --git a/osu.Game/Overlays/Settings/Sections/General/UpdateSettings.cs b/osu.Game/Overlays/Settings/Sections/General/UpdateSettings.cs index 82cc952e53..53567109e3 100644 --- a/osu.Game/Overlays/Settings/Sections/General/UpdateSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/General/UpdateSettings.cs @@ -53,8 +53,16 @@ namespace osu.Game.Overlays.Settings.Sections.General Action = () => { checkForUpdatesButton.Enabled.Value = false; + + var checkingNotification = new ProgressNotification { Text = GeneralSettingsStrings.CheckingForUpdates, }; + notifications?.Post(checkingNotification); + Task.Run(updateManager.CheckForUpdateAsync).ContinueWith(task => Schedule(() => { + // This sequence allows the notification to be immediately dismissed. + checkingNotification.State = ProgressNotificationState.Cancelled; + checkingNotification.Close(false); + if (!task.GetResultSafely()) { notifications?.Post(new SimpleNotification From 457957d3b8d9a68b359e15953d4f151a3cc5b44b Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 3 Dec 2024 14:20:39 +0900 Subject: [PATCH 0201/1275] Refactor check-update flow to better handle unobserved exceptions --- .../Sections/General/UpdateSettings.cs | 71 ++++++++++++------- 1 file changed, 44 insertions(+), 27 deletions(-) diff --git a/osu.Game/Overlays/Settings/Sections/General/UpdateSettings.cs b/osu.Game/Overlays/Settings/Sections/General/UpdateSettings.cs index 53567109e3..261103173e 100644 --- a/osu.Game/Overlays/Settings/Sections/General/UpdateSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/General/UpdateSettings.cs @@ -4,7 +4,6 @@ using System.Threading.Tasks; using osu.Framework; using osu.Framework.Allocation; -using osu.Framework.Extensions; using osu.Framework.Graphics.Sprites; using osu.Framework.Localisation; using osu.Framework.Logging; @@ -13,6 +12,7 @@ using osu.Framework.Screens; using osu.Framework.Statistics; using osu.Game.Configuration; using osu.Game.Localisation; +using osu.Game.Online.Multiplayer; using osu.Game.Overlays.Notifications; using osu.Game.Overlays.Settings.Sections.Maintenance; using osu.Game.Updater; @@ -36,8 +36,11 @@ namespace osu.Game.Overlays.Settings.Sections.General [Resolved] private Storage storage { get; set; } = null!; + [Resolved] + private OsuGame? game { get; set; } + [BackgroundDependencyLoader] - private void load(OsuConfigManager config, OsuGame? game) + private void load(OsuConfigManager config) { Add(new SettingsEnumDropdown { @@ -50,31 +53,7 @@ namespace osu.Game.Overlays.Settings.Sections.General Add(checkForUpdatesButton = new SettingsButton { Text = GeneralSettingsStrings.CheckUpdate, - Action = () => - { - checkForUpdatesButton.Enabled.Value = false; - - var checkingNotification = new ProgressNotification { Text = GeneralSettingsStrings.CheckingForUpdates, }; - notifications?.Post(checkingNotification); - - Task.Run(updateManager.CheckForUpdateAsync).ContinueWith(task => Schedule(() => - { - // This sequence allows the notification to be immediately dismissed. - checkingNotification.State = ProgressNotificationState.Cancelled; - checkingNotification.Close(false); - - if (!task.GetResultSafely()) - { - notifications?.Post(new SimpleNotification - { - Text = GeneralSettingsStrings.RunningLatestRelease(game!.Version), - Icon = FontAwesome.Solid.CheckCircle, - }); - } - - checkForUpdatesButton.Enabled.Value = true; - })); - } + Action = () => checkForUpdates().FireAndForget() }); } @@ -102,6 +81,44 @@ namespace osu.Game.Overlays.Settings.Sections.General } } + private async Task checkForUpdates() + { + if (updateManager == null || game == null) + return; + + checkForUpdatesButton.Enabled.Value = false; + + var checkingNotification = new ProgressNotification + { + Text = GeneralSettingsStrings.CheckingForUpdates, + }; + notifications?.Post(checkingNotification); + + try + { + bool foundUpdate = await updateManager.CheckForUpdateAsync().ConfigureAwait(true); + + if (!foundUpdate) + { + notifications?.Post(new SimpleNotification + { + Text = GeneralSettingsStrings.RunningLatestRelease(game.Version), + Icon = FontAwesome.Solid.CheckCircle, + }); + } + } + catch + { + } + finally + { + // This sequence allows the notification to be immediately dismissed. + checkingNotification.State = ProgressNotificationState.Cancelled; + checkingNotification.Close(false); + checkForUpdatesButton.Enabled.Value = true; + } + } + private void exportLogs() { ProgressNotification notification = new ProgressNotification From 6ff1dec7b2b9fa2eebcd96620c316ddfc7a67c6e Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Tue, 3 Dec 2024 15:45:58 +0900 Subject: [PATCH 0202/1275] Add tests --- .../TestScenePlaylistsRoomSubScreen.cs | 76 +++++++++++++++++++ 1 file changed, 76 insertions(+) diff --git a/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsRoomSubScreen.cs b/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsRoomSubScreen.cs index 5f9e06fda5..de84ca680d 100644 --- a/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsRoomSubScreen.cs +++ b/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsRoomSubScreen.cs @@ -5,25 +5,64 @@ using System; using System.Linq; using NUnit.Framework; using osu.Framework.Allocation; +using osu.Framework.Audio; +using osu.Framework.Extensions; +using osu.Framework.Platform; using osu.Framework.Screens; using osu.Framework.Testing; +using osu.Game.Beatmaps; using osu.Game.Graphics.UserInterface; using osu.Game.Online.API; using osu.Game.Online.API.Requests.Responses; using osu.Game.Online.Rooms; using osu.Game.Online.Rooms.RoomStatuses; +using osu.Game.Rulesets; +using osu.Game.Rulesets.Osu.Mods; +using osu.Game.Scoring; using osu.Game.Screens.OnlinePlay.Playlists; +using osu.Game.Tests.Resources; using osu.Game.Tests.Visual.OnlinePlay; namespace osu.Game.Tests.Visual.Playlists { public partial class TestScenePlaylistsRoomSubScreen : OnlinePlayTestScene { + private const double track_length = 10000; + [Resolved] private IAPIProvider api { get; set; } = null!; protected new TestRoomManager RoomManager => (TestRoomManager)base.RoomManager; + private BeatmapManager beatmaps = null!; + private RulesetStore rulesets = null!; + private BeatmapSetInfo? importedSet; + + [BackgroundDependencyLoader] + private void load(GameHost host, AudioManager audio) + { + Dependencies.Cache(rulesets = new RealmRulesetStore(Realm)); + Dependencies.Cache(beatmaps = new BeatmapManager(LocalStorage, Realm, null, audio, Resources, host, Beatmap.Default)); + Dependencies.Cache(new ScoreManager(rulesets, () => beatmaps, LocalStorage, Realm, API)); + Dependencies.Cache(Realm); + + beatmaps.Import(TestResources.GetQuickTestBeatmapForImport()).WaitSafely(); + + Realm.Write(r => + { + foreach (var set in r.All()) + { + foreach (var b in set.Beatmaps) + { + // These will all have a virtual track length of 1000, see WorkingBeatmap.GetVirtualTrack(). + b.Length = track_length - 1000; + } + } + }); + + importedSet = beatmaps.GetAllUsableBeatmapSets().First(); + } + [Test] public void TestStatusUpdateOnEnter() { @@ -69,5 +108,42 @@ namespace osu.Game.Tests.Visual.Playlists AddAssert("close button present", () => roomScreen.ChildrenOfType().Any()); AddUntilStep("wait for close button to disappear", () => !roomScreen.ChildrenOfType().Any()); } + + [TestCase(120_000, true)] // Definitely enough time. + [TestCase(45_000, true)] // Enough time. + [TestCase(35_000, false)] // Not enough time to complete beatmap after lenience. + [TestCase(20_000, false)] // Not enough time. + [TestCase(5_000, false)] // Not enough time to complete beatmap before lenience. + [TestCase(37_500, true, 2)] // Enough time to complete beatmap after mods are applied. + public void TestReadyButtonEnablementPeriod(int offsetMs, bool enabled, double rate = 1) + { + Room room = null!; + PlaylistsRoomSubScreen roomScreen = null!; + + AddStep("create room", () => + { + RoomManager.AddRoom(room = new Room + { + Name = @"Test Room", + Host = api.LocalUser.Value, + Category = RoomCategory.Normal, + StartDate = DateTimeOffset.Now, + EndDate = DateTimeOffset.Now.AddMilliseconds(offsetMs), + Playlist = + [ + new PlaylistItem(importedSet!.Beatmaps[0]) + { + RequiredMods = rate == 1 + ? [] + : [new APIMod(new OsuModDoubleTime { SpeedChange = { Value = rate } })] + } + ] + }); + }); + + AddStep("push screen", () => LoadScreen(roomScreen = new PlaylistsRoomSubScreen(room))); + AddUntilStep("wait for screen load", () => roomScreen.IsCurrentScreen()); + AddUntilStep("ready button enabled", () => roomScreen.ChildrenOfType().SingleOrDefault()?.Enabled.Value, () => Is.EqualTo(enabled)); + } } } From 75781e3436adcde31e915da4b3ffa0da01574f47 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 3 Dec 2024 16:34:23 +0900 Subject: [PATCH 0203/1275] Update usages of IPC in line with framework changes --- osu.Desktop/Program.cs | 2 +- .../Visual/Navigation/TestSceneInterProcessCommunication.cs | 2 +- osu.Game/OsuGame.cs | 4 ++-- osu.Game/Tests/CleanRunHeadlessGameHost.cs | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/osu.Desktop/Program.cs b/osu.Desktop/Program.cs index ebc7509af6..df872ae6c6 100644 --- a/osu.Desktop/Program.cs +++ b/osu.Desktop/Program.cs @@ -99,7 +99,7 @@ namespace osu.Desktop var hostOptions = new HostOptions { - IPCPort = !tournamentClient ? OsuGame.IPC_PORT : null, + IPCPipeName = !tournamentClient ? OsuGame.IPC_PIPE_NAME : null, FriendlyGameName = OsuGameBase.GAME_NAME, }; diff --git a/osu.Game.Tests/Visual/Navigation/TestSceneInterProcessCommunication.cs b/osu.Game.Tests/Visual/Navigation/TestSceneInterProcessCommunication.cs index 83430b5665..be9dc387f2 100644 --- a/osu.Game.Tests/Visual/Navigation/TestSceneInterProcessCommunication.cs +++ b/osu.Game.Tests/Visual/Navigation/TestSceneInterProcessCommunication.cs @@ -63,7 +63,7 @@ namespace osu.Game.Tests.Visual.Navigation }); AddStep("create IPC sender channels", () => { - ipcSenderHost = new HeadlessGameHost(gameHost.Name, new HostOptions { IPCPort = OsuGame.IPC_PORT }); + ipcSenderHost = new HeadlessGameHost(gameHost.Name, new HostOptions { IPCPipeName = OsuGame.IPC_PIPE_NAME }); osuSchemeLinkIPCSender = new OsuSchemeLinkIPCChannel(ipcSenderHost); archiveImportIPCSender = new ArchiveImportIPCChannel(ipcSenderHost); }); diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs index 08960e3ebb..279530a579 100644 --- a/osu.Game/OsuGame.cs +++ b/osu.Game/OsuGame.cs @@ -87,9 +87,9 @@ namespace osu.Game { #if DEBUG // Different port allows running release and debug builds alongside each other. - public const int IPC_PORT = 44824; + public const string IPC_PIPE_NAME = "osu-lazer-debug"; #else - public const int IPC_PORT = 44823; + public const string IPC_PORT = "osu-lazer"; #endif /// diff --git a/osu.Game/Tests/CleanRunHeadlessGameHost.cs b/osu.Game/Tests/CleanRunHeadlessGameHost.cs index 00e5b38b1a..df4a91c4e2 100644 --- a/osu.Game/Tests/CleanRunHeadlessGameHost.cs +++ b/osu.Game/Tests/CleanRunHeadlessGameHost.cs @@ -27,7 +27,7 @@ namespace osu.Game.Tests [CallerMemberName] string callingMethodName = @"") : base($"{callingMethodName}-{Guid.NewGuid()}", new HostOptions { - IPCPort = bindIPC ? OsuGame.IPC_PORT : null, + IPCPipeName = bindIPC ? OsuGame.IPC_PIPE_NAME : null, }, bypassCleanup: bypassCleanupOnDispose, realtime: realtime) { this.bypassCleanupOnSetup = bypassCleanupOnSetup; From 808934581fa0956e111941efef55fb99f69c54bf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 3 Dec 2024 14:17:14 +0100 Subject: [PATCH 0204/1275] Move bookmarks out of `BeatmapInfo` Not sure why I didn't do that in https://github.com/ppy/osu/pull/28473... --- osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapDecoderTest.cs | 4 ++-- osu.Game.Tests/Beatmaps/Formats/OsuJsonDecoderTest.cs | 4 ++-- .../Visual/Editing/TestSceneEditorSummaryTimeline.cs | 2 +- osu.Game/Beatmaps/Beatmap.cs | 2 ++ osu.Game/Beatmaps/BeatmapConverter.cs | 1 + osu.Game/Beatmaps/BeatmapInfo.cs | 3 --- osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs | 2 +- osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs | 4 ++-- osu.Game/Beatmaps/IBeatmap.cs | 2 ++ osu.Game/Rulesets/Difficulty/DifficultyCalculator.cs | 6 ++++++ .../Edit/Components/Timelines/Summary/Parts/BookmarkPart.cs | 2 +- osu.Game/Screens/Edit/EditorBeatmap.cs | 6 ++++++ 12 files changed, 26 insertions(+), 12 deletions(-) diff --git a/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapDecoderTest.cs b/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapDecoderTest.cs index be411128f7..adb1755c11 100644 --- a/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapDecoderTest.cs +++ b/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapDecoderTest.cs @@ -109,9 +109,9 @@ namespace osu.Game.Tests.Beatmaps.Formats 95901, 106450, 116999, 119637, 130186, 140735, 151285, 161834, 164471, 175020, 185570, 196119, 206669, 209306 }; - Assert.AreEqual(expectedBookmarks.Length, beatmap.BeatmapInfo.Bookmarks.Length); + Assert.AreEqual(expectedBookmarks.Length, beatmap.Bookmarks.Length); for (int i = 0; i < expectedBookmarks.Length; i++) - Assert.AreEqual(expectedBookmarks[i], beatmap.BeatmapInfo.Bookmarks[i]); + Assert.AreEqual(expectedBookmarks[i], beatmap.Bookmarks[i]); Assert.AreEqual(1.8, beatmap.DistanceSpacing); Assert.AreEqual(4, beatmap.BeatmapInfo.BeatDivisor); Assert.AreEqual(4, beatmap.GridSize); diff --git a/osu.Game.Tests/Beatmaps/Formats/OsuJsonDecoderTest.cs b/osu.Game.Tests/Beatmaps/Formats/OsuJsonDecoderTest.cs index e57a4fff62..c20cf7befd 100644 --- a/osu.Game.Tests/Beatmaps/Formats/OsuJsonDecoderTest.cs +++ b/osu.Game.Tests/Beatmaps/Formats/OsuJsonDecoderTest.cs @@ -73,9 +73,9 @@ namespace osu.Game.Tests.Beatmaps.Formats 95901, 106450, 116999, 119637, 130186, 140735, 151285, 161834, 164471, 175020, 185570, 196119, 206669, 209306 }; - Assert.AreEqual(expectedBookmarks.Length, beatmapInfo.Bookmarks.Length); + Assert.AreEqual(expectedBookmarks.Length, beatmap.Bookmarks.Length); for (int i = 0; i < expectedBookmarks.Length; i++) - Assert.AreEqual(expectedBookmarks[i], beatmapInfo.Bookmarks[i]); + Assert.AreEqual(expectedBookmarks[i], beatmap.Bookmarks[i]); Assert.AreEqual(1.8, beatmap.DistanceSpacing); Assert.AreEqual(4, beatmapInfo.BeatDivisor); Assert.AreEqual(4, beatmap.GridSize); diff --git a/osu.Game.Tests/Visual/Editing/TestSceneEditorSummaryTimeline.cs b/osu.Game.Tests/Visual/Editing/TestSceneEditorSummaryTimeline.cs index 677d3135ba..e584f1b9d7 100644 --- a/osu.Game.Tests/Visual/Editing/TestSceneEditorSummaryTimeline.cs +++ b/osu.Game.Tests/Visual/Editing/TestSceneEditorSummaryTimeline.cs @@ -26,7 +26,7 @@ namespace osu.Game.Tests.Visual.Editing beatmap.ControlPointInfo.Add(50000, new DifficultyControlPoint { SliderVelocity = 2 }); beatmap.ControlPointInfo.Add(80000, new EffectControlPoint { KiaiMode = true }); beatmap.ControlPointInfo.Add(110000, new EffectControlPoint { KiaiMode = false }); - beatmap.BeatmapInfo.Bookmarks = new[] { 75000, 125000 }; + beatmap.Bookmarks = new[] { 75000, 125000 }; beatmap.Breaks.Add(new ManualBreakPeriod(90000, 120000)); editorBeatmap = new EditorBeatmap(beatmap); diff --git a/osu.Game/Beatmaps/Beatmap.cs b/osu.Game/Beatmaps/Beatmap.cs index d8effc2f22..8ea6fa1f51 100644 --- a/osu.Game/Beatmaps/Beatmap.cs +++ b/osu.Game/Beatmaps/Beatmap.cs @@ -139,6 +139,8 @@ namespace osu.Game.Beatmaps public int CountdownOffset { get; set; } + public int[] Bookmarks { get; set; } = Array.Empty(); + IBeatmap IBeatmap.Clone() => Clone(); public Beatmap Clone() => (Beatmap)MemberwiseClone(); diff --git a/osu.Game/Beatmaps/BeatmapConverter.cs b/osu.Game/Beatmaps/BeatmapConverter.cs index 82b40c0318..0cf10c659b 100644 --- a/osu.Game/Beatmaps/BeatmapConverter.cs +++ b/osu.Game/Beatmaps/BeatmapConverter.cs @@ -85,6 +85,7 @@ namespace osu.Game.Beatmaps beatmap.TimelineZoom = original.TimelineZoom; beatmap.Countdown = original.Countdown; beatmap.CountdownOffset = original.CountdownOffset; + beatmap.Bookmarks = original.Bookmarks; return beatmap; } diff --git a/osu.Game/Beatmaps/BeatmapInfo.cs b/osu.Game/Beatmaps/BeatmapInfo.cs index 2df262eba3..333ec89eab 100644 --- a/osu.Game/Beatmaps/BeatmapInfo.cs +++ b/osu.Game/Beatmaps/BeatmapInfo.cs @@ -231,9 +231,6 @@ namespace osu.Game.Beatmaps [Obsolete("Use ScoreManager.GetMaximumAchievableComboAsync instead.")] public int? MaxCombo { get; set; } - [Ignored] - public int[] Bookmarks { get; set; } = Array.Empty(); - public int BeatmapVersion; public BeatmapInfo Clone() => (BeatmapInfo)this.Detach().MemberwiseClone(); diff --git a/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs b/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs index 0b5450e5ac..153db6d6b9 100644 --- a/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs +++ b/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs @@ -305,7 +305,7 @@ namespace osu.Game.Beatmaps.Formats switch (pair.Key) { case @"Bookmarks": - beatmap.BeatmapInfo.Bookmarks = pair.Value.Split(',').Select(v => + beatmap.Bookmarks = pair.Value.Split(',').Select(v => { bool result = int.TryParse(v, out int val); return new { result, val }; diff --git a/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs b/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs index 093e76a535..6c855e1346 100644 --- a/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs +++ b/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs @@ -110,8 +110,8 @@ namespace osu.Game.Beatmaps.Formats { writer.WriteLine("[Editor]"); - if (beatmap.BeatmapInfo.Bookmarks.Length > 0) - writer.WriteLine(FormattableString.Invariant($"Bookmarks: {string.Join(',', beatmap.BeatmapInfo.Bookmarks)}")); + if (beatmap.Bookmarks.Length > 0) + writer.WriteLine(FormattableString.Invariant($"Bookmarks: {string.Join(',', beatmap.Bookmarks)}")); writer.WriteLine(FormattableString.Invariant($"DistanceSpacing: {beatmap.DistanceSpacing}")); writer.WriteLine(FormattableString.Invariant($"BeatDivisor: {beatmap.BeatmapInfo.BeatDivisor}")); writer.WriteLine(FormattableString.Invariant($"GridSize: {beatmap.GridSize}")); diff --git a/osu.Game/Beatmaps/IBeatmap.cs b/osu.Game/Beatmaps/IBeatmap.cs index 6a41b8ee6c..826d4e19a7 100644 --- a/osu.Game/Beatmaps/IBeatmap.cs +++ b/osu.Game/Beatmaps/IBeatmap.cs @@ -107,6 +107,8 @@ namespace osu.Game.Beatmaps /// int CountdownOffset { get; internal set; } + int[] Bookmarks { get; internal set; } + /// /// Creates a shallow-clone of this beatmap and returns it. /// diff --git a/osu.Game/Rulesets/Difficulty/DifficultyCalculator.cs b/osu.Game/Rulesets/Difficulty/DifficultyCalculator.cs index 59b1ac22bc..14acc9b908 100644 --- a/osu.Game/Rulesets/Difficulty/DifficultyCalculator.cs +++ b/osu.Game/Rulesets/Difficulty/DifficultyCalculator.cs @@ -413,6 +413,12 @@ namespace osu.Game.Rulesets.Difficulty set => baseBeatmap.CountdownOffset = value; } + public int[] Bookmarks + { + get => baseBeatmap.Bookmarks; + set => baseBeatmap.Bookmarks = value; + } + #endregion } } diff --git a/osu.Game/Screens/Edit/Components/Timelines/Summary/Parts/BookmarkPart.cs b/osu.Game/Screens/Edit/Components/Timelines/Summary/Parts/BookmarkPart.cs index 189cb4ba4a..04d5a5d618 100644 --- a/osu.Game/Screens/Edit/Components/Timelines/Summary/Parts/BookmarkPart.cs +++ b/osu.Game/Screens/Edit/Components/Timelines/Summary/Parts/BookmarkPart.cs @@ -18,7 +18,7 @@ namespace osu.Game.Screens.Edit.Components.Timelines.Summary.Parts protected override void LoadBeatmap(EditorBeatmap beatmap) { base.LoadBeatmap(beatmap); - foreach (int bookmark in beatmap.BeatmapInfo.Bookmarks) + foreach (int bookmark in beatmap.Bookmarks) Add(new BookmarkVisualisation(bookmark)); } diff --git a/osu.Game/Screens/Edit/EditorBeatmap.cs b/osu.Game/Screens/Edit/EditorBeatmap.cs index b5e18fd38c..66fb5d07fe 100644 --- a/osu.Game/Screens/Edit/EditorBeatmap.cs +++ b/osu.Game/Screens/Edit/EditorBeatmap.cs @@ -270,6 +270,12 @@ namespace osu.Game.Screens.Edit set => PlayableBeatmap.CountdownOffset = value; } + public int[] Bookmarks + { + get => PlayableBeatmap.Bookmarks; + set => PlayableBeatmap.Bookmarks = value; + } + public IBeatmap Clone() => (EditorBeatmap)MemberwiseClone(); private IList mutableHitObjects => (IList)PlayableBeatmap.HitObjects; From d60b7f479801f7a08ea588f17241abaff937ece2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 3 Dec 2024 15:14:22 +0100 Subject: [PATCH 0205/1275] Implement basic bookmark support in editor --- .../Input/Bindings/GlobalActionContainer.cs | 16 +++++ osu.Game/Localisation/EditorStrings.cs | 32 +++++++++- .../GlobalActionKeyBindingStrings.cs | 20 ++++++ .../Timelines/Summary/Parts/BookmarkPart.cs | 63 ++++++++++++++++--- osu.Game/Screens/Edit/Editor.cs | 62 ++++++++++++++++++ osu.Game/Screens/Edit/EditorBeatmap.cs | 12 +++- .../Edit/LegacyEditorBeatmapPatcher.cs | 22 +++++++ 7 files changed, 218 insertions(+), 9 deletions(-) diff --git a/osu.Game/Input/Bindings/GlobalActionContainer.cs b/osu.Game/Input/Bindings/GlobalActionContainer.cs index 02ede0a2f8..42028c044f 100644 --- a/osu.Game/Input/Bindings/GlobalActionContainer.cs +++ b/osu.Game/Input/Bindings/GlobalActionContainer.cs @@ -152,6 +152,10 @@ namespace osu.Game.Input.Bindings new KeyBinding(new[] { InputKey.Control, InputKey.Right }, GlobalAction.EditorSeekToNextHitObject), new KeyBinding(new[] { InputKey.Control, InputKey.Shift, InputKey.Left }, GlobalAction.EditorSeekToPreviousSamplePoint), new KeyBinding(new[] { InputKey.Control, InputKey.Shift, InputKey.Right }, GlobalAction.EditorSeekToNextSamplePoint), + new KeyBinding(new[] { InputKey.Control, InputKey.B }, GlobalAction.EditorAddBookmark), + new KeyBinding(new[] { InputKey.Control, InputKey.Shift, InputKey.B }, GlobalAction.EditorRemoveClosestBookmark), + new KeyBinding(InputKey.None, GlobalAction.EditorSeekToPreviousBookmark), + new KeyBinding(InputKey.None, GlobalAction.EditorSeekToNextBookmark), }; private static IEnumerable editorTestPlayKeyBindings => new[] @@ -476,6 +480,18 @@ namespace osu.Game.Input.Bindings [LocalisableDescription(typeof(GlobalActionKeyBindingStrings), nameof(GlobalActionKeyBindingStrings.EditorCycleGridType))] EditorCycleGridType, + + [LocalisableDescription(typeof(GlobalActionKeyBindingStrings), nameof(GlobalActionKeyBindingStrings.EditorAddBookmark))] + EditorAddBookmark, + + [LocalisableDescription(typeof(GlobalActionKeyBindingStrings), nameof(GlobalActionKeyBindingStrings.EditorRemoveClosestBookmark))] + EditorRemoveClosestBookmark, + + [LocalisableDescription(typeof(GlobalActionKeyBindingStrings), nameof(GlobalActionKeyBindingStrings.EditorSeekToPreviousBookmark))] + EditorSeekToPreviousBookmark, + + [LocalisableDescription(typeof(GlobalActionKeyBindingStrings), nameof(GlobalActionKeyBindingStrings.EditorSeekToNextBookmark))] + EditorSeekToNextBookmark, } public enum GlobalActionCategory diff --git a/osu.Game/Localisation/EditorStrings.cs b/osu.Game/Localisation/EditorStrings.cs index 127bdd8355..3b4026be11 100644 --- a/osu.Game/Localisation/EditorStrings.cs +++ b/osu.Game/Localisation/EditorStrings.cs @@ -154,6 +154,36 @@ namespace osu.Game.Localisation /// public static LocalisableString TimelineShowTicks => new TranslatableString(getKey(@"timeline_show_ticks"), @"Show ticks"); + /// + /// "Bookmarks" + /// + public static LocalisableString Bookmarks => new TranslatableString(getKey(@"bookmarks"), @"Bookmarks"); + + /// + /// "Add bookmark" + /// + public static LocalisableString AddBookmark => new TranslatableString(getKey(@"add_bookmark"), @"Add bookmark"); + + /// + /// "Remove closest bookmark" + /// + public static LocalisableString RemoveClosestBookmark => new TranslatableString(getKey(@"remove_closest_bookmark"), @"Remove closest bookmark"); + + /// + /// "Seek to previous bookmark" + /// + public static LocalisableString SeekToPreviousBookmark => new TranslatableString(getKey(@"seek_to_previous_bookmark"), @"Seek to previous bookmark"); + + /// + /// "Seek to next bookmark" + /// + public static LocalisableString SeekToNextBookmark => new TranslatableString(getKey(@"seek_to_next_bookmark"), @"Seek to next bookmark"); + + /// + /// "Reset bookmarks" + /// + public static LocalisableString ResetBookmarks => new TranslatableString(getKey(@"reset_bookmarks"), @"Reset bookmarks"); + private static string getKey(string key) => $@"{prefix}:{key}"; } -} +} \ No newline at end of file diff --git a/osu.Game/Localisation/GlobalActionKeyBindingStrings.cs b/osu.Game/Localisation/GlobalActionKeyBindingStrings.cs index ed80704a0a..f9db0461ce 100644 --- a/osu.Game/Localisation/GlobalActionKeyBindingStrings.cs +++ b/osu.Game/Localisation/GlobalActionKeyBindingStrings.cs @@ -429,6 +429,26 @@ namespace osu.Game.Localisation /// public static LocalisableString EditorSeekToNextSamplePoint => new TranslatableString(getKey(@"editor_seek_to_next_sample_point"), @"Seek to next sample point"); + /// + /// "Add bookmark" + /// + public static LocalisableString EditorAddBookmark => new TranslatableString(getKey(@"editor_add_bookmark"), @"Add bookmark"); + + /// + /// "Remove closest bookmark" + /// + public static LocalisableString EditorRemoveClosestBookmark => new TranslatableString(getKey(@"editor_remove_closest_bookmark"), @"Remove closest bookmark"); + + /// + /// "Seek to previous bookmark" + /// + public static LocalisableString EditorSeekToPreviousBookmark => new TranslatableString(getKey(@"editor_seek_to_previous_bookmark"), @"Seek to previous bookmark"); + + /// + /// "Seek to next bookmark" + /// + public static LocalisableString EditorSeekToNextBookmark => new TranslatableString(getKey(@"editor_seek_to_next_bookmark"), @"Seek to next bookmark"); + private static string getKey(string key) => $@"{prefix}:{key}"; } } diff --git a/osu.Game/Screens/Edit/Components/Timelines/Summary/Parts/BookmarkPart.cs b/osu.Game/Screens/Edit/Components/Timelines/Summary/Parts/BookmarkPart.cs index 04d5a5d618..4b178dd831 100644 --- a/osu.Game/Screens/Edit/Components/Timelines/Summary/Parts/BookmarkPart.cs +++ b/osu.Game/Screens/Edit/Components/Timelines/Summary/Parts/BookmarkPart.cs @@ -2,7 +2,11 @@ // See the LICENCE file in the repository root for full licence text. using osu.Framework.Allocation; +using osu.Framework.Bindables; +using osu.Framework.Graphics; using osu.Framework.Graphics.Cursor; +using osu.Framework.Graphics.Pooling; +using osu.Framework.Graphics.Shapes; using osu.Framework.Localisation; using osu.Game.Extensions; using osu.Game.Graphics; @@ -15,24 +19,69 @@ namespace osu.Game.Screens.Edit.Components.Timelines.Summary.Parts /// public partial class BookmarkPart : TimelinePart { + private readonly BindableList bookmarks = new BindableList(); + + private DrawablePool pool = null!; + + [BackgroundDependencyLoader] + private void load() + { + AddInternal(pool = new DrawablePool(10)); + } + protected override void LoadBeatmap(EditorBeatmap beatmap) { base.LoadBeatmap(beatmap); - foreach (int bookmark in beatmap.Bookmarks) - Add(new BookmarkVisualisation(bookmark)); + + bookmarks.UnbindAll(); + bookmarks.BindTo(beatmap.Bookmarks); } - private partial class BookmarkVisualisation : PointVisualisation, IHasTooltip + protected override void LoadComplete() { - public BookmarkVisualisation(double startTime) - : base(startTime) + base.LoadComplete(); + bookmarks.BindCollectionChanged((_, _) => { + Clear(disposeChildren: false); + foreach (int bookmark in bookmarks) + Add(pool.Get(v => v.StartTime = bookmark)); + }, true); + } + + private partial class BookmarkVisualisation : PoolableDrawable, IHasTooltip + { + private int startTime; + + public int StartTime + { + get => startTime; + set + { + if (startTime == value) + return; + + startTime = value; + X = startTime; + } } [BackgroundDependencyLoader] - private void load(OsuColour colours) => Colour = colours.Blue; + private void load(OsuColour colours) + { + RelativePositionAxes = Axes.Both; + RelativeSizeAxes = Axes.Y; - public LocalisableString TooltipText => $"{StartTime.ToEditorFormattedString()} bookmark"; + Anchor = Anchor.CentreLeft; + Origin = Anchor.Centre; + + Width = PointVisualisation.MAX_WIDTH; + Height = 0.4f; + + Colour = colours.Blue; + InternalChild = new FastCircle { RelativeSizeAxes = Axes.Both }; + } + + public LocalisableString TooltipText => $"{((double)StartTime).ToEditorFormattedString()} bookmark"; } } } diff --git a/osu.Game/Screens/Edit/Editor.cs b/osu.Game/Screens/Edit/Editor.cs index 0e4807dc78..a022ca5435 100644 --- a/osu.Game/Screens/Edit/Editor.cs +++ b/osu.Game/Screens/Edit/Editor.cs @@ -422,6 +422,29 @@ namespace osu.Game.Screens.Edit Items = new MenuItem[] { new EditorMenuItem(EditorStrings.SetPreviewPointToCurrent, MenuItemType.Standard, SetPreviewPointToCurrentTime), + new EditorMenuItem(EditorStrings.Bookmarks) + { + Items = new MenuItem[] + { + new EditorMenuItem(EditorStrings.AddBookmark, MenuItemType.Standard, addBookmarkAtCurrentTime) + { + Hotkey = new Hotkey(GlobalAction.EditorAddBookmark), + }, + new EditorMenuItem(EditorStrings.RemoveClosestBookmark, MenuItemType.Destructive, removeBookmarksInProximityToCurrentTime) + { + Hotkey = new Hotkey(GlobalAction.EditorRemoveClosestBookmark) + }, + new EditorMenuItem(EditorStrings.SeekToPreviousBookmark, MenuItemType.Standard, () => seekBookmark(-1)) + { + Hotkey = new Hotkey(GlobalAction.EditorSeekToPreviousBookmark) + }, + new EditorMenuItem(EditorStrings.SeekToNextBookmark, MenuItemType.Standard, () => seekBookmark(1)) + { + Hotkey = new Hotkey(GlobalAction.EditorSeekToNextBookmark) + }, + new EditorMenuItem(EditorStrings.ResetBookmarks, MenuItemType.Destructive, () => editorBeatmap.Bookmarks.Clear()) + } + } } } } @@ -753,6 +776,14 @@ namespace osu.Game.Screens.Edit case GlobalAction.EditorSeekToNextSamplePoint: seekSamplePoint(1); return true; + + case GlobalAction.EditorSeekToPreviousBookmark: + seekBookmark(-1); + return true; + + case GlobalAction.EditorSeekToNextBookmark: + seekBookmark(1); + return true; } if (e.Repeat) @@ -760,6 +791,14 @@ namespace osu.Game.Screens.Edit switch (e.Action) { + case GlobalAction.EditorAddBookmark: + addBookmarkAtCurrentTime(); + return true; + + case GlobalAction.EditorRemoveClosestBookmark: + removeBookmarksInProximityToCurrentTime(); + return true; + case GlobalAction.EditorCloneSelection: Clone(); return true; @@ -792,6 +831,19 @@ namespace osu.Game.Screens.Edit return false; } + private void addBookmarkAtCurrentTime() + { + int bookmark = (int)clock.CurrentTimeAccurate; + int idx = editorBeatmap.Bookmarks.BinarySearch(bookmark); + if (idx < 0) + editorBeatmap.Bookmarks.Insert(~idx, bookmark); + } + + private void removeBookmarksInProximityToCurrentTime() + { + editorBeatmap.Bookmarks.RemoveAll(b => Math.Abs(b - clock.CurrentTimeAccurate) < 2000); + } + public void OnReleased(KeyBindingReleaseEvent e) { } @@ -1127,6 +1179,16 @@ namespace osu.Game.Screens.Edit clock.SeekSmoothlyTo(found.StartTime); } + private void seekBookmark(int direction) + { + int? targetBookmark = direction < 1 + ? editorBeatmap.Bookmarks.Cast().LastOrDefault(b => b < clock.CurrentTimeAccurate) + : editorBeatmap.Bookmarks.Cast().FirstOrDefault(b => b > clock.CurrentTimeAccurate); + + if (targetBookmark != null) + clock.SeekSmoothlyTo(targetBookmark.Value); + } + private void seekSamplePoint(int direction) { double currentTime = clock.CurrentTimeAccurate; diff --git a/osu.Game/Screens/Edit/EditorBeatmap.cs b/osu.Game/Screens/Edit/EditorBeatmap.cs index 66fb5d07fe..44f9646889 100644 --- a/osu.Game/Screens/Edit/EditorBeatmap.cs +++ b/osu.Game/Screens/Edit/EditorBeatmap.cs @@ -118,6 +118,14 @@ namespace osu.Game.Screens.Edit playableBeatmap.Breaks.AddRange(Breaks); }); + Bookmarks = new BindableList(playableBeatmap.Bookmarks); + Bookmarks.BindCollectionChanged((_, _) => + { + BeginChange(); + playableBeatmap.Bookmarks = Bookmarks.OrderBy(x => x).Distinct().ToArray(); + EndChange(); + }); + PreviewTime = new BindableInt(BeatmapInfo.Metadata.PreviewTime); PreviewTime.BindValueChanged(s => { @@ -270,7 +278,9 @@ namespace osu.Game.Screens.Edit set => PlayableBeatmap.CountdownOffset = value; } - public int[] Bookmarks + public readonly BindableList Bookmarks; + + int[] IBeatmap.Bookmarks { get => PlayableBeatmap.Bookmarks; set => PlayableBeatmap.Bookmarks = value; diff --git a/osu.Game/Screens/Edit/LegacyEditorBeatmapPatcher.cs b/osu.Game/Screens/Edit/LegacyEditorBeatmapPatcher.cs index a1ee41fc48..f3d58a3c3c 100644 --- a/osu.Game/Screens/Edit/LegacyEditorBeatmapPatcher.cs +++ b/osu.Game/Screens/Edit/LegacyEditorBeatmapPatcher.cs @@ -46,6 +46,7 @@ namespace osu.Game.Screens.Edit processHitObjects(result, () => newBeatmap ??= readBeatmap(newState)); processTimingPoints(() => newBeatmap ??= readBeatmap(newState)); processBreaks(() => newBeatmap ??= readBeatmap(newState)); + processBookmarks(() => newBeatmap ??= readBeatmap(newState)); processHitObjectLocalData(() => newBeatmap ??= readBeatmap(newState)); editorBeatmap.EndChange(); } @@ -97,6 +98,27 @@ namespace osu.Game.Screens.Edit } } + private void processBookmarks(Func getNewBeatmap) + { + var newBookmarks = getNewBeatmap().Bookmarks.ToHashSet(); + + foreach (int oldBookmark in editorBeatmap.Bookmarks.ToArray()) + { + if (newBookmarks.Contains(oldBookmark)) + continue; + + editorBeatmap.Bookmarks.Remove(oldBookmark); + } + + foreach (int newBookmark in newBookmarks) + { + if (editorBeatmap.Bookmarks.Contains(newBookmark)) + continue; + + editorBeatmap.Bookmarks.Add(newBookmark); + } + } + private void processHitObjects(DiffResult result, Func getNewBeatmap) { findChangedIndices(result, LegacyDecoder.Section.HitObjects, out var removedIndices, out var addedIndices); From 837744b0aa9ffbd2587f332721eef868ff80878d Mon Sep 17 00:00:00 2001 From: Susko3 Date: Tue, 3 Dec 2024 23:26:33 +0000 Subject: [PATCH 0206/1275] Use LocalisationManager.GetLocalisedString() instead of bindable hack Made possible by https://github.com/ppy/osu-framework/pull/6377. --- osu.Desktop/Windows/WindowsAssociationManager.cs | 10 +--------- .../Overlays/Settings/Sections/Input/TabletSettings.cs | 4 ++-- 2 files changed, 3 insertions(+), 11 deletions(-) diff --git a/osu.Desktop/Windows/WindowsAssociationManager.cs b/osu.Desktop/Windows/WindowsAssociationManager.cs index c8066cabda..6f53c65ca9 100644 --- a/osu.Desktop/Windows/WindowsAssociationManager.cs +++ b/osu.Desktop/Windows/WindowsAssociationManager.cs @@ -148,15 +148,7 @@ namespace osu.Desktop.Windows foreach (var association in uri_associations) association.UpdateDescription(getLocalisedString(association.Description)); - string getLocalisedString(LocalisableString s) - { - if (localisation == null) - return s.ToString(); - - var b = localisation.GetLocalisedBindableString(s); - b.UnbindAll(); - return b.Value; - } + string getLocalisedString(LocalisableString s) => localisation?.GetLocalisedString(s) ?? s.ToString(); } #region Native interop diff --git a/osu.Game/Overlays/Settings/Sections/Input/TabletSettings.cs b/osu.Game/Overlays/Settings/Sections/Input/TabletSettings.cs index 4c9320c2a6..00ffbc1120 100644 --- a/osu.Game/Overlays/Settings/Sections/Input/TabletSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/Input/TabletSettings.cs @@ -114,10 +114,10 @@ namespace osu.Game.Overlays.Settings.Sections.Input if (RuntimeInfo.OS == RuntimeInfo.Platform.Windows || RuntimeInfo.OS == RuntimeInfo.Platform.Linux) { t.NewLine(); - var formattedSource = MessageFormatter.FormatText(localisation.GetLocalisedBindableString(TabletSettingsStrings.NoTabletDetectedDescription( + var formattedSource = MessageFormatter.FormatText(localisation.GetLocalisedString(TabletSettingsStrings.NoTabletDetectedDescription( RuntimeInfo.OS == RuntimeInfo.Platform.Windows ? @"https://opentabletdriver.net/Wiki/FAQ/Windows" - : @"https://opentabletdriver.net/Wiki/FAQ/Linux")).Value); + : @"https://opentabletdriver.net/Wiki/FAQ/Linux"))); t.AddLinks(formattedSource.Text, formattedSource.Links); } }), From 296fa69edd24c658d7525e8fe903923abb874bfc Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 4 Dec 2024 14:30:59 +0900 Subject: [PATCH 0207/1275] Add "buttons" as a search term for key bindings --- osu.Game/Overlays/Settings/Sections/Input/BindingSettings.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Overlays/Settings/Sections/Input/BindingSettings.cs b/osu.Game/Overlays/Settings/Sections/Input/BindingSettings.cs index a93e6c37af..704fa6e907 100644 --- a/osu.Game/Overlays/Settings/Sections/Input/BindingSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/Input/BindingSettings.cs @@ -13,7 +13,7 @@ namespace osu.Game.Overlays.Settings.Sections.Input { protected override LocalisableString Header => BindingSettingsStrings.ShortcutAndGameplayBindings; - public override IEnumerable FilterTerms => base.FilterTerms.Concat(new LocalisableString[] { @"keybindings", @"controls", @"keyboard", @"keys" }); + public override IEnumerable FilterTerms => base.FilterTerms.Concat(new LocalisableString[] { @"keybindings", @"controls", @"keyboard", @"keys", @"buttons" }); public BindingSettings(KeyBindingPanel keyConfig) { From a4d58648e23e19ef9cda687dbb5127ba17becc14 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 4 Dec 2024 14:31:39 +0900 Subject: [PATCH 0208/1275] Fix quick retry transition from results screen --- osu.Game/Screens/Ranking/ResultsScreen.cs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/osu.Game/Screens/Ranking/ResultsScreen.cs b/osu.Game/Screens/Ranking/ResultsScreen.cs index 0209fbd39c..507d138d90 100644 --- a/osu.Game/Screens/Ranking/ResultsScreen.cs +++ b/osu.Game/Screens/Ranking/ResultsScreen.cs @@ -55,6 +55,8 @@ namespace osu.Game.Screens.Ranking [Resolved] private Player? player { get; set; } + private bool skipExitTransition; + [Resolved] private IAPIProvider api { get; set; } = null!; @@ -203,6 +205,7 @@ namespace osu.Game.Screens.Ranking { if (!this.IsCurrentScreen()) return; + skipExitTransition = true; player?.Restart(true); }, }); @@ -313,7 +316,8 @@ namespace osu.Game.Screens.Ranking // HitObject references from HitEvent. Score?.HitEvents.Clear(); - this.FadeOut(100); + if (!skipExitTransition) + this.FadeOut(100); return false; } From ad4df82593e334b9b9c0522326655479077717f8 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Wed, 4 Dec 2024 16:26:36 +0900 Subject: [PATCH 0209/1275] Improve multiplayer listing search by making it fuzzy --- .../Lounge/Components/RoomsContainer.cs | 30 ++++++++++++++----- 1 file changed, 23 insertions(+), 7 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/Lounge/Components/RoomsContainer.cs b/osu.Game/Screens/OnlinePlay/Lounge/Components/RoomsContainer.cs index 17aed021b2..6eda993f94 100644 --- a/osu.Game/Screens/OnlinePlay/Lounge/Components/RoomsContainer.cs +++ b/osu.Game/Screens/OnlinePlay/Lounge/Components/RoomsContainer.cs @@ -5,6 +5,7 @@ using System; using System.Collections.Generic; using System.Collections.Specialized; using System.Diagnostics; +using System.Globalization; using System.Linq; using osu.Framework.Allocation; using osu.Framework.Bindables; @@ -80,19 +81,34 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components bool matchingFilter = true; matchingFilter &= criteria.Ruleset == null || r.Room.PlaylistItemStats?.RulesetIDs.Any(id => id == criteria.Ruleset.OnlineID) != false; - - if (!string.IsNullOrEmpty(criteria.SearchString)) - { - // Room name isn't translatable, so ToString() is used here for simplicity. - matchingFilter &= r.FilterTerms.Any(term => term.ToString().Contains(criteria.SearchString, StringComparison.InvariantCultureIgnoreCase)); - } - matchingFilter &= matchPermissions(r, criteria.Permissions); + // Room name isn't translatable, so ToString() is used here for simplicity. + string[] filterTerms = r.FilterTerms.Select(t => t.ToString()).ToArray(); + string[] searchTerms = criteria.SearchString.Split(' ', StringSplitOptions.RemoveEmptyEntries); + matchingFilter &= searchTerms.All(searchTerm => filterTerms.Any(filterTerm => checkTerm(filterTerm, searchTerm))); + r.MatchingFilter = matchingFilter; } }); + // Lifted from SearchContainer. + static bool checkTerm(string haystack, string needle) + { + int index = 0; + + for (int i = 0; i < needle.Length; i++) + { + int found = CultureInfo.InvariantCulture.CompareInfo.IndexOf(haystack, needle[i], index, CompareOptions.OrdinalIgnoreCase); + if (found < 0) + return false; + + index = found + 1; + } + + return true; + } + static bool matchPermissions(DrawableLoungeRoom room, RoomPermissionsFilter accessType) { switch (accessType) From 2a6fbb59ffec80028e1b313a2a331c2d16386adb Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Wed, 4 Dec 2024 02:12:05 -0500 Subject: [PATCH 0210/1275] Add failing test case --- .../Editing/TestSceneEditorBeatmapCreation.cs | 105 ++++++++++++++++++ 1 file changed, 105 insertions(+) diff --git a/osu.Game.Tests/Visual/Editing/TestSceneEditorBeatmapCreation.cs b/osu.Game.Tests/Visual/Editing/TestSceneEditorBeatmapCreation.cs index db87987815..ddf6502899 100644 --- a/osu.Game.Tests/Visual/Editing/TestSceneEditorBeatmapCreation.cs +++ b/osu.Game.Tests/Visual/Editing/TestSceneEditorBeatmapCreation.cs @@ -19,6 +19,7 @@ using osu.Game.Overlays.Dialog; using osu.Game.Rulesets; using osu.Game.Rulesets.Catch; using osu.Game.Rulesets.Catch.Objects; +using osu.Game.Rulesets.Mania; using osu.Game.Rulesets.Osu; using osu.Game.Rulesets.Osu.Objects; using osu.Game.Rulesets.Osu.UI; @@ -154,6 +155,7 @@ namespace osu.Game.Tests.Visual.Editing AddStep("set unique difficulty name", () => EditorBeatmap.BeatmapInfo.DifficultyName = firstDifficultyName); AddStep("add timing point", () => EditorBeatmap.ControlPointInfo.Add(0, new TimingControlPoint { BeatLength = 1000 })); + AddStep("add effect point", () => EditorBeatmap.ControlPointInfo.Add(500, new EffectControlPoint { KiaiMode = true })); AddStep("add hitobjects", () => EditorBeatmap.AddRange(new[] { new HitCircle @@ -200,6 +202,11 @@ namespace osu.Game.Tests.Visual.Editing var timingPoint = EditorBeatmap.ControlPointInfo.TimingPoints.Single(); return timingPoint.Time == 0 && timingPoint.BeatLength == 1000; }); + AddAssert("created difficulty has effect points", () => + { + var effectPoint = EditorBeatmap.ControlPointInfo.EffectPoints.Single(); + return effectPoint.Time == 500 && effectPoint.KiaiMode && effectPoint.ScrollSpeedBindable.IsDefault; + }); AddAssert("created difficulty has no objects", () => EditorBeatmap.HitObjects.Count == 0); AddAssert("status is modified", () => EditorBeatmap.BeatmapInfo.Status == BeatmapOnlineStatus.LocallyModified); @@ -219,6 +226,104 @@ namespace osu.Game.Tests.Visual.Editing }); } + [Test] + public void TestCreateNewDifficultyWithScrollSpeed_SameRuleset() + { + string firstDifficultyName = Guid.NewGuid().ToString(); + + AddStep("save beatmap", () => Editor.Save()); + AddStep("create new difficulty", () => Editor.CreateNewDifficulty(new ManiaRuleset().RulesetInfo)); + + AddStep("set unique difficulty name", () => EditorBeatmap.BeatmapInfo.DifficultyName = firstDifficultyName); + AddStep("add timing point", () => EditorBeatmap.ControlPointInfo.Add(0, new TimingControlPoint { BeatLength = 1000 })); + AddStep("add effect points", () => + { + EditorBeatmap.ControlPointInfo.Add(250, new EffectControlPoint { KiaiMode = false, ScrollSpeed = 0.05 }); + EditorBeatmap.ControlPointInfo.Add(500, new EffectControlPoint { KiaiMode = true, ScrollSpeed = 0.1 }); + EditorBeatmap.ControlPointInfo.Add(750, new EffectControlPoint { KiaiMode = true, ScrollSpeed = 0.15 }); + EditorBeatmap.ControlPointInfo.Add(1000, new EffectControlPoint { KiaiMode = false, ScrollSpeed = 0.2 }); + EditorBeatmap.ControlPointInfo.Add(1500, new EffectControlPoint { KiaiMode = false, ScrollSpeed = 0.3 }); + }); + + AddStep("save beatmap", () => Editor.Save()); + + AddStep("create new difficulty", () => Editor.CreateNewDifficulty(new ManiaRuleset().RulesetInfo)); + + AddUntilStep("wait for dialog", () => DialogOverlay.CurrentDialog is CreateNewDifficultyDialog); + AddStep("confirm creation with no objects", () => DialogOverlay.CurrentDialog!.PerformOkAction()); + + AddUntilStep("wait for created", () => + { + string? difficultyName = Editor.ChildrenOfType().SingleOrDefault()?.BeatmapInfo.DifficultyName; + return difficultyName != null && difficultyName != firstDifficultyName; + }); + + AddAssert("created difficulty has timing point", () => + { + var timingPoint = EditorBeatmap.ControlPointInfo.TimingPoints.Single(); + return timingPoint.Time == 0 && timingPoint.BeatLength == 1000; + }); + + AddAssert("created difficulty has effect points", () => + { + return EditorBeatmap.ControlPointInfo.EffectPoints.SequenceEqual(new[] + { + new EffectControlPoint { Time = 250, KiaiMode = false, ScrollSpeed = 0.05 }, + new EffectControlPoint { Time = 500, KiaiMode = true, ScrollSpeed = 0.1 }, + new EffectControlPoint { Time = 750, KiaiMode = true, ScrollSpeed = 0.15 }, + new EffectControlPoint { Time = 1000, KiaiMode = false, ScrollSpeed = 0.2 }, + new EffectControlPoint { Time = 1500, KiaiMode = false, ScrollSpeed = 0.3 }, + }); + }); + } + + [Test] + public void TestCreateNewDifficultyWithScrollSpeed_DifferentRuleset() + { + string firstDifficultyName = Guid.NewGuid().ToString(); + + AddStep("save beatmap", () => Editor.Save()); + AddStep("create new difficulty", () => Editor.CreateNewDifficulty(new ManiaRuleset().RulesetInfo)); + + AddStep("set unique difficulty name", () => EditorBeatmap.BeatmapInfo.DifficultyName = firstDifficultyName); + AddStep("add timing point", () => EditorBeatmap.ControlPointInfo.Add(0, new TimingControlPoint { BeatLength = 1000 })); + AddStep("add effect points", () => + { + EditorBeatmap.ControlPointInfo.Add(250, new EffectControlPoint { KiaiMode = false, ScrollSpeed = 0.05 }); + EditorBeatmap.ControlPointInfo.Add(500, new EffectControlPoint { KiaiMode = true, ScrollSpeed = 0.1 }); + EditorBeatmap.ControlPointInfo.Add(750, new EffectControlPoint { KiaiMode = true, ScrollSpeed = 0.15 }); + EditorBeatmap.ControlPointInfo.Add(1000, new EffectControlPoint { KiaiMode = false, ScrollSpeed = 0.2 }); + EditorBeatmap.ControlPointInfo.Add(1500, new EffectControlPoint { KiaiMode = false, ScrollSpeed = 0.3 }); + }); + + AddStep("save beatmap", () => Editor.Save()); + + AddStep("create new difficulty", () => Editor.CreateNewDifficulty(new TaikoRuleset().RulesetInfo)); + + AddUntilStep("wait for created", () => + { + string? difficultyName = Editor.ChildrenOfType().SingleOrDefault()?.BeatmapInfo.DifficultyName; + return difficultyName != null && difficultyName != firstDifficultyName; + }); + + AddAssert("created difficulty has timing point", () => + { + var timingPoint = EditorBeatmap.ControlPointInfo.TimingPoints.Single(); + return timingPoint.Time == 0 && timingPoint.BeatLength == 1000; + }); + + AddAssert("created difficulty has effect points", () => + { + // since this difficulty is on another ruleset, scroll speed specifications are completely reset, + // therefore discarding some effect points in the process due to being redundant. + return EditorBeatmap.ControlPointInfo.EffectPoints.SequenceEqual(new[] + { + new EffectControlPoint { Time = 500, KiaiMode = true }, + new EffectControlPoint { Time = 1000, KiaiMode = false }, + }); + }); + } + [Test] public void TestCopyDifficulty() { From e3abbf1177418ced2251ebbd7720a27bfd1691dc Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Wed, 4 Dec 2024 02:12:21 -0500 Subject: [PATCH 0211/1275] Copy effect points across on blank difficulty creation --- osu.Game/Beatmaps/BeatmapManager.cs | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/osu.Game/Beatmaps/BeatmapManager.cs b/osu.Game/Beatmaps/BeatmapManager.cs index 07fcdb9d62..da556316cd 100644 --- a/osu.Game/Beatmaps/BeatmapManager.cs +++ b/osu.Game/Beatmaps/BeatmapManager.cs @@ -154,9 +154,18 @@ namespace osu.Game.Beatmaps DifficultyName = NamingUtils.GetNextBestName(targetBeatmapSet.Beatmaps.Select(b => b.DifficultyName), "New Difficulty") }; var newBeatmap = new Beatmap { BeatmapInfo = newBeatmapInfo }; + foreach (var timingPoint in referenceWorkingBeatmap.Beatmap.ControlPointInfo.TimingPoints) newBeatmap.ControlPointInfo.Add(timingPoint.Time, timingPoint.DeepClone()); + foreach (var effectPoint in referenceWorkingBeatmap.Beatmap.ControlPointInfo.EffectPoints) + { + if (!rulesetInfo.Equals(referenceWorkingBeatmap.BeatmapInfo.Ruleset)) + effectPoint.ScrollSpeedBindable.SetDefault(); + + newBeatmap.ControlPointInfo.Add(effectPoint.Time, effectPoint.DeepClone()); + } + return addDifficultyToSet(targetBeatmapSet, newBeatmap, referenceWorkingBeatmap.Skin); } From 06824c1658c714849276f13e614edc084db05536 Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Wed, 4 Dec 2024 04:20:09 -0500 Subject: [PATCH 0212/1275] Add failing test case --- .../Editing/TestSceneEditorBeatmapCreation.cs | 51 +++++++++++++++++++ 1 file changed, 51 insertions(+) diff --git a/osu.Game.Tests/Visual/Editing/TestSceneEditorBeatmapCreation.cs b/osu.Game.Tests/Visual/Editing/TestSceneEditorBeatmapCreation.cs index 7a390ac131..75759edaea 100644 --- a/osu.Game.Tests/Visual/Editing/TestSceneEditorBeatmapCreation.cs +++ b/osu.Game.Tests/Visual/Editing/TestSceneEditorBeatmapCreation.cs @@ -4,15 +4,20 @@ using System; using System.IO; using System.Linq; +using System.Text; using NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Audio.Track; +using osu.Framework.Extensions; using osu.Framework.Extensions.ObjectExtensions; using osu.Framework.Graphics; +using osu.Framework.IO.Stores; +using osu.Framework.Platform; using osu.Framework.Screens; using osu.Framework.Testing; using osu.Game.Beatmaps; using osu.Game.Beatmaps.ControlPoints; +using osu.Game.Beatmaps.Formats; using osu.Game.Collections; using osu.Game.Database; using osu.Game.Overlays.Dialog; @@ -27,6 +32,7 @@ using osu.Game.Rulesets.Taiko.Objects; using osu.Game.Screens.Edit; using osu.Game.Screens.Edit.Compose.Components.Timeline; using osu.Game.Screens.Edit.Setup; +using osu.Game.Skinning; using osu.Game.Storyboards; using osu.Game.Tests.Resources; using osuTK; @@ -527,6 +533,32 @@ namespace osu.Game.Tests.Visual.Editing AddAssert("other files removed", () => !Beatmap.Value.BeatmapSetInfo.Files.Any(f => f.Filename == "bg (1).jpg" || f.Filename == "bg (2).jpg")); } + [Test] + public void TestBackgroundFileChangesPreserveOnEncode() + { + AddStep("enter setup mode", () => Editor.Mode.Value = EditorScreenMode.SongSetup); + AddAssert("set background", () => setBackground(applyToAllDifficulties: true, expected: "bg.jpg")); + + createNewDifficulty(); + createNewDifficulty(); + + switchToDifficulty(0); + + AddAssert("set different background on all diff", () => setBackgroundDifferentExtension(applyToAllDifficulties: true, expected: "bg.jpeg")); + AddAssert("all diff uses one background", () => Beatmap.Value.BeatmapSetInfo.Beatmaps.All(b => b.Metadata.BackgroundFile == "bg.jpeg")); + AddAssert("all diff encode same background", () => + { + return Beatmap.Value.BeatmapSetInfo.Beatmaps.All(b => + { + var files = new RealmFileStore(Realm, Dependencies.Get().Storage); + using var store = new RealmBackedResourceStore(b.BeatmapSet!.ToLive(Realm), files.Store, Realm); + string[] osu = Encoding.UTF8.GetString(store.Get(b.File!.Filename)).Split(Environment.NewLine); + Assert.That(osu, Does.Contain("0,0,\"bg.jpeg\",0,0")); + return true; + }); + }); + } + [Test] public void TestSingleAudioFile() { @@ -644,6 +676,25 @@ namespace osu.Game.Tests.Visual.Editing }); } + private bool setBackgroundDifferentExtension(bool applyToAllDifficulties, string expected) + { + var setup = Editor.ChildrenOfType().First(); + + return setFile(TestResources.GetQuickTestBeatmapForImport(), extractedFolder => + { + File.Move( + Path.Combine(extractedFolder, @"machinetop_background.jpg"), + Path.Combine(extractedFolder, @"machinetop_background.jpeg")); + + bool success = setup.ChildrenOfType().First().ChangeBackgroundImage( + new FileInfo(Path.Combine(extractedFolder, @"machinetop_background.jpeg")), + applyToAllDifficulties); + + Assert.That(Beatmap.Value.Metadata.BackgroundFile, Is.EqualTo(expected)); + return success; + }); + } + private bool setAudio(bool applyToAllDifficulties, string expected) { var setup = Editor.ChildrenOfType().First(); From 8e0f6fc12dc04a224a9aefb0121f990a8b007af2 Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Wed, 4 Dec 2024 04:36:00 -0500 Subject: [PATCH 0213/1275] Re-encode difficulties on resource change --- .../Visual/Editing/TestSceneEditorBeatmapCreation.cs | 3 --- osu.Game/Screens/Edit/Setup/ResourcesSection.cs | 10 ++++++++-- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/osu.Game.Tests/Visual/Editing/TestSceneEditorBeatmapCreation.cs b/osu.Game.Tests/Visual/Editing/TestSceneEditorBeatmapCreation.cs index 75759edaea..157deef80a 100644 --- a/osu.Game.Tests/Visual/Editing/TestSceneEditorBeatmapCreation.cs +++ b/osu.Game.Tests/Visual/Editing/TestSceneEditorBeatmapCreation.cs @@ -8,16 +8,13 @@ using System.Text; using NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Audio.Track; -using osu.Framework.Extensions; using osu.Framework.Extensions.ObjectExtensions; using osu.Framework.Graphics; -using osu.Framework.IO.Stores; using osu.Framework.Platform; using osu.Framework.Screens; using osu.Framework.Testing; using osu.Game.Beatmaps; using osu.Game.Beatmaps.ControlPoints; -using osu.Game.Beatmaps.Formats; using osu.Game.Collections; using osu.Game.Database; using osu.Game.Overlays.Dialog; diff --git a/osu.Game/Screens/Edit/Setup/ResourcesSection.cs b/osu.Game/Screens/Edit/Setup/ResourcesSection.cs index 098877ebe7..84107a57e9 100644 --- a/osu.Game/Screens/Edit/Setup/ResourcesSection.cs +++ b/osu.Game/Screens/Edit/Setup/ResourcesSection.cs @@ -103,6 +103,7 @@ namespace osu.Game.Screens.Edit.Setup private void changeResource(FileInfo source, bool applyToAllDifficulties, string baseFilename, Func readFilename, Action writeFilename) { + var thisBeatmap = working.Value.BeatmapInfo; var set = working.Value.BeatmapSetInfo; string newFilename = string.Empty; @@ -117,12 +118,17 @@ namespace osu.Game.Screens.Edit.Setup beatmaps.DeleteFile(set, otherExistingFile); writeFilename(beatmap.Metadata, newFilename); + + if (!beatmap.Equals(thisBeatmap)) + { + // save the difficulty to re-encode the .osu file, updating any reference of the old filename. + var beatmapWorking = beatmaps.GetWorkingBeatmap(beatmap); + beatmaps.Save(beatmap, beatmapWorking.Beatmap, beatmapWorking.GetSkin()); + } } } else { - var thisBeatmap = working.Value.BeatmapInfo; - string[] filenames = set.Files.Select(f => f.Filename).Where(f => f.StartsWith(baseFilename, StringComparison.OrdinalIgnoreCase) && f.EndsWith(source.Extension, StringComparison.OrdinalIgnoreCase)).ToArray(); From fa87df6c6af7879f1bc2e60b276724dddd3c2136 Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Wed, 4 Dec 2024 04:55:40 -0500 Subject: [PATCH 0214/1275] Move non-current handling to `PerformExit` Co-authored-by: Dean Herbert --- osu.Game/Screens/Play/Player.cs | 17 ++++++----------- 1 file changed, 6 insertions(+), 11 deletions(-) diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs index 3a0a0613f3..1866ed26ce 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -646,7 +646,6 @@ namespace osu.Game.Screens.Play // import current score if possible. prepareAndImportScoreAsync(); - // Screen may not be current if a restart has been performed. if (this.IsCurrentScreen()) { skipExitTransition = skipTransition; @@ -657,6 +656,12 @@ namespace osu.Game.Screens.Play // - the pause / fail dialog was requested but couldn't be displayed due to the type or state of this Player instance. this.Exit(); } + else + { + // May be restarting from results screen. + if (this.GetChildScreen() != null) + this.MakeCurrent(); + } return true; } @@ -722,16 +727,6 @@ namespace osu.Game.Screens.Play skipExitTransition = quickRestart; PrepareLoaderForRestart?.Invoke(quickRestart); - if (!this.IsCurrentScreen()) - { - // if we're called externally (i.e. from results screen), - // use MakeCurrent to exit results screen as well as this player screen - // since ValidForResume = false in here - Debug.Assert(!ValidForResume); - this.MakeCurrent(); - return true; - } - return PerformExit(quickRestart); } From f83ec721fb31f9f3ac9840c246d6bf3f176fdd96 Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Thu, 5 Dec 2024 02:41:00 -0500 Subject: [PATCH 0215/1275] Move latency certifier and import files button outside debug section --- .../Sections/DebugSettings/GeneralSettings.cs | 16 +-------- .../Sections/Maintenance/GeneralSettings.cs | 36 +++++++++++++++++++ .../Settings/Sections/MaintenanceSection.cs | 1 + 3 files changed, 38 insertions(+), 15 deletions(-) create mode 100644 osu.Game/Overlays/Settings/Sections/Maintenance/GeneralSettings.cs diff --git a/osu.Game/Overlays/Settings/Sections/DebugSettings/GeneralSettings.cs b/osu.Game/Overlays/Settings/Sections/DebugSettings/GeneralSettings.cs index df46e38491..57f36e2875 100644 --- a/osu.Game/Overlays/Settings/Sections/DebugSettings/GeneralSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/DebugSettings/GeneralSettings.cs @@ -5,11 +5,7 @@ using osu.Framework.Allocation; using osu.Framework.Configuration; using osu.Framework.Graphics; using osu.Framework.Localisation; -using osu.Framework.Screens; using osu.Game.Localisation; -using osu.Game.Screens; -using osu.Game.Screens.Import; -using osu.Game.Screens.Utility; namespace osu.Game.Overlays.Settings.Sections.DebugSettings { @@ -18,7 +14,7 @@ namespace osu.Game.Overlays.Settings.Sections.DebugSettings protected override LocalisableString Header => CommonStrings.General; [BackgroundDependencyLoader] - private void load(FrameworkDebugConfigManager config, FrameworkConfigManager frameworkConfig, IPerformFromScreenRunner? performer) + private void load(FrameworkDebugConfigManager config, FrameworkConfigManager frameworkConfig) { Children = new Drawable[] { @@ -32,16 +28,6 @@ namespace osu.Game.Overlays.Settings.Sections.DebugSettings LabelText = DebugSettingsStrings.BypassFrontToBackPass, Current = config.GetBindable(DebugSetting.BypassFrontToBackPass) }, - new SettingsButton - { - Text = DebugSettingsStrings.ImportFiles, - Action = () => performer?.PerformFromScreen(menu => menu.Push(new FileImportScreen())) - }, - new SettingsButton - { - Text = DebugSettingsStrings.RunLatencyCertifier, - Action = () => performer?.PerformFromScreen(menu => menu.Push(new LatencyCertifierScreen())) - } }; } } diff --git a/osu.Game/Overlays/Settings/Sections/Maintenance/GeneralSettings.cs b/osu.Game/Overlays/Settings/Sections/Maintenance/GeneralSettings.cs new file mode 100644 index 0000000000..f75fc2c8bc --- /dev/null +++ b/osu.Game/Overlays/Settings/Sections/Maintenance/GeneralSettings.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 osu.Framework.Allocation; +using osu.Framework.Localisation; +using osu.Framework.Screens; +using osu.Game.Localisation; +using osu.Game.Screens; +using osu.Game.Screens.Import; +using osu.Game.Screens.Utility; + +namespace osu.Game.Overlays.Settings.Sections.Maintenance +{ + public partial class GeneralSettings : SettingsSubsection + { + protected override LocalisableString Header => CommonStrings.General; + + [BackgroundDependencyLoader] + private void load(IPerformFromScreenRunner? performer) + { + Children = new[] + { + new SettingsButton + { + Text = DebugSettingsStrings.ImportFiles, + Action = () => performer?.PerformFromScreen(menu => menu.Push(new FileImportScreen())) + }, + new SettingsButton + { + Text = DebugSettingsStrings.RunLatencyCertifier, + Action = () => performer?.PerformFromScreen(menu => menu.Push(new LatencyCertifierScreen())) + } + }; + } + } +} diff --git a/osu.Game/Overlays/Settings/Sections/MaintenanceSection.cs b/osu.Game/Overlays/Settings/Sections/MaintenanceSection.cs index bd90e4c35d..f1b1511df8 100644 --- a/osu.Game/Overlays/Settings/Sections/MaintenanceSection.cs +++ b/osu.Game/Overlays/Settings/Sections/MaintenanceSection.cs @@ -23,6 +23,7 @@ namespace osu.Game.Overlays.Settings.Sections { Children = new Drawable[] { + new GeneralSettings(), new BeatmapSettings(), new SkinSettings(), new CollectionsSettings(), From 7ab16a55e561f37a4ffe07b2076f6917a46ea414 Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Thu, 5 Dec 2024 02:41:17 -0500 Subject: [PATCH 0216/1275] Make debug section only visible on debug builds --- .../Overlays/FirstRunSetup/ScreenBehaviour.cs | 5 ++- osu.Game/Overlays/SettingsOverlay.cs | 36 +++++++++++-------- 2 files changed, 26 insertions(+), 15 deletions(-) diff --git a/osu.Game/Overlays/FirstRunSetup/ScreenBehaviour.cs b/osu.Game/Overlays/FirstRunSetup/ScreenBehaviour.cs index 31a56c9748..d31ce7ea18 100644 --- a/osu.Game/Overlays/FirstRunSetup/ScreenBehaviour.cs +++ b/osu.Game/Overlays/FirstRunSetup/ScreenBehaviour.cs @@ -5,6 +5,7 @@ using System.Linq; using osu.Framework.Allocation; +using osu.Framework.Development; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Localisation; @@ -90,11 +91,13 @@ namespace osu.Game.Overlays.FirstRunSetup new GraphicsSection(), new OnlineSection(), new MaintenanceSection(), - new DebugSection(), }, SearchTerm = SettingsItem.CLASSIC_DEFAULT_SEARCH_TERM, } }; + + if (DebugUtils.IsDebugBuild) + searchContainer.Add(new DebugSection()); } private void applyClassic() diff --git a/osu.Game/Overlays/SettingsOverlay.cs b/osu.Game/Overlays/SettingsOverlay.cs index 9076dadf93..1157860e03 100644 --- a/osu.Game/Overlays/SettingsOverlay.cs +++ b/osu.Game/Overlays/SettingsOverlay.cs @@ -7,6 +7,7 @@ using System.Collections.Generic; using System.Linq; using osu.Framework.Allocation; using osu.Framework.Bindables; +using osu.Framework.Development; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Sprites; @@ -27,21 +28,28 @@ namespace osu.Game.Overlays public LocalisableString Title => SettingsStrings.HeaderTitle; public LocalisableString Description => SettingsStrings.HeaderDescription; - protected override IEnumerable CreateSections() => new SettingsSection[] + protected override IEnumerable CreateSections() { - // This list should be kept in sync with ScreenBehaviour. - new GeneralSection(), - new SkinSection(), - new InputSection(createSubPanel(new KeyBindingPanel())), - new UserInterfaceSection(), - new GameplaySection(), - new RulesetSection(), - new AudioSection(), - new GraphicsSection(), - new OnlineSection(), - new MaintenanceSection(), - new DebugSection(), - }; + var sections = new List + { + // This list should be kept in sync with ScreenBehaviour. + new GeneralSection(), + new SkinSection(), + new InputSection(createSubPanel(new KeyBindingPanel())), + new UserInterfaceSection(), + new GameplaySection(), + new RulesetSection(), + new AudioSection(), + new GraphicsSection(), + new OnlineSection(), + new MaintenanceSection(), + }; + + if (DebugUtils.IsDebugBuild) + sections.Add(new DebugSection()); + + return sections; + } private readonly List subPanels = new List(); From 7c1be5eca25cdf1749329b8a9c0972711673dbe5 Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Thu, 5 Dec 2024 02:45:43 -0500 Subject: [PATCH 0217/1275] Remove unnecessary localisations --- osu.Game/Localisation/DebugSettingsStrings.cs | 25 ------------------- .../Settings/Sections/DebugSection.cs | 3 +-- .../Sections/DebugSettings/GeneralSettings.cs | 7 +++--- .../Sections/DebugSettings/MemorySettings.cs | 5 ++-- 4 files changed, 6 insertions(+), 34 deletions(-) diff --git a/osu.Game/Localisation/DebugSettingsStrings.cs b/osu.Game/Localisation/DebugSettingsStrings.cs index 066c07858c..bdb0348981 100644 --- a/osu.Game/Localisation/DebugSettingsStrings.cs +++ b/osu.Game/Localisation/DebugSettingsStrings.cs @@ -9,21 +9,6 @@ namespace osu.Game.Localisation { private const string prefix = @"osu.Game.Resources.Localisation.DebugSettings"; - /// - /// "Debug" - /// - public static LocalisableString DebugSectionHeader => new TranslatableString(getKey(@"debug_section_header"), @"Debug"); - - /// - /// "Show log overlay" - /// - public static LocalisableString ShowLogOverlay => new TranslatableString(getKey(@"show_log_overlay"), @"Show log overlay"); - - /// - /// "Bypass front-to-back render pass" - /// - public static LocalisableString BypassFrontToBackPass => new TranslatableString(getKey(@"bypass_front_to_back_pass"), @"Bypass front-to-back render pass"); - /// /// "Import files" /// @@ -34,16 +19,6 @@ namespace osu.Game.Localisation /// public static LocalisableString RunLatencyCertifier => new TranslatableString(getKey(@"run_latency_certifier"), @"Run latency certifier"); - /// - /// "Memory" - /// - public static LocalisableString MemoryHeader => new TranslatableString(getKey(@"memory_header"), @"Memory"); - - /// - /// "Clear all caches" - /// - public static LocalisableString ClearAllCaches => new TranslatableString(getKey(@"clear_all_caches"), @"Clear all caches"); - private static string getKey(string key) => $"{prefix}:{key}"; } } diff --git a/osu.Game/Overlays/Settings/Sections/DebugSection.cs b/osu.Game/Overlays/Settings/Sections/DebugSection.cs index b84c441057..15951d462b 100644 --- a/osu.Game/Overlays/Settings/Sections/DebugSection.cs +++ b/osu.Game/Overlays/Settings/Sections/DebugSection.cs @@ -6,14 +6,13 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Sprites; using osu.Framework.Localisation; using osu.Game.Graphics; -using osu.Game.Localisation; using osu.Game.Overlays.Settings.Sections.DebugSettings; namespace osu.Game.Overlays.Settings.Sections { public partial class DebugSection : SettingsSection { - public override LocalisableString Header => DebugSettingsStrings.DebugSectionHeader; + public override LocalisableString Header => "Debug"; public override Drawable CreateIcon() => new SpriteIcon { diff --git a/osu.Game/Overlays/Settings/Sections/DebugSettings/GeneralSettings.cs b/osu.Game/Overlays/Settings/Sections/DebugSettings/GeneralSettings.cs index 57f36e2875..280aa685a9 100644 --- a/osu.Game/Overlays/Settings/Sections/DebugSettings/GeneralSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/DebugSettings/GeneralSettings.cs @@ -5,13 +5,12 @@ using osu.Framework.Allocation; using osu.Framework.Configuration; using osu.Framework.Graphics; using osu.Framework.Localisation; -using osu.Game.Localisation; namespace osu.Game.Overlays.Settings.Sections.DebugSettings { public partial class GeneralSettings : SettingsSubsection { - protected override LocalisableString Header => CommonStrings.General; + protected override LocalisableString Header => "General"; [BackgroundDependencyLoader] private void load(FrameworkDebugConfigManager config, FrameworkConfigManager frameworkConfig) @@ -20,12 +19,12 @@ namespace osu.Game.Overlays.Settings.Sections.DebugSettings { new SettingsCheckbox { - LabelText = DebugSettingsStrings.ShowLogOverlay, + LabelText = "Show log overlay", Current = frameworkConfig.GetBindable(FrameworkSetting.ShowLogOverlay) }, new SettingsCheckbox { - LabelText = DebugSettingsStrings.BypassFrontToBackPass, + LabelText = @"Bypass front-to-back render pass", Current = config.GetBindable(DebugSetting.BypassFrontToBackPass) }, }; diff --git a/osu.Game/Overlays/Settings/Sections/DebugSettings/MemorySettings.cs b/osu.Game/Overlays/Settings/Sections/DebugSettings/MemorySettings.cs index d5de7ae2db..d43abd58a8 100644 --- a/osu.Game/Overlays/Settings/Sections/DebugSettings/MemorySettings.cs +++ b/osu.Game/Overlays/Settings/Sections/DebugSettings/MemorySettings.cs @@ -11,13 +11,12 @@ using osu.Framework.Localisation; using osu.Framework.Logging; using osu.Framework.Platform; using osu.Game.Database; -using osu.Game.Localisation; namespace osu.Game.Overlays.Settings.Sections.DebugSettings { public partial class MemorySettings : SettingsSubsection { - protected override LocalisableString Header => DebugSettingsStrings.MemoryHeader; + protected override LocalisableString Header => "Memory"; [BackgroundDependencyLoader] private void load(GameHost host, RealmAccess realm) @@ -29,7 +28,7 @@ namespace osu.Game.Overlays.Settings.Sections.DebugSettings { new SettingsButton { - Text = DebugSettingsStrings.ClearAllCaches, + Text = "Clear all caches", Action = host.Collect }, new SettingsButton From 1b1e7b63e96717903f71e73ec775ae77bbee407f Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Thu, 5 Dec 2024 02:48:46 -0500 Subject: [PATCH 0218/1275] Clean up code slightly --- .../Overlays/Settings/Sections/DebugSection.cs | 15 +++++++-------- .../Sections/DebugSettings/GeneralSettings.cs | 4 ++-- .../Sections/DebugSettings/MemorySettings.cs | 16 ++++++++-------- 3 files changed, 17 insertions(+), 18 deletions(-) diff --git a/osu.Game/Overlays/Settings/Sections/DebugSection.cs b/osu.Game/Overlays/Settings/Sections/DebugSection.cs index 15951d462b..1d2129413c 100644 --- a/osu.Game/Overlays/Settings/Sections/DebugSection.cs +++ b/osu.Game/Overlays/Settings/Sections/DebugSection.cs @@ -1,7 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using osu.Framework.Development; using osu.Framework.Graphics; using osu.Framework.Graphics.Sprites; using osu.Framework.Localisation; @@ -12,7 +11,7 @@ namespace osu.Game.Overlays.Settings.Sections { public partial class DebugSection : SettingsSection { - public override LocalisableString Header => "Debug"; + public override LocalisableString Header => @"Debug"; public override Drawable CreateIcon() => new SpriteIcon { @@ -21,12 +20,12 @@ namespace osu.Game.Overlays.Settings.Sections public DebugSection() { - Add(new GeneralSettings()); - - if (DebugUtils.IsDebugBuild) - Add(new BatchImportSettings()); - - Add(new MemorySettings()); + Children = new Drawable[] + { + new GeneralSettings(), + new BatchImportSettings(), + new MemorySettings(), + }; } } } diff --git a/osu.Game/Overlays/Settings/Sections/DebugSettings/GeneralSettings.cs b/osu.Game/Overlays/Settings/Sections/DebugSettings/GeneralSettings.cs index 280aa685a9..bd6ada4ca7 100644 --- a/osu.Game/Overlays/Settings/Sections/DebugSettings/GeneralSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/DebugSettings/GeneralSettings.cs @@ -10,7 +10,7 @@ namespace osu.Game.Overlays.Settings.Sections.DebugSettings { public partial class GeneralSettings : SettingsSubsection { - protected override LocalisableString Header => "General"; + protected override LocalisableString Header => @"General"; [BackgroundDependencyLoader] private void load(FrameworkDebugConfigManager config, FrameworkConfigManager frameworkConfig) @@ -19,7 +19,7 @@ namespace osu.Game.Overlays.Settings.Sections.DebugSettings { new SettingsCheckbox { - LabelText = "Show log overlay", + LabelText = @"Show log overlay", Current = frameworkConfig.GetBindable(FrameworkSetting.ShowLogOverlay) }, new SettingsCheckbox diff --git a/osu.Game/Overlays/Settings/Sections/DebugSettings/MemorySettings.cs b/osu.Game/Overlays/Settings/Sections/DebugSettings/MemorySettings.cs index d43abd58a8..b693822838 100644 --- a/osu.Game/Overlays/Settings/Sections/DebugSettings/MemorySettings.cs +++ b/osu.Game/Overlays/Settings/Sections/DebugSettings/MemorySettings.cs @@ -16,7 +16,7 @@ namespace osu.Game.Overlays.Settings.Sections.DebugSettings { public partial class MemorySettings : SettingsSubsection { - protected override LocalisableString Header => "Memory"; + protected override LocalisableString Header => @"Memory"; [BackgroundDependencyLoader] private void load(GameHost host, RealmAccess realm) @@ -28,27 +28,27 @@ namespace osu.Game.Overlays.Settings.Sections.DebugSettings { new SettingsButton { - Text = "Clear all caches", + Text = @"Clear all caches", Action = host.Collect }, new SettingsButton { - Text = "Compact realm", + Text = @"Compact realm", Action = () => { // Blocking operations implicitly causes a Compact(). - using (realm.BlockAllOperations("compact")) + using (realm.BlockAllOperations(@"compact")) { } } }, blockAction = new SettingsButton { - Text = "Block realm", + Text = @"Block realm", }, unblockAction = new SettingsButton { - Text = "Unblock realm", + Text = @"Unblock realm", }, }; @@ -56,7 +56,7 @@ namespace osu.Game.Overlays.Settings.Sections.DebugSettings { try { - IDisposable? token = realm.BlockAllOperations("maintenance"); + IDisposable? token = realm.BlockAllOperations(@"maintenance"); blockAction.Enabled.Value = false; @@ -88,7 +88,7 @@ namespace osu.Game.Overlays.Settings.Sections.DebugSettings } catch (Exception e) { - Logger.Error(e, "Blocking realm failed"); + Logger.Error(e, @"Blocking realm failed"); } }; } From 68e400dd0c8d4839bab9a2265f9c699fcc8d1b27 Mon Sep 17 00:00:00 2001 From: Huo Yaoyuan Date: Thu, 5 Dec 2024 18:00:42 +0800 Subject: [PATCH 0219/1275] Put globalconfig into seperated folder and reference explicitly --- .globalconfig => CodeAnalysis/osu.globalconfig | 1 + Directory.Build.props | 2 ++ osu.sln | 7 ++++++- 3 files changed, 9 insertions(+), 1 deletion(-) rename .globalconfig => CodeAnalysis/osu.globalconfig (99%) diff --git a/.globalconfig b/CodeAnalysis/osu.globalconfig similarity index 99% rename from .globalconfig rename to CodeAnalysis/osu.globalconfig index ca7b86c778..247a825033 100644 --- a/.globalconfig +++ b/CodeAnalysis/osu.globalconfig @@ -1,5 +1,6 @@ # .NET Code Style # IDE styles reference: https://docs.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ +is_global = true # IDE0001: Simplify names dotnet_diagnostic.IDE0001.severity = warning diff --git a/Directory.Build.props b/Directory.Build.props index 0ab41d27a0..3acb86ee0c 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -18,6 +18,8 @@ + + Default diff --git a/osu.sln b/osu.sln index 2d9a4e86d0..63da18c23e 100644 --- a/osu.sln +++ b/osu.sln @@ -56,7 +56,6 @@ EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{10DF8F12-50FD-45D8-8A38-17BA764BF54D}" ProjectSection(SolutionItems) = preProject .editorconfig = .editorconfig - .globalconfig = .globalconfig Directory.Build.props = Directory.Build.props osu.Android.props = osu.Android.props osu.iOS.props = osu.iOS.props @@ -95,6 +94,12 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "osu.Game.Rulesets.Pippidon" EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "osu.Game.Rulesets.Pippidon.Tests", "Templates\Rulesets\ruleset-scrolling-example\osu.Game.Rulesets.Pippidon.Tests\osu.Game.Rulesets.Pippidon.Tests.csproj", "{1743BF7C-E6AE-4A06-BAD9-166D62894303}" EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "CodeAnalysis", "CodeAnalysis", "{FB156649-D457-4D1A-969C-D3A23FD31513}" + ProjectSection(SolutionItems) = preProject + CodeAnalysis\BannedSymbols.txt = CodeAnalysis\BannedSymbols.txt + CodeAnalysis\osu.globalconfig = CodeAnalysis\osu.globalconfig + EndProjectSection +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU From 84a85000afab3f37985ef59e3efe03b0bdd69d36 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 5 Dec 2024 21:07:08 +0900 Subject: [PATCH 0220/1275] 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 02898623a9..ebfb136150 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 80e695e5d1..252c7a14c4 100644 --- a/osu.iOS.props +++ b/osu.iOS.props @@ -17,6 +17,6 @@ -all - + From 49f4b0e6eff6423758382a5c7d79aed21e9c7077 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 5 Dec 2024 21:07:13 +0900 Subject: [PATCH 0221/1275] 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 43353370b7..7b4453b015 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -36,7 +36,7 @@ - + From 69014550b5499a1bc60bcd6144a43180b94ff5ae Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 6 Dec 2024 12:48:06 +0900 Subject: [PATCH 0222/1275] Remove unnecessary null checks --- osu.Game/Graphics/UserInterface/OsuContextMenu.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Game/Graphics/UserInterface/OsuContextMenu.cs b/osu.Game/Graphics/UserInterface/OsuContextMenu.cs index 7a5d2c369b..433d37834f 100644 --- a/osu.Game/Graphics/UserInterface/OsuContextMenu.cs +++ b/osu.Game/Graphics/UserInterface/OsuContextMenu.cs @@ -53,8 +53,8 @@ namespace osu.Game.Graphics.UserInterface if (!playClickSample) return; - menuSamples?.PlayClickSample(); - menuSamples?.PlayOpenSample(); + menuSamples.PlayClickSample(); + menuSamples.PlayOpenSample(); } protected override void AnimateClose() @@ -62,7 +62,7 @@ namespace osu.Game.Graphics.UserInterface this.FadeOut(fade_duration, Easing.OutQuint); if (wasOpened) - menuSamples?.PlayCloseSample(); + menuSamples.PlayCloseSample(); wasOpened = false; } From 62ea4e09709eb51906e732e30c449103ff5ac2e1 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Fri, 6 Dec 2024 16:37:00 +0900 Subject: [PATCH 0223/1275] 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 0224/1275] 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 0225/1275] 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 0226/1275] 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 0227/1275] 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 0228/1275] 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 0229/1275] 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 0230/1275] 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 0231/1275] 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 5cb6b86b1cd0814b76fcd11cfcf29a04680d4022 Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Fri, 6 Dec 2024 05:47:41 -0500 Subject: [PATCH 0232/1275] Fix reference effect point getting mutated --- osu.Game/Beatmaps/BeatmapManager.cs | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/osu.Game/Beatmaps/BeatmapManager.cs b/osu.Game/Beatmaps/BeatmapManager.cs index da556316cd..148bd90f28 100644 --- a/osu.Game/Beatmaps/BeatmapManager.cs +++ b/osu.Game/Beatmaps/BeatmapManager.cs @@ -15,6 +15,7 @@ using osu.Framework.Audio.Track; using osu.Framework.Extensions; using osu.Framework.IO.Stores; using osu.Framework.Platform; +using osu.Game.Beatmaps.ControlPoints; using osu.Game.Beatmaps.Formats; using osu.Game.Database; using osu.Game.Extensions; @@ -160,10 +161,12 @@ namespace osu.Game.Beatmaps foreach (var effectPoint in referenceWorkingBeatmap.Beatmap.ControlPointInfo.EffectPoints) { - if (!rulesetInfo.Equals(referenceWorkingBeatmap.BeatmapInfo.Ruleset)) - effectPoint.ScrollSpeedBindable.SetDefault(); + var clonedEffectPoint = (EffectControlPoint)effectPoint.DeepClone(); - newBeatmap.ControlPointInfo.Add(effectPoint.Time, effectPoint.DeepClone()); + if (!rulesetInfo.Equals(referenceWorkingBeatmap.BeatmapInfo.Ruleset)) + clonedEffectPoint.ScrollSpeedBindable.SetDefault(); + + newBeatmap.ControlPointInfo.Add(clonedEffectPoint.Time, clonedEffectPoint); } return addDifficultyToSet(targetBeatmapSet, newBeatmap, referenceWorkingBeatmap.Skin); From b9f1fef2501ea0cce933b7522d658d3ae9f3d577 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sat, 7 Dec 2024 10:46:03 +0900 Subject: [PATCH 0233/1275] 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 ebfb136150..632325725a 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 252c7a14c4..62a65f291d 100644 --- a/osu.iOS.props +++ b/osu.iOS.props @@ -17,6 +17,6 @@ -all - + 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 0234/1275] 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 0235/1275] 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 zcmZ{kc{mh!7{`AyQ<`BMazAwi@q1RIu+^o!VvT?%eg46Q$;zymHfrhs z`AXzHjG?irTYRJy4WD{>jT8UF9v2PDr2PQ^aR30GfA^qyQ+-rC{CvDiXNY+Gs#!PA zx{8SJJf&hyiY8!{usuDo1Y$2*4XxHs>V3ts>@e>#$8o4Jevy|<5T4B&Y)jfXmQdc? zxY)R}iixgsbGT;e>|jcDHl>C?aC5NJ8MN09%8?9h8A%TbNe`I|jX#Rg#YjTa3IuGF z!H?q=c+Gc&Z~`DA1%NOB0Ov&WHnBD|@S$Jz@pkq0_xnEQQaZtB6wTNEW?K>e2}UqE z>Un%4H{aEKK)t4R!NB^Wf~ElP71^=eijp_iYjc%3f&{F%=tCcMSD*N?M=nfkCOwU` zV{Jn1DMneAkEvwGY+wJ_gOES^Wvt$1^(SWtoYgfXJ7zA75;f=|Yln-4@7x{Vz4`L( zgLa7#!ujEm7y<*HR6`o?TPFYQNr@e{5ml!k$XgSJ6DSVRu2A zPa-066DrQ3U8E^vu}fRmLE5TF1cxdV_9XYZd)_OZx8EKKY%*R7e=%sO(AQ6z(wW5= zLA*HC=`-1t;kS|L4?n%FaBnuKx;mQv2no zXfE3TP|&6>w4g)Lf~v?Sa#W2MY2}!0H$moU9^A&Z0*qWxkux$Ets zqj7lV`|-NJp1>{cGAE26JU1}56P|&k4e(3 z__joK9ihrZYE)kczro-=A}uRsknJRt>~N4Aikc5$RminsK3~1URoE2J8pw_3?K;gq z@)ZFSO=0n!IuXXAJwj;-_1lb?E(Mj(J2Z7fa~1J*aJS3C^gnx+ZPuE2B~GGomCc;y z!o~DksL0VY+)yw-QCNqUXV?lD% zBZu6`q^K|X-S<5M4|`G5r^HL@(H_khmw(9q{KiDZ`PyQ4V?&h^C2KB5NM{5e1v== X-G3&$8T8<{I0Qhz3IHGw`Yrn(Jf)~4 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 0236/1275] 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 0237/1275] 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 b04f5ab6e4dbd0a486015dbc513f35d8d84d48c5 Mon Sep 17 00:00:00 2001 From: Endrik Tombak Date: Sat, 7 Dec 2024 14:13:14 +0200 Subject: [PATCH 0238/1275] Fix IPC not working in release --- 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 279530a579..d8145c8246 100644 --- a/osu.Game/OsuGame.cs +++ b/osu.Game/OsuGame.cs @@ -89,7 +89,7 @@ namespace osu.Game // Different port allows running release and debug builds alongside each other. public const string IPC_PIPE_NAME = "osu-lazer-debug"; #else - public const string IPC_PORT = "osu-lazer"; + public const string IPC_PIPE_NAME = "osu-lazer"; #endif /// From a99a992ceba30b3ff0208de4873eacd41719b65e Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Mon, 9 Dec 2024 13:48:05 +0900 Subject: [PATCH 0239/1275] Adjust test to load song select during setup --- .../Multiplayer/TestSceneMultiplayerMatchSongSelect.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSongSelect.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSongSelect.cs index 2a5f16d091..a266b1d95e 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSongSelect.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSongSelect.cs @@ -60,14 +60,15 @@ namespace osu.Game.Tests.Visual.Multiplayer private void setUp() { - AddStep("reset", () => + AddStep("create song select", () => { Ruleset.Value = new OsuRuleset().RulesetInfo; Beatmap.SetDefault(); SelectedMods.SetDefault(); + + LoadScreen(songSelect = new TestMultiplayerMatchSongSelect(SelectedRoom.Value!)); }); - AddStep("create song select", () => LoadScreen(songSelect = new TestMultiplayerMatchSongSelect(SelectedRoom.Value!))); AddUntilStep("wait for present", () => songSelect.IsCurrentScreen() && songSelect.BeatmapSetsLoaded); } From 2bae93d7add0d6d24758040b46d3542200a40480 Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Mon, 9 Dec 2024 01:59:16 -0500 Subject: [PATCH 0240/1275] Add special handling for file import button on iOS --- .../Sections/Maintenance/GeneralSettings.cs | 20 ++++++-- .../Maintenance/SystemFileImportComponent.cs | 51 +++++++++++++++++++ 2 files changed, 67 insertions(+), 4 deletions(-) create mode 100644 osu.Game/Overlays/Settings/Sections/Maintenance/SystemFileImportComponent.cs diff --git a/osu.Game/Overlays/Settings/Sections/Maintenance/GeneralSettings.cs b/osu.Game/Overlays/Settings/Sections/Maintenance/GeneralSettings.cs index f75fc2c8bc..ed3e72adbe 100644 --- a/osu.Game/Overlays/Settings/Sections/Maintenance/GeneralSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/Maintenance/GeneralSettings.cs @@ -2,7 +2,9 @@ // See the LICENCE file in the repository root for full licence text. using osu.Framework.Allocation; +using osu.Framework.Graphics; using osu.Framework.Localisation; +using osu.Framework.Platform; using osu.Framework.Screens; using osu.Game.Localisation; using osu.Game.Screens; @@ -15,22 +17,32 @@ namespace osu.Game.Overlays.Settings.Sections.Maintenance { protected override LocalisableString Header => CommonStrings.General; + private SystemFileImportComponent systemFileImport = null!; + [BackgroundDependencyLoader] - private void load(IPerformFromScreenRunner? performer) + private void load(OsuGame game, GameHost host, IPerformFromScreenRunner? performer) { - Children = new[] + Add(systemFileImport = new SystemFileImportComponent(game, host)); + + AddRange(new Drawable[] { new SettingsButton { Text = DebugSettingsStrings.ImportFiles, - Action = () => performer?.PerformFromScreen(menu => menu.Push(new FileImportScreen())) + Action = () => + { + if (systemFileImport.PresentIfAvailable()) + return; + + performer?.PerformFromScreen(menu => menu.Push(new FileImportScreen())); + }, }, new SettingsButton { Text = DebugSettingsStrings.RunLatencyCertifier, Action = () => performer?.PerformFromScreen(menu => menu.Push(new LatencyCertifierScreen())) } - }; + }); } } } diff --git a/osu.Game/Overlays/Settings/Sections/Maintenance/SystemFileImportComponent.cs b/osu.Game/Overlays/Settings/Sections/Maintenance/SystemFileImportComponent.cs new file mode 100644 index 0000000000..9827872702 --- /dev/null +++ b/osu.Game/Overlays/Settings/Sections/Maintenance/SystemFileImportComponent.cs @@ -0,0 +1,51 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Linq; +using System.Threading.Tasks; +using osu.Framework.Graphics; +using osu.Framework.Platform; + +namespace osu.Game.Overlays.Settings.Sections.Maintenance +{ + public partial class SystemFileImportComponent : Component + { + private readonly OsuGame game; + private readonly GameHost host; + + private ISystemFileSelector? selector; + + public SystemFileImportComponent(OsuGame game, GameHost host) + { + this.game = game; + this.host = host; + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + selector = host.CreateSystemFileSelector(game.HandledExtensions.ToArray()); + + if (selector != null) + selector.Selected += f => Schedule(() => startImport(f.FullName)); + } + + public bool PresentIfAvailable() + { + if (selector == null) + return false; + + selector.Present(); + return true; + } + + private void startImport(string path) + { + Task.Factory.StartNew(async () => + { + await game.Import(path).ConfigureAwait(false); + }, TaskCreationOptions.LongRunning); + } + } +} From a46070be30588410675e618bba79c51f852175a3 Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Mon, 9 Dec 2024 07:02:40 -0500 Subject: [PATCH 0241/1275] Add description to osu! file associations in iOS --- osu.iOS/Info.plist | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/osu.iOS/Info.plist b/osu.iOS/Info.plist index 1330e29bc1..ae36d00910 100644 --- a/osu.iOS/Info.plist +++ b/osu.iOS/Info.plist @@ -68,6 +68,8 @@ sh.ppy.osu.items + UTTypeDescription + osu! replay UTTypeIdentifier sh.ppy.osu.osr UTTypeTagSpecification @@ -81,6 +83,8 @@ sh.ppy.osu.items + UTTypeDescription + osu! skin UTTypeIdentifier sh.ppy.osu.osk UTTypeTagSpecification @@ -94,6 +98,8 @@ sh.ppy.osu.items + UTTypeDescription + osu! beatmap UTTypeIdentifier sh.ppy.osu.osz UTTypeTagSpecification @@ -107,6 +113,8 @@ sh.ppy.osu.items + UTTypeDescription + osu! beatmap UTTypeIdentifier sh.ppy.osu.olz UTTypeTagSpecification From e9868c631851a1f8f41e5296db787648ece29597 Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Mon, 9 Dec 2024 07:47:28 -0500 Subject: [PATCH 0242/1275] Enable exporting beatmaps in iOS --- osu.Game/Screens/Edit/Editor.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/osu.Game/Screens/Edit/Editor.cs b/osu.Game/Screens/Edit/Editor.cs index 0e4807dc78..47ccc8f72e 100644 --- a/osu.Game/Screens/Edit/Editor.cs +++ b/osu.Game/Screens/Edit/Editor.cs @@ -1215,12 +1215,15 @@ namespace osu.Game.Screens.Edit saveRelatedMenuItems.Add(save); yield return save; - if (RuntimeInfo.IsDesktop) + if (RuntimeInfo.OS != RuntimeInfo.Platform.Android) { var export = createExportMenu(); saveRelatedMenuItems.AddRange(export.Items); yield return export; + } + if (RuntimeInfo.IsDesktop) + { var externalEdit = new EditorMenuItem("Edit externally", MenuItemType.Standard, editExternally); saveRelatedMenuItems.Add(externalEdit); yield return externalEdit; From 0c0dcb1e1545febd175212fcdff25625052b161d Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Mon, 9 Dec 2024 08:16:37 -0500 Subject: [PATCH 0243/1275] Use temporary storage for exported files on iOS --- osu.Game/Database/LegacyExporter.cs | 10 ++++++++-- osu.Game/IO/OsuStorage.cs | 5 +++++ osu.iOS/OsuGameIOS.cs | 4 ++++ osu.iOS/OsuStorageIOS.cs | 23 +++++++++++++++++++++++ 4 files changed, 40 insertions(+), 2 deletions(-) create mode 100644 osu.iOS/OsuStorageIOS.cs diff --git a/osu.Game/Database/LegacyExporter.cs b/osu.Game/Database/LegacyExporter.cs index f9164e34cd..193887765d 100644 --- a/osu.Game/Database/LegacyExporter.cs +++ b/osu.Game/Database/LegacyExporter.cs @@ -3,12 +3,14 @@ using System; using System.Collections.Generic; +using System.Diagnostics; using System.IO; using System.Linq; using System.Threading; using System.Threading.Tasks; using osu.Framework.Platform; using osu.Game.Extensions; +using osu.Game.IO; using osu.Game.Overlays.Notifications; using osu.Game.Utils; using Realms; @@ -40,13 +42,15 @@ namespace osu.Game.Database protected abstract string FileExtension { get; } protected readonly Storage UserFileStorage; - private readonly Storage exportStorage; + private readonly Storage? exportStorage; public Action? PostNotification { get; set; } protected LegacyExporter(Storage storage) { - exportStorage = storage.GetStorageForDirectory(@"exports"); + if (storage is OsuStorage osuStorage) + exportStorage = osuStorage.GetExportStorage(); + UserFileStorage = storage.GetStorageForDirectory(@"files"); } @@ -68,6 +72,8 @@ namespace osu.Game.Database /// A cancellation token. public async Task ExportAsync(Live model, CancellationToken cancellationToken = default) { + Debug.Assert(exportStorage != null); + string itemFilename = model.PerformRead(s => GetFilename(s).GetValidFilename()); if (itemFilename.Length > MAX_FILENAME_LENGTH - FileExtension.Length) diff --git a/osu.Game/IO/OsuStorage.cs b/osu.Game/IO/OsuStorage.cs index a936fa74da..27e1889c6a 100644 --- a/osu.Game/IO/OsuStorage.cs +++ b/osu.Game/IO/OsuStorage.cs @@ -61,6 +61,11 @@ namespace osu.Game.IO TryChangeToCustomStorage(out Error); } + /// + /// Returns the used for storing exported files. + /// + public virtual Storage GetExportStorage() => GetStorageForDirectory(@"exports"); + /// /// Resets the custom storage path, changing the target storage to the default location. /// diff --git a/osu.iOS/OsuGameIOS.cs b/osu.iOS/OsuGameIOS.cs index 2a4f9b87ac..c0bd77366e 100644 --- a/osu.iOS/OsuGameIOS.cs +++ b/osu.iOS/OsuGameIOS.cs @@ -5,6 +5,8 @@ using System; using Foundation; using Microsoft.Maui.Devices; using osu.Framework.Graphics; +using osu.Framework.iOS; +using osu.Framework.Platform; using osu.Game; using osu.Game.Updater; using osu.Game.Utils; @@ -19,6 +21,8 @@ namespace osu.iOS protected override BatteryInfo CreateBatteryInfo() => new IOSBatteryInfo(); + protected override Storage CreateStorage(GameHost host, Storage defaultStorage) => new OsuStorageIOS((IOSGameHost)host, defaultStorage); + protected override Edges SafeAreaOverrideEdges => // iOS shows a home indicator at the bottom, and adds a safe area to account for this. // Because we have the home indicator (mostly) hidden we don't really care about drawing in this region. diff --git a/osu.iOS/OsuStorageIOS.cs b/osu.iOS/OsuStorageIOS.cs new file mode 100644 index 0000000000..f3a5eec737 --- /dev/null +++ b/osu.iOS/OsuStorageIOS.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.IO; +using osu.Framework.iOS; +using osu.Framework.Platform; +using osu.Game.IO; + +namespace osu.iOS +{ + public class OsuStorageIOS : OsuStorage + { + private readonly IOSGameHost host; + + public OsuStorageIOS(IOSGameHost host, Storage defaultStorage) + : base(host, defaultStorage) + { + this.host = host; + } + + public override Storage GetExportStorage() => new IOSStorage(Path.GetTempPath(), host); + } +} From f0f3c5357164334ea788ec928292b6b59768e55f Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Mon, 9 Dec 2024 08:26:18 -0500 Subject: [PATCH 0244/1275] Update exporter test to use `OsuStorage` --- osu.Game.Tests/Database/LegacyModelExporterTest.cs | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/osu.Game.Tests/Database/LegacyModelExporterTest.cs b/osu.Game.Tests/Database/LegacyModelExporterTest.cs index 0c4b0cc9c4..d261c49517 100644 --- a/osu.Game.Tests/Database/LegacyModelExporterTest.cs +++ b/osu.Game.Tests/Database/LegacyModelExporterTest.cs @@ -12,6 +12,7 @@ using osu.Framework.Extensions.ObjectExtensions; using osu.Framework.Platform; using osu.Framework.Testing; using osu.Game.Database; +using osu.Game.IO; using osu.Game.Overlays.Notifications; using Realms; @@ -21,7 +22,9 @@ namespace osu.Game.Tests.Database public class LegacyModelExporterTest { private TestLegacyModelExporter legacyExporter = null!; - private TemporaryNativeStorage storage = null!; + + private OsuStorage storage = null!; + private TemporaryNativeStorage underlyingStorage = null!; private const string short_filename = "normal file name"; @@ -31,7 +34,7 @@ namespace osu.Game.Tests.Database [SetUp] public void SetUp() { - storage = new TemporaryNativeStorage("export-storage"); + storage = new OsuStorage(new HeadlessGameHost(), underlyingStorage = new TemporaryNativeStorage("export-storage")); legacyExporter = new TestLegacyModelExporter(storage); } @@ -102,8 +105,8 @@ namespace osu.Game.Tests.Database [TearDown] public void TearDown() { - if (storage.IsNotNull()) - storage.Dispose(); + if (underlyingStorage.IsNotNull()) + underlyingStorage.Dispose(); } private class TestLegacyModelExporter : LegacyExporter From 1febed66cfabc03c2930c55c8aeecd9cd288e1d8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 9 Dec 2024 23:51:57 +0900 Subject: [PATCH 0245/1275] Fix top score statistics section total score display being terminally broken Closes https://github.com/ppy/osu/issues/31038. If you don't realise why this does anything, realise this: the drawable creation callback runs for every created sprite text in the text flow. ANd the created sprite texts are split by whitespace. And Russian / Ukrainian / Polish etc. use spaces as thousands separators. So on those languages the first encountered part of the score would duplicate itself to the remaining parts. I'm actively convinced it was _more difficult_ to produce what was in place in `master` than to do it properly. Why did `TextColumn` even have `LocalisableString Text` and `Bindable Current` next to each other????? --- .../Scores/TopScoreStatisticsSection.cs | 41 +++++++++++-------- 1 file changed, 25 insertions(+), 16 deletions(-) diff --git a/osu.Game/Overlays/BeatmapSet/Scores/TopScoreStatisticsSection.cs b/osu.Game/Overlays/BeatmapSet/Scores/TopScoreStatisticsSection.cs index e8833fa0a3..8e342b49c9 100644 --- a/osu.Game/Overlays/BeatmapSet/Scores/TopScoreStatisticsSection.cs +++ b/osu.Game/Overlays/BeatmapSet/Scores/TopScoreStatisticsSection.cs @@ -12,7 +12,6 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Sprites; -using osu.Framework.Graphics.UserInterface; using osu.Framework.Localisation; using osu.Game.Beatmaps; using osu.Game.Graphics; @@ -35,7 +34,7 @@ namespace osu.Game.Overlays.BeatmapSet.Scores private readonly FontUsage smallFont = OsuFont.GetFont(size: 16); private readonly FontUsage largeFont = OsuFont.GetFont(size: 22, weight: FontWeight.Light); - private readonly TextColumn totalScoreColumn; + private readonly TotalScoreColumn totalScoreColumn; private readonly TextColumn accuracyColumn; private readonly TextColumn maxComboColumn; private readonly TextColumn ppColumn; @@ -67,7 +66,7 @@ namespace osu.Game.Overlays.BeatmapSet.Scores Spacing = new Vector2(margin, 0), Children = new Drawable[] { - totalScoreColumn = new TextColumn(BeatmapsetsStrings.ShowScoreboardHeadersScoreTotal, largeFont, top_columns_min_width), + totalScoreColumn = new TotalScoreColumn(BeatmapsetsStrings.ShowScoreboardHeadersScoreTotal, largeFont, top_columns_min_width), accuracyColumn = new TextColumn(BeatmapsetsStrings.ShowScoreboardHeadersAccuracy, largeFont, top_columns_min_width), maxComboColumn = new TextColumn(BeatmapsetsStrings.ShowScoreboardHeadersCombo, largeFont, top_columns_min_width) } @@ -226,7 +225,7 @@ namespace osu.Game.Overlays.BeatmapSet.Scores } } - private partial class TextColumn : InfoColumn, IHasCurrentValue + private partial class TextColumn : InfoColumn { private readonly OsuTextFlowContainer text; @@ -249,18 +248,6 @@ namespace osu.Game.Overlays.BeatmapSet.Scores } } - private Bindable current; - - public Bindable Current - { - get => current; - set - { - text.Clear(); - text.AddText(value.Value, t => t.Current = current = value); - } - } - public TextColumn(LocalisableString title, FontUsage font, float? minWidth = null) : this(title, new OsuTextFlowContainer(t => t.Font = font) { @@ -276,6 +263,28 @@ namespace osu.Game.Overlays.BeatmapSet.Scores } } + private partial class TotalScoreColumn : TextColumn + { + private readonly BindableWithCurrent current = new BindableWithCurrent(); + + public TotalScoreColumn(LocalisableString title, FontUsage font, float? minWidth = null) + : base(title, font, minWidth) + { + } + + public Bindable Current + { + get => current; + set => current.Current = value; + } + + protected override void LoadComplete() + { + base.LoadComplete(); + Current.BindValueChanged(_ => Text = current.Value, true); + } + } + private partial class ModsInfoColumn : InfoColumn { private readonly FillFlowContainer modsContainer; From 92dfcae6eba4a545c6f2bdab0fdb6ddb6536ff0a Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 10 Dec 2024 14:35:09 +0900 Subject: [PATCH 0246/1275] Adjust bad grammar --- osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs b/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs index 0b5450e5ac..975f962f7f 100644 --- a/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs +++ b/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs @@ -51,7 +51,7 @@ namespace osu.Game.Beatmaps.Formats } /// - /// Whether or not beatmap or runtime offsets should be applied. Defaults on; only disable for testing purposes. + /// Whether beatmap or runtime offsets should be applied. Defaults on; only disable for testing purposes. /// public bool ApplyOffsets = true; From d69f5fd4cfd9bdc5720c12a4ccca9400e78784bf Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 10 Dec 2024 14:45:12 +0900 Subject: [PATCH 0247/1275] Avoid beatmap lookup per bar in logo visualisation Just noticed in passing. --- osu.Game/Screens/Menu/LogoVisualisation.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/osu.Game/Screens/Menu/LogoVisualisation.cs b/osu.Game/Screens/Menu/LogoVisualisation.cs index 6d9d2f69b7..53d153ab31 100644 --- a/osu.Game/Screens/Menu/LogoVisualisation.cs +++ b/osu.Game/Screens/Menu/LogoVisualisation.cs @@ -106,9 +106,12 @@ namespace osu.Game.Screens.Menu foreach (var source in amplitudeSources) addAmplitudesFromSource(source); + float kiaiMultiplier = beatSyncProvider.CheckIsKiaiTime() ? 1 : 0.5f; + for (int i = 0; i < bars_per_visualiser; i++) { - float targetAmplitude = (temporalAmplitudes[(i + indexOffset) % bars_per_visualiser]) * (beatSyncProvider.CheckIsKiaiTime() ? 1 : 0.5f); + float targetAmplitude = (temporalAmplitudes[(i + indexOffset) % bars_per_visualiser]) * kiaiMultiplier; + if (targetAmplitude > frequencyAmplitudes[i]) frequencyAmplitudes[i] = targetAmplitude; } From 7fcfebf4b43b5eee6e3f981d4df291d348641835 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sat, 7 Dec 2024 22:51:09 +0900 Subject: [PATCH 0248/1275] Use Alt-{Left,Right} as default bindings for bookmark navigation --- 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 42028c044f..170d247023 100644 --- a/osu.Game/Input/Bindings/GlobalActionContainer.cs +++ b/osu.Game/Input/Bindings/GlobalActionContainer.cs @@ -154,8 +154,8 @@ namespace osu.Game.Input.Bindings new KeyBinding(new[] { InputKey.Control, InputKey.Shift, InputKey.Right }, GlobalAction.EditorSeekToNextSamplePoint), new KeyBinding(new[] { InputKey.Control, InputKey.B }, GlobalAction.EditorAddBookmark), new KeyBinding(new[] { InputKey.Control, InputKey.Shift, InputKey.B }, GlobalAction.EditorRemoveClosestBookmark), - new KeyBinding(InputKey.None, GlobalAction.EditorSeekToPreviousBookmark), - new KeyBinding(InputKey.None, GlobalAction.EditorSeekToNextBookmark), + new KeyBinding(new[] { InputKey.Alt, InputKey.Left }, GlobalAction.EditorSeekToPreviousBookmark), + new KeyBinding(new[] { InputKey.Alt, InputKey.Right }, GlobalAction.EditorSeekToNextBookmark), }; private static IEnumerable editorTestPlayKeyBindings => new[] From 3cac5837547f5ca08baa9f615948d4a4166a4505 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 10 Dec 2024 16:40:47 +0900 Subject: [PATCH 0249/1275] Rewrite resource changing code to be more legible (to my eye) --- .../Screens/Edit/Setup/ResourcesSection.cs | 71 +++++++++++-------- 1 file changed, 40 insertions(+), 31 deletions(-) diff --git a/osu.Game/Screens/Edit/Setup/ResourcesSection.cs b/osu.Game/Screens/Edit/Setup/ResourcesSection.cs index 84107a57e9..6cde0e6792 100644 --- a/osu.Game/Screens/Edit/Setup/ResourcesSection.cs +++ b/osu.Game/Screens/Edit/Setup/ResourcesSection.cs @@ -103,55 +103,64 @@ namespace osu.Game.Screens.Edit.Setup private void changeResource(FileInfo source, bool applyToAllDifficulties, string baseFilename, Func readFilename, Action writeFilename) { - var thisBeatmap = working.Value.BeatmapInfo; var set = working.Value.BeatmapSetInfo; + var beatmap = working.Value.BeatmapInfo; - string newFilename = string.Empty; + var otherBeatmaps = set.Beatmaps.Where(b => !b.Equals(beatmap)); + // First, clean up files which will no longer be used. if (applyToAllDifficulties) { - newFilename = $"{baseFilename}{source.Extension}"; - - foreach (var beatmap in set.Beatmaps) + foreach (var b in set.Beatmaps) { - if (set.GetFile(readFilename(beatmap.Metadata)) is RealmNamedFileUsage otherExistingFile) + if (set.GetFile(readFilename(b.Metadata)) is RealmNamedFileUsage otherExistingFile) beatmaps.DeleteFile(set, otherExistingFile); - - writeFilename(beatmap.Metadata, newFilename); - - if (!beatmap.Equals(thisBeatmap)) - { - // save the difficulty to re-encode the .osu file, updating any reference of the old filename. - var beatmapWorking = beatmaps.GetWorkingBeatmap(beatmap); - beatmaps.Save(beatmap, beatmapWorking.Beatmap, beatmapWorking.GetSkin()); - } } } else { - string[] filenames = set.Files.Select(f => f.Filename).Where(f => + RealmNamedFileUsage? oldFile = set.GetFile(readFilename(working.Value.Metadata)); + + if (oldFile != null) + { + bool oldFileUsedInOtherDiff = otherBeatmaps + .Any(b => readFilename(b.Metadata) == oldFile.Filename); + if (!oldFileUsedInOtherDiff) + beatmaps.DeleteFile(set, oldFile); + } + } + + // Choose a new filename that doesn't clash with any other existing files. + string newFilename = $"{baseFilename}{source.Extension}"; + + if (set.GetFile(newFilename) != null) + { + string[] existingFilenames = set.Files.Select(f => f.Filename).Where(f => f.StartsWith(baseFilename, StringComparison.OrdinalIgnoreCase) && f.EndsWith(source.Extension, StringComparison.OrdinalIgnoreCase)).ToArray(); - - string currentFilename = readFilename(working.Value.Metadata); - - var oldFile = set.GetFile(currentFilename); - - if (oldFile != null && set.Beatmaps.Where(b => !b.Equals(thisBeatmap)).All(b => readFilename(b.Metadata) != currentFilename)) - { - beatmaps.DeleteFile(set, oldFile); - newFilename = currentFilename; - } - - if (string.IsNullOrEmpty(newFilename)) - newFilename = NamingUtils.GetNextBestFilename(filenames, $@"{baseFilename}{source.Extension}"); - - writeFilename(working.Value.Metadata, newFilename); + newFilename = NamingUtils.GetNextBestFilename(existingFilenames, $@"{baseFilename}{source.Extension}"); } using (var stream = source.OpenRead()) beatmaps.AddFile(set, stream, newFilename); + if (applyToAllDifficulties) + { + foreach (var b in otherBeatmaps) + { + if (readFilename(b.Metadata) != newFilename) + { + writeFilename(b.Metadata, newFilename); + + // save the difficulty to re-encode the .osu file, updating any reference of the old filename. + var beatmapWorking = beatmaps.GetWorkingBeatmap(b); + beatmaps.Save(b, beatmapWorking.Beatmap, beatmapWorking.GetSkin()); + } + } + } + + writeFilename(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. editor?.Save(); From bbaa542d4a376e490c7e58d794ff49ca7e1bdddb Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 10 Dec 2024 16:44:35 +0900 Subject: [PATCH 0250/1275] Add note about expensive operation --- osu.Game/Screens/Edit/Setup/ResourcesSection.cs | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/osu.Game/Screens/Edit/Setup/ResourcesSection.cs b/osu.Game/Screens/Edit/Setup/ResourcesSection.cs index 6cde0e6792..7fcd09d7e7 100644 --- a/osu.Game/Screens/Edit/Setup/ResourcesSection.cs +++ b/osu.Game/Screens/Edit/Setup/ResourcesSection.cs @@ -148,14 +148,17 @@ namespace osu.Game.Screens.Edit.Setup { foreach (var b in otherBeatmaps) { - if (readFilename(b.Metadata) != newFilename) - { - writeFilename(b.Metadata, newFilename); + // This operation is quite expensive, so only perform it if required. + if (readFilename(b.Metadata) == newFilename) continue; - // save the difficulty to re-encode the .osu file, updating any reference of the old filename. - var beatmapWorking = beatmaps.GetWorkingBeatmap(b); - beatmaps.Save(b, beatmapWorking.Beatmap, beatmapWorking.GetSkin()); - } + writeFilename(b.Metadata, newFilename); + + // save the difficulty to re-encode the .osu file, updating any reference of the old filename. + // + // note that this triggers a full save flow, including triggering a difficulty calculation. + // this is not a cheap operation and should be reconsidered in the future. + var beatmapWorking = beatmaps.GetWorkingBeatmap(b); + beatmaps.Save(b, beatmapWorking.Beatmap, beatmapWorking.GetSkin()); } } From dae380b7fa927c351e2e413c5b23834f717908d9 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 10 Dec 2024 22:03:07 +0900 Subject: [PATCH 0251/1275] Fix lookups of hit circle slider pieces potentially using wrong source skin Addresses https://github.com/ppy/osu/discussions/30927. --- .../Skinning/Legacy/LegacyMainCirclePiece.cs | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyMainCirclePiece.cs b/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyMainCirclePiece.cs index ef616ae964..0dc0f065d4 100644 --- a/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyMainCirclePiece.cs +++ b/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyMainCirclePiece.cs @@ -57,11 +57,22 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy [BackgroundDependencyLoader] private void load() { + const string base_lookup = @"hitcircle"; + var drawableOsuObject = (DrawableOsuHitObject?)drawableObject; + // As a precondition, ensure 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. + 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. // otherwise fall back to the default prefix "hitcircle". - string circleName = (priorityLookupPrefix != null && skin.GetTexture(priorityLookupPrefix) != null) ? priorityLookupPrefix : @"hitcircle"; + string circleName = (priorityLookupPrefix != null && provider.GetTexture(priorityLookupPrefix) != null) ? priorityLookupPrefix : base_lookup; Vector2 maxSize = OsuHitObject.OBJECT_DIMENSIONS * 2; @@ -70,7 +81,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 = skin.GetTexture(circleName)?.WithMaximumSize(maxSize) }) + CircleSprite = new LegacyKiaiFlashingDrawable(() => new Sprite { Texture = provider.GetTexture(circleName)?.WithMaximumSize(maxSize) }) { Anchor = Anchor.Centre, Origin = Anchor.Centre, @@ -79,7 +90,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy { Anchor = Anchor.Centre, Origin = Anchor.Centre, - Child = OverlaySprite = new LegacyKiaiFlashingDrawable(() => new Sprite { Texture = skin.GetTexture(@$"{circleName}overlay")?.WithMaximumSize(maxSize) }) + Child = OverlaySprite = new LegacyKiaiFlashingDrawable(() => new Sprite { Texture = provider.GetTexture(@$"{circleName}overlay")?.WithMaximumSize(maxSize) }) { Anchor = Anchor.Centre, Origin = Anchor.Centre, From 9abb92a8d659982b76d0ece4ac45c7bb98132020 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Tue, 10 Dec 2024 15:46:28 +0900 Subject: [PATCH 0252/1275] Add BeatmapSetId to playlist items --- osu.Game/Online/Rooms/MultiplayerPlaylistItem.cs | 3 +++ osu.Game/Online/Rooms/PlaylistItem.cs | 6 ++++++ .../OnlinePlay/Multiplayer/MultiplayerMatchSongSelect.cs | 1 + 3 files changed, 10 insertions(+) diff --git a/osu.Game/Online/Rooms/MultiplayerPlaylistItem.cs b/osu.Game/Online/Rooms/MultiplayerPlaylistItem.cs index 8be703e620..027d5b4a17 100644 --- a/osu.Game/Online/Rooms/MultiplayerPlaylistItem.cs +++ b/osu.Game/Online/Rooms/MultiplayerPlaylistItem.cs @@ -56,6 +56,9 @@ namespace osu.Game.Online.Rooms [Key(10)] public double StarRating { get; set; } + [Key(11)] + public int? BeatmapSetID { get; set; } + [SerializationConstructor] public MultiplayerPlaylistItem() { diff --git a/osu.Game/Online/Rooms/PlaylistItem.cs b/osu.Game/Online/Rooms/PlaylistItem.cs index 47d4e163bf..3d829d1e4e 100644 --- a/osu.Game/Online/Rooms/PlaylistItem.cs +++ b/osu.Game/Online/Rooms/PlaylistItem.cs @@ -67,6 +67,9 @@ namespace osu.Game.Online.Rooms set => Beatmap = new APIBeatmap { OnlineID = value }; } + [JsonProperty("beatmapset_id")] + public int? BeatmapSetId { get; set; } + /// /// A beatmap representing this playlist item. /// In many cases, this will *not* contain any usable information apart from OnlineID. @@ -101,6 +104,7 @@ namespace osu.Game.Online.Rooms PlayedAt = item.PlayedAt; RequiredMods = item.RequiredMods.ToArray(); AllowedMods = item.AllowedMods.ToArray(); + BeatmapSetId = item.BeatmapSetID; } public void MarkInvalid() => valid.Value = false; @@ -133,12 +137,14 @@ namespace osu.Game.Online.Rooms AllowedMods = AllowedMods, RequiredMods = RequiredMods, valid = { Value = Valid.Value }, + BeatmapSetId = BeatmapSetId }; } public bool Equals(PlaylistItem? other) => ID == other?.ID && Beatmap.OnlineID == other.Beatmap.OnlineID + && BeatmapSetId == other.BeatmapSetId && RulesetID == other.RulesetID && Expired == other.Expired && PlaylistOrder == other.PlaylistOrder diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSongSelect.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSongSelect.cs index 4e03c19095..9f9e6349a6 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSongSelect.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSongSelect.cs @@ -83,6 +83,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer { ID = itemToEdit?.ID ?? 0, BeatmapID = item.Beatmap.OnlineID, + BeatmapSetID = item.BeatmapSetId, BeatmapChecksum = item.Beatmap.MD5Hash, RulesetID = item.RulesetID, RequiredMods = item.RequiredMods.ToArray(), From 0fb75233ffe501b51ca5cf605f3390c87695dcb9 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Tue, 10 Dec 2024 23:02:26 +0900 Subject: [PATCH 0253/1275] Add "freeplay" button to multiplayer song select --- .../OnlinePlay/FooterButtonFreePlay.cs | 94 +++++++++++++++++++ .../OnlinePlay/OnlinePlaySongSelect.cs | 55 ++++++++--- .../Playlists/PlaylistsSongSelect.cs | 3 +- osu.Game/Screens/Select/SongSelect.cs | 7 +- 4 files changed, 146 insertions(+), 13 deletions(-) create mode 100644 osu.Game/Screens/OnlinePlay/FooterButtonFreePlay.cs diff --git a/osu.Game/Screens/OnlinePlay/FooterButtonFreePlay.cs b/osu.Game/Screens/OnlinePlay/FooterButtonFreePlay.cs new file mode 100644 index 0000000000..367857e780 --- /dev/null +++ b/osu.Game/Screens/OnlinePlay/FooterButtonFreePlay.cs @@ -0,0 +1,94 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Allocation; +using osu.Framework.Bindables; +using osu.Framework.Extensions.Color4Extensions; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; +using osu.Framework.Graphics.UserInterface; +using osu.Game.Graphics; +using osu.Game.Graphics.Sprites; +using osu.Game.Screens.Select; + +namespace osu.Game.Screens.OnlinePlay +{ + public class FooterButtonFreePlay : FooterButton, IHasCurrentValue + { + private readonly BindableWithCurrent current = new BindableWithCurrent(); + + public Bindable Current + { + get => current.Current; + set => current.Current = value; + } + + private OsuSpriteText text = null!; + private Circle circle = null!; + + [Resolved] + private OsuColour colours { get; set; } = null!; + + [BackgroundDependencyLoader] + private void load() + { + ButtonContentContainer.AddRange(new[] + { + new Container + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + AutoSizeAxes = Axes.Both, + Children = new Drawable[] + { + circle = new Circle + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Colour = colours.YellowDark, + RelativeSizeAxes = Axes.Both, + }, + text = new OsuSpriteText + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Padding = new MarginPadding(5), + UseFullGlyphHeight = false, + } + } + } + }); + + SelectedColour = colours.Yellow; + DeselectedColour = SelectedColour.Opacity(0.5f); + Text = @"freeplay"; + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + Current.BindValueChanged(_ => updateDisplay(), true); + + // Overwrite any external behaviour as we delegate the main toggle action to a sub-button. + Action = () => current.Value = !current.Value; + } + + private void updateDisplay() + { + if (current.Value) + { + text.Text = "on"; + text.FadeColour(colours.Gray2, 200, Easing.OutQuint); + circle.FadeColour(colours.Yellow, 200, Easing.OutQuint); + } + else + { + text.Text = "off"; + text.FadeColour(colours.GrayF, 200, Easing.OutQuint); + circle.FadeColour(colours.Gray4, 200, Easing.OutQuint); + } + } + } +} diff --git a/osu.Game/Screens/OnlinePlay/OnlinePlaySongSelect.cs b/osu.Game/Screens/OnlinePlay/OnlinePlaySongSelect.cs index f6b6dfd3ab..1f1d259d0a 100644 --- a/osu.Game/Screens/OnlinePlay/OnlinePlaySongSelect.cs +++ b/osu.Game/Screens/OnlinePlay/OnlinePlaySongSelect.cs @@ -41,10 +41,12 @@ namespace osu.Game.Screens.OnlinePlay protected override UserActivity InitialActivity => new UserActivity.InLobby(room); protected readonly Bindable> FreeMods = new Bindable>(Array.Empty()); + protected readonly Bindable FreePlay = new Bindable(); private readonly Room room; private readonly PlaylistItem? initialItem; - private readonly FreeModSelectOverlay freeModSelectOverlay; + private readonly FreeModSelectOverlay freeModSelect; + private FooterButton freeModsFooterButton = null!; private IDisposable? freeModSelectOverlayRegistration; @@ -61,7 +63,7 @@ namespace osu.Game.Screens.OnlinePlay Padding = new MarginPadding { Horizontal = HORIZONTAL_OVERFLOW_PADDING }; - freeModSelectOverlay = new FreeModSelectOverlay + freeModSelect = new FreeModSelectOverlay { SelectedMods = { BindTarget = FreeMods }, IsValidMod = IsValidFreeMod, @@ -72,7 +74,7 @@ namespace osu.Game.Screens.OnlinePlay private void load() { LeftArea.Padding = new MarginPadding { Top = Header.HEIGHT }; - LoadComponent(freeModSelectOverlay); + LoadComponent(freeModSelect); } protected override void LoadComplete() @@ -108,12 +110,36 @@ namespace osu.Game.Screens.OnlinePlay Mods.Value = initialItem.RequiredMods.Select(m => m.ToMod(rulesetInstance)).ToArray(); FreeMods.Value = initialItem.AllowedMods.Select(m => m.ToMod(rulesetInstance)).ToArray(); } + + if (initialItem.BeatmapSetId != null) + FreePlay.Value = true; } Mods.BindValueChanged(onModsChanged); Ruleset.BindValueChanged(onRulesetChanged); + FreePlay.BindValueChanged(onFreePlayChanged, true); - freeModSelectOverlayRegistration = OverlayManager?.RegisterBlockingOverlay(freeModSelectOverlay); + freeModSelectOverlayRegistration = OverlayManager?.RegisterBlockingOverlay(freeModSelect); + } + + private void onFreePlayChanged(ValueChangedEvent enabled) + { + if (enabled.NewValue) + { + freeModsFooterButton.Enabled.Value = false; + ModsFooterButton.Enabled.Value = false; + + ModSelect.Hide(); + freeModSelect.Hide(); + + Mods.Value = []; + FreeMods.Value = []; + } + else + { + freeModsFooterButton.Enabled.Value = true; + ModsFooterButton.Enabled.Value = true; + } } private void onModsChanged(ValueChangedEvent> mods) @@ -121,7 +147,7 @@ namespace osu.Game.Screens.OnlinePlay FreeMods.Value = FreeMods.Value.Where(checkCompatibleFreeMod).ToList(); // Reset the validity delegate to update the overlay's display. - freeModSelectOverlay.IsValidMod = IsValidFreeMod; + freeModSelect.IsValidMod = IsValidFreeMod; } private void onRulesetChanged(ValueChangedEvent ruleset) @@ -135,7 +161,8 @@ namespace osu.Game.Screens.OnlinePlay { RulesetID = Ruleset.Value.OnlineID, RequiredMods = Mods.Value.Select(m => new APIMod(m)).ToArray(), - AllowedMods = FreeMods.Value.Select(m => new APIMod(m)).ToArray() + AllowedMods = FreeMods.Value.Select(m => new APIMod(m)).ToArray(), + BeatmapSetId = FreePlay.Value ? Beatmap.Value.BeatmapSetInfo.OnlineID : null }; return SelectItem(item); @@ -150,9 +177,9 @@ namespace osu.Game.Screens.OnlinePlay public override bool OnBackButton() { - if (freeModSelectOverlay.State.Value == Visibility.Visible) + if (freeModSelect.State.Value == Visibility.Visible) { - freeModSelectOverlay.Hide(); + freeModSelect.Hide(); return true; } @@ -161,7 +188,7 @@ namespace osu.Game.Screens.OnlinePlay public override bool OnExiting(ScreenExitEvent e) { - freeModSelectOverlay.Hide(); + freeModSelect.Hide(); return base.OnExiting(e); } @@ -173,9 +200,15 @@ namespace osu.Game.Screens.OnlinePlay protected override IEnumerable<(FooterButton, OverlayContainer?)> CreateSongSelectFooterButtons() { var baseButtons = base.CreateSongSelectFooterButtons().ToList(); - var freeModsButton = new FooterButtonFreeMods(freeModSelectOverlay) { Current = FreeMods }; - baseButtons.Insert(baseButtons.FindIndex(b => b.Item1 is FooterButtonMods) + 1, (freeModsButton, freeModSelectOverlay)); + freeModsFooterButton = new FooterButtonFreeMods(freeModSelect) { Current = FreeMods }; + var freePlayButton = new FooterButtonFreePlay { Current = FreePlay }; + + baseButtons.InsertRange(baseButtons.FindIndex(b => b.Item1 is FooterButtonMods) + 1, new (FooterButton, OverlayContainer?)[] + { + (freeModsFooterButton, freeModSelect), + (freePlayButton, null) + }); return baseButtons; } diff --git a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsSongSelect.cs b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsSongSelect.cs index 23824b6a73..f9e014a727 100644 --- a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsSongSelect.cs +++ b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsSongSelect.cs @@ -37,9 +37,10 @@ namespace osu.Game.Screens.OnlinePlay.Playlists private PlaylistItem createNewItem() => new PlaylistItem(Beatmap.Value.BeatmapInfo) { ID = room.Playlist.Count == 0 ? 0 : room.Playlist.Max(p => p.ID) + 1, + BeatmapSetId = FreePlay.Value ? Beatmap.Value.BeatmapSetInfo.OnlineID : null, RulesetID = Ruleset.Value.OnlineID, RequiredMods = Mods.Value.Select(m => new APIMod(m)).ToArray(), - AllowedMods = FreeMods.Value.Select(m => new APIMod(m)).ToArray() + AllowedMods = FreeMods.Value.Select(m => new APIMod(m)).ToArray(), }; } } diff --git a/osu.Game/Screens/Select/SongSelect.cs b/osu.Game/Screens/Select/SongSelect.cs index 9f7a2c02ff..9ebd9c9846 100644 --- a/osu.Game/Screens/Select/SongSelect.cs +++ b/osu.Game/Screens/Select/SongSelect.cs @@ -82,6 +82,11 @@ namespace osu.Game.Screens.Select /// protected Container FooterPanels { get; private set; } = null!; + /// + /// The that opens the mod select dialog. + /// + protected FooterButton ModsFooterButton { get; private set; } = null!; + /// /// Whether entering editor mode should be allowed. /// @@ -407,7 +412,7 @@ namespace osu.Game.Screens.Select /// A set of and an optional which the button opens when pressed. protected virtual IEnumerable<(FooterButton, OverlayContainer?)> CreateSongSelectFooterButtons() => new (FooterButton, OverlayContainer?)[] { - (new FooterButtonMods { Current = Mods }, ModSelect), + (ModsFooterButton = new FooterButtonMods { Current = Mods }, ModSelect), (new FooterButtonRandom { NextRandom = () => Carousel.SelectNextRandom(), From 315a9dba9bd0d9f3a994a7e50d7a8acf0c6a024d Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Wed, 11 Dec 2024 09:59:18 +0900 Subject: [PATCH 0254/1275] Allow tsunyoku and stanriders to trigger diffcalc spreadsheet generator --- .github/workflows/diffcalc.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/diffcalc.yml b/.github/workflows/diffcalc.yml index 4297a88e89..8461208a2e 100644 --- a/.github/workflows/diffcalc.yml +++ b/.github/workflows/diffcalc.yml @@ -115,7 +115,7 @@ jobs: steps: - name: Check permissions run: | - ALLOWED_USERS=(smoogipoo peppy bdach frenzibyte) + ALLOWED_USERS=(smoogipoo peppy bdach frenzibyte tsunyoku stanriders) for i in "${ALLOWED_USERS[@]}"; do if [[ "${{ github.actor }}" == "$i" ]]; then exit 0 From 637fe07b31066087361f7ed305383021f581c647 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 11 Dec 2024 12:36:42 +0900 Subject: [PATCH 0255/1275] Rename `Room{Status -> Mode}Filter` I need the "status" term free for an upcoming change. And web calls this parameter "mode" as well: https://github.com/ppy/osu-web/blob/642e973f916f315fb505aa79d4376675d0a2ec95/app/Models/Multiplayer/Room.php#L184-L199 so it works in my head. --- osu.Game/Online/Rooms/GetRoomsRequest.cs | 10 +++++----- .../OnlinePlay/Components/ListingPollingComponent.cs | 2 +- .../OnlinePlay/Lounge/Components/FilterCriteria.cs | 2 +- .../{RoomStatusFilter.cs => RoomModeFilter.cs} | 2 +- osu.Game/Screens/OnlinePlay/Lounge/LoungeSubScreen.cs | 6 +++--- 5 files changed, 11 insertions(+), 11 deletions(-) rename osu.Game/Screens/OnlinePlay/Lounge/Components/{RoomStatusFilter.cs => RoomModeFilter.cs} (91%) diff --git a/osu.Game/Online/Rooms/GetRoomsRequest.cs b/osu.Game/Online/Rooms/GetRoomsRequest.cs index 7feb709acb..b36e6fc088 100644 --- a/osu.Game/Online/Rooms/GetRoomsRequest.cs +++ b/osu.Game/Online/Rooms/GetRoomsRequest.cs @@ -11,12 +11,12 @@ namespace osu.Game.Online.Rooms { public class GetRoomsRequest : APIRequest> { - private readonly RoomStatusFilter status; + private readonly RoomModeFilter mode; private readonly string category; - public GetRoomsRequest(RoomStatusFilter status, string category) + public GetRoomsRequest(RoomModeFilter mode, string category) { - this.status = status; + this.mode = mode; this.category = category; } @@ -24,8 +24,8 @@ namespace osu.Game.Online.Rooms { var req = base.CreateWebRequest(); - if (status != RoomStatusFilter.Open) - req.AddParameter("mode", status.ToString().ToSnakeCase().ToLowerInvariant()); + if (mode != RoomModeFilter.Open) + req.AddParameter("mode", mode.ToString().ToSnakeCase().ToLowerInvariant()); if (!string.IsNullOrEmpty(category)) req.AddParameter("category", category); diff --git a/osu.Game/Screens/OnlinePlay/Components/ListingPollingComponent.cs b/osu.Game/Screens/OnlinePlay/Components/ListingPollingComponent.cs index b213d424df..88bd595202 100644 --- a/osu.Game/Screens/OnlinePlay/Components/ListingPollingComponent.cs +++ b/osu.Game/Screens/OnlinePlay/Components/ListingPollingComponent.cs @@ -47,7 +47,7 @@ namespace osu.Game.Screens.OnlinePlay.Components lastPollRequest?.Cancel(); - var req = new GetRoomsRequest(Filter.Value.Status, Filter.Value.Category); + var req = new GetRoomsRequest(Filter.Value.Mode, Filter.Value.Category); req.Success += result => { diff --git a/osu.Game/Screens/OnlinePlay/Lounge/Components/FilterCriteria.cs b/osu.Game/Screens/OnlinePlay/Lounge/Components/FilterCriteria.cs index 0f63718355..cc8b0247f6 100644 --- a/osu.Game/Screens/OnlinePlay/Lounge/Components/FilterCriteria.cs +++ b/osu.Game/Screens/OnlinePlay/Lounge/Components/FilterCriteria.cs @@ -8,7 +8,7 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components public class FilterCriteria { public string SearchString = string.Empty; - public RoomStatusFilter Status; + public RoomModeFilter Mode; public string Category = string.Empty; public RulesetInfo? Ruleset; public RoomPermissionsFilter Permissions; diff --git a/osu.Game/Screens/OnlinePlay/Lounge/Components/RoomStatusFilter.cs b/osu.Game/Screens/OnlinePlay/Lounge/Components/RoomModeFilter.cs similarity index 91% rename from osu.Game/Screens/OnlinePlay/Lounge/Components/RoomStatusFilter.cs rename to osu.Game/Screens/OnlinePlay/Lounge/Components/RoomModeFilter.cs index 53fbf670e1..0c07233bff 100644 --- a/osu.Game/Screens/OnlinePlay/Lounge/Components/RoomStatusFilter.cs +++ b/osu.Game/Screens/OnlinePlay/Lounge/Components/RoomModeFilter.cs @@ -5,7 +5,7 @@ using System.ComponentModel; namespace osu.Game.Screens.OnlinePlay.Lounge.Components { - public enum RoomStatusFilter + public enum RoomModeFilter { Open, diff --git a/osu.Game/Screens/OnlinePlay/Lounge/LoungeSubScreen.cs b/osu.Game/Screens/OnlinePlay/Lounge/LoungeSubScreen.cs index 5d0983f09c..9a02e4bec8 100644 --- a/osu.Game/Screens/OnlinePlay/Lounge/LoungeSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Lounge/LoungeSubScreen.cs @@ -83,7 +83,7 @@ namespace osu.Game.Screens.OnlinePlay.Lounge private LoadingLayer loadingLayer = null!; private RoomsContainer roomsContainer = null!; private SearchTextBox searchTextBox = null!; - private Dropdown statusDropdown = null!; + private Dropdown statusDropdown = null!; [BackgroundDependencyLoader(true)] private void load() @@ -223,12 +223,12 @@ namespace osu.Game.Screens.OnlinePlay.Lounge { SearchString = searchTextBox.Current.Value, Ruleset = ruleset.Value, - Status = statusDropdown.Current.Value + Mode = statusDropdown.Current.Value }; protected virtual IEnumerable CreateFilterControls() { - statusDropdown = new SlimEnumDropdown + statusDropdown = new SlimEnumDropdown { RelativeSizeAxes = Axes.None, Width = 160, From 3352571f2aa3378bdf9dbb0068bac21dbb823890 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 11 Dec 2024 13:01:11 +0900 Subject: [PATCH 0256/1275] Add ability to filter out currently playing rooms Addresses https://osu.ppy.sh/community/forums/topics/2013293?n=1. --- osu.Game/Online/Rooms/GetRoomsRequest.cs | 17 +++++++++++------ .../Components/ListingPollingComponent.cs | 2 +- .../Lounge/Components/FilterCriteria.cs | 1 + .../Lounge/Components/RoomStatusFilter.cs | 11 +++++++++++ .../OnlinePlay/Lounge/LoungeSubScreen.cs | 11 ++++++----- .../Multiplayer/MultiplayerLoungeSubScreen.cs | 13 ++++++++++++- 6 files changed, 42 insertions(+), 13 deletions(-) create mode 100644 osu.Game/Screens/OnlinePlay/Lounge/Components/RoomStatusFilter.cs diff --git a/osu.Game/Online/Rooms/GetRoomsRequest.cs b/osu.Game/Online/Rooms/GetRoomsRequest.cs index b36e6fc088..2d0d572e84 100644 --- a/osu.Game/Online/Rooms/GetRoomsRequest.cs +++ b/osu.Game/Online/Rooms/GetRoomsRequest.cs @@ -12,12 +12,14 @@ namespace osu.Game.Online.Rooms public class GetRoomsRequest : APIRequest> { private readonly RoomModeFilter mode; + private readonly RoomStatusFilter? status; private readonly string category; - public GetRoomsRequest(RoomModeFilter mode, string category) + public GetRoomsRequest(FilterCriteria filterCriteria) { - this.mode = mode; - this.category = category; + mode = filterCriteria.Mode; + category = filterCriteria.Category; + status = filterCriteria.Status; } protected override WebRequest CreateWebRequest() @@ -25,14 +27,17 @@ namespace osu.Game.Online.Rooms var req = base.CreateWebRequest(); if (mode != RoomModeFilter.Open) - req.AddParameter("mode", mode.ToString().ToSnakeCase().ToLowerInvariant()); + req.AddParameter(@"mode", mode.ToString().ToSnakeCase().ToLowerInvariant()); + + if (status != null) + req.AddParameter(@"status", status.Value.ToString().ToSnakeCase().ToLowerInvariant()); if (!string.IsNullOrEmpty(category)) - req.AddParameter("category", category); + req.AddParameter(@"category", category); return req; } - protected override string Target => "rooms"; + protected override string Target => @"rooms"; } } diff --git a/osu.Game/Screens/OnlinePlay/Components/ListingPollingComponent.cs b/osu.Game/Screens/OnlinePlay/Components/ListingPollingComponent.cs index 88bd595202..21452727b8 100644 --- a/osu.Game/Screens/OnlinePlay/Components/ListingPollingComponent.cs +++ b/osu.Game/Screens/OnlinePlay/Components/ListingPollingComponent.cs @@ -47,7 +47,7 @@ namespace osu.Game.Screens.OnlinePlay.Components lastPollRequest?.Cancel(); - var req = new GetRoomsRequest(Filter.Value.Mode, Filter.Value.Category); + var req = new GetRoomsRequest(Filter.Value); req.Success += result => { diff --git a/osu.Game/Screens/OnlinePlay/Lounge/Components/FilterCriteria.cs b/osu.Game/Screens/OnlinePlay/Lounge/Components/FilterCriteria.cs index cc8b0247f6..121dffde1f 100644 --- a/osu.Game/Screens/OnlinePlay/Lounge/Components/FilterCriteria.cs +++ b/osu.Game/Screens/OnlinePlay/Lounge/Components/FilterCriteria.cs @@ -9,6 +9,7 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components { public string SearchString = string.Empty; public RoomModeFilter Mode; + public RoomStatusFilter? Status; public string Category = string.Empty; public RulesetInfo? Ruleset; public RoomPermissionsFilter Permissions; diff --git a/osu.Game/Screens/OnlinePlay/Lounge/Components/RoomStatusFilter.cs b/osu.Game/Screens/OnlinePlay/Lounge/Components/RoomStatusFilter.cs new file mode 100644 index 0000000000..a4d5043ff5 --- /dev/null +++ b/osu.Game/Screens/OnlinePlay/Lounge/Components/RoomStatusFilter.cs @@ -0,0 +1,11 @@ +// 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.Screens.OnlinePlay.Lounge.Components +{ + public enum RoomStatusFilter + { + Idle, + Playing, + } +} diff --git a/osu.Game/Screens/OnlinePlay/Lounge/LoungeSubScreen.cs b/osu.Game/Screens/OnlinePlay/Lounge/LoungeSubScreen.cs index 9a02e4bec8..f00cf7427c 100644 --- a/osu.Game/Screens/OnlinePlay/Lounge/LoungeSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Lounge/LoungeSubScreen.cs @@ -83,7 +83,8 @@ namespace osu.Game.Screens.OnlinePlay.Lounge private LoadingLayer loadingLayer = null!; private RoomsContainer roomsContainer = null!; private SearchTextBox searchTextBox = null!; - private Dropdown statusDropdown = null!; + + protected Dropdown StatusDropdown { get; private set; } = null!; [BackgroundDependencyLoader(true)] private void load() @@ -223,20 +224,20 @@ namespace osu.Game.Screens.OnlinePlay.Lounge { SearchString = searchTextBox.Current.Value, Ruleset = ruleset.Value, - Mode = statusDropdown.Current.Value + Mode = StatusDropdown.Current.Value }; protected virtual IEnumerable CreateFilterControls() { - statusDropdown = new SlimEnumDropdown + StatusDropdown = new SlimEnumDropdown { RelativeSizeAxes = Axes.None, Width = 160, }; - statusDropdown.Current.BindValueChanged(_ => UpdateFilter()); + StatusDropdown.Current.BindValueChanged(_ => UpdateFilter()); - yield return statusDropdown; + yield return StatusDropdown; } #endregion diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerLoungeSubScreen.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerLoungeSubScreen.cs index 50358ea9d3..23216c86b2 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerLoungeSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerLoungeSubScreen.cs @@ -31,6 +31,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer private MultiplayerClient client { get; set; } = null!; private Dropdown roomAccessTypeDropdown = null!; + private OsuCheckbox showInProgress = null!; public override void OnResuming(ScreenTransitionEvent e) { @@ -56,7 +57,16 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer roomAccessTypeDropdown.Current.BindValueChanged(_ => UpdateFilter()); - return base.CreateFilterControls().Append(roomAccessTypeDropdown); + showInProgress = new OsuCheckbox + { + LabelText = "Show playing rooms", + RelativeSizeAxes = Axes.None, + Width = 200, + Current = { Value = true } + }; + showInProgress.Current.BindValueChanged(_ => UpdateFilter()); + + return base.CreateFilterControls().Concat([roomAccessTypeDropdown, showInProgress]); } protected override FilterCriteria CreateFilterCriteria() @@ -64,6 +74,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer var criteria = base.CreateFilterCriteria(); criteria.Category = @"realtime"; criteria.Permissions = roomAccessTypeDropdown.Current.Value; + criteria.Status = showInProgress.Current.Value ? null : RoomStatusFilter.Idle; return criteria; } From b37a06c0fe0ce05b6b82292219faeafd024c86e2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 11 Dec 2024 13:24:54 +0900 Subject: [PATCH 0257/1275] Hide "show playing rooms" toggle when in filter mode it doesn't make sense with --- .../Multiplayer/MultiplayerLoungeSubScreen.cs | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerLoungeSubScreen.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerLoungeSubScreen.cs index 23216c86b2..303ba60875 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerLoungeSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerLoungeSubScreen.cs @@ -2,7 +2,6 @@ // See the LICENCE file in the repository root for full licence text. using System.Collections.Generic; -using System.Linq; using System.Threading.Tasks; using osu.Framework.Allocation; using osu.Framework.Bindables; @@ -48,7 +47,10 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer protected override IEnumerable CreateFilterControls() { - roomAccessTypeDropdown = new SlimEnumDropdown + foreach (var control in base.CreateFilterControls()) + yield return control; + + yield return roomAccessTypeDropdown = new SlimEnumDropdown { RelativeSizeAxes = Axes.None, Current = Config.GetBindable(OsuSetting.MultiplayerRoomFilter), @@ -57,16 +59,17 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer roomAccessTypeDropdown.Current.BindValueChanged(_ => UpdateFilter()); - showInProgress = new OsuCheckbox + yield return showInProgress = new OsuCheckbox { LabelText = "Show playing rooms", RelativeSizeAxes = Axes.None, Width = 200, + Padding = new MarginPadding { Vertical = 5, }, Current = { Value = true } }; - showInProgress.Current.BindValueChanged(_ => UpdateFilter()); - return base.CreateFilterControls().Concat([roomAccessTypeDropdown, showInProgress]); + showInProgress.Current.BindValueChanged(_ => UpdateFilter()); + StatusDropdown.Current.BindValueChanged(_ => showInProgress.Alpha = StatusDropdown.Current.Value == RoomModeFilter.Open ? 1 : 0, true); } protected override FilterCriteria CreateFilterCriteria() @@ -74,7 +77,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer var criteria = base.CreateFilterCriteria(); criteria.Category = @"realtime"; criteria.Permissions = roomAccessTypeDropdown.Current.Value; - criteria.Status = showInProgress.Current.Value ? null : RoomStatusFilter.Idle; + criteria.Status = showInProgress.Current.Value && criteria.Mode == RoomModeFilter.Open ? null : RoomStatusFilter.Idle; return criteria; } From 723883e1f06dc7277f1441c8ab73130b0437adbc Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Wed, 11 Dec 2024 01:03:20 -0500 Subject: [PATCH 0258/1275] Revert "Update exporter test to use `OsuStorage`" This reverts commit f0f3c5357164334ea788ec928292b6b59768e55f. --- osu.Game.Tests/Database/LegacyModelExporterTest.cs | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/osu.Game.Tests/Database/LegacyModelExporterTest.cs b/osu.Game.Tests/Database/LegacyModelExporterTest.cs index d261c49517..0c4b0cc9c4 100644 --- a/osu.Game.Tests/Database/LegacyModelExporterTest.cs +++ b/osu.Game.Tests/Database/LegacyModelExporterTest.cs @@ -12,7 +12,6 @@ using osu.Framework.Extensions.ObjectExtensions; using osu.Framework.Platform; using osu.Framework.Testing; using osu.Game.Database; -using osu.Game.IO; using osu.Game.Overlays.Notifications; using Realms; @@ -22,9 +21,7 @@ namespace osu.Game.Tests.Database public class LegacyModelExporterTest { private TestLegacyModelExporter legacyExporter = null!; - - private OsuStorage storage = null!; - private TemporaryNativeStorage underlyingStorage = null!; + private TemporaryNativeStorage storage = null!; private const string short_filename = "normal file name"; @@ -34,7 +31,7 @@ namespace osu.Game.Tests.Database [SetUp] public void SetUp() { - storage = new OsuStorage(new HeadlessGameHost(), underlyingStorage = new TemporaryNativeStorage("export-storage")); + storage = new TemporaryNativeStorage("export-storage"); legacyExporter = new TestLegacyModelExporter(storage); } @@ -105,8 +102,8 @@ namespace osu.Game.Tests.Database [TearDown] public void TearDown() { - if (underlyingStorage.IsNotNull()) - underlyingStorage.Dispose(); + if (storage.IsNotNull()) + storage.Dispose(); } private class TestLegacyModelExporter : LegacyExporter From e0aec6f907f81f04cf6c802eea618b7f2c0c062a Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Wed, 11 Dec 2024 01:03:55 -0500 Subject: [PATCH 0259/1275] Revert unnecessary complexity --- osu.Game/Database/LegacyExporter.cs | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/osu.Game/Database/LegacyExporter.cs b/osu.Game/Database/LegacyExporter.cs index 193887765d..80393c27f7 100644 --- a/osu.Game/Database/LegacyExporter.cs +++ b/osu.Game/Database/LegacyExporter.cs @@ -3,7 +3,6 @@ using System; using System.Collections.Generic; -using System.Diagnostics; using System.IO; using System.Linq; using System.Threading; @@ -42,15 +41,13 @@ namespace osu.Game.Database protected abstract string FileExtension { get; } protected readonly Storage UserFileStorage; - private readonly Storage? exportStorage; + private readonly Storage exportStorage; public Action? PostNotification { get; set; } protected LegacyExporter(Storage storage) { - if (storage is OsuStorage osuStorage) - exportStorage = osuStorage.GetExportStorage(); - + exportStorage = (storage as OsuStorage)?.GetExportStorage() ?? storage.GetStorageForDirectory(@"exports"); UserFileStorage = storage.GetStorageForDirectory(@"files"); } @@ -72,8 +69,6 @@ namespace osu.Game.Database /// A cancellation token. public async Task ExportAsync(Live model, CancellationToken cancellationToken = default) { - Debug.Assert(exportStorage != null); - string itemFilename = model.PerformRead(s => GetFilename(s).GetValidFilename()); if (itemFilename.Length > MAX_FILENAME_LENGTH - FileExtension.Length) From 5a0b732ee32e66e24118e390706795a419cd3954 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 11 Dec 2024 16:26:11 +0900 Subject: [PATCH 0260/1275] Add comments backreferences to copies of duplicated code for future use --- .../Edit/Blueprints/JuiceStreamSelectionBlueprint.cs | 2 ++ .../Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs | 2 ++ 2 files changed, 4 insertions(+) diff --git a/osu.Game.Rulesets.Catch/Edit/Blueprints/JuiceStreamSelectionBlueprint.cs b/osu.Game.Rulesets.Catch/Edit/Blueprints/JuiceStreamSelectionBlueprint.cs index a61478f5d5..6a0ce35a07 100644 --- a/osu.Game.Rulesets.Catch/Edit/Blueprints/JuiceStreamSelectionBlueprint.cs +++ b/osu.Game.Rulesets.Catch/Edit/Blueprints/JuiceStreamSelectionBlueprint.cs @@ -190,6 +190,8 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints lastSliderPathVersion = HitObject.Path.Version.Value; } + // duplicated in `SliderSelectionBlueprint.convertToStream()` + // consider extracting common helper when applying changes here private void convertToStream() { if (editorBeatmap == null || beatDivisor == null) diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs index 34de81f1ba..02f76b51b0 100644 --- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs +++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs @@ -551,6 +551,8 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders HitObject.Position += first; } + // duplicated in `JuiceStreamSelectionBlueprint.convertToStream()` + // consider extracting common helper when applying changes here private void convertToStream() { if (editorBeatmap == null || beatDivisor == null) From de31a48beb3d1f2ec47b9421a12492a70c054667 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 9 Dec 2024 23:29:50 +0900 Subject: [PATCH 0261/1275] Some `Carousel` classes can be abstract --- .../Screens/Select/Carousel/CarouselGroup.cs | 48 +++++++++---------- .../Carousel/CarouselGroupEagerSelect.cs | 4 +- 2 files changed, 26 insertions(+), 26 deletions(-) diff --git a/osu.Game/Screens/Select/Carousel/CarouselGroup.cs b/osu.Game/Screens/Select/Carousel/CarouselGroup.cs index 62d694976f..c0fb5fa397 100644 --- a/osu.Game/Screens/Select/Carousel/CarouselGroup.cs +++ b/osu.Game/Screens/Select/Carousel/CarouselGroup.cs @@ -10,8 +10,31 @@ namespace osu.Game.Screens.Select.Carousel /// /// A group which ensures only one item is selected. /// - public class CarouselGroup : CarouselItem + public abstract class CarouselGroup : CarouselItem { + protected CarouselGroup(List? items = null) + { + if (items != null) this.items = items; + + State.ValueChanged += state => + { + switch (state.NewValue) + { + case CarouselItemState.Collapsed: + case CarouselItemState.NotSelected: + this.items.ForEach(c => c.State.Value = CarouselItemState.Collapsed); + break; + + case CarouselItemState.Selected: + this.items.ForEach(c => + { + if (c.State.Value == CarouselItemState.Collapsed) c.State.Value = CarouselItemState.NotSelected; + }); + break; + } + }; + } + public override DrawableCarouselItem? CreateDrawableRepresentation() => null; public SlimReadOnlyListWrapper Items => items.AsSlimReadOnly(); @@ -67,29 +90,6 @@ namespace osu.Game.Screens.Select.Carousel TotalItemsNotFiltered++; } - public CarouselGroup(List? items = null) - { - if (items != null) this.items = items; - - State.ValueChanged += state => - { - switch (state.NewValue) - { - case CarouselItemState.Collapsed: - case CarouselItemState.NotSelected: - this.items.ForEach(c => c.State.Value = CarouselItemState.Collapsed); - break; - - case CarouselItemState.Selected: - this.items.ForEach(c => - { - if (c.State.Value == CarouselItemState.Collapsed) c.State.Value = CarouselItemState.NotSelected; - }); - break; - } - }; - } - public override void Filter(FilterCriteria criteria) { base.Filter(criteria); diff --git a/osu.Game/Screens/Select/Carousel/CarouselGroupEagerSelect.cs b/osu.Game/Screens/Select/Carousel/CarouselGroupEagerSelect.cs index cf4ba5924f..8cc1ea258a 100644 --- a/osu.Game/Screens/Select/Carousel/CarouselGroupEagerSelect.cs +++ b/osu.Game/Screens/Select/Carousel/CarouselGroupEagerSelect.cs @@ -10,9 +10,9 @@ namespace osu.Game.Screens.Select.Carousel /// /// A group which ensures at least one item is selected (if the group itself is selected). /// - public class CarouselGroupEagerSelect : CarouselGroup + public abstract class CarouselGroupEagerSelect : CarouselGroup { - public CarouselGroupEagerSelect() + protected CarouselGroupEagerSelect() { State.ValueChanged += state => { From bab9b9c937748bc283febc553b8b0b8e2510599b Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 10 Dec 2024 23:52:37 +0900 Subject: [PATCH 0262/1275] Remove no-longer-correct comment --- osu.Game/Screens/Select/BeatmapCarousel.cs | 3 --- 1 file changed, 3 deletions(-) diff --git a/osu.Game/Screens/Select/BeatmapCarousel.cs b/osu.Game/Screens/Select/BeatmapCarousel.cs index fc7c7989e2..f0c3b1f477 100644 --- a/osu.Game/Screens/Select/BeatmapCarousel.cs +++ b/osu.Game/Screens/Select/BeatmapCarousel.cs @@ -246,9 +246,6 @@ namespace osu.Game.Screens.Select if (detachedBeatmapStore != null && detachedBeatmapSets == null) { - // This is performing an unnecessary second lookup on realm (in addition to the subscription), but for performance reasons - // we require it to be separate: the subscription's initial callback (with `ChangeSet` of `null`) will run on the update - // thread. If we attempt to detach beatmaps in this callback the game will fall over (it takes time). detachedBeatmapSets = detachedBeatmapStore.GetDetachedBeatmaps(cancellationToken); detachedBeatmapSets.BindCollectionChanged(beatmapSetsChanged); loadNewRoot(); From c94b393e309cd4a9f0e5d4f0b5f3e53a6d2e5b30 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 11 Dec 2024 00:18:33 +0900 Subject: [PATCH 0263/1275] Access beatmap store via abstract base class The intention here is to make things more testable going forward. Specifically, to remove the "back-door" entrance into `BeatmapCarousel` where `BeatmapSets` can be set by tests and bypas/block realm retrieval. --- .../Background/TestSceneUserDimBackgrounds.cs | 6 ++-- .../Visual/Multiplayer/QueueModeTestScene.cs | 6 ++-- .../Multiplayer/TestSceneMultiplayer.cs | 6 ++-- .../TestSceneMultiplayerMatchSongSelect.cs | 6 ++-- .../TestScenePlaylistsSongSelect.cs | 6 ++-- .../SongSelect/TestScenePlaySongSelect.cs | 6 ++-- osu.Game/Database/BeatmapStore.cs | 35 +++++++++++++++++++ ...pStore.cs => RealmDetachedBeatmapStore.cs} | 5 ++- osu.Game/OsuGame.cs | 2 +- osu.Game/Screens/Select/BeatmapCarousel.cs | 6 ++-- 10 files changed, 59 insertions(+), 25 deletions(-) create mode 100644 osu.Game/Database/BeatmapStore.cs rename osu.Game/Database/{DetachedBeatmapStore.cs => RealmDetachedBeatmapStore.cs} (96%) diff --git a/osu.Game.Tests/Visual/Background/TestSceneUserDimBackgrounds.cs b/osu.Game.Tests/Visual/Background/TestSceneUserDimBackgrounds.cs index d8be57382f..5bbbfb0284 100644 --- a/osu.Game.Tests/Visual/Background/TestSceneUserDimBackgrounds.cs +++ b/osu.Game.Tests/Visual/Background/TestSceneUserDimBackgrounds.cs @@ -49,17 +49,17 @@ namespace osu.Game.Tests.Visual.Background [BackgroundDependencyLoader] private void load(GameHost host, AudioManager audio) { - DetachedBeatmapStore detachedBeatmapStore; + BeatmapStore beatmapStore; Dependencies.Cache(rulesets = new RealmRulesetStore(Realm)); Dependencies.Cache(manager = new BeatmapManager(LocalStorage, Realm, null, audio, Resources, host, Beatmap.Default)); Dependencies.Cache(new OsuConfigManager(LocalStorage)); - Dependencies.Cache(detachedBeatmapStore = new DetachedBeatmapStore()); + Dependencies.Cache(beatmapStore = new RealmDetachedBeatmapStore()); Dependencies.Cache(Realm); manager.Import(TestResources.GetQuickTestBeatmapForImport()).WaitSafely(); - Add(detachedBeatmapStore); + Add(beatmapStore); Beatmap.SetDefault(); } diff --git a/osu.Game.Tests/Visual/Multiplayer/QueueModeTestScene.cs b/osu.Game.Tests/Visual/Multiplayer/QueueModeTestScene.cs index 2b738743ea..ab0a4e8e03 100644 --- a/osu.Game.Tests/Visual/Multiplayer/QueueModeTestScene.cs +++ b/osu.Game.Tests/Visual/Multiplayer/QueueModeTestScene.cs @@ -44,14 +44,14 @@ namespace osu.Game.Tests.Visual.Multiplayer [BackgroundDependencyLoader] private void load(GameHost host, AudioManager audio) { - DetachedBeatmapStore detachedBeatmapStore; + BeatmapStore beatmapStore; Dependencies.Cache(new RealmRulesetStore(Realm)); Dependencies.Cache(beatmaps = new BeatmapManager(LocalStorage, Realm, null, audio, Resources, host, Beatmap.Default)); - Dependencies.Cache(detachedBeatmapStore = new DetachedBeatmapStore()); + Dependencies.Cache(beatmapStore = new RealmDetachedBeatmapStore()); Dependencies.Cache(Realm); - Add(detachedBeatmapStore); + Add(beatmapStore); } public override void SetUpSteps() diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs index 9213a52c0e..0f3fa7511d 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs @@ -66,14 +66,14 @@ namespace osu.Game.Tests.Visual.Multiplayer [BackgroundDependencyLoader] private void load(GameHost host, AudioManager audio) { - DetachedBeatmapStore detachedBeatmapStore; + BeatmapStore beatmapStore; Dependencies.Cache(new RealmRulesetStore(Realm)); Dependencies.Cache(beatmaps = new BeatmapManager(LocalStorage, Realm, API, audio, Resources, host, Beatmap.Default)); - Dependencies.Cache(detachedBeatmapStore = new DetachedBeatmapStore()); + Dependencies.Cache(beatmapStore = new RealmDetachedBeatmapStore()); Dependencies.Cache(Realm); - Add(detachedBeatmapStore); + Add(beatmapStore); } public override void SetUpSteps() diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSongSelect.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSongSelect.cs index 2a5f16d091..3ea96bae84 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSongSelect.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSongSelect.cs @@ -46,16 +46,16 @@ namespace osu.Game.Tests.Visual.Multiplayer [BackgroundDependencyLoader] private void load(GameHost host, AudioManager audio) { - DetachedBeatmapStore detachedBeatmapStore; + BeatmapStore beatmapStore; Dependencies.Cache(rulesets = new RealmRulesetStore(Realm)); Dependencies.Cache(manager = new BeatmapManager(LocalStorage, Realm, null, audio, Resources, host, Beatmap.Default)); - Dependencies.Cache(detachedBeatmapStore = new DetachedBeatmapStore()); + Dependencies.Cache(beatmapStore = new RealmDetachedBeatmapStore()); Dependencies.Cache(Realm); importedBeatmapSet = manager.Import(TestResources.CreateTestBeatmapSetInfo(8, rulesets.AvailableRulesets.ToArray()))!; - Add(detachedBeatmapStore); + Add(beatmapStore); } private void setUp() diff --git a/osu.Game.Tests/Visual/Multiplayer/TestScenePlaylistsSongSelect.cs b/osu.Game.Tests/Visual/Multiplayer/TestScenePlaylistsSongSelect.cs index fa1909254a..24b67bc4a1 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestScenePlaylistsSongSelect.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestScenePlaylistsSongSelect.cs @@ -31,18 +31,18 @@ namespace osu.Game.Tests.Visual.Multiplayer [BackgroundDependencyLoader] private void load(GameHost host, AudioManager audio) { - DetachedBeatmapStore detachedBeatmapStore; + BeatmapStore beatmapStore; Dependencies.Cache(new RealmRulesetStore(Realm)); Dependencies.Cache(manager = new BeatmapManager(LocalStorage, Realm, null, audio, Resources, host, Beatmap.Default)); - Dependencies.Cache(detachedBeatmapStore = new DetachedBeatmapStore()); + Dependencies.Cache(beatmapStore = new RealmDetachedBeatmapStore()); Dependencies.Cache(Realm); var beatmapSet = TestResources.CreateTestBeatmapSetInfo(); manager.Import(beatmapSet); - Add(detachedBeatmapStore); + Add(beatmapStore); } public override void SetUpSteps() diff --git a/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs b/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs index 3a95aca6b9..3d86b214fd 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs @@ -56,20 +56,20 @@ namespace osu.Game.Tests.Visual.SongSelect [BackgroundDependencyLoader] private void load(GameHost host, AudioManager audio) { - DetachedBeatmapStore detachedBeatmapStore; + BeatmapStore beatmapStore; // These DI caches are required to ensure for interactive runs this test scene doesn't nuke all user beatmaps in the local install. // At a point we have isolated interactive test runs enough, this can likely be removed. Dependencies.Cache(rulesets = new RealmRulesetStore(Realm)); Dependencies.Cache(Realm); Dependencies.Cache(manager = new BeatmapManager(LocalStorage, Realm, null, audio, Resources, host, defaultBeatmap = Beatmap.Default)); - Dependencies.Cache(detachedBeatmapStore = new DetachedBeatmapStore()); + Dependencies.Cache(beatmapStore = new RealmDetachedBeatmapStore()); Dependencies.Cache(music = new MusicController()); // required to get bindables attached Add(music); - Add(detachedBeatmapStore); + Add(beatmapStore); Dependencies.Cache(config = new OsuConfigManager(LocalStorage)); } diff --git a/osu.Game/Database/BeatmapStore.cs b/osu.Game/Database/BeatmapStore.cs new file mode 100644 index 0000000000..f288279a79 --- /dev/null +++ b/osu.Game/Database/BeatmapStore.cs @@ -0,0 +1,35 @@ +// 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 osu.Framework.Bindables; +using osu.Framework.Graphics; +using osu.Game.Beatmaps; + +namespace osu.Game.Database +{ + /// + /// A store which contains a thread-safe representation of beatmaps available game-wide. + /// This exposes changes to available beatmaps, such as post-import or deletion. + /// + /// + /// The main goal of classes which implement this interface should be to provide change + /// tracking and thread safety in a performant way, rather than having to worry about such + /// concerns at the point of usage. + /// + public abstract partial class BeatmapStore : Component + { + /// + /// Get all available beatmaps. + /// + /// A cancellation token which allows early abort from the operation. + /// A bindable list of all available beatmap sets. + /// + /// This operation may block during the initial load process. + /// + /// It is generally expected that once a beatmap store is in a good state, the overhead of this call + /// should be negligible. + /// + public abstract IBindableList GetBeatmaps(CancellationToken? cancellationToken); + } +} diff --git a/osu.Game/Database/DetachedBeatmapStore.cs b/osu.Game/Database/RealmDetachedBeatmapStore.cs similarity index 96% rename from osu.Game/Database/DetachedBeatmapStore.cs rename to osu.Game/Database/RealmDetachedBeatmapStore.cs index 5b65f608b2..bc0dc2ae93 100644 --- a/osu.Game/Database/DetachedBeatmapStore.cs +++ b/osu.Game/Database/RealmDetachedBeatmapStore.cs @@ -8,14 +8,13 @@ using System.Threading; using System.Threading.Tasks; using osu.Framework.Allocation; using osu.Framework.Bindables; -using osu.Framework.Graphics; using osu.Game.Beatmaps; using osu.Game.Online.Multiplayer; using Realms; namespace osu.Game.Database { - public partial class DetachedBeatmapStore : Component + public partial class RealmDetachedBeatmapStore : BeatmapStore { private readonly ManualResetEventSlim loaded = new ManualResetEventSlim(); @@ -28,7 +27,7 @@ namespace osu.Game.Database [Resolved] private RealmAccess realm { get; set; } = null!; - public IBindableList GetDetachedBeatmaps(CancellationToken? cancellationToken) + public override IBindableList GetBeatmaps(CancellationToken? cancellationToken) { loaded.Wait(cancellationToken ?? CancellationToken.None); return detachedBeatmapSets.GetBoundCopy(); diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs index d8145c8246..e808e570c7 100644 --- a/osu.Game/OsuGame.cs +++ b/osu.Game/OsuGame.cs @@ -1143,7 +1143,7 @@ namespace osu.Game loadComponentSingleFile(new MedalOverlay(), topMostOverlayContent.Add); loadComponentSingleFile(new BackgroundDataStoreProcessor(), Add); - loadComponentSingleFile(new DetachedBeatmapStore(), Add, true); + loadComponentSingleFile(new RealmDetachedBeatmapStore(), Add, true); Add(externalLinkOpener = new ExternalLinkOpener()); Add(new MusicKeyBindingHandler()); diff --git a/osu.Game/Screens/Select/BeatmapCarousel.cs b/osu.Game/Screens/Select/BeatmapCarousel.cs index f0c3b1f477..6dfb834317 100644 --- a/osu.Game/Screens/Select/BeatmapCarousel.cs +++ b/osu.Game/Screens/Select/BeatmapCarousel.cs @@ -113,7 +113,7 @@ namespace osu.Game.Screens.Select private RealmAccess realm { get; set; } = null!; [Resolved] - private DetachedBeatmapStore? detachedBeatmapStore { get; set; } + private BeatmapStore? beatmapStore { get; set; } private IBindableList? detachedBeatmapSets; @@ -244,9 +244,9 @@ namespace osu.Game.Screens.Select RightClickScrollingEnabled.BindValueChanged(enabled => Scroll.RightMouseScrollbar = enabled.NewValue, true); - if (detachedBeatmapStore != null && detachedBeatmapSets == null) + if (beatmapStore != null && detachedBeatmapSets == null) { - detachedBeatmapSets = detachedBeatmapStore.GetDetachedBeatmaps(cancellationToken); + detachedBeatmapSets = beatmapStore.GetBeatmaps(cancellationToken); detachedBeatmapSets.BindCollectionChanged(beatmapSetsChanged); loadNewRoot(); } From a868c33380e4423c572e3a3ce13cedbe63753d88 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 11 Dec 2024 16:17:18 +0900 Subject: [PATCH 0264/1275] Remove `BeatmapCarousel` testing backdoor --- .../Background/TestSceneUserDimBackgrounds.cs | 2 +- .../Visual/Multiplayer/QueueModeTestScene.cs | 2 +- .../Multiplayer/TestSceneMultiplayer.cs | 2 +- .../TestSceneMultiplayerMatchSongSelect.cs | 2 +- .../TestScenePlaylistsSongSelect.cs | 2 +- .../SongSelect/TestSceneBeatmapCarousel.cs | 8 +++++- .../SongSelect/TestScenePlaySongSelect.cs | 2 +- .../TestSceneUpdateBeatmapSetButton.cs | 13 +++++---- .../TestSceneFirstRunScreenUIScale.cs | 5 ++++ .../TestSceneFirstRunSetupOverlay.cs | 3 +++ osu.Game/Database/BeatmapStore.cs | 2 +- .../Database/RealmDetachedBeatmapStore.cs | 2 +- osu.Game/Screens/Select/BeatmapCarousel.cs | 27 ++++--------------- osu.Game/Tests/Beatmaps/TestBeatmapStore.cs | 16 +++++++++++ 14 files changed, 52 insertions(+), 36 deletions(-) create mode 100644 osu.Game/Tests/Beatmaps/TestBeatmapStore.cs diff --git a/osu.Game.Tests/Visual/Background/TestSceneUserDimBackgrounds.cs b/osu.Game.Tests/Visual/Background/TestSceneUserDimBackgrounds.cs index 5bbbfb0284..693e1e48d4 100644 --- a/osu.Game.Tests/Visual/Background/TestSceneUserDimBackgrounds.cs +++ b/osu.Game.Tests/Visual/Background/TestSceneUserDimBackgrounds.cs @@ -54,7 +54,7 @@ namespace osu.Game.Tests.Visual.Background Dependencies.Cache(rulesets = new RealmRulesetStore(Realm)); Dependencies.Cache(manager = new BeatmapManager(LocalStorage, Realm, null, audio, Resources, host, Beatmap.Default)); Dependencies.Cache(new OsuConfigManager(LocalStorage)); - Dependencies.Cache(beatmapStore = new RealmDetachedBeatmapStore()); + Dependencies.CacheAs(beatmapStore = new RealmDetachedBeatmapStore()); Dependencies.Cache(Realm); manager.Import(TestResources.GetQuickTestBeatmapForImport()).WaitSafely(); diff --git a/osu.Game.Tests/Visual/Multiplayer/QueueModeTestScene.cs b/osu.Game.Tests/Visual/Multiplayer/QueueModeTestScene.cs index ab0a4e8e03..0e01751d76 100644 --- a/osu.Game.Tests/Visual/Multiplayer/QueueModeTestScene.cs +++ b/osu.Game.Tests/Visual/Multiplayer/QueueModeTestScene.cs @@ -48,7 +48,7 @@ namespace osu.Game.Tests.Visual.Multiplayer Dependencies.Cache(new RealmRulesetStore(Realm)); Dependencies.Cache(beatmaps = new BeatmapManager(LocalStorage, Realm, null, audio, Resources, host, Beatmap.Default)); - Dependencies.Cache(beatmapStore = new RealmDetachedBeatmapStore()); + Dependencies.CacheAs(beatmapStore = new RealmDetachedBeatmapStore()); Dependencies.Cache(Realm); Add(beatmapStore); diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs index 0f3fa7511d..fb653cea8b 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs @@ -70,7 +70,7 @@ namespace osu.Game.Tests.Visual.Multiplayer Dependencies.Cache(new RealmRulesetStore(Realm)); Dependencies.Cache(beatmaps = new BeatmapManager(LocalStorage, Realm, API, audio, Resources, host, Beatmap.Default)); - Dependencies.Cache(beatmapStore = new RealmDetachedBeatmapStore()); + Dependencies.CacheAs(beatmapStore = new RealmDetachedBeatmapStore()); Dependencies.Cache(Realm); Add(beatmapStore); diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSongSelect.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSongSelect.cs index 3ea96bae84..8e4c83c4b4 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSongSelect.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSongSelect.cs @@ -50,7 +50,7 @@ namespace osu.Game.Tests.Visual.Multiplayer Dependencies.Cache(rulesets = new RealmRulesetStore(Realm)); Dependencies.Cache(manager = new BeatmapManager(LocalStorage, Realm, null, audio, Resources, host, Beatmap.Default)); - Dependencies.Cache(beatmapStore = new RealmDetachedBeatmapStore()); + Dependencies.CacheAs(beatmapStore = new RealmDetachedBeatmapStore()); Dependencies.Cache(Realm); importedBeatmapSet = manager.Import(TestResources.CreateTestBeatmapSetInfo(8, rulesets.AvailableRulesets.ToArray()))!; diff --git a/osu.Game.Tests/Visual/Multiplayer/TestScenePlaylistsSongSelect.cs b/osu.Game.Tests/Visual/Multiplayer/TestScenePlaylistsSongSelect.cs index 24b67bc4a1..726d0ac9f9 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestScenePlaylistsSongSelect.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestScenePlaylistsSongSelect.cs @@ -35,7 +35,7 @@ namespace osu.Game.Tests.Visual.Multiplayer Dependencies.Cache(new RealmRulesetStore(Realm)); Dependencies.Cache(manager = new BeatmapManager(LocalStorage, Realm, null, audio, Resources, host, Beatmap.Default)); - Dependencies.Cache(beatmapStore = new RealmDetachedBeatmapStore()); + Dependencies.CacheAs(beatmapStore = new RealmDetachedBeatmapStore()); Dependencies.Cache(Realm); var beatmapSet = TestResources.CreateTestBeatmapSetInfo(); diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarousel.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarousel.cs index 97c46a11fc..11e754c868 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarousel.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarousel.cs @@ -16,6 +16,7 @@ using osu.Framework.Testing; using osu.Framework.Utils; using osu.Game.Beatmaps; using osu.Game.Configuration; +using osu.Game.Database; using osu.Game.Rulesets; using osu.Game.Rulesets.Catch; using osu.Game.Rulesets.Osu; @@ -23,6 +24,7 @@ using osu.Game.Rulesets.Taiko; using osu.Game.Screens.Select; using osu.Game.Screens.Select.Carousel; using osu.Game.Screens.Select.Filter; +using osu.Game.Tests.Beatmaps; using osu.Game.Tests.Resources; using osuTK.Input; @@ -42,6 +44,9 @@ namespace osu.Game.Tests.Visual.SongSelect private const int set_count = 5; private const int diff_count = 3; + [Cached(typeof(BeatmapStore))] + private TestBeatmapStore beatmaps = new TestBeatmapStore(); + [BackgroundDependencyLoader] private void load(RulesetStore rulesets) { @@ -1329,7 +1334,8 @@ namespace osu.Game.Tests.Visual.SongSelect carouselAdjust?.Invoke(carousel); - carousel.BeatmapSets = beatmapSets; + beatmaps.BeatmapSets.Clear(); + beatmaps.BeatmapSets.AddRange(beatmapSets); (target ?? this).Child = carousel; }); diff --git a/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs b/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs index 3d86b214fd..c415fc876f 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs @@ -63,7 +63,7 @@ namespace osu.Game.Tests.Visual.SongSelect Dependencies.Cache(rulesets = new RealmRulesetStore(Realm)); Dependencies.Cache(Realm); Dependencies.Cache(manager = new BeatmapManager(LocalStorage, Realm, null, audio, Resources, host, defaultBeatmap = Beatmap.Default)); - Dependencies.Cache(beatmapStore = new RealmDetachedBeatmapStore()); + Dependencies.CacheAs(beatmapStore = new RealmDetachedBeatmapStore()); Dependencies.Cache(music = new MusicController()); diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneUpdateBeatmapSetButton.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneUpdateBeatmapSetButton.cs index 0b0cd0317a..ff0f35576c 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneUpdateBeatmapSetButton.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneUpdateBeatmapSetButton.cs @@ -2,7 +2,6 @@ // See the LICENCE file in the repository root for full licence text. using System; -using System.Collections.Generic; using System.Linq; using NUnit.Framework; using osu.Framework.Allocation; @@ -10,12 +9,14 @@ using osu.Framework.Extensions.ObjectExtensions; using osu.Framework.Graphics; using osu.Framework.Testing; using osu.Game.Beatmaps; +using osu.Game.Database; using osu.Game.Online.API; using osu.Game.Overlays; using osu.Game.Overlays.Dialog; using osu.Game.Screens.Select; using osu.Game.Screens.Select.Carousel; using osu.Game.Screens.Select.Filter; +using osu.Game.Tests.Beatmaps; using osu.Game.Tests.Online; using osu.Game.Tests.Resources; using osuTK.Input; @@ -31,6 +32,9 @@ namespace osu.Game.Tests.Visual.SongSelect private BeatmapSetInfo testBeatmapSetInfo = null!; + [Cached(typeof(BeatmapStore))] + private TestBeatmapStore beatmaps = new TestBeatmapStore(); + protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent) { var dependencies = new DependencyContainer(base.CreateChildDependencies(parent)); @@ -246,13 +250,12 @@ namespace osu.Game.Tests.Visual.SongSelect private BeatmapCarousel createCarousel() { + beatmaps.BeatmapSets.Clear(); + beatmaps.BeatmapSets.Add(testBeatmapSetInfo = TestResources.CreateTestBeatmapSetInfo(5)); + return carousel = new BeatmapCarousel(new FilterCriteria()) { RelativeSizeAxes = Axes.Both, - BeatmapSets = new List - { - (testBeatmapSetInfo = TestResources.CreateTestBeatmapSetInfo(5)), - } }; } } diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneFirstRunScreenUIScale.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneFirstRunScreenUIScale.cs index 2dee57f4cb..4d180f6507 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneFirstRunScreenUIScale.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneFirstRunScreenUIScale.cs @@ -3,8 +3,10 @@ using osu.Framework.Allocation; using osu.Framework.Screens; +using osu.Game.Database; using osu.Game.Overlays; using osu.Game.Overlays.FirstRunSetup; +using osu.Game.Tests.Beatmaps; namespace osu.Game.Tests.Visual.UserInterface { @@ -13,6 +15,9 @@ namespace osu.Game.Tests.Visual.UserInterface [Cached] private OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Purple); + [Cached(typeof(BeatmapStore))] + private BeatmapStore beatmapStore = new TestBeatmapStore(); + public TestSceneFirstRunScreenUIScale() { AddStep("load screen", () => diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneFirstRunSetupOverlay.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneFirstRunSetupOverlay.cs index 2ca06bf2f4..dc51e5516a 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneFirstRunSetupOverlay.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneFirstRunSetupOverlay.cs @@ -17,12 +17,14 @@ using osu.Framework.Graphics.Containers; using osu.Framework.Screens; using osu.Framework.Testing; using osu.Game.Configuration; +using osu.Game.Database; using osu.Game.Localisation; using osu.Game.Overlays; using osu.Game.Overlays.FirstRunSetup; using osu.Game.Overlays.Notifications; using osu.Game.Screens; using osu.Game.Screens.Footer; +using osu.Game.Tests.Beatmaps; using osuTK; using osuTK.Input; @@ -47,6 +49,7 @@ namespace osu.Game.Tests.Visual.UserInterface Dependencies.Cache(LocalConfig = new OsuConfigManager(LocalStorage)); Dependencies.CacheAs(performer.Object); Dependencies.CacheAs(notificationOverlay.Object); + Dependencies.CacheAs(new TestBeatmapStore()); } [SetUpSteps] diff --git a/osu.Game/Database/BeatmapStore.cs b/osu.Game/Database/BeatmapStore.cs index f288279a79..9853e4b9cf 100644 --- a/osu.Game/Database/BeatmapStore.cs +++ b/osu.Game/Database/BeatmapStore.cs @@ -30,6 +30,6 @@ namespace osu.Game.Database /// It is generally expected that once a beatmap store is in a good state, the overhead of this call /// should be negligible. /// - public abstract IBindableList GetBeatmaps(CancellationToken? cancellationToken); + public abstract IBindableList GetBeatmapSets(CancellationToken? cancellationToken); } } diff --git a/osu.Game/Database/RealmDetachedBeatmapStore.cs b/osu.Game/Database/RealmDetachedBeatmapStore.cs index bc0dc2ae93..b05e07ef31 100644 --- a/osu.Game/Database/RealmDetachedBeatmapStore.cs +++ b/osu.Game/Database/RealmDetachedBeatmapStore.cs @@ -27,7 +27,7 @@ namespace osu.Game.Database [Resolved] private RealmAccess realm { get; set; } = null!; - public override IBindableList GetBeatmaps(CancellationToken? cancellationToken) + public override IBindableList GetBeatmapSets(CancellationToken? cancellationToken) { loaded.Wait(cancellationToken ?? CancellationToken.None); return detachedBeatmapSets.GetBoundCopy(); diff --git a/osu.Game/Screens/Select/BeatmapCarousel.cs b/osu.Game/Screens/Select/BeatmapCarousel.cs index 6dfb834317..65c4133ea2 100644 --- a/osu.Game/Screens/Select/BeatmapCarousel.cs +++ b/osu.Game/Screens/Select/BeatmapCarousel.cs @@ -112,27 +112,13 @@ namespace osu.Game.Screens.Select [Resolved] private RealmAccess realm { get; set; } = null!; - [Resolved] - private BeatmapStore? beatmapStore { get; set; } - private IBindableList? detachedBeatmapSets; private readonly NoResultsPlaceholder noResultsPlaceholder; private IEnumerable beatmapSets => root.Items.OfType(); - internal IEnumerable BeatmapSets - { - get => beatmapSets.Select(g => g.BeatmapSet); - set - { - if (LoadState != LoadState.NotLoaded) - throw new InvalidOperationException("If not using a realm source, beatmap sets must be set before load."); - - detachedBeatmapSets = new BindableList(value); - Schedule(loadNewRoot); - } - } + internal IEnumerable BeatmapSets => beatmapSets.Select(g => g.BeatmapSet); private void loadNewRoot() { @@ -234,7 +220,7 @@ namespace osu.Game.Screens.Select } [BackgroundDependencyLoader] - private void load(OsuConfigManager config, AudioManager audio, CancellationToken? cancellationToken) + private void load(OsuConfigManager config, AudioManager audio, BeatmapStore beatmaps, CancellationToken? cancellationToken) { spinSample = audio.Samples.Get("SongSelect/random-spin"); randomSelectSample = audio.Samples.Get(@"SongSelect/select-random"); @@ -244,12 +230,9 @@ namespace osu.Game.Screens.Select RightClickScrollingEnabled.BindValueChanged(enabled => Scroll.RightMouseScrollbar = enabled.NewValue, true); - if (beatmapStore != null && detachedBeatmapSets == null) - { - detachedBeatmapSets = beatmapStore.GetBeatmaps(cancellationToken); - detachedBeatmapSets.BindCollectionChanged(beatmapSetsChanged); - loadNewRoot(); - } + detachedBeatmapSets = beatmaps.GetBeatmapSets(cancellationToken); + detachedBeatmapSets.BindCollectionChanged(beatmapSetsChanged); + loadNewRoot(); } private readonly HashSet setsRequiringUpdate = new HashSet(); diff --git a/osu.Game/Tests/Beatmaps/TestBeatmapStore.cs b/osu.Game/Tests/Beatmaps/TestBeatmapStore.cs new file mode 100644 index 0000000000..1734f1397f --- /dev/null +++ b/osu.Game/Tests/Beatmaps/TestBeatmapStore.cs @@ -0,0 +1,16 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Threading; +using osu.Framework.Bindables; +using osu.Game.Beatmaps; +using osu.Game.Database; + +namespace osu.Game.Tests.Beatmaps +{ + internal partial class TestBeatmapStore : BeatmapStore + { + public readonly BindableList BeatmapSets = new BindableList(); + public override IBindableList GetBeatmapSets(CancellationToken? cancellationToken) => BeatmapSets; + } +} From 0aa17a905b45dcc55e7444722b8593e2957b365f Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 11 Dec 2024 18:08:34 +0900 Subject: [PATCH 0265/1275] Increase timed update frequency and add inline comment --- .../Screens/OnlinePlay/Components/StatusColouredContainer.cs | 3 ++- .../Screens/OnlinePlay/Lounge/Components/RoomStatusPill.cs | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/Components/StatusColouredContainer.cs b/osu.Game/Screens/OnlinePlay/Components/StatusColouredContainer.cs index a811ee3371..7147803412 100644 --- a/osu.Game/Screens/OnlinePlay/Components/StatusColouredContainer.cs +++ b/osu.Game/Screens/OnlinePlay/Components/StatusColouredContainer.cs @@ -30,7 +30,8 @@ namespace osu.Game.Screens.OnlinePlay.Components room.PropertyChanged += onRoomPropertyChanged; - Scheduler.AddDelayed(updateRoomStatus, 5000, true); + // Timed update required to track rooms which have hit the end time, see `HasEnded`. + Scheduler.AddDelayed(updateRoomStatus, 1000, true); updateRoomStatus(); } diff --git a/osu.Game/Screens/OnlinePlay/Lounge/Components/RoomStatusPill.cs b/osu.Game/Screens/OnlinePlay/Lounge/Components/RoomStatusPill.cs index 6da8f3ecbd..092f17a643 100644 --- a/osu.Game/Screens/OnlinePlay/Lounge/Components/RoomStatusPill.cs +++ b/osu.Game/Screens/OnlinePlay/Lounge/Components/RoomStatusPill.cs @@ -37,7 +37,8 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components room.PropertyChanged += onRoomPropertyChanged; - Scheduler.AddDelayed(updateDisplay, 5000, true); + // Timed update required to track rooms which have hit the end time, see `HasEnded`. + Scheduler.AddDelayed(updateDisplay, 1000, true); updateDisplay(); FinishTransforms(true); } From e8c0e27cc0e826d18e60abd3b665c56d6d3c2964 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 11 Dec 2024 18:17:59 +0900 Subject: [PATCH 0266/1275] Adjust in line with upstream changes --- osu.Game/Screens/OnlinePlay/Lounge/DrawableLoungeRoom.cs | 3 +-- .../Screens/OnlinePlay/Playlists/PlaylistsRoomFooter.cs | 3 +-- .../Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs | 7 +------ 3 files changed, 3 insertions(+), 10 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/Lounge/DrawableLoungeRoom.cs b/osu.Game/Screens/OnlinePlay/Lounge/DrawableLoungeRoom.cs index 7d36cec7ba..0a55472c2d 100644 --- a/osu.Game/Screens/OnlinePlay/Lounge/DrawableLoungeRoom.cs +++ b/osu.Game/Screens/OnlinePlay/Lounge/DrawableLoungeRoom.cs @@ -26,7 +26,6 @@ using osu.Game.Input.Bindings; using osu.Game.Online.API; using osu.Game.Online.API.Requests; using osu.Game.Online.Rooms; -using osu.Game.Online.Rooms.RoomStatuses; using osu.Game.Overlays; using osu.Game.Screens.OnlinePlay.Components; using osu.Game.Screens.OnlinePlay.Lounge.Components; @@ -168,7 +167,7 @@ namespace osu.Game.Screens.OnlinePlay.Lounge }) }; - if (Room.Type == MatchType.Playlists && Room.Host?.Id == api.LocalUser.Value.Id && Room.StartDate?.AddMinutes(5) >= DateTimeOffset.Now && Room.Status is not RoomStatusEnded) + if (Room.Type == MatchType.Playlists && Room.Host?.Id == api.LocalUser.Value.Id && Room.StartDate?.AddMinutes(5) >= DateTimeOffset.Now && !Room.HasEnded) { items.Add(new OsuMenuItem("Close playlist", MenuItemType.Destructive, () => { diff --git a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomFooter.cs b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomFooter.cs index 6089b4734e..f9b1edcd59 100644 --- a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomFooter.cs +++ b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomFooter.cs @@ -9,7 +9,6 @@ using osu.Framework.Graphics.Containers; using osu.Game.Graphics.UserInterface; using osu.Game.Online.API; using osu.Game.Online.Rooms; -using osu.Game.Online.Rooms.RoomStatuses; using osuTK; namespace osu.Game.Screens.OnlinePlay.Playlists @@ -99,7 +98,7 @@ namespace osu.Game.Screens.OnlinePlay.Playlists if (room.Host?.Id == api.LocalUser.Value.Id) { - if (deletionGracePeriodRemaining > TimeSpan.Zero && room.Status is not RoomStatusEnded) + if (deletionGracePeriodRemaining > TimeSpan.Zero && !room.HasEnded) { closeButton.FadeIn(); using (BeginDelayedSequence(deletionGracePeriodRemaining.Value.TotalMilliseconds)) diff --git a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs index 9573155f5a..9b4630ac0b 100644 --- a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs @@ -16,7 +16,6 @@ using osu.Game.Input; using osu.Game.Online.API; using osu.Game.Online.API.Requests; using osu.Game.Online.Rooms; -using osu.Game.Online.Rooms.RoomStatuses; using osu.Game.Screens.OnlinePlay.Components; using osu.Game.Screens.OnlinePlay.Match; using osu.Game.Screens.OnlinePlay.Match.Components; @@ -286,11 +285,7 @@ namespace osu.Game.Screens.OnlinePlay.Playlists DialogOverlay?.Push(new ClosePlaylistDialog(Room, () => { var request = new ClosePlaylistRequest(Room.RoomID!.Value); - request.Success += () => - { - Room.Status = new RoomStatusEnded(); - Room.EndDate = DateTimeOffset.UtcNow; - }; + request.Success += () => Room.EndDate = DateTimeOffset.UtcNow; API.Queue(request); })); } From 26f15def70a6c2ccf1f66bdac5aad14097524efe Mon Sep 17 00:00:00 2001 From: Nicholas Chin Date: Wed, 11 Dec 2024 23:15:05 +0800 Subject: [PATCH 0267/1275] Add missing mania tooltip overlay for 4k and 7k --- osu.Game/Localisation/CommonStrings.cs | 12 ++++- .../Profile/Header/Components/MainDetails.cs | 48 ++++++++++++++++++- osu.Game/Users/UserStatistics.cs | 40 ++++++++++++++++ 3 files changed, 97 insertions(+), 3 deletions(-) diff --git a/osu.Game/Localisation/CommonStrings.cs b/osu.Game/Localisation/CommonStrings.cs index 243a100029..88766a608c 100644 --- a/osu.Game/Localisation/CommonStrings.cs +++ b/osu.Game/Localisation/CommonStrings.cs @@ -179,6 +179,16 @@ namespace osu.Game.Localisation /// public static LocalisableString CopyLink => new TranslatableString(getKey(@"copy_link"), @"Copy link"); + /// + /// "4K" + /// + public static LocalisableString FourKey => new TranslatableString(getKey(@"four_key"), @"4K"); + + /// + /// "7K" + /// + public static LocalisableString SevenKey => new TranslatableString(getKey(@"seven_key"), @"7K"); + private static string getKey(string key) => $@"{prefix}:{key}"; } -} \ No newline at end of file +} diff --git a/osu.Game/Overlays/Profile/Header/Components/MainDetails.cs b/osu.Game/Overlays/Profile/Header/Components/MainDetails.cs index 3d97082230..84919d18bb 100644 --- a/osu.Game/Overlays/Profile/Header/Components/MainDetails.cs +++ b/osu.Game/Overlays/Profile/Header/Components/MainDetails.cs @@ -2,6 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using System.Collections.Generic; +using System.Linq; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Extensions.LocalisationExtensions; @@ -164,13 +165,56 @@ namespace osu.Game.Overlays.Profile.Header.Components detailGlobalRank.Content = user?.Statistics?.GlobalRank?.ToLocalisableString("\\##,##0") ?? (LocalisableString)"-"; var rankHighest = user?.RankHighest; + var variants = user?.Statistics.Variants; - detailGlobalRank.ContentTooltipText = rankHighest != null - ? UsersStrings.ShowRankHighest(rankHighest.Rank.ToLocalisableString("\\##,##0"), rankHighest.UpdatedAt.ToLocalisableString(@"d MMM yyyy")) + #region Global rank tooltip + var tooltipParts = new List(); + + if (variants?.Count > 0) + { + foreach (var variant in variants) + { + if (variant.GlobalRank != null) + { + tooltipParts.Add($"{variant.VariantDisplay}: {variant.GlobalRank.ToLocalisableString("\\##,##0")}"); + } + } + } + + if (rankHighest != null) + { + tooltipParts.Add(UsersStrings.ShowRankHighest( + rankHighest.Rank.ToLocalisableString("\\##,##0"), + rankHighest.UpdatedAt.ToLocalisableString(@"d MMM yyyy")) + ); + } + + detailGlobalRank.ContentTooltipText = tooltipParts.Any() + ? string.Join("\n", tooltipParts) : string.Empty; + #endregion detailCountryRank.Content = user?.Statistics?.CountryRank?.ToLocalisableString("\\##,##0") ?? (LocalisableString)"-"; + #region Country rank tooltip + var countryTooltipParts = new List(); + + if (variants?.Count > 0) + { + foreach (var variant in variants) + { + if (variant.CountryRank != null) + { + countryTooltipParts.Add($"{variant.VariantDisplay}: {variant.CountryRank.Value.ToLocalisableString("\\##,##0")}"); + } + } + } + + detailCountryRank.ContentTooltipText = countryTooltipParts.Any() + ? string.Join("\n", countryTooltipParts) + : string.Empty; + #endregion + rankGraph.Statistics.Value = user?.Statistics; } diff --git a/osu.Game/Users/UserStatistics.cs b/osu.Game/Users/UserStatistics.cs index 918a1b6968..d18675198f 100644 --- a/osu.Game/Users/UserStatistics.cs +++ b/osu.Game/Users/UserStatistics.cs @@ -4,8 +4,12 @@ #nullable disable using System; +using System.Collections.Generic; +using System.Runtime.Serialization; using Newtonsoft.Json; +using Newtonsoft.Json.Converters; using osu.Framework.Localisation; +using osu.Game.Localisation; using osu.Game.Online.API.Requests.Responses; using osu.Game.Scoring; using osu.Game.Utils; @@ -74,6 +78,9 @@ namespace osu.Game.Users [JsonProperty(@"grade_counts")] public Grades GradesCount; + [JsonProperty(@"variants")] + public List Variants = null!; + public struct Grades { [JsonProperty(@"ssh")] @@ -118,5 +125,38 @@ namespace osu.Game.Users } } } + public enum GameVariant + { + [EnumMember(Value = "4k")] + FourKey, + [EnumMember(Value = "7k")] + SevenKey + } + + public class Variant + { + [JsonProperty("country_rank")] + public int? CountryRank; + + [JsonProperty("global_rank")] + public int? GlobalRank; + + [JsonProperty("mode")] + public string Mode; + + [JsonProperty("pp")] + public decimal PP; + + [JsonProperty("variant")] + [JsonConverter(typeof(StringEnumConverter))] + public GameVariant? VariantType; + + public LocalisableString VariantDisplay => VariantType switch + { + GameVariant.FourKey => CommonStrings.FourKey, + GameVariant.SevenKey => CommonStrings.SevenKey, + _ => string.Empty + }; + } } } From 862b41c38e90bb39b6a106da8ae0b42954fa42a0 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 12 Dec 2024 12:53:05 +0900 Subject: [PATCH 0268/1275] Move `BeatmapInfoWedgeV2` to correct namespace --- .../Visual/SongSelectV2/TestSceneBeatmapInfoWedge.cs | 1 + .../{Select => SelectV2}/BeatmapInfoWedgeV2.cs | 11 ++++++----- 2 files changed, 7 insertions(+), 5 deletions(-) rename osu.Game/Screens/{Select => SelectV2}/BeatmapInfoWedgeV2.cs (99%) diff --git a/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapInfoWedge.cs b/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapInfoWedge.cs index fbbab3a604..5b717887e2 100644 --- a/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapInfoWedge.cs +++ b/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapInfoWedge.cs @@ -15,6 +15,7 @@ using osu.Game.Rulesets; using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects.Legacy; using osu.Game.Screens.Select; +using osu.Game.Screens.SelectV2; namespace osu.Game.Tests.Visual.SongSelectV2 { diff --git a/osu.Game/Screens/Select/BeatmapInfoWedgeV2.cs b/osu.Game/Screens/SelectV2/BeatmapInfoWedgeV2.cs similarity index 99% rename from osu.Game/Screens/Select/BeatmapInfoWedgeV2.cs rename to osu.Game/Screens/SelectV2/BeatmapInfoWedgeV2.cs index 3c76ae1f08..b294896c77 100644 --- a/osu.Game/Screens/Select/BeatmapInfoWedgeV2.cs +++ b/osu.Game/Screens/SelectV2/BeatmapInfoWedgeV2.cs @@ -3,23 +3,24 @@ using System; using System.Threading; -using osuTK; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Effects; +using osu.Framework.Graphics.Shapes; +using osu.Framework.Localisation; using osu.Game.Beatmaps; using osu.Game.Beatmaps.Drawables; using osu.Game.Graphics; -using osu.Game.Graphics.Sprites; -using osu.Framework.Graphics.Shapes; -using osu.Framework.Localisation; using osu.Game.Graphics.Containers; +using osu.Game.Graphics.Sprites; using osu.Game.Graphics.UserInterface; using osu.Game.Rulesets; +using osu.Game.Screens.Select; +using osuTK; -namespace osu.Game.Screens.Select +namespace osu.Game.Screens.SelectV2 { public partial class BeatmapInfoWedgeV2 : VisibilityContainer { From 61ee830588f10275253d8f4daac9649bce381afc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 12 Dec 2024 15:16:11 +0900 Subject: [PATCH 0269/1275] Adjust copy --- .../OnlinePlay/Multiplayer/MultiplayerLoungeSubScreen.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerLoungeSubScreen.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerLoungeSubScreen.cs index 303ba60875..9904d503f7 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerLoungeSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerLoungeSubScreen.cs @@ -61,7 +61,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer yield return showInProgress = new OsuCheckbox { - LabelText = "Show playing rooms", + LabelText = "Show in-progress rooms", RelativeSizeAxes = Axes.None, Width = 200, Padding = new MarginPadding { Vertical = 5, }, From 032870888920d7d86502cfc6b35e58491e005612 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 12 Dec 2024 15:16:24 +0900 Subject: [PATCH 0270/1275] Store value of toggle to setting --- osu.Game/Configuration/OsuConfigManager.cs | 4 +++- .../OnlinePlay/Multiplayer/MultiplayerLoungeSubScreen.cs | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/osu.Game/Configuration/OsuConfigManager.cs b/osu.Game/Configuration/OsuConfigManager.cs index 4f62db8cf7..df0a823648 100644 --- a/osu.Game/Configuration/OsuConfigManager.cs +++ b/osu.Game/Configuration/OsuConfigManager.cs @@ -202,6 +202,7 @@ namespace osu.Game.Configuration SetDefault(OsuSetting.HideCountryFlags, false); SetDefault(OsuSetting.MultiplayerRoomFilter, RoomPermissionsFilter.All); + SetDefault(OsuSetting.MultiplayerShowInProgressFilter, true); SetDefault(OsuSetting.LastProcessedMetadataId, -1); @@ -447,6 +448,7 @@ namespace osu.Game.Configuration EditorRotationOrigin, EditorTimelineShowBreaks, EditorAdjustExistingObjectsOnTimingChanges, - AlwaysRequireHoldingForPause + AlwaysRequireHoldingForPause, + MultiplayerShowInProgressFilter, } } diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerLoungeSubScreen.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerLoungeSubScreen.cs index 9904d503f7..7f7f94504f 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerLoungeSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerLoungeSubScreen.cs @@ -65,7 +65,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer RelativeSizeAxes = Axes.None, Width = 200, Padding = new MarginPadding { Vertical = 5, }, - Current = { Value = true } + Current = Config.GetBindable(OsuSetting.MultiplayerShowInProgressFilter), }; showInProgress.Current.BindValueChanged(_ => UpdateFilter()); From a22f3416d6f7c0a03f5fbfddb0ec4e47cff0e723 Mon Sep 17 00:00:00 2001 From: Nicholas Chin Date: Thu, 12 Dec 2024 22:39:21 +0800 Subject: [PATCH 0271/1275] Replace switch expression with LocalisableDescription attribute for variant display Use existing localisation strings from BeatmapsStrings instead of CommonStrings for consistent localisation handling --- osu.Game/Localisation/CommonStrings.cs | 12 +----------- osu.Game/Users/UserStatistics.cs | 12 +++++------- 2 files changed, 6 insertions(+), 18 deletions(-) diff --git a/osu.Game/Localisation/CommonStrings.cs b/osu.Game/Localisation/CommonStrings.cs index 88766a608c..243a100029 100644 --- a/osu.Game/Localisation/CommonStrings.cs +++ b/osu.Game/Localisation/CommonStrings.cs @@ -179,16 +179,6 @@ namespace osu.Game.Localisation /// public static LocalisableString CopyLink => new TranslatableString(getKey(@"copy_link"), @"Copy link"); - /// - /// "4K" - /// - public static LocalisableString FourKey => new TranslatableString(getKey(@"four_key"), @"4K"); - - /// - /// "7K" - /// - public static LocalisableString SevenKey => new TranslatableString(getKey(@"seven_key"), @"7K"); - private static string getKey(string key) => $@"{prefix}:{key}"; } -} +} \ No newline at end of file diff --git a/osu.Game/Users/UserStatistics.cs b/osu.Game/Users/UserStatistics.cs index d18675198f..b485485d48 100644 --- a/osu.Game/Users/UserStatistics.cs +++ b/osu.Game/Users/UserStatistics.cs @@ -8,9 +8,10 @@ using System.Collections.Generic; using System.Runtime.Serialization; using Newtonsoft.Json; using Newtonsoft.Json.Converters; +using osu.Framework.Extensions; using osu.Framework.Localisation; -using osu.Game.Localisation; using osu.Game.Online.API.Requests.Responses; +using osu.Game.Resources.Localisation.Web; using osu.Game.Scoring; using osu.Game.Utils; @@ -128,8 +129,10 @@ namespace osu.Game.Users public enum GameVariant { [EnumMember(Value = "4k")] + [LocalisableDescription(typeof(BeatmapsStrings), nameof(BeatmapsStrings.VariantMania4k))] FourKey, [EnumMember(Value = "7k")] + [LocalisableDescription(typeof(BeatmapsStrings), nameof(BeatmapsStrings.VariantMania7k))] SevenKey } @@ -151,12 +154,7 @@ namespace osu.Game.Users [JsonConverter(typeof(StringEnumConverter))] public GameVariant? VariantType; - public LocalisableString VariantDisplay => VariantType switch - { - GameVariant.FourKey => CommonStrings.FourKey, - GameVariant.SevenKey => CommonStrings.SevenKey, - _ => string.Empty - }; + public LocalisableString VariantDisplay => VariantType?.GetLocalisableDescription() ?? string.Empty; } } } From 3035e8435d3f7c45ffa58d31b425c286d6b5b4fc Mon Sep 17 00:00:00 2001 From: Joseph Madamba Date: Tue, 10 Dec 2024 15:02:41 -0800 Subject: [PATCH 0272/1275] Apply `NRT` to incoming changed files --- osu.Game/Beatmaps/DifficultyRecommender.cs | 10 +++------- .../BeatmapListing/BeatmapListingSearchControl.cs | 10 ++++------ .../BeatmapListing/BeatmapSearchGeneralFilterRow.cs | 12 +++++------- 3 files changed, 12 insertions(+), 20 deletions(-) diff --git a/osu.Game/Beatmaps/DifficultyRecommender.cs b/osu.Game/Beatmaps/DifficultyRecommender.cs index d132b86052..e50f877a9b 100644 --- a/osu.Game/Beatmaps/DifficultyRecommender.cs +++ b/osu.Game/Beatmaps/DifficultyRecommender.cs @@ -1,12 +1,9 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System; using System.Collections.Generic; using System.Linq; -using JetBrains.Annotations; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Extensions.ObjectExtensions; @@ -26,7 +23,7 @@ namespace osu.Game.Beatmaps private readonly LocalUserStatisticsProvider statisticsProvider; [Resolved] - private Bindable gameRuleset { get; set; } + private Bindable gameRuleset { get; set; } = null!; [Resolved] private RulesetStore rulesets { get; set; } = null!; @@ -90,15 +87,14 @@ namespace osu.Game.Beatmaps /// /// A collection of beatmaps to select a difficulty from. /// The recommended difficulty, or null if a recommendation could not be provided. - [CanBeNull] - public BeatmapInfo GetRecommendedBeatmap(IEnumerable beatmaps) + public BeatmapInfo? GetRecommendedBeatmap(IEnumerable beatmaps) { foreach (string r in orderedRulesets) { if (!recommendedDifficultyMapping.TryGetValue(r, out double recommendation)) continue; - BeatmapInfo beatmapInfo = beatmaps.Where(b => b.Ruleset.ShortName.Equals(r, StringComparison.Ordinal)).MinBy(b => + BeatmapInfo? beatmapInfo = beatmaps.Where(b => b.Ruleset.ShortName.Equals(r, StringComparison.Ordinal)).MinBy(b => { double difference = b.StarRating - recommendation; return difference >= 0 ? difference * 2 : difference * -1; // prefer easier over harder diff --git a/osu.Game/Overlays/BeatmapListing/BeatmapListingSearchControl.cs b/osu.Game/Overlays/BeatmapListing/BeatmapListingSearchControl.cs index bab64165cb..77a0e64fd1 100644 --- a/osu.Game/Overlays/BeatmapListing/BeatmapListingSearchControl.cs +++ b/osu.Game/Overlays/BeatmapListing/BeatmapListingSearchControl.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 osu.Framework.Allocation; using osu.Framework.Bindables; @@ -29,7 +27,7 @@ namespace osu.Game.Overlays.BeatmapListing /// /// Any time the text box receives key events (even while masked). /// - public Action TypingStarted; + public Action? TypingStarted; public Bindable Query => textBox.Current; @@ -51,7 +49,7 @@ namespace osu.Game.Overlays.BeatmapListing public Bindable ExplicitContent => explicitContentFilter.Current; - public APIBeatmapSet BeatmapSet + public APIBeatmapSet? BeatmapSet { set { @@ -151,7 +149,7 @@ namespace osu.Game.Overlays.BeatmapListing categoryFilter.Current.Value = SearchCategory.Leaderboard; } - private IBindable allowExplicitContent; + private IBindable allowExplicitContent = null!; [BackgroundDependencyLoader] private void load(OverlayColourProvider colourProvider, OsuConfigManager config) @@ -172,7 +170,7 @@ namespace osu.Game.Overlays.BeatmapListing /// /// Any time the text box receives key events (even while masked). /// - public Action TextChanged; + public Action? TextChanged; protected override Color4 SelectionColour => Color4.Gray; diff --git a/osu.Game/Overlays/BeatmapListing/BeatmapSearchGeneralFilterRow.cs b/osu.Game/Overlays/BeatmapListing/BeatmapSearchGeneralFilterRow.cs index 2d56c60de6..044910980d 100644 --- a/osu.Game/Overlays/BeatmapListing/BeatmapSearchGeneralFilterRow.cs +++ b/osu.Game/Overlays/BeatmapListing/BeatmapSearchGeneralFilterRow.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 osu.Framework.Allocation; using osu.Framework.Bindables; @@ -40,7 +38,7 @@ namespace osu.Game.Overlays.BeatmapListing private partial class FeaturedArtistsTabItem : MultipleSelectionFilterTabItem { - private Bindable disclaimerShown; + private Bindable disclaimerShown = null!; public FeaturedArtistsTabItem() : base(SearchGeneral.FeaturedArtists) @@ -48,13 +46,13 @@ namespace osu.Game.Overlays.BeatmapListing } [Resolved] - private OsuColour colours { get; set; } + private OsuColour colours { get; set; } = null!; [Resolved] - private SessionStatics sessionStatics { get; set; } + private SessionStatics sessionStatics { get; set; } = null!; - [Resolved(canBeNull: true)] - private IDialogOverlay dialogOverlay { get; set; } + [Resolved] + private IDialogOverlay? dialogOverlay { get; set; } protected override void LoadComplete() { From e95dc2b308fa186d75dcc1f6758d6b9d2d24fa18 Mon Sep 17 00:00:00 2001 From: Joseph Madamba Date: Tue, 10 Dec 2024 15:04:06 -0800 Subject: [PATCH 0273/1275] Add `FormatStarRating()` method util --- osu.Game.Tournament/Components/SongBar.cs | 3 ++- osu.Game/Beatmaps/Drawables/StarRatingDisplay.cs | 4 ++-- osu.Game/Overlays/BeatmapSet/BeatmapPicker.cs | 3 ++- osu.Game/Skinning/Components/BeatmapAttributeText.cs | 2 +- osu.Game/Utils/FormatUtils.cs | 6 ++++++ 5 files changed, 13 insertions(+), 5 deletions(-) diff --git a/osu.Game.Tournament/Components/SongBar.cs b/osu.Game.Tournament/Components/SongBar.cs index ae59e92e33..cff86cf0a1 100644 --- a/osu.Game.Tournament/Components/SongBar.cs +++ b/osu.Game.Tournament/Components/SongBar.cs @@ -15,6 +15,7 @@ using osu.Game.Graphics; using osu.Game.Models; using osu.Game.Rulesets; using osu.Game.Screens.Menu; +using osu.Game.Utils; using osuTK; using osuTK.Graphics; @@ -207,7 +208,7 @@ namespace osu.Game.Tournament.Components Children = new Drawable[] { new DiffPiece(stats), - new DiffPiece(("Star Rating", $"{beatmap.StarRating:0.00}{srExtra}")) + new DiffPiece(("Star Rating", $"{beatmap.StarRating.FormatStarRating()}{srExtra}")) } }, new FillFlowContainer diff --git a/osu.Game/Beatmaps/Drawables/StarRatingDisplay.cs b/osu.Game/Beatmaps/Drawables/StarRatingDisplay.cs index 55ef6f705e..4119ffb636 100644 --- a/osu.Game/Beatmaps/Drawables/StarRatingDisplay.cs +++ b/osu.Game/Beatmaps/Drawables/StarRatingDisplay.cs @@ -5,7 +5,6 @@ using System; 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.Containers; using osu.Framework.Graphics.Shapes; @@ -14,6 +13,7 @@ using osu.Framework.Graphics.UserInterface; using osu.Game.Graphics; using osu.Game.Graphics.Sprites; using osu.Game.Overlays; +using osu.Game.Utils; using osuTK; using osuTK.Graphics; @@ -156,7 +156,7 @@ namespace osu.Game.Beatmaps.Drawables displayedStars.BindValueChanged(s => { - starsText.Text = s.NewValue < 0 ? "-" : s.NewValue.ToLocalisableString("0.00"); + starsText.Text = s.NewValue < 0 ? "-" : s.NewValue.FormatStarRating(); background.Colour = colours.ForStarDifficulty(s.NewValue); diff --git a/osu.Game/Overlays/BeatmapSet/BeatmapPicker.cs b/osu.Game/Overlays/BeatmapSet/BeatmapPicker.cs index 5f021803b0..a7838651a9 100644 --- a/osu.Game/Overlays/BeatmapSet/BeatmapPicker.cs +++ b/osu.Game/Overlays/BeatmapSet/BeatmapPicker.cs @@ -21,6 +21,7 @@ using osu.Game.Graphics.Sprites; using osu.Game.Online.API.Requests.Responses; using osu.Game.Resources.Localisation.Web; using osu.Game.Rulesets; +using osu.Game.Utils; using osuTK; namespace osu.Game.Overlays.BeatmapSet @@ -185,7 +186,7 @@ namespace osu.Game.Overlays.BeatmapSet OnHovered = beatmap => { showBeatmap(beatmap); - starRating.Text = beatmap.StarRating.ToLocalisableString(@"0.00"); + starRating.Text = beatmap.StarRating.FormatStarRating(); starRatingContainer.FadeIn(100); }, OnClicked = beatmap => { Beatmap.Value = beatmap; }, diff --git a/osu.Game/Skinning/Components/BeatmapAttributeText.cs b/osu.Game/Skinning/Components/BeatmapAttributeText.cs index f1c27434fa..d9f7eedfb5 100644 --- a/osu.Game/Skinning/Components/BeatmapAttributeText.cs +++ b/osu.Game/Skinning/Components/BeatmapAttributeText.cs @@ -226,7 +226,7 @@ namespace osu.Game.Skinning.Components return computeDifficulty().ApproachRate.ToLocalisableString(@"0.##"); case BeatmapAttribute.StarRating: - return (starDifficulty?.Stars ?? 0).ToLocalisableString(@"F2"); + return (starDifficulty?.Stars ?? 0).FormatStarRating(); case BeatmapAttribute.MaxPP: return Math.Round(starDifficulty?.PerformanceAttributes?.Total ?? 0, MidpointRounding.AwayFromZero).ToLocalisableString(); diff --git a/osu.Game/Utils/FormatUtils.cs b/osu.Game/Utils/FormatUtils.cs index cccad3711c..e93a494b65 100644 --- a/osu.Game/Utils/FormatUtils.cs +++ b/osu.Game/Utils/FormatUtils.cs @@ -32,6 +32,12 @@ namespace osu.Game.Utils /// The rank/position to be formatted. public static string FormatRank(this int rank) => rank.ToMetric(decimals: rank < 100_000 ? 1 : 0); + /// + /// Formats the supplied star rating in a consistent, simplified way. + /// + /// The star rating to be formatted. + public static LocalisableString FormatStarRating(this double starRating) => starRating.ToLocalisableString("0.00"); + /// /// Finds the number of digits after the decimal. /// From 92e07b4f99c8610848ec2509a9f20c846d84e3b7 Mon Sep 17 00:00:00 2001 From: Joseph Madamba Date: Tue, 10 Dec 2024 15:09:11 -0800 Subject: [PATCH 0274/1275] Add recommended difficulty numerical value near filter in beatmap listing --- osu.Game/Beatmaps/DifficultyRecommender.cs | 6 ++ .../BeatmapListingSearchControl.cs | 9 ++- .../BeatmapSearchGeneralFilterRow.cs | 68 +++++++++++++++++-- 3 files changed, 78 insertions(+), 5 deletions(-) diff --git a/osu.Game/Beatmaps/DifficultyRecommender.cs b/osu.Game/Beatmaps/DifficultyRecommender.cs index e50f877a9b..bd864422d1 100644 --- a/osu.Game/Beatmaps/DifficultyRecommender.cs +++ b/osu.Game/Beatmaps/DifficultyRecommender.cs @@ -20,6 +20,8 @@ namespace osu.Game.Beatmaps /// public partial class DifficultyRecommender : Component { + public event Action? StarRatingUpdated; + private readonly LocalUserStatisticsProvider statisticsProvider; [Resolved] @@ -77,8 +79,12 @@ namespace osu.Game.Beatmaps { // algorithm taken from https://github.com/ppy/osu-web/blob/e6e2825516449e3d0f3f5e1852c6bdd3428c3437/app/Models/User.php#L1505 recommendedDifficultyMapping[ruleset.ShortName] = Math.Pow((double)(statistics.PP ?? 0), 0.4) * 0.195; + + StarRatingUpdated?.Invoke(); } + public double GetRecommendedStarRatingFor(RulesetInfo ruleset) => recommendedDifficultyMapping[ruleset.ShortName]; + /// /// Find the recommended difficulty from a selection of available difficulties for the current local user. /// diff --git a/osu.Game/Overlays/BeatmapListing/BeatmapListingSearchControl.cs b/osu.Game/Overlays/BeatmapListing/BeatmapListingSearchControl.cs index 77a0e64fd1..72d7e0c752 100644 --- a/osu.Game/Overlays/BeatmapListing/BeatmapListingSearchControl.cs +++ b/osu.Game/Overlays/BeatmapListing/BeatmapListingSearchControl.cs @@ -65,7 +65,7 @@ namespace osu.Game.Overlays.BeatmapListing } private readonly BeatmapSearchTextBox textBox; - private readonly BeatmapSearchMultipleSelectionFilterRow generalFilter; + private readonly BeatmapSearchGeneralFilterRow generalFilter; private readonly BeatmapSearchRulesetFilterRow modeFilter; private readonly BeatmapSearchFilterRow categoryFilter; private readonly BeatmapSearchFilterRow genreFilter; @@ -163,6 +163,13 @@ namespace osu.Game.Overlays.BeatmapListing }, true); } + protected override void LoadComplete() + { + base.LoadComplete(); + + generalFilter.Ruleset.BindTo(Ruleset); + } + public void TakeFocus() => textBox.TakeFocus(); private partial class BeatmapSearchTextBox : BasicSearchTextBox diff --git a/osu.Game/Overlays/BeatmapListing/BeatmapSearchGeneralFilterRow.cs b/osu.Game/Overlays/BeatmapListing/BeatmapSearchGeneralFilterRow.cs index 044910980d..42d788dad7 100644 --- a/osu.Game/Overlays/BeatmapListing/BeatmapSearchGeneralFilterRow.cs +++ b/osu.Game/Overlays/BeatmapListing/BeatmapSearchGeneralFilterRow.cs @@ -4,13 +4,20 @@ using System; using osu.Framework.Allocation; using osu.Framework.Bindables; +using osu.Framework.Extensions; using osu.Framework.Graphics.Sprites; using osu.Framework.Input.Events; +using osu.Framework.Localisation; +using osu.Game.Beatmaps; using osu.Game.Configuration; +using osu.Game.Extensions; using osu.Game.Graphics; using osu.Game.Localisation; +using osu.Game.Online.API; using osu.Game.Overlays.Dialog; using osu.Game.Resources.Localisation.Web; +using osu.Game.Rulesets; +using osu.Game.Utils; using osuTK.Graphics; using CommonStrings = osu.Game.Resources.Localisation.Web.CommonStrings; @@ -18,21 +25,74 @@ namespace osu.Game.Overlays.BeatmapListing { public partial class BeatmapSearchGeneralFilterRow : BeatmapSearchMultipleSelectionFilterRow { + public readonly IBindable Ruleset = new Bindable(); + public BeatmapSearchGeneralFilterRow() : base(BeatmapsStrings.ListingSearchFiltersGeneral) { } - protected override MultipleSelectionFilter CreateMultipleSelectionFilter() => new GeneralFilter(); + protected override MultipleSelectionFilter CreateMultipleSelectionFilter() => new GeneralFilter + { + Ruleset = { BindTarget = Ruleset } + }; private partial class GeneralFilter : MultipleSelectionFilter { + public readonly IBindable Ruleset = new Bindable(); + protected override MultipleSelectionFilterTabItem CreateTabItem(SearchGeneral value) { - if (value == SearchGeneral.FeaturedArtists) - return new FeaturedArtistsTabItem(); + switch (value) + { + case SearchGeneral.Recommended: + return new RecommendedDifficultyTabItem + { + Ruleset = { BindTarget = Ruleset } + }; - return new MultipleSelectionFilterTabItem(value); + case SearchGeneral.FeaturedArtists: + return new FeaturedArtistsTabItem(); + + default: + return new MultipleSelectionFilterTabItem(value); + } + } + } + + private partial class RecommendedDifficultyTabItem : MultipleSelectionFilterTabItem + { + public readonly IBindable Ruleset = new Bindable(); + + [Resolved] + private DifficultyRecommender? recommender { get; set; } + + [Resolved] + private IAPIProvider api { get; set; } = null!; + + [Resolved] + private RulesetStore rulesets { get; set; } = null!; + + public RecommendedDifficultyTabItem() + : base(SearchGeneral.Recommended) + { + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + if (recommender != null) recommender.StarRatingUpdated += updateText; + + Ruleset.BindValueChanged(_ => updateText(), true); + } + + private void updateText() + { + // fallback to profile default game mode if beatmap listing mode filter is set to Any + // TODO: find a way to update `PlayMode` when the profile default game mode has changed + var ruleset = Ruleset.Value.IsLegacyRuleset() ? Ruleset.Value : rulesets.GetRuleset(api.LocalUser.Value.PlayMode)!; + Text.Text = LocalisableString.Interpolate($"{Value.GetLocalisableDescription()} ({recommender?.GetRecommendedStarRatingFor(ruleset).FormatStarRating()})"); } } From f7364de01af250ec7e292702086544b6b4fe8f36 Mon Sep 17 00:00:00 2001 From: Joseph Madamba Date: Tue, 10 Dec 2024 18:18:34 -0800 Subject: [PATCH 0275/1275] Add test and null protections --- .../TestSceneBeatmapRecommendations.cs | 44 +++++++++++++++++++ osu.Game/Beatmaps/DifficultyRecommender.cs | 3 +- .../BeatmapSearchGeneralFilterRow.cs | 12 ++++- 3 files changed, 56 insertions(+), 3 deletions(-) diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapRecommendations.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapRecommendations.cs index bd5c43d242..4c8c1d7ad2 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapRecommendations.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapRecommendations.cs @@ -13,9 +13,12 @@ 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; using osu.Game.Online.API.Requests.Responses; +using osu.Game.Overlays; +using osu.Game.Overlays.BeatmapListing; using osu.Game.Rulesets; using osu.Game.Rulesets.Catch; using osu.Game.Rulesets.Mania; @@ -23,6 +26,8 @@ using osu.Game.Rulesets.Osu; using osu.Game.Rulesets.Taiko; using osu.Game.Tests.Resources; using osu.Game.Users; +using osu.Game.Utils; +using osuTK.Input; namespace osu.Game.Tests.Visual.SongSelect { @@ -170,6 +175,45 @@ namespace osu.Game.Tests.Visual.SongSelect presentAndConfirm(() => maniaSet, 5); } + [Test] + public void TestBeatmapListingFilter() + { + AddStep("set playmode to taiko", () => ((DummyAPIAccess)API).LocalUser.Value.PlayMode = "taiko"); + + AddStep("open beatmap listing", () => + { + InputManager.PressKey(Key.ControlLeft); + InputManager.PressKey(Key.B); + InputManager.ReleaseKey(Key.B); + InputManager.ReleaseKey(Key.ControlLeft); + }); + + AddUntilStep("wait for load", () => Game.ChildrenOfType().SingleOrDefault()?.IsLoaded, () => Is.True); + + checkRecommendedDifficulty(3); + + AddStep("change mode filter to osu!", () => Game.ChildrenOfType().Single().ChildrenOfType>().ElementAt(1).TriggerClick()); + + checkRecommendedDifficulty(2); + + AddStep("change mode filter to osu!taiko", () => Game.ChildrenOfType().Single().ChildrenOfType>().ElementAt(2).TriggerClick()); + + checkRecommendedDifficulty(3); + + AddStep("change mode filter to osu!catch", () => Game.ChildrenOfType().Single().ChildrenOfType>().ElementAt(3).TriggerClick()); + + checkRecommendedDifficulty(4); + + AddStep("change mode filter to osu!mania", () => Game.ChildrenOfType().Single().ChildrenOfType>().ElementAt(4).TriggerClick()); + + checkRecommendedDifficulty(5); + + void checkRecommendedDifficulty(double starRating) + => AddAssert($"recommended difficulty is {starRating}", + () => Game.ChildrenOfType().Single().ChildrenOfType().ElementAt(1).Text.ToString(), + () => Is.EqualTo($"Recommended difficulty ({starRating.FormatStarRating()})")); + } + private BeatmapSetInfo importBeatmapSet(IEnumerable difficultyRulesets) { var rulesets = difficultyRulesets.ToArray(); diff --git a/osu.Game/Beatmaps/DifficultyRecommender.cs b/osu.Game/Beatmaps/DifficultyRecommender.cs index bd864422d1..a5c7371b4d 100644 --- a/osu.Game/Beatmaps/DifficultyRecommender.cs +++ b/osu.Game/Beatmaps/DifficultyRecommender.cs @@ -83,7 +83,8 @@ namespace osu.Game.Beatmaps StarRatingUpdated?.Invoke(); } - public double GetRecommendedStarRatingFor(RulesetInfo ruleset) => recommendedDifficultyMapping[ruleset.ShortName]; + public double? GetRecommendedStarRatingFor(RulesetInfo ruleset) + => recommendedDifficultyMapping.TryGetValue(ruleset.ShortName, out double starRating) ? starRating : null; /// /// Find the recommended difficulty from a selection of available difficulties for the current local user. diff --git a/osu.Game/Overlays/BeatmapListing/BeatmapSearchGeneralFilterRow.cs b/osu.Game/Overlays/BeatmapListing/BeatmapSearchGeneralFilterRow.cs index 42d788dad7..66a0a16549 100644 --- a/osu.Game/Overlays/BeatmapListing/BeatmapSearchGeneralFilterRow.cs +++ b/osu.Game/Overlays/BeatmapListing/BeatmapSearchGeneralFilterRow.cs @@ -91,8 +91,16 @@ namespace osu.Game.Overlays.BeatmapListing { // fallback to profile default game mode if beatmap listing mode filter is set to Any // TODO: find a way to update `PlayMode` when the profile default game mode has changed - var ruleset = Ruleset.Value.IsLegacyRuleset() ? Ruleset.Value : rulesets.GetRuleset(api.LocalUser.Value.PlayMode)!; - Text.Text = LocalisableString.Interpolate($"{Value.GetLocalisableDescription()} ({recommender?.GetRecommendedStarRatingFor(ruleset).FormatStarRating()})"); + RulesetInfo? ruleset = Ruleset.Value.IsLegacyRuleset() ? Ruleset.Value : rulesets.GetRuleset(api.LocalUser.Value.PlayMode); + + if (ruleset == null) return; + + double? starRating = recommender?.GetRecommendedStarRatingFor(ruleset); + + if (starRating != null) + Text.Text = LocalisableString.Interpolate($"{Value.GetLocalisableDescription()} ({starRating.Value.FormatStarRating()})"); + else + Text.Text = Value.GetLocalisableDescription(); } } From 38b3d5fc00e7d38d35a88cf52e5d611ff39abfdb Mon Sep 17 00:00:00 2001 From: Joseph Madamba Date: Thu, 12 Dec 2024 16:17:57 -0800 Subject: [PATCH 0276/1275] Update recommended difficulty for osu!taiko --- osu.Game/Beatmaps/DifficultyRecommender.cs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/osu.Game/Beatmaps/DifficultyRecommender.cs b/osu.Game/Beatmaps/DifficultyRecommender.cs index d132b86052..31c9fcafe6 100644 --- a/osu.Game/Beatmaps/DifficultyRecommender.cs +++ b/osu.Game/Beatmaps/DifficultyRecommender.cs @@ -78,8 +78,11 @@ namespace osu.Game.Beatmaps private void updateMapping(RulesetInfo ruleset, UserStatistics statistics) { - // algorithm taken from https://github.com/ppy/osu-web/blob/e6e2825516449e3d0f3f5e1852c6bdd3428c3437/app/Models/User.php#L1505 - recommendedDifficultyMapping[ruleset.ShortName] = Math.Pow((double)(statistics.PP ?? 0), 0.4) * 0.195; + // algorithm taken from https://github.com/ppy/osu-web/blob/027026fccc91525e39cee5d2f369f1b343eb1bf1/app/Models/UserStatistics/Model.php#L93-L94 + recommendedDifficultyMapping[ruleset.ShortName] = + ruleset.ShortName == @"taiko" + ? Math.Pow((double)(statistics.PP ?? 0), 0.35) * 0.27 + : Math.Pow((double)(statistics.PP ?? 0), 0.4) * 0.195; } /// From 313de33986467f12b8c7e0847f757be2718f1bdd Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 13 Dec 2024 15:42:30 +0900 Subject: [PATCH 0277/1275] Adjust padding to avoid wrapping on checkbox text --- .../OnlinePlay/Multiplayer/MultiplayerLoungeSubScreen.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerLoungeSubScreen.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerLoungeSubScreen.cs index 7f7f94504f..dd61caa3db 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerLoungeSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerLoungeSubScreen.cs @@ -63,7 +63,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer { LabelText = "Show in-progress rooms", RelativeSizeAxes = Axes.None, - Width = 200, + Width = 220, Padding = new MarginPadding { Vertical = 5, }, Current = Config.GetBindable(OsuSetting.MultiplayerShowInProgressFilter), }; From 12e5999700bf1a6082b3fbc64a372bf2164e158a Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Fri, 13 Dec 2024 15:53:27 +0900 Subject: [PATCH 0278/1275] 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 0279/1275] 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 7bb1a5118e26d761b89e3b7e27a10c5a3c8ffb08 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Fri, 13 Dec 2024 16:39:16 +0900 Subject: [PATCH 0280/1275] Unbind event on disposal --- .../BeatmapListing/BeatmapSearchGeneralFilterRow.cs | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/osu.Game/Overlays/BeatmapListing/BeatmapSearchGeneralFilterRow.cs b/osu.Game/Overlays/BeatmapListing/BeatmapSearchGeneralFilterRow.cs index 66a0a16549..34b7d45a77 100644 --- a/osu.Game/Overlays/BeatmapListing/BeatmapSearchGeneralFilterRow.cs +++ b/osu.Game/Overlays/BeatmapListing/BeatmapSearchGeneralFilterRow.cs @@ -82,7 +82,8 @@ namespace osu.Game.Overlays.BeatmapListing { base.LoadComplete(); - if (recommender != null) recommender.StarRatingUpdated += updateText; + if (recommender != null) + recommender.StarRatingUpdated += updateText; Ruleset.BindValueChanged(_ => updateText(), true); } @@ -102,6 +103,14 @@ namespace osu.Game.Overlays.BeatmapListing else Text.Text = Value.GetLocalisableDescription(); } + + protected override void Dispose(bool isDisposing) + { + base.Dispose(isDisposing); + + if (recommender != null) + recommender.StarRatingUpdated -= updateText; + } } private partial class FeaturedArtistsTabItem : MultipleSelectionFilterTabItem From b470e30cc0efb2d40ed579aa63bcc3a1955b0d43 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 13 Dec 2024 17:17:52 +0900 Subject: [PATCH 0281/1275] Add failing test showing player settings appearing in skin editor --- .../TestSceneSkinEditorNavigation.cs | 28 +++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/osu.Game.Tests/Visual/Navigation/TestSceneSkinEditorNavigation.cs b/osu.Game.Tests/Visual/Navigation/TestSceneSkinEditorNavigation.cs index 5267a57a05..0af4dacb92 100644 --- a/osu.Game.Tests/Visual/Navigation/TestSceneSkinEditorNavigation.cs +++ b/osu.Game.Tests/Visual/Navigation/TestSceneSkinEditorNavigation.cs @@ -23,6 +23,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; @@ -212,6 +213,33 @@ namespace osu.Game.Tests.Visual.Navigation AddAssert("no mod selected", () => !((Player)Game.ScreenStack.CurrentScreen).Mods.Value.Any()); } + [Test] + public void TestGameplaySettingsDoesNotExpandWhenSkinOverlayPresent() + { + advanceToSongSelect(); + openSkinEditor(); + AddStep("select autoplay", () => Game.SelectedMods.Value = new Mod[] { new OsuModAutoplay() }); + AddStep("import beatmap", () => BeatmapImportHelper.LoadQuickOszIntoOsu(Game).WaitSafely()); + AddUntilStep("wait for selected", () => !Game.Beatmap.IsDefault); + switchToGameplayScene(); + + AddUntilStep("wait for settings", () => getPlayerSettingsOverlay() != null); + AddAssert("settings not visible", () => getPlayerSettingsOverlay().DrawWidth, () => Is.EqualTo(0)); + + AddStep("move cursor to right of screen", () => InputManager.MoveMouseTo(InputManager.ScreenSpaceDrawQuad.TopRight)); + AddAssert("settings not visible", () => getPlayerSettingsOverlay().DrawWidth, () => Is.EqualTo(0)); + + toggleSkinEditor(); + + AddStep("move cursor slightly", () => InputManager.MoveMouseTo(InputManager.ScreenSpaceDrawQuad.TopRight + new Vector2(1))); + AddUntilStep("settings visible", () => getPlayerSettingsOverlay().DrawWidth, () => Is.GreaterThan(0)); + + AddStep("move cursor to right of screen too far", () => InputManager.MoveMouseTo(InputManager.ScreenSpaceDrawQuad.TopRight + new Vector2(10240, 0))); + AddUntilStep("settings not visible", () => getPlayerSettingsOverlay().DrawWidth, () => Is.EqualTo(0)); + + PlayerSettingsOverlay getPlayerSettingsOverlay() => ((Player)Game.ScreenStack.CurrentScreen).ChildrenOfType().SingleOrDefault(); + } + [Test] public void TestCinemaModRemovedOnEnteringGameplay() { From 1e809c7f16dcd5e7b543f82e75cc791189e57209 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 13 Dec 2024 17:18:00 +0900 Subject: [PATCH 0282/1275] Fix player settings overlay appearing while in skin editor --- .../Screens/Play/HUD/PlayerSettingsOverlay.cs | 22 ++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/osu.Game/Screens/Play/HUD/PlayerSettingsOverlay.cs b/osu.Game/Screens/Play/HUD/PlayerSettingsOverlay.cs index 18d7f6a503..d2fb2e719a 100644 --- a/osu.Game/Screens/Play/HUD/PlayerSettingsOverlay.cs +++ b/osu.Game/Screens/Play/HUD/PlayerSettingsOverlay.cs @@ -86,11 +86,31 @@ namespace osu.Game.Screens.Play.HUD inputManager = GetContainingInputManager()!; } + public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => + screenSpacePos.X > button.ScreenSpaceDrawQuad.TopLeft.X; + + protected override bool OnMouseMove(MouseMoveEvent e) + { + checkExpanded(); + return base.OnMouseMove(e); + } + protected override void Update() { base.Update(); - Expanded.Value = inputManager.CurrentState.Mouse.Position.X >= button.ScreenSpaceDrawQuad.TopLeft.X; + // Only check expanded if already expanded. + // This is because if we are always checking, it would bypass blocking overlays. + // Case in point: the skin editor overlay blocks input from reaching the player, but checking raw coordinates would make settings pop out. + if (Expanded.Value) + checkExpanded(); + } + + private void checkExpanded() + { + float screenMouseX = inputManager.CurrentState.Mouse.Position.X; + + Expanded.Value = screenMouseX >= button.ScreenSpaceDrawQuad.TopLeft.X && screenMouseX <= ToScreenSpace(new Vector2(DrawWidth + EXPANDED_WIDTH, 0)).X; } protected override void OnHoverLost(HoverLostEvent e) From a796af95110d2f21a4f1431d3748ca5c2a0f68e5 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 13 Dec 2024 17:28:15 +0900 Subject: [PATCH 0283/1275] Fix player settings overlay cog overlapping skin elements This brings it down to be in line with the flowing elements that usually do their best to not get in the way. Decided against putting it in the `HUDOverlay` flow for simplicity. It will work fine until it doesn't. --- osu.Game/Screens/Play/HUD/PlayerSettingsOverlay.cs | 7 +++++++ osu.Game/Screens/Play/HUDOverlay.cs | 11 ++++++----- 2 files changed, 13 insertions(+), 5 deletions(-) diff --git a/osu.Game/Screens/Play/HUD/PlayerSettingsOverlay.cs b/osu.Game/Screens/Play/HUD/PlayerSettingsOverlay.cs index d2fb2e719a..1cac4db021 100644 --- a/osu.Game/Screens/Play/HUD/PlayerSettingsOverlay.cs +++ b/osu.Game/Screens/Play/HUD/PlayerSettingsOverlay.cs @@ -1,6 +1,7 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using osu.Framework.Allocation; using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Colour; @@ -43,6 +44,9 @@ namespace osu.Game.Screens.Play.HUD private InputManager inputManager = null!; + [Resolved] + private HUDOverlay? hudOverlay { get; set; } + public PlayerSettingsOverlay() : base(0, EXPANDED_WIDTH) { @@ -99,6 +103,9 @@ namespace osu.Game.Screens.Play.HUD { base.Update(); + if (hudOverlay != null) + button.Y = ToLocalSpace(hudOverlay.TopRightElements.ScreenSpaceDrawQuad.BottomRight).Y; + // Only check expanded if already expanded. // This is because if we are always checking, it would bypass blocking overlays. // Case in point: the skin editor overlay blocks input from reaching the player, but checking raw coordinates would make settings pop out. diff --git a/osu.Game/Screens/Play/HUDOverlay.cs b/osu.Game/Screens/Play/HUDOverlay.cs index fca871e42f..ad165d7d9f 100644 --- a/osu.Game/Screens/Play/HUDOverlay.cs +++ b/osu.Game/Screens/Play/HUDOverlay.cs @@ -87,7 +87,8 @@ namespace osu.Game.Screens.Play private static bool hasShownNotificationOnce; private readonly FillFlowContainer bottomRightElements; - private readonly FillFlowContainer topRightElements; + + internal readonly FillFlowContainer TopRightElements; internal readonly IBindable IsPlaying = new Bindable(); @@ -136,7 +137,7 @@ namespace osu.Game.Screens.Play PlayfieldSkinLayer = drawableRuleset != null ? new SkinnableContainer(new GlobalSkinnableContainerLookup(GlobalSkinnableContainers.Playfield, drawableRuleset.Ruleset.RulesetInfo)) { AlwaysPresent = true, } : Empty(), - topRightElements = new FillFlowContainer + TopRightElements = new FillFlowContainer { Anchor = Anchor.TopRight, Origin = Anchor.TopRight, @@ -182,7 +183,7 @@ namespace osu.Game.Screens.Play }, }; - hideTargets = new List { mainComponents, topRightElements, rightSettings }; + hideTargets = new List { mainComponents, TopRightElements, rightSettings }; if (rulesetComponents != null) hideTargets.Add(rulesetComponents); @@ -275,9 +276,9 @@ namespace osu.Game.Screens.Play processDrawables(rulesetComponents); if (lowestTopScreenSpaceRight.HasValue) - topRightElements.Y = MathHelper.Clamp(ToLocalSpace(new Vector2(0, lowestTopScreenSpaceRight.Value)).Y, 0, DrawHeight - topRightElements.DrawHeight); + TopRightElements.Y = MathHelper.Clamp(ToLocalSpace(new Vector2(0, lowestTopScreenSpaceRight.Value)).Y, 0, DrawHeight - TopRightElements.DrawHeight); else - topRightElements.Y = 0; + TopRightElements.Y = 0; if (lowestTopScreenSpaceLeft.HasValue) LeaderboardFlow.Y = MathHelper.Clamp(ToLocalSpace(new Vector2(0, lowestTopScreenSpaceLeft.Value)).Y, 0, DrawHeight - LeaderboardFlow.DrawHeight); From fdc41ace7e8e8a442c311615b62dd36de3f2eb65 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 13 Dec 2024 17:33:49 +0900 Subject: [PATCH 0284/1275] Fix flaky editor beatmap creation test As seen in https://github.com/ppy/osu/actions/runs/12289146465/job/34294167417#step:5:1588 or https://github.com/ppy/osu/actions/runs/12310133160/job/34358241666#step:5:53. Exception messages hint pretty strongly at this being a threading issue and there does seem to be a rather frivolous lack of waiting for `CreateNewDifficulty()` to do its thing, so I'm thinking maybe this will help. --- .../Editing/TestSceneEditorBeatmapCreation.cs | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/osu.Game.Tests/Visual/Editing/TestSceneEditorBeatmapCreation.cs b/osu.Game.Tests/Visual/Editing/TestSceneEditorBeatmapCreation.cs index 32d019dd9f..b7990b64c1 100644 --- a/osu.Game.Tests/Visual/Editing/TestSceneEditorBeatmapCreation.cs +++ b/osu.Game.Tests/Visual/Editing/TestSceneEditorBeatmapCreation.cs @@ -203,12 +203,19 @@ namespace osu.Game.Tests.Visual.Editing [Test] public void TestCreateNewDifficultyWithScrollSpeed_SameRuleset() { - string firstDifficultyName = Guid.NewGuid().ToString(); + string previousDifficultyName = null!; + AddStep("set unique difficulty name", () => EditorBeatmap.BeatmapInfo.DifficultyName = previousDifficultyName = Guid.NewGuid().ToString()); AddStep("save beatmap", () => Editor.Save()); AddStep("create new difficulty", () => Editor.CreateNewDifficulty(new ManiaRuleset().RulesetInfo)); - AddStep("set unique difficulty name", () => EditorBeatmap.BeatmapInfo.DifficultyName = firstDifficultyName); + AddUntilStep("wait for created", () => + { + string? difficultyName = Editor.ChildrenOfType().SingleOrDefault()?.BeatmapInfo.DifficultyName; + return difficultyName != null && difficultyName != previousDifficultyName; + }); + + AddStep("set unique difficulty name", () => EditorBeatmap.BeatmapInfo.DifficultyName = previousDifficultyName = Guid.NewGuid().ToString()); AddStep("add timing point", () => EditorBeatmap.ControlPointInfo.Add(0, new TimingControlPoint { BeatLength = 1000 })); AddStep("add effect points", () => { @@ -229,7 +236,7 @@ namespace osu.Game.Tests.Visual.Editing AddUntilStep("wait for created", () => { string? difficultyName = Editor.ChildrenOfType().SingleOrDefault()?.BeatmapInfo.DifficultyName; - return difficultyName != null && difficultyName != firstDifficultyName; + return difficultyName != null && difficultyName != previousDifficultyName; }); AddAssert("created difficulty has timing point", () => From edbaaa94685a1b934b2c889299c4e4ac67e3df2b Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Fri, 13 Dec 2024 17:41:55 +0900 Subject: [PATCH 0285/1275] Fix test --- .../Visual/SongSelect/TestSceneBeatmapRecommendations.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapRecommendations.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapRecommendations.cs index 4c8c1d7ad2..aa452101bf 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapRecommendations.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapRecommendations.cs @@ -68,7 +68,7 @@ namespace osu.Game.Tests.Visual.SongSelect return 336; // recommended star rating of 2 case 1: - return 928; // SR 3 + return 973; // SR 3 case 2: return 1905; // SR 4 From 0e0d96829f45c65eb0befea8e7a35b7e0208f68a Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 13 Dec 2024 18:08:29 +0900 Subject: [PATCH 0286/1275] Fix "quick retry" hotkey not working for autoplay --- osu.Game/Screens/Play/ReplayPlayer.cs | 11 +++++++++-- osu.Game/Screens/Ranking/RetryButton.cs | 9 +++++++-- 2 files changed, 16 insertions(+), 4 deletions(-) diff --git a/osu.Game/Screens/Play/ReplayPlayer.cs b/osu.Game/Screens/Play/ReplayPlayer.cs index 0c125264a1..c1b5397e61 100644 --- a/osu.Game/Screens/Play/ReplayPlayer.cs +++ b/osu.Game/Screens/Play/ReplayPlayer.cs @@ -34,10 +34,12 @@ namespace osu.Game.Screens.Play protected override UserActivity InitialActivity => new UserActivity.WatchingReplay(Score.ScoreInfo); + private bool isAutoplayPlayback => GameplayState.Mods.OfType().Any(); + // Disallow replays from failing. (see https://github.com/ppy/osu/issues/6108) protected override bool CheckModsAllowFailure() { - if (!replayIsFailedScore && !GameplayState.Mods.OfType().Any()) + if (!replayIsFailedScore && !isAutoplayPlayback) return false; return base.CheckModsAllowFailure(); @@ -102,7 +104,12 @@ namespace osu.Game.Screens.Play Scores = { BindTarget = LeaderboardScores } }; - protected override ResultsScreen CreateResults(ScoreInfo score) => new SoloResultsScreen(score); + protected override ResultsScreen CreateResults(ScoreInfo score) => new SoloResultsScreen(score) + { + // Only show the relevant button otherwise things look silly. + AllowWatchingReplay = !isAutoplayPlayback, + AllowRetry = isAutoplayPlayback, + }; public bool OnPressed(KeyBindingPressEvent e) { diff --git a/osu.Game/Screens/Ranking/RetryButton.cs b/osu.Game/Screens/Ranking/RetryButton.cs index d977f25323..8b4f3ca14c 100644 --- a/osu.Game/Screens/Ranking/RetryButton.cs +++ b/osu.Game/Screens/Ranking/RetryButton.cs @@ -38,8 +38,6 @@ namespace osu.Game.Screens.Ranking Icon = FontAwesome.Solid.Redo, }, }; - - TooltipText = "retry"; } [BackgroundDependencyLoader] @@ -48,7 +46,14 @@ namespace osu.Game.Screens.Ranking background.Colour = colours.Green; if (player != null) + { + TooltipText = player is ReplayPlayer ? "replay" : "retry"; Action = () => player.Restart(); + } + else + { + TooltipText = "retry"; + } } } } From d00bc4bdd1e97a1400de9ca95a6b1167334cf16a Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 13 Dec 2024 18:14:45 +0900 Subject: [PATCH 0287/1275] Also allow using "quick retry" for other replays --- osu.Game/Screens/Ranking/ResultsScreen.cs | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/osu.Game/Screens/Ranking/ResultsScreen.cs b/osu.Game/Screens/Ranking/ResultsScreen.cs index 507d138d90..e3284aac70 100644 --- a/osu.Game/Screens/Ranking/ResultsScreen.cs +++ b/osu.Game/Screens/Ranking/ResultsScreen.cs @@ -84,6 +84,7 @@ namespace osu.Game.Screens.Ranking /// public bool ShowUserStatistics { get; init; } + // Only show the relevant button otherwise things look silly. private Sample? popInSample; protected ResultsScreen(ScoreInfo? score) @@ -186,6 +187,8 @@ namespace osu.Game.Screens.Ranking Scheduler.AddDelayed(() => OverlayActivationMode.Value = OverlayActivation.All, shouldFlair ? AccuracyCircle.TOTAL_DURATION + 1000 : 0); } + bool allowHotkeyRetry = false; + if (AllowWatchingReplay) { buttons.Add(new ReplayDownloadButton(SelectedScore.Value) @@ -193,12 +196,19 @@ namespace osu.Game.Screens.Ranking Score = { BindTarget = SelectedScore }, Width = 300 }); + + // for simplicity, only allow when we're guaranteed the replay is already downloaded and present. + allowHotkeyRetry = player is ReplayPlayer; } if (player != null && AllowRetry) { buttons.Add(new RetryButton { Width = 300 }); + allowHotkeyRetry = true; + } + if (allowHotkeyRetry) + { AddInternal(new HotkeyRetryOverlay { Action = () => From 4b0cdd761dd82ba8153c32848b59108a6748b552 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 13 Dec 2024 18:58:10 +0900 Subject: [PATCH 0288/1275] Add note about player settings overlay button --- osu.Game/Screens/Play/HUD/PlayerSettingsOverlay.cs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/osu.Game/Screens/Play/HUD/PlayerSettingsOverlay.cs b/osu.Game/Screens/Play/HUD/PlayerSettingsOverlay.cs index d2fb2e719a..2968602564 100644 --- a/osu.Game/Screens/Play/HUD/PlayerSettingsOverlay.cs +++ b/osu.Game/Screens/Play/HUD/PlayerSettingsOverlay.cs @@ -62,6 +62,11 @@ namespace osu.Game.Screens.Play.HUD } }); + // For future consideration, this icon should probably not exist. + // + // If we remove it, the following needs attention: + // - Mobile support (swipe from side of screen?) + // - Consolidating this overlay with the one at player loader (to have the animation hint at its presence) AddInternal(button = new IconButton { Icon = FontAwesome.Solid.Cog, From 64555debc29bc4c16dc721d54537dee885f20d10 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 13 Dec 2024 19:33:47 +0900 Subject: [PATCH 0289/1275] Fix adjusting control point offset after undo/redo causing catastrophic failure Closes https://github.com/ppy/osu/issues/31098. Low effort fix because it was already half broken. The test was testing in isolation but in actual editor usage it wasn't working as expected. --- .../Visual/Editing/TestSceneTimingScreen.cs | 66 ++++++++++++++----- .../Screens/Edit/Timing/ControlPointList.cs | 22 +++++++ 2 files changed, 71 insertions(+), 17 deletions(-) diff --git a/osu.Game.Tests/Visual/Editing/TestSceneTimingScreen.cs b/osu.Game.Tests/Visual/Editing/TestSceneTimingScreen.cs index cf07ce2431..eecfb7cb6e 100644 --- a/osu.Game.Tests/Visual/Editing/TestSceneTimingScreen.cs +++ b/osu.Game.Tests/Visual/Editing/TestSceneTimingScreen.cs @@ -32,6 +32,7 @@ namespace osu.Game.Tests.Visual.Editing private TimingScreen timingScreen; private EditorBeatmap editorBeatmap; + private BeatmapEditorChangeHandler changeHandler; protected override bool ScrollUsingMouseWheel => false; @@ -46,6 +47,7 @@ namespace osu.Game.Tests.Visual.Editing private void reloadEditorBeatmap() { editorBeatmap = new EditorBeatmap(Beatmap.Value.GetPlayableBeatmap(Ruleset.Value)); + changeHandler = new BeatmapEditorChangeHandler(editorBeatmap); Child = new DependencyProvidingContainer { @@ -53,6 +55,7 @@ namespace osu.Game.Tests.Visual.Editing CachedDependencies = new (Type, object)[] { (typeof(EditorBeatmap), editorBeatmap), + (typeof(IEditorChangeHandler), changeHandler), (typeof(IBeatSnapProvider), editorBeatmap) }, Child = timingScreen = new TimingScreen @@ -72,8 +75,10 @@ namespace osu.Game.Tests.Visual.Editing AddUntilStep("Wait for rows to load", () => Child.ChildrenOfType().Any()); } + // TODO: this is best-effort for now, but the comment out test below should probably be how things should work. + // Was originally working as of https://github.com/ppy/osu/pull/26141; Regressed at some point. [Test] - public void TestSelectedRetainedOverUndo() + public void TestSelectionDismissedOnUndo() { AddStep("Select first timing point", () => { @@ -95,25 +100,52 @@ namespace osu.Game.Tests.Visual.Editing return timingScreen.SelectedGroup.Value.ControlPoints.Any(c => c is TimingControlPoint) && timingScreen.SelectedGroup.Value.Time > 2170; }); - AddStep("simulate undo", () => - { - var clone = editorBeatmap.ControlPointInfo.DeepClone(); + AddStep("undo", () => changeHandler?.RestoreState(-1)); - editorBeatmap.ControlPointInfo.Clear(); - - foreach (var group in clone.Groups) - { - foreach (var cp in group.ControlPoints) - editorBeatmap.ControlPointInfo.Add(group.Time, cp); - } - }); - - AddUntilStep("selection retained", () => - { - return timingScreen.SelectedGroup.Value.ControlPoints.Any(c => c is TimingControlPoint) && timingScreen.SelectedGroup.Value.Time > 2170; - }); + AddUntilStep("selection dismissed", () => timingScreen.SelectedGroup.Value, () => Is.Null); } + // [Test] + // public void TestSelectedRetainedOverUndo() + // { + // AddStep("Select first timing point", () => + // { + // InputManager.MoveMouseTo(Child.ChildrenOfType().First()); + // InputManager.Click(MouseButton.Left); + // }); + // + // AddUntilStep("Selection changed", () => timingScreen.SelectedGroup.Value.Time == 2170); + // AddUntilStep("Ensure seeked to correct time", () => EditorClock.CurrentTimeAccurate == 2170); + // + // AddStep("Adjust offset", () => + // { + // InputManager.MoveMouseTo(timingScreen.ChildrenOfType().First().ScreenSpaceDrawQuad.Centre + new Vector2(20, 0)); + // InputManager.Click(MouseButton.Left); + // }); + // + // AddUntilStep("wait for offset changed", () => + // { + // return timingScreen.SelectedGroup.Value.ControlPoints.Any(c => c is TimingControlPoint) && timingScreen.SelectedGroup.Value.Time > 2170; + // }); + // + // AddStep("undo", () => changeHandler?.RestoreState(-1)); + // + // AddUntilStep("selection retained", () => + // { + // return timingScreen.SelectedGroup.Value.ControlPoints.Any(c => c is TimingControlPoint) && timingScreen.SelectedGroup.Value.Time > 2170; + // }); + // + // AddAssert("check group count", () => editorBeatmap.ControlPointInfo.Groups.Count, () => Is.EqualTo(10)); + // + // AddStep("Adjust offset", () => + // { + // InputManager.MoveMouseTo(timingScreen.ChildrenOfType().First().ScreenSpaceDrawQuad.Centre + new Vector2(20, 0)); + // InputManager.Click(MouseButton.Left); + // }); + // + // AddAssert("check group count", () => editorBeatmap.ControlPointInfo.Groups.Count, () => Is.EqualTo(10)); + // } + [Test] public void TestScrollControlGroupIntoView() { diff --git a/osu.Game/Screens/Edit/Timing/ControlPointList.cs b/osu.Game/Screens/Edit/Timing/ControlPointList.cs index 49e5b76dd6..12c6390812 100644 --- a/osu.Game/Screens/Edit/Timing/ControlPointList.cs +++ b/osu.Game/Screens/Edit/Timing/ControlPointList.cs @@ -34,6 +34,9 @@ namespace osu.Game.Screens.Edit.Timing [Resolved] private Bindable selectedGroup { get; set; } = null!; + [Resolved] + private IEditorChangeHandler? editorChangeHandler { get; set; } + [BackgroundDependencyLoader] private void load(OsuColour colours, OverlayColourProvider colourProvider) { @@ -110,6 +113,9 @@ namespace osu.Game.Screens.Edit.Timing } }, }; + + if (editorChangeHandler != null) + editorChangeHandler.OnStateChange += onUndoRedo; } protected override void LoadComplete() @@ -185,5 +191,21 @@ namespace osu.Game.Screens.Edit.Timing selectedGroup.Value = group; } + + private void onUndoRedo() + { + // Best effort. We have no tracking of control points through undo/redo changes. + // If we don't deselect, things like offset changes could spawn groups to be added from previous states (see https://github.com/ppy/osu/issues/31098). + if (selectedGroup.Value != null && !Beatmap.ControlPointInfo.Groups.Contains(selectedGroup.Value)) + selectedGroup.Value = null; + } + + protected override void Dispose(bool isDisposing) + { + base.Dispose(isDisposing); + + if (editorChangeHandler != null) + editorChangeHandler.OnStateChange -= onUndoRedo; + } } } From da840e3fac164afa7fcd900895d42c38c305f518 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 13 Dec 2024 19:45:18 +0900 Subject: [PATCH 0290/1275] Change the way "current" points are hinted on timing screen I actually thought things were bugged with the previous display method, since the hinting was very similar to the hover colour/state. I've adjusted this to hopefully give users a better idea of what this is intending to show them. --- .../Screens/Edit/Timing/ControlPointTable.cs | 35 ++++++++++++------- 1 file changed, 23 insertions(+), 12 deletions(-) diff --git a/osu.Game/Screens/Edit/Timing/ControlPointTable.cs b/osu.Game/Screens/Edit/Timing/ControlPointTable.cs index fd812cfe2b..56fa251bd3 100644 --- a/osu.Game/Screens/Edit/Timing/ControlPointTable.cs +++ b/osu.Game/Screens/Edit/Timing/ControlPointTable.cs @@ -5,6 +5,7 @@ using System; using System.Linq; using osu.Framework.Allocation; using osu.Framework.Bindables; +using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Colour; using osu.Framework.Graphics.Containers; @@ -21,6 +22,7 @@ using osu.Game.Graphics.Sprites; using osu.Game.Overlays; using osu.Game.Screens.Edit.Timing.RowAttributes; using osuTK; +using osuTK.Graphics; namespace osu.Game.Screens.Edit.Timing { @@ -177,7 +179,7 @@ namespace osu.Game.Screens.Edit.Timing private readonly BindableWithCurrent current = new BindableWithCurrent(); private Box background = null!; - private Box currentIndicator = null!; + private Drawable currentIndicator = null!; [Resolved] private OverlayColourProvider colourProvider { get; set; } = null!; @@ -210,11 +212,26 @@ namespace osu.Game.Screens.Edit.Timing Colour = colourProvider.Background1, Alpha = 0, }, - currentIndicator = new Box + currentIndicator = new Container { - RelativeSizeAxes = Axes.Y, - Width = 5, + RelativeSizeAxes = Axes.Both, Alpha = 0, + Children = new Drawable[] + { + new Box + { + RelativeSizeAxes = Axes.Y, + Width = 5, + }, + new Box + { + RelativeSizeAxes = Axes.Y, + Blending = BlendingParameters.Additive, + X = 5, + Width = 150, + Colour = ColourInfo.GradientHorizontal(Color4.White.Opacity(0.1f), Color4.White.Opacity(0)) + }, + } }, new Container { @@ -281,14 +298,8 @@ namespace osu.Game.Screens.Edit.Timing bool hasCurrentTimingPoint = activeTimingPoint.Value != null && current.Value.ControlPoints.Contains(activeTimingPoint.Value); bool hasCurrentEffectPoint = activeEffectPoint.Value != null && current.Value.ControlPoints.Contains(activeEffectPoint.Value); - if (IsHovered || isSelected) - background.FadeIn(100, Easing.OutQuint); - else if (hasCurrentTimingPoint || hasCurrentEffectPoint) - background.FadeTo(0.2f, 100, Easing.OutQuint); - else - background.FadeOut(100, Easing.OutQuint); - - background.Colour = isSelected ? colourProvider.Colour3 : colourProvider.Background1; + background.FadeTo(IsHovered || isSelected ? 1 : 0, 100, Easing.OutQuint); + background.FadeColour(isSelected ? colourProvider.Colour3 : colourProvider.Background1, 100, Easing.OutQuint); if (hasCurrentTimingPoint || hasCurrentEffectPoint) { From 9025103b8bb9bcb2dbb4e5fcca80c0ec96b84e96 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 13 Dec 2024 20:02:17 +0900 Subject: [PATCH 0291/1275] Reword comment to hopefully be more understandable --- osu.Game/Screens/Ranking/ResultsScreen.cs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/Ranking/ResultsScreen.cs b/osu.Game/Screens/Ranking/ResultsScreen.cs index e3284aac70..5e91171051 100644 --- a/osu.Game/Screens/Ranking/ResultsScreen.cs +++ b/osu.Game/Screens/Ranking/ResultsScreen.cs @@ -84,7 +84,6 @@ namespace osu.Game.Screens.Ranking /// public bool ShowUserStatistics { get; init; } - // Only show the relevant button otherwise things look silly. private Sample? popInSample; protected ResultsScreen(ScoreInfo? score) @@ -197,7 +196,10 @@ namespace osu.Game.Screens.Ranking Width = 300 }); - // for simplicity, only allow when we're guaranteed the replay is already downloaded and present. + // for simplicity, only allow this when coming from a replay player where we know the replay is ready to be played. + // + // if we show it in all cases, consider the case where a user comes from song select and potentially has to download + // the replay before it can be played back. it wouldn't flow well with the quick retry in such a case. allowHotkeyRetry = player is ReplayPlayer; } From c0b6e784a5076dbaf6addbfdae00bdebd35c3f6f Mon Sep 17 00:00:00 2001 From: Nicholas Chin Date: Fri, 13 Dec 2024 21:58:23 +0800 Subject: [PATCH 0292/1275] Fix text anchor for mania tooltip --- osu.Game/Graphics/Cursor/OsuTooltipContainer.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/osu.Game/Graphics/Cursor/OsuTooltipContainer.cs b/osu.Game/Graphics/Cursor/OsuTooltipContainer.cs index 0d36cc1d08..4180825a8d 100644 --- a/osu.Game/Graphics/Cursor/OsuTooltipContainer.cs +++ b/osu.Game/Graphics/Cursor/OsuTooltipContainer.cs @@ -80,6 +80,7 @@ namespace osu.Game.Graphics.Cursor Margin = new MarginPadding(5), AutoSizeAxes = Axes.Both, MaximumSize = new Vector2(max_width, float.PositiveInfinity), + TextAnchor = Anchor.TopCentre, } }; } From 153e6c0c22504fb5b1e8b32068f54ddd0832de48 Mon Sep 17 00:00:00 2001 From: Nicholas Chin Date: Sat, 14 Dec 2024 08:29:32 +0800 Subject: [PATCH 0293/1275] Use Count comparison instead of Any --- osu.Game/Overlays/Profile/Header/Components/MainDetails.cs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/osu.Game/Overlays/Profile/Header/Components/MainDetails.cs b/osu.Game/Overlays/Profile/Header/Components/MainDetails.cs index 84919d18bb..a3208bb85d 100644 --- a/osu.Game/Overlays/Profile/Header/Components/MainDetails.cs +++ b/osu.Game/Overlays/Profile/Header/Components/MainDetails.cs @@ -2,7 +2,6 @@ // See the LICENCE file in the repository root for full licence text. using System.Collections.Generic; -using System.Linq; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Extensions.LocalisationExtensions; @@ -189,7 +188,7 @@ namespace osu.Game.Overlays.Profile.Header.Components ); } - detailGlobalRank.ContentTooltipText = tooltipParts.Any() + detailGlobalRank.ContentTooltipText = tooltipParts.Count > 0 ? string.Join("\n", tooltipParts) : string.Empty; #endregion @@ -210,7 +209,7 @@ namespace osu.Game.Overlays.Profile.Header.Components } } - detailCountryRank.ContentTooltipText = countryTooltipParts.Any() + detailCountryRank.ContentTooltipText = countryTooltipParts.Count > 0 ? string.Join("\n", countryTooltipParts) : string.Empty; #endregion From e2edd9e0d5351a295468f068d8a643ed57dea3ba Mon Sep 17 00:00:00 2001 From: Nicholas Chin Date: Sun, 15 Dec 2024 13:53:33 +0800 Subject: [PATCH 0294/1275] Fix code quality issues --- osu.Game/Overlays/Profile/Header/Components/MainDetails.cs | 6 +++++- osu.Game/Users/UserStatistics.cs | 2 ++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/osu.Game/Overlays/Profile/Header/Components/MainDetails.cs b/osu.Game/Overlays/Profile/Header/Components/MainDetails.cs index a3208bb85d..5df755473d 100644 --- a/osu.Game/Overlays/Profile/Header/Components/MainDetails.cs +++ b/osu.Game/Overlays/Profile/Header/Components/MainDetails.cs @@ -164,9 +164,10 @@ namespace osu.Game.Overlays.Profile.Header.Components detailGlobalRank.Content = user?.Statistics?.GlobalRank?.ToLocalisableString("\\##,##0") ?? (LocalisableString)"-"; var rankHighest = user?.RankHighest; - var variants = user?.Statistics.Variants; + var variants = user?.Statistics?.Variants; #region Global rank tooltip + var tooltipParts = new List(); if (variants?.Count > 0) @@ -191,11 +192,13 @@ namespace osu.Game.Overlays.Profile.Header.Components detailGlobalRank.ContentTooltipText = tooltipParts.Count > 0 ? string.Join("\n", tooltipParts) : string.Empty; + #endregion detailCountryRank.Content = user?.Statistics?.CountryRank?.ToLocalisableString("\\##,##0") ?? (LocalisableString)"-"; #region Country rank tooltip + var countryTooltipParts = new List(); if (variants?.Count > 0) @@ -212,6 +215,7 @@ namespace osu.Game.Overlays.Profile.Header.Components detailCountryRank.ContentTooltipText = countryTooltipParts.Count > 0 ? string.Join("\n", countryTooltipParts) : string.Empty; + #endregion rankGraph.Statistics.Value = user?.Statistics; diff --git a/osu.Game/Users/UserStatistics.cs b/osu.Game/Users/UserStatistics.cs index b485485d48..1effacb36b 100644 --- a/osu.Game/Users/UserStatistics.cs +++ b/osu.Game/Users/UserStatistics.cs @@ -126,11 +126,13 @@ namespace osu.Game.Users } } } + public enum GameVariant { [EnumMember(Value = "4k")] [LocalisableDescription(typeof(BeatmapsStrings), nameof(BeatmapsStrings.VariantMania4k))] FourKey, + [EnumMember(Value = "7k")] [LocalisableDescription(typeof(BeatmapsStrings), nameof(BeatmapsStrings.VariantMania7k))] SevenKey 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 0295/1275] 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 1058abb4ab63cdc0878f436d4414206535fce868 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 16 Dec 2024 12:22:06 +0900 Subject: [PATCH 0296/1275] Fix code quality --- osu.Game/Screens/Edit/Timing/ControlPointTable.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/Edit/Timing/ControlPointTable.cs b/osu.Game/Screens/Edit/Timing/ControlPointTable.cs index 56fa251bd3..a37674b104 100644 --- a/osu.Game/Screens/Edit/Timing/ControlPointTable.cs +++ b/osu.Game/Screens/Edit/Timing/ControlPointTable.cs @@ -204,7 +204,7 @@ namespace osu.Game.Screens.Edit.Timing { RelativeSizeAxes = Axes.Both; - InternalChildren = new Drawable[] + InternalChildren = new[] { background = new Box { From a8948628e69b4203b0ada2decc71268417c1e144 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 16 Dec 2024 13:12:21 +0900 Subject: [PATCH 0297/1275] Expose high precision mouse toggle when searching for "sensitivity" and other keywords --- osu.Game/Overlays/Settings/Sections/Input/MouseSettings.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/osu.Game/Overlays/Settings/Sections/Input/MouseSettings.cs b/osu.Game/Overlays/Settings/Sections/Input/MouseSettings.cs index 6eb512fa35..3fb4016498 100644 --- a/osu.Game/Overlays/Settings/Sections/Input/MouseSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/Input/MouseSettings.cs @@ -57,10 +57,11 @@ namespace osu.Game.Overlays.Settings.Sections.Input LabelText = MouseSettingsStrings.HighPrecisionMouse, TooltipText = MouseSettingsStrings.HighPrecisionMouseTooltip, Current = relativeMode, - Keywords = new[] { @"raw", @"input", @"relative", @"cursor" } + Keywords = new[] { @"raw", @"input", @"relative", @"cursor", "sensitivity", "speed", "velocity" }, }, new SensitivitySetting { + Keywords = new[] { "speed", "velocity" }, LabelText = MouseSettingsStrings.CursorSensitivity, Current = localSensitivity }, From 8d1d026f56bc8bfc0f4ef6eee2c7babce9adcae5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 16 Dec 2024 12:46:25 +0900 Subject: [PATCH 0298/1275] Clean up model - Properly annotate things as nullable - Remove weird passthrough property (more on that later) --- .../Overlays/Profile/Header/Components/MainDetails.cs | 5 +++-- osu.Game/Users/UserStatistics.cs | 11 +++++------ 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/osu.Game/Overlays/Profile/Header/Components/MainDetails.cs b/osu.Game/Overlays/Profile/Header/Components/MainDetails.cs index 5df755473d..6d7eaa4265 100644 --- a/osu.Game/Overlays/Profile/Header/Components/MainDetails.cs +++ b/osu.Game/Overlays/Profile/Header/Components/MainDetails.cs @@ -4,6 +4,7 @@ using System.Collections.Generic; using osu.Framework.Allocation; using osu.Framework.Bindables; +using osu.Framework.Extensions; using osu.Framework.Extensions.LocalisationExtensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; @@ -176,7 +177,7 @@ namespace osu.Game.Overlays.Profile.Header.Components { if (variant.GlobalRank != null) { - tooltipParts.Add($"{variant.VariantDisplay}: {variant.GlobalRank.ToLocalisableString("\\##,##0")}"); + tooltipParts.Add($"{variant.VariantType.GetLocalisableDescription()}: {variant.GlobalRank.ToLocalisableString("\\##,##0")}"); } } } @@ -207,7 +208,7 @@ namespace osu.Game.Overlays.Profile.Header.Components { if (variant.CountryRank != null) { - countryTooltipParts.Add($"{variant.VariantDisplay}: {variant.CountryRank.Value.ToLocalisableString("\\##,##0")}"); + countryTooltipParts.Add($"{variant.VariantType.GetLocalisableDescription()}: {variant.CountryRank.Value.ToLocalisableString("\\##,##0")}"); } } } diff --git a/osu.Game/Users/UserStatistics.cs b/osu.Game/Users/UserStatistics.cs index 1effacb36b..687dd52594 100644 --- a/osu.Game/Users/UserStatistics.cs +++ b/osu.Game/Users/UserStatistics.cs @@ -6,9 +6,9 @@ using System; using System.Collections.Generic; using System.Runtime.Serialization; +using JetBrains.Annotations; using Newtonsoft.Json; using Newtonsoft.Json.Converters; -using osu.Framework.Extensions; using osu.Framework.Localisation; using osu.Game.Online.API.Requests.Responses; using osu.Game.Resources.Localisation.Web; @@ -80,7 +80,8 @@ namespace osu.Game.Users public Grades GradesCount; [JsonProperty(@"variants")] - public List Variants = null!; + [CanBeNull] + public List Variants; public struct Grades { @@ -127,7 +128,7 @@ namespace osu.Game.Users } } - public enum GameVariant + public enum RulesetVariant { [EnumMember(Value = "4k")] [LocalisableDescription(typeof(BeatmapsStrings), nameof(BeatmapsStrings.VariantMania4k))] @@ -154,9 +155,7 @@ namespace osu.Game.Users [JsonProperty("variant")] [JsonConverter(typeof(StringEnumConverter))] - public GameVariant? VariantType; - - public LocalisableString VariantDisplay => VariantType?.GetLocalisableDescription() ?? string.Empty; + public RulesetVariant VariantType; } } } From cfdb959cf69287a8bed61576103313c27f27a331 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 16 Dec 2024 13:14:07 +0900 Subject: [PATCH 0299/1275] Split actual methods & fix completely broken localisation Localisable strings cannot be plainly interpolated or joined. That is a lossy operation that loses data. --- .../Profile/Header/Components/MainDetails.cs | 61 +++++++++++-------- 1 file changed, 37 insertions(+), 24 deletions(-) diff --git a/osu.Game/Overlays/Profile/Header/Components/MainDetails.cs b/osu.Game/Overlays/Profile/Header/Components/MainDetails.cs index 6d7eaa4265..4bdd5425c0 100644 --- a/osu.Game/Overlays/Profile/Header/Components/MainDetails.cs +++ b/osu.Game/Overlays/Profile/Header/Components/MainDetails.cs @@ -11,6 +11,7 @@ using osu.Framework.Graphics.Containers; using osu.Framework.Localisation; using osu.Game.Graphics; using osu.Game.Graphics.Sprites; +using osu.Game.Online.API.Requests.Responses; using osu.Game.Online.Leaderboards; using osu.Game.Resources.Localisation.Web; using osu.Game.Scoring; @@ -163,13 +164,20 @@ namespace osu.Game.Overlays.Profile.Header.Components scoreRankInfo.Value.RankCount = user?.Statistics?.GradesCount[scoreRankInfo.Key] ?? 0; detailGlobalRank.Content = user?.Statistics?.GlobalRank?.ToLocalisableString("\\##,##0") ?? (LocalisableString)"-"; + detailGlobalRank.ContentTooltipText = getGlobalRankTooltipText(user); + detailCountryRank.Content = user?.Statistics?.CountryRank?.ToLocalisableString("\\##,##0") ?? (LocalisableString)"-"; + detailCountryRank.ContentTooltipText = getCountryRankTooltipText(user); + + rankGraph.Statistics.Value = user?.Statistics; + } + + private static LocalisableString getGlobalRankTooltipText(APIUser? user) + { var rankHighest = user?.RankHighest; var variants = user?.Statistics?.Variants; - #region Global rank tooltip - - var tooltipParts = new List(); + LocalisableString? result = null; if (variants?.Count > 0) { @@ -177,30 +185,36 @@ namespace osu.Game.Overlays.Profile.Header.Components { if (variant.GlobalRank != null) { - tooltipParts.Add($"{variant.VariantType.GetLocalisableDescription()}: {variant.GlobalRank.ToLocalisableString("\\##,##0")}"); + var variantText = LocalisableString.Interpolate($"{variant.VariantType.GetLocalisableDescription()}: {variant.GlobalRank.ToLocalisableString("\\##,##0")}"); + + if (result == null) + result = variantText; + else + result = LocalisableString.Interpolate($"{result}\n{variantText}"); } } } if (rankHighest != null) { - tooltipParts.Add(UsersStrings.ShowRankHighest( + var rankHighestText = UsersStrings.ShowRankHighest( rankHighest.Rank.ToLocalisableString("\\##,##0"), - rankHighest.UpdatedAt.ToLocalisableString(@"d MMM yyyy")) - ); + rankHighest.UpdatedAt.ToLocalisableString(@"d MMM yyyy")); + + if (result == null) + result = rankHighestText; + else + result = LocalisableString.Interpolate($"{result}\n{rankHighestText}"); } - detailGlobalRank.ContentTooltipText = tooltipParts.Count > 0 - ? string.Join("\n", tooltipParts) - : string.Empty; + return result ?? default; + } - #endregion + private static LocalisableString getCountryRankTooltipText(APIUser? user) + { + var variants = user?.Statistics?.Variants; - detailCountryRank.Content = user?.Statistics?.CountryRank?.ToLocalisableString("\\##,##0") ?? (LocalisableString)"-"; - - #region Country rank tooltip - - var countryTooltipParts = new List(); + LocalisableString? result = null; if (variants?.Count > 0) { @@ -208,18 +222,17 @@ namespace osu.Game.Overlays.Profile.Header.Components { if (variant.CountryRank != null) { - countryTooltipParts.Add($"{variant.VariantType.GetLocalisableDescription()}: {variant.CountryRank.Value.ToLocalisableString("\\##,##0")}"); + var variantText = LocalisableString.Interpolate($"{variant.VariantType.GetLocalisableDescription()}: {variant.CountryRank.ToLocalisableString("\\##,##0")}"); + + if (result == null) + result = variantText; + else + result = LocalisableString.Interpolate($"{result}\n{variantText}"); } } } - detailCountryRank.ContentTooltipText = countryTooltipParts.Count > 0 - ? string.Join("\n", countryTooltipParts) - : string.Empty; - - #endregion - - rankGraph.Statistics.Value = user?.Statistics; + return result ?? default; } private partial class ScoreRankInfo : CompositeDrawable From ecb7a809f2242ad4f71b1a22f0a1d8cb453fb67a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 16 Dec 2024 13:18:45 +0900 Subject: [PATCH 0300/1275] Revert "Fix text anchor for mania tooltip" This reverts commit c0b6e784a5076dbaf6addbfdae00bdebd35c3f6f. The change affects editor and other stuff and I'm not sure it's correct. It's not like client needs to match the appearance really. It already doesn't in many places. --- osu.Game/Graphics/Cursor/OsuTooltipContainer.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/osu.Game/Graphics/Cursor/OsuTooltipContainer.cs b/osu.Game/Graphics/Cursor/OsuTooltipContainer.cs index 4180825a8d..0d36cc1d08 100644 --- a/osu.Game/Graphics/Cursor/OsuTooltipContainer.cs +++ b/osu.Game/Graphics/Cursor/OsuTooltipContainer.cs @@ -80,7 +80,6 @@ namespace osu.Game.Graphics.Cursor Margin = new MarginPadding(5), AutoSizeAxes = Axes.Both, MaximumSize = new Vector2(max_width, float.PositiveInfinity), - TextAnchor = Anchor.TopCentre, } }; } From 85ada3275b23d49bd5dea344c07072a204cb7e07 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 16 Dec 2024 14:14:30 +0900 Subject: [PATCH 0301/1275] 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 0302/1275] 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 0303/1275] 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 0304/1275] 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 0305/1275] 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 0306/1275] 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 4bf90a5571bb508444178f3d98a2b2a10c549534 Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Mon, 16 Dec 2024 08:24:22 -0500 Subject: [PATCH 0307/1275] Use time-based resume overlay when playing osu! on touchscreen --- osu.Game.Rulesets.Osu/UI/DrawableOsuRuleset.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu/UI/DrawableOsuRuleset.cs b/osu.Game.Rulesets.Osu/UI/DrawableOsuRuleset.cs index ab69b67051..12d5363469 100644 --- a/osu.Game.Rulesets.Osu/UI/DrawableOsuRuleset.cs +++ b/osu.Game.Rulesets.Osu/UI/DrawableOsuRuleset.cs @@ -69,7 +69,7 @@ namespace osu.Game.Rulesets.Osu.UI protected override ResumeOverlay CreateResumeOverlay() { - if (Mods.Any(m => m is OsuModAutopilot)) + if (Mods.Any(m => m is OsuModAutopilot or OsuModTouchDevice)) return new DelayedResumeOverlay { Scale = new Vector2(0.65f) }; return new OsuResumeOverlay(); From 22e74cc0ee2c83b3e52c84286523041a6c3b1b06 Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Mon, 16 Dec 2024 12:22:28 -0500 Subject: [PATCH 0308/1275] 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 0309/1275] 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 5a2cae89ff8a9035ca17af7e76a8b1ac7325a060 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Tue, 10 Dec 2024 23:02:35 +0900 Subject: [PATCH 0310/1275] Fix free mod button overriding enabled state --- osu.Game/Screens/OnlinePlay/FooterButtonFreeMods.cs | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/FooterButtonFreeMods.cs b/osu.Game/Screens/OnlinePlay/FooterButtonFreeMods.cs index dd6536cf26..952b15a873 100644 --- a/osu.Game/Screens/OnlinePlay/FooterButtonFreeMods.cs +++ b/osu.Game/Screens/OnlinePlay/FooterButtonFreeMods.cs @@ -36,8 +36,9 @@ namespace osu.Game.Screens.OnlinePlay } } - private OsuSpriteText count = null!; + public new Action Action { set => throw new NotSupportedException("The click action is handled by the button itself."); } + private OsuSpriteText count = null!; private Circle circle = null!; private readonly FreeModSelectOverlay freeModSelectOverlay; @@ -45,6 +46,9 @@ namespace osu.Game.Screens.OnlinePlay public FooterButtonFreeMods(FreeModSelectOverlay freeModSelectOverlay) { this.freeModSelectOverlay = freeModSelectOverlay; + + // Overwrite any external behaviour as we delegate the main toggle action to a sub-button. + base.Action = toggleAllFreeMods; } [Resolved] @@ -98,9 +102,6 @@ namespace osu.Game.Screens.OnlinePlay base.LoadComplete(); Current.BindValueChanged(_ => updateModDisplay(), true); - - // Overwrite any external behaviour as we delegate the main toggle action to a sub-button. - Action = toggleAllFreeMods; } /// From 159f6025b8a80a4d666506c47833190c0fcdcb71 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Wed, 18 Dec 2024 23:19:14 +0900 Subject: [PATCH 0311/1275] Fix incorrect behaviour --- osu.Game/Screens/OnlinePlay/FooterButtonFreePlay.cs | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/FooterButtonFreePlay.cs b/osu.Game/Screens/OnlinePlay/FooterButtonFreePlay.cs index 367857e780..bcc7bb787d 100644 --- a/osu.Game/Screens/OnlinePlay/FooterButtonFreePlay.cs +++ b/osu.Game/Screens/OnlinePlay/FooterButtonFreePlay.cs @@ -1,6 +1,7 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Extensions.Color4Extensions; @@ -24,12 +25,20 @@ namespace osu.Game.Screens.OnlinePlay set => current.Current = value; } + public new Action Action { set => throw new NotSupportedException("The click action is handled by the button itself."); } + private OsuSpriteText text = null!; private Circle circle = null!; [Resolved] private OsuColour colours { get; set; } = null!; + public FooterButtonFreePlay() + { + // Overwrite any external behaviour as we delegate the main toggle action to a sub-button. + base.Action = () => current.Value = !current.Value; + } + [BackgroundDependencyLoader] private void load() { @@ -70,9 +79,6 @@ namespace osu.Game.Screens.OnlinePlay base.LoadComplete(); Current.BindValueChanged(_ => updateDisplay(), true); - - // Overwrite any external behaviour as we delegate the main toggle action to a sub-button. - Action = () => current.Value = !current.Value; } private void updateDisplay() From c68dc1141215e97cae0c2f8f27d41e54bbe028d2 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 19 Dec 2024 00:01:36 +0900 Subject: [PATCH 0312/1275] 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 79a3afe06feffe9db9aa60760a1509b01bfee3ba Mon Sep 17 00:00:00 2001 From: Jay Lawton Date: Thu, 19 Dec 2024 01:16:27 +1000 Subject: [PATCH 0313/1275] Implement considerations for Relax within osu!taiko diffcalc (#30591) --- .../Difficulty/TaikoDifficultyCalculator.cs | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs b/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs index 7f2558c406..b3efb7f46d 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs @@ -77,6 +77,8 @@ namespace osu.Game.Rulesets.Taiko.Difficulty if (beatmap.HitObjects.Count == 0) return new TaikoDifficultyAttributes { Mods = mods }; + bool isRelax = mods.Any(h => h is TaikoModRelax); + Colour colour = (Colour)skills.First(x => x is Colour); Rhythm rhythm = (Rhythm)skills.First(x => x is Rhythm); Stamina stamina = (Stamina)skills.First(x => x is Stamina); @@ -88,15 +90,18 @@ namespace osu.Game.Rulesets.Taiko.Difficulty double monoStaminaRating = singleColourStamina.DifficultyValue() * stamina_skill_multiplier; double monoStaminaFactor = staminaRating == 0 ? 1 : Math.Pow(monoStaminaRating / staminaRating, 5); - double combinedRating = combinedDifficultyValue(rhythm, colour, stamina); + double combinedRating = combinedDifficultyValue(rhythm, colour, stamina, isRelax); double starRating = rescale(combinedRating * 1.4); // TODO: This is temporary measure as we don't detect abuse of multiple-input playstyles of converts within the current system. if (beatmap.BeatmapInfo.Ruleset.OnlineID == 0) { starRating *= 0.925; - // For maps with low colour variance and high stamina requirement, multiple inputs are more likely to be abused. - if (colourRating < 2 && staminaRating > 8) + + // For maps with either relax or low colour variance and high stamina requirement, multiple inputs are more likely to be abused. + if (isRelax) + starRating *= 0.60; + else if (colourRating < 2 && staminaRating > 8) starRating *= 0.80; } @@ -138,7 +143,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty /// For each section, the peak strains of all separate skills are combined into a single peak strain for the section. /// The resulting partial rating of the beatmap is a weighted sum of the combined peaks (higher peaks are weighted more). /// - private double combinedDifficultyValue(Rhythm rhythm, Colour colour, Stamina stamina) + private double combinedDifficultyValue(Rhythm rhythm, Colour colour, Stamina stamina, bool isRelax) { List peaks = new List(); @@ -152,6 +157,12 @@ namespace osu.Game.Rulesets.Taiko.Difficulty double rhythmPeak = rhythmPeaks[i] * rhythm_skill_multiplier; double staminaPeak = staminaPeaks[i] * stamina_skill_multiplier; + if (isRelax) + { + colourPeak = 0; // There is no colour difficulty in relax. + staminaPeak /= 1.5; // Stamina difficulty is decreased with an increased available finger count. + } + double peak = norm(1.5, colourPeak, staminaPeak); peak = norm(2, peak, rhythmPeak); From 75d694d3dff0131e24b04deef4a34689628b1b76 Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Wed, 18 Dec 2024 12:43:20 -0500 Subject: [PATCH 0314/1275] 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 0315/1275] 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 0f2f25db532418ff5f8deba221ef14ed7a4867e7 Mon Sep 17 00:00:00 2001 From: YaniFR <58740803+YaniFR@users.noreply.github.com> Date: Wed, 18 Dec 2024 19:11:51 +0100 Subject: [PATCH 0316/1275] Adjust `DifficultyValue` curve to avoid lower star rating of osu!taiko being too inflated (#31067) * low sr * merge two line * update decimal * fix formatting --------- Co-authored-by: StanR --- .../Difficulty/TaikoPerformanceCalculator.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceCalculator.cs b/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceCalculator.cs index c672b7a1d9..ed7d41bf72 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceCalculator.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceCalculator.cs @@ -73,7 +73,8 @@ namespace osu.Game.Rulesets.Taiko.Difficulty private double computeDifficultyValue(ScoreInfo score, TaikoDifficultyAttributes attributes) { - double difficultyValue = Math.Pow(5 * Math.Max(1.0, attributes.StarRating / 0.115) - 4.0, 2.25) / 1150.0; + double baseDifficulty = 5 * Math.Max(1.0, attributes.StarRating / 0.115) - 4.0; + double difficultyValue = Math.Min(Math.Pow(baseDifficulty, 3) / 69052.51, Math.Pow(baseDifficulty, 2.25) / 1150.0); double lengthBonus = 1 + 0.1 * Math.Min(1.0, totalHits / 1500.0); difficultyValue *= lengthBonus; 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 0317/1275] 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 0318/1275] 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 4ca88ae2d66dd417a8380144b5fe5010821ad9ec Mon Sep 17 00:00:00 2001 From: Jay Lawton Date: Thu, 19 Dec 2024 21:32:59 +1000 Subject: [PATCH 0319/1275] Refactor `TaikoDifficultyCalculator` and add `DifficultStrain` attributes (#31191) * refactor + countdifficultstrain * norm in utils * adjust scaling shift * fix comment * revert all value changes * add the else back * remove cds comments --- .../Difficulty/TaikoDifficultyAttributes.cs | 13 ++-- .../Difficulty/TaikoDifficultyCalculator.cs | 75 ++++++++++--------- .../Utils/DifficultyCalculationUtils.cs | 9 +++ 3 files changed, 58 insertions(+), 39 deletions(-) diff --git a/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyAttributes.cs b/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyAttributes.cs index c8f0448767..4a35c30e60 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyAttributes.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyAttributes.cs @@ -34,11 +34,14 @@ namespace osu.Game.Rulesets.Taiko.Difficulty [JsonProperty("colour_difficulty")] public double ColourDifficulty { get; set; } - /// - /// The difficulty corresponding to the hardest parts of the map. - /// - [JsonProperty("peak_difficulty")] - public double PeakDifficulty { get; set; } + [JsonProperty("rhythm_difficult_strains")] + public double RhythmTopStrains { get; set; } + + [JsonProperty("colour_difficult_strains")] + public double ColourTopStrains { get; set; } + + [JsonProperty("stamina_difficult_strains")] + public double StaminaTopStrains { get; set; } /// /// The perceived hit window for a GREAT hit inclusive of rate-adjusting mods (DT/HT/etc). diff --git a/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs b/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs index b3efb7f46d..05081d471e 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs @@ -8,6 +8,7 @@ using osu.Game.Beatmaps; using osu.Game.Rulesets.Difficulty; using osu.Game.Rulesets.Difficulty.Preprocessing; using osu.Game.Rulesets.Difficulty.Skills; +using osu.Game.Rulesets.Difficulty.Utils; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.Taiko.Difficulty.Preprocessing; @@ -53,18 +54,25 @@ namespace osu.Game.Rulesets.Taiko.Difficulty protected override IEnumerable CreateDifficultyHitObjects(IBeatmap beatmap, double clockRate) { - List difficultyHitObjects = new List(); - List centreObjects = new List(); - List rimObjects = new List(); - List noteObjects = new List(); + var difficultyHitObjects = new List(); + var centreObjects = new List(); + var rimObjects = new List(); + var noteObjects = new List(); + // Generate TaikoDifficultyHitObjects from the beatmap's hit objects. for (int i = 2; i < beatmap.HitObjects.Count; i++) { - difficultyHitObjects.Add( - new TaikoDifficultyHitObject( - beatmap.HitObjects[i], beatmap.HitObjects[i - 1], beatmap.HitObjects[i - 2], clockRate, difficultyHitObjects, - centreObjects, rimObjects, noteObjects, difficultyHitObjects.Count) - ); + difficultyHitObjects.Add(new TaikoDifficultyHitObject( + beatmap.HitObjects[i], + beatmap.HitObjects[i - 1], + beatmap.HitObjects[i - 2], + clockRate, + difficultyHitObjects, + centreObjects, + rimObjects, + noteObjects, + difficultyHitObjects.Count + )); } TaikoColourDifficultyPreprocessor.ProcessAndAssign(difficultyHitObjects); @@ -79,28 +87,33 @@ namespace osu.Game.Rulesets.Taiko.Difficulty bool isRelax = mods.Any(h => h is TaikoModRelax); - Colour colour = (Colour)skills.First(x => x is Colour); Rhythm rhythm = (Rhythm)skills.First(x => x is Rhythm); + Colour colour = (Colour)skills.First(x => x is Colour); Stamina stamina = (Stamina)skills.First(x => x is Stamina); Stamina singleColourStamina = (Stamina)skills.Last(x => x is Stamina); - double colourRating = colour.DifficultyValue() * colour_skill_multiplier; double rhythmRating = rhythm.DifficultyValue() * rhythm_skill_multiplier; + double colourRating = colour.DifficultyValue() * colour_skill_multiplier; double staminaRating = stamina.DifficultyValue() * stamina_skill_multiplier; double monoStaminaRating = singleColourStamina.DifficultyValue() * stamina_skill_multiplier; double monoStaminaFactor = staminaRating == 0 ? 1 : Math.Pow(monoStaminaRating / staminaRating, 5); + double rhythmDifficultStrains = rhythm.CountTopWeightedStrains(); + double colourDifficultStrains = colour.CountTopWeightedStrains(); + double staminaDifficultStrains = stamina.CountTopWeightedStrains(); + double combinedRating = combinedDifficultyValue(rhythm, colour, stamina, isRelax); double starRating = rescale(combinedRating * 1.4); - // TODO: This is temporary measure as we don't detect abuse of multiple-input playstyles of converts within the current system. + // Converts are penalised outside the scope of difficulty calculation, as our assumptions surrounding standard play-styles becomes out-of-scope. if (beatmap.BeatmapInfo.Ruleset.OnlineID == 0) { starRating *= 0.925; - // For maps with either relax or low colour variance and high stamina requirement, multiple inputs are more likely to be abused. + // For maps with relax, multiple inputs are more likely to be abused. if (isRelax) starRating *= 0.60; + // For maps with either relax or low colour variance and high stamina requirement, multiple inputs are more likely to be abused. else if (colourRating < 2 && staminaRating > 8) starRating *= 0.80; } @@ -112,11 +125,13 @@ namespace osu.Game.Rulesets.Taiko.Difficulty { StarRating = starRating, Mods = mods, - StaminaDifficulty = staminaRating, - MonoStaminaFactor = monoStaminaFactor, RhythmDifficulty = rhythmRating, ColourDifficulty = colourRating, - PeakDifficulty = combinedRating, + StaminaDifficulty = staminaRating, + MonoStaminaFactor = monoStaminaFactor, + StaminaTopStrains = staminaDifficultStrains, + RhythmTopStrains = rhythmDifficultStrains, + ColourTopStrains = colourDifficultStrains, GreatHitWindow = hitWindows.WindowFor(HitResult.Great) / clockRate, OkHitWindow = hitWindows.WindowFor(HitResult.Ok) / clockRate, MaxCombo = beatmap.GetMaxCombo(), @@ -125,17 +140,6 @@ namespace osu.Game.Rulesets.Taiko.Difficulty return attributes; } - /// - /// Applies a final re-scaling of the star rating. - /// - /// The raw star rating value before re-scaling. - private double rescale(double sr) - { - if (sr < 0) return sr; - - return 10.43 * Math.Log(sr / 8 + 1); - } - /// /// Returns the combined star rating of the beatmap, calculated using peak strains from all sections of the map. /// @@ -153,8 +157,8 @@ namespace osu.Game.Rulesets.Taiko.Difficulty for (int i = 0; i < colourPeaks.Count; i++) { - double colourPeak = colourPeaks[i] * colour_skill_multiplier; double rhythmPeak = rhythmPeaks[i] * rhythm_skill_multiplier; + double colourPeak = colourPeaks[i] * colour_skill_multiplier; double staminaPeak = staminaPeaks[i] * stamina_skill_multiplier; if (isRelax) @@ -163,8 +167,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty staminaPeak /= 1.5; // Stamina difficulty is decreased with an increased available finger count. } - double peak = norm(1.5, colourPeak, staminaPeak); - peak = norm(2, peak, rhythmPeak); + double peak = DifficultyCalculationUtils.Norm(2, DifficultyCalculationUtils.Norm(1.5, colourPeak, staminaPeak), rhythmPeak); // Sections with 0 strain are excluded to avoid worst-case time complexity of the following sort (e.g. /b/2351871). // These sections will not contribute to the difficulty. @@ -185,10 +188,14 @@ namespace osu.Game.Rulesets.Taiko.Difficulty } /// - /// Returns the p-norm of an n-dimensional vector. + /// Applies a final re-scaling of the star rating. /// - /// The value of p to calculate the norm for. - /// The coefficients of the vector. - private double norm(double p, params double[] values) => Math.Pow(values.Sum(x => Math.Pow(x, p)), 1 / p); + /// The raw star rating value before re-scaling. + private double rescale(double sr) + { + if (sr < 0) return sr; + + return 10.43 * Math.Log(sr / 8 + 1); + } } } diff --git a/osu.Game/Rulesets/Difficulty/Utils/DifficultyCalculationUtils.cs b/osu.Game/Rulesets/Difficulty/Utils/DifficultyCalculationUtils.cs index b9efcd683d..df2d84d6f2 100644 --- a/osu.Game/Rulesets/Difficulty/Utils/DifficultyCalculationUtils.cs +++ b/osu.Game/Rulesets/Difficulty/Utils/DifficultyCalculationUtils.cs @@ -2,6 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using System; +using System.Linq; namespace osu.Game.Rulesets.Difficulty.Utils { @@ -46,5 +47,13 @@ namespace osu.Game.Rulesets.Difficulty.Utils /// Exponent /// The output of logistic function public static double Logistic(double exponent, double maxValue = 1) => maxValue / (1 + Math.Exp(exponent)); + + /// + /// Returns the p-norm of an n-dimensional vector (https://en.wikipedia.org/wiki/Norm_(mathematics)) + /// + /// The value of p to calculate the norm for. + /// The coefficients of the vector. + /// The p-norm of the vector. + public static double Norm(double p, params double[] values) => Math.Pow(values.Sum(x => Math.Pow(x, p)), 1 / p); } } 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 0320/1275] 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 0321/1275] 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 0322/1275] 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 0323/1275] 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 0324/1275] 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 ecd6b4192816391591ca8e96b77d80fe7c1fa948 Mon Sep 17 00:00:00 2001 From: Jay Lawton Date: Fri, 20 Dec 2024 00:45:11 +1000 Subject: [PATCH 0325/1275] Increase `accscalingshift` and include `countok` in hit proportion (#31195) * revert acc scaling shift to previous values * increase variance in accuracy values across od * move return values, move nullcheck into return --------- Co-authored-by: James Wilson --- .../Difficulty/TaikoPerformanceCalculator.cs | 17 +++++++---------- 1 file changed, 7 insertions(+), 10 deletions(-) diff --git a/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceCalculator.cs b/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceCalculator.cs index ed7d41bf72..a93f4c66ab 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceCalculator.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceCalculator.cs @@ -98,7 +98,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty // Scale accuracy more harshly on nearly-completely mono (single coloured) speed maps. double accScalingExponent = 2 + attributes.MonoStaminaFactor; - double accScalingShift = 300 - 100 * attributes.MonoStaminaFactor; + double accScalingShift = 400 - 100 * attributes.MonoStaminaFactor; return difficultyValue * Math.Pow(SpecialFunctions.Erf(accScalingShift / (Math.Sqrt(2) * estimatedUnstableRate.Value)), accScalingExponent); } @@ -134,6 +134,11 @@ namespace osu.Game.Rulesets.Taiko.Difficulty const double z = 2.32634787404; // 99% critical value for the normal distribution (one-tailed). + double? deviationGreatWindow = calcDeviationGreatWindow(); + double? deviationGoodWindow = calcDeviationGoodWindow(); + + return deviationGreatWindow is null ? deviationGoodWindow : Math.Min(deviationGreatWindow.Value, deviationGoodWindow!.Value); + // The upper bound on deviation, calculated with the ratio of 300s to objects, and the great hit window. double? calcDeviationGreatWindow() { @@ -160,7 +165,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty double n = totalHits; // Proportion of greats + goods hit. - double p = totalSuccessfulHits / n; + double p = Math.Max(0, totalSuccessfulHits - 0.0005 * countOk) / n; // We can be 99% confident that p is at least this value. double pLowerBound = (n * p + z * z / 2) / (n + z * z) - z / (n + z * z) * Math.Sqrt(n * p * (1 - p) + z * z / 4); @@ -168,14 +173,6 @@ namespace osu.Game.Rulesets.Taiko.Difficulty // We can be 99% confident that the deviation is not higher than: return h100 / (Math.Sqrt(2) * SpecialFunctions.ErfInv(pLowerBound)); } - - double? deviationGreatWindow = calcDeviationGreatWindow(); - double? deviationGoodWindow = calcDeviationGoodWindow(); - - if (deviationGreatWindow is null) - return deviationGoodWindow; - - return Math.Min(deviationGreatWindow.Value, deviationGoodWindow!.Value); } private int totalHits => countGreat + countOk + countMeh + countMiss; From df607ac3ea33cd531272e35df0fb1023cf21dcfd Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 20 Dec 2024 00:38:46 +0900 Subject: [PATCH 0326/1275] 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 0327/1275] 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 d8c3d899ebb4660b97301f9a5d07902bb4598cbe Mon Sep 17 00:00:00 2001 From: Jay Lawton Date: Fri, 20 Dec 2024 03:22:16 +1000 Subject: [PATCH 0328/1275] remove particular condition on convert nerf (#31196) Co-authored-by: James Wilson --- .../Difficulty/TaikoDifficultyCalculator.cs | 3 --- 1 file changed, 3 deletions(-) diff --git a/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs b/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs index 05081d471e..8f725d4f94 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs @@ -113,9 +113,6 @@ namespace osu.Game.Rulesets.Taiko.Difficulty // For maps with relax, multiple inputs are more likely to be abused. if (isRelax) starRating *= 0.60; - // For maps with either relax or low colour variance and high stamina requirement, multiple inputs are more likely to be abused. - else if (colourRating < 2 && staminaRating > 8) - starRating *= 0.80; } HitWindows hitWindows = new TaikoHitWindows(); From 9f8c390735e5acc96a872dcf5f0bbca52d62cb43 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 20 Dec 2024 12:39:33 +0900 Subject: [PATCH 0329/1275] 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 0330/1275] 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 0331/1275] 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 0332/1275] 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 0333/1275] 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 0334/1275] 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 0335/1275] 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 0336/1275] 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 0337/1275] 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 0338/1275] 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 0339/1275] 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 0340/1275] 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 0341/1275] 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 0342/1275] 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 0343/1275] 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 0344/1275] 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 0345/1275] 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 0346/1275] 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 0347/1275] 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 0348/1275] 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 0349/1275] 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 0350/1275] 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 0351/1275] 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 0352/1275] 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 0353/1275] 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 0354/1275] 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 f722f94f26f0055f7a68bb867b60600aff6bac81 Mon Sep 17 00:00:00 2001 From: StanR Date: Sat, 21 Dec 2024 04:32:51 +0500 Subject: [PATCH 0355/1275] Simplify osu! high-bpm acute angle jumps bonus (#30902) * Simplify osu! high-bpm acute angle jumps bonus * Add aim wiggle bonus * Add hitwindow-based aim velocity decrease * Revert "Add hitwindow-based aim velocity decrease" This reverts commit bcebe9662cfcb7a72805e48712525ef54ec9820e. * Move wiggle multiplier to a const, slightly decrease acute bonus multiplier * Make sure the previous object in the wiggle bonus is also part of the wiggle * Scale the wiggle bonus multiplayer down * Increase the acute angle jump bonus multiplier * Make wiggle bonus only apply on >150 bpm streams, make repetitive angle penalty * Reduce wiggle bonus multiplier to not break velocity>difficulty relation * Adjust wiggle falloff function to fix stability issues * Adjust wiggle consts * Update tests --- .../OsuDifficultyCalculatorTest.cs | 6 ++-- .../Difficulty/Evaluators/AimEvaluator.cs | 33 ++++++++++++------- .../Utils/DifficultyCalculationUtils.cs | 24 ++++++++++++++ 3 files changed, 48 insertions(+), 15 deletions(-) diff --git a/osu.Game.Rulesets.Osu.Tests/OsuDifficultyCalculatorTest.cs b/osu.Game.Rulesets.Osu.Tests/OsuDifficultyCalculatorTest.cs index efda3fa369..9798611488 100644 --- a/osu.Game.Rulesets.Osu.Tests/OsuDifficultyCalculatorTest.cs +++ b/osu.Game.Rulesets.Osu.Tests/OsuDifficultyCalculatorTest.cs @@ -15,20 +15,20 @@ namespace osu.Game.Rulesets.Osu.Tests { protected override string ResourceAssembly => "osu.Game.Rulesets.Osu.Tests"; - [TestCase(6.7171144000821119d, 239, "diffcalc-test")] + [TestCase(6.718709884850683d, 239, "diffcalc-test")] [TestCase(1.4485749025771304d, 54, "zero-length-sliders")] [TestCase(0.42630400627180914d, 4, "very-fast-slider")] [TestCase(0.14143808967817237d, 2, "nan-slider")] public void Test(double expectedStarRating, int expectedMaxCombo, string name) => base.Test(expectedStarRating, expectedMaxCombo, name); - [TestCase(8.9825709931204205d, 239, "diffcalc-test")] + [TestCase(9.4310274277499619d, 239, "diffcalc-test")] [TestCase(1.7550169162648608d, 54, "zero-length-sliders")] [TestCase(0.55231632896800109d, 4, "very-fast-slider")] public void TestClockRateAdjusted(double expectedStarRating, int expectedMaxCombo, string name) => Test(expectedStarRating, expectedMaxCombo, name, new OsuModDoubleTime()); - [TestCase(6.7171144000821119d, 239, "diffcalc-test")] + [TestCase(6.718709884850683d, 239, "diffcalc-test")] [TestCase(1.4485749025771304d, 54, "zero-length-sliders")] [TestCase(0.42630400627180914d, 4, "very-fast-slider")] public void TestClassicMod(double expectedStarRating, int expectedMaxCombo, string name) diff --git a/osu.Game.Rulesets.Osu/Difficulty/Evaluators/AimEvaluator.cs b/osu.Game.Rulesets.Osu/Difficulty/Evaluators/AimEvaluator.cs index 9816f6d0a4..c3270f25f8 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/Evaluators/AimEvaluator.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/Evaluators/AimEvaluator.cs @@ -12,9 +12,10 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators public static class AimEvaluator { private const double wide_angle_multiplier = 1.5; - private const double acute_angle_multiplier = 1.95; + private const double acute_angle_multiplier = 2.35; private const double slider_multiplier = 1.35; private const double velocity_change_multiplier = 0.75; + private const double wiggle_multiplier = 1.02; /// /// Evaluates the difficulty of aiming the current object, based on: @@ -64,6 +65,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators double acuteAngleBonus = 0; double sliderBonus = 0; double velocityChangeBonus = 0; + double wiggleBonus = 0; double aimStrain = currVelocity; // Start strain with regular velocity. @@ -79,22 +81,27 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators double angleBonus = Math.Min(currVelocity, prevVelocity); wideAngleBonus = calcWideAngleBonus(currAngle); - acuteAngleBonus = calcAcuteAngleBonus(currAngle); - if (DifficultyCalculationUtils.MillisecondsToBPM(osuCurrObj.StrainTime, 2) < 300) // Only buff deltaTime exceeding 300 bpm 1/2. - acuteAngleBonus = 0; - else - { - acuteAngleBonus *= calcAcuteAngleBonus(lastAngle) // Multiply by previous angle, we don't want to buff unless this is a wiggle type pattern. - * Math.Min(angleBonus, diameter * 1.25 / osuCurrObj.StrainTime) // The maximum velocity we buff is equal to 125 / strainTime - * Math.Pow(Math.Sin(Math.PI / 2 * Math.Min(1, (100 - osuCurrObj.StrainTime) / 25)), 2) // scale buff from 150 bpm 1/4 to 200 bpm 1/4 - * Math.Pow(Math.Sin(Math.PI / 2 * (Math.Clamp(osuCurrObj.LazyJumpDistance, radius, diameter) - radius) / radius), 2); // Buff distance exceeding radius up to diameter. - } + // Apply acute angle bonus for BPM above 300 1/2 and distance more than one diameter + acuteAngleBonus = calcAcuteAngleBonus(currAngle) * + angleBonus * + DifficultyCalculationUtils.Smootherstep(DifficultyCalculationUtils.MillisecondsToBPM(osuCurrObj.StrainTime, 2), 300, 400) * + DifficultyCalculationUtils.Smootherstep(osuCurrObj.LazyJumpDistance, diameter, diameter * 2); // Penalize wide angles if they're repeated, reducing the penalty as the lastAngle gets more acute. wideAngleBonus *= angleBonus * (1 - Math.Min(wideAngleBonus, Math.Pow(calcWideAngleBonus(lastAngle), 3))); // Penalize acute angles if they're repeated, reducing the penalty as the lastLastAngle gets more obtuse. - acuteAngleBonus *= 0.5 + 0.5 * (1 - Math.Min(acuteAngleBonus, Math.Pow(calcAcuteAngleBonus(lastLastAngle), 3))); + acuteAngleBonus *= 0.03 + 0.97 * (1 - Math.Min(acuteAngleBonus, Math.Pow(calcAcuteAngleBonus(lastLastAngle), 3))); + + // Apply wiggle bonus for jumps that are [radius, 2*diameter] in distance, with < 110 angle and bpm > 150 + // https://www.desmos.com/calculator/iis7lgbppe + wiggleBonus = angleBonus + * DifficultyCalculationUtils.Smootherstep(osuCurrObj.LazyJumpDistance, radius, diameter) + * Math.Pow(DifficultyCalculationUtils.ReverseLerp(osuCurrObj.LazyJumpDistance, diameter * 3, diameter), 1.8) + * DifficultyCalculationUtils.Smootherstep(currAngle, double.DegreesToRadians(110), double.DegreesToRadians(60)) + * DifficultyCalculationUtils.Smootherstep(osuLastObj.LazyJumpDistance, radius, diameter) + * Math.Pow(DifficultyCalculationUtils.ReverseLerp(osuLastObj.LazyJumpDistance, diameter * 3, diameter), 1.8) + * DifficultyCalculationUtils.Smootherstep(lastAngle, double.DegreesToRadians(110), double.DegreesToRadians(60)); } } @@ -122,6 +129,8 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators sliderBonus = osuLastObj.TravelDistance / osuLastObj.TravelTime; } + aimStrain += wiggleBonus * wiggle_multiplier; + // Add in acute angle bonus or wide angle bonus + velocity change bonus, whichever is larger. aimStrain += Math.Max(acuteAngleBonus * acute_angle_multiplier, wideAngleBonus * wide_angle_multiplier + velocityChangeBonus * velocity_change_multiplier); diff --git a/osu.Game/Rulesets/Difficulty/Utils/DifficultyCalculationUtils.cs b/osu.Game/Rulesets/Difficulty/Utils/DifficultyCalculationUtils.cs index df2d84d6f2..055d8a458b 100644 --- a/osu.Game/Rulesets/Difficulty/Utils/DifficultyCalculationUtils.cs +++ b/osu.Game/Rulesets/Difficulty/Utils/DifficultyCalculationUtils.cs @@ -55,5 +55,29 @@ namespace osu.Game.Rulesets.Difficulty.Utils /// The coefficients of the vector. /// The p-norm of the vector. public static double Norm(double p, params double[] values) => Math.Pow(values.Sum(x => Math.Pow(x, p)), 1 / p); + + /// + /// Smootherstep function (https://en.wikipedia.org/wiki/Smoothstep#Variations) + /// + /// Value to calculate the function for + /// Value at which function returns 0 + /// Value at which function returns 1 + public static double Smootherstep(double x, double start, double end) + { + x = Math.Clamp((x - start) / (end - start), 0.0, 1.0); + + return x * x * x * (x * (6.0 * x - 15.0) + 10.0); + } + + /// + /// Reverse linear interpolation function (https://en.wikipedia.org/wiki/Linear_interpolation) + /// + /// Value to calculate the function for + /// Value at which function returns 0 + /// Value at which function returns 1 + public static double ReverseLerp(double x, double start, double end) + { + return Math.Clamp((x - start) / (end - start), 0.0, 1.0); + } } } From f6a36f7b2e1427f858b087052bfe7f3dc50b2ab2 Mon Sep 17 00:00:00 2001 From: Jay Lawton Date: Sat, 21 Dec 2024 20:19:14 +1000 Subject: [PATCH 0356/1275] Implement `Reading` Skill into osu!taiko (#31208) --- .../Difficulty/Evaluators/ReadingEvaluator.cs | 43 ++++++++++++++++ .../Preprocessing/Reading/EffectiveBPM.cs | 50 +++++++++++++++++++ .../Preprocessing/TaikoDifficultyHitObject.cs | 10 ++++ .../Difficulty/Skills/Reading.cs | 44 ++++++++++++++++ .../Difficulty/TaikoDifficultyAttributes.cs | 6 +++ .../Difficulty/TaikoDifficultyCalculator.cs | 22 +++++--- .../Difficulty/TaikoPerformanceCalculator.cs | 3 -- 7 files changed, 169 insertions(+), 9 deletions(-) create mode 100644 osu.Game.Rulesets.Taiko/Difficulty/Evaluators/ReadingEvaluator.cs create mode 100644 osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Reading/EffectiveBPM.cs create mode 100644 osu.Game.Rulesets.Taiko/Difficulty/Skills/Reading.cs diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Evaluators/ReadingEvaluator.cs b/osu.Game.Rulesets.Taiko/Difficulty/Evaluators/ReadingEvaluator.cs new file mode 100644 index 0000000000..a6a1513842 --- /dev/null +++ b/osu.Game.Rulesets.Taiko/Difficulty/Evaluators/ReadingEvaluator.cs @@ -0,0 +1,43 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Game.Rulesets.Difficulty.Utils; +using osu.Game.Rulesets.Taiko.Difficulty.Preprocessing; + +namespace osu.Game.Rulesets.Taiko.Difficulty.Evaluators +{ + public static class ReadingEvaluator + { + private readonly struct VelocityRange + { + public double Min { get; } + public double Max { get; } + public double Center => (Max + Min) / 2; + public double Range => Max - Min; + + public VelocityRange(double min, double max) + { + Min = min; + Max = max; + } + } + + /// + /// Calculates the influence of higher slider velocities on hitobject difficulty. + /// The bonus is determined based on the EffectiveBPM, shifting within a defined range + /// between the upper and lower boundaries to reflect how increased slider velocity impacts difficulty. + /// + /// The hit object to evaluate. + /// The reading difficulty value for the given hit object. + public static double EvaluateDifficultyOf(TaikoDifficultyHitObject noteObject) + { + double effectiveBPM = noteObject.EffectiveBPM; + + var highVelocity = new VelocityRange(480, 640); + var midVelocity = new VelocityRange(360, 480); + + return 1.0 * DifficultyCalculationUtils.Logistic(effectiveBPM, highVelocity.Center, 1.0 / (highVelocity.Range / 10)) + + 0.5 * DifficultyCalculationUtils.Logistic(effectiveBPM, midVelocity.Center, 1.0 / (midVelocity.Range / 10)); + } + } +} diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Reading/EffectiveBPM.cs b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Reading/EffectiveBPM.cs new file mode 100644 index 0000000000..17e05d5fbf --- /dev/null +++ b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Reading/EffectiveBPM.cs @@ -0,0 +1,50 @@ +// 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.Beatmaps.ControlPoints; + +namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Reading +{ + public class EffectiveBPMPreprocessor + { + private readonly IList noteObjects; + private readonly double globalSliderVelocity; + + public EffectiveBPMPreprocessor(IBeatmap beatmap, List noteObjects) + { + this.noteObjects = noteObjects; + globalSliderVelocity = beatmap.Difficulty.SliderMultiplier; + } + + /// + /// Calculates and sets the effective BPM and slider velocity for each note object, considering clock rate and scroll speed. + /// + public void ProcessEffectiveBPM(ControlPointInfo controlPointInfo, double clockRate) + { + foreach (var currentNoteObject in noteObjects) + { + double startTime = currentNoteObject.StartTime * clockRate; + + // Retrieve the timing point at the note's start time + TimingControlPoint currentControlPoint = controlPointInfo.TimingPointAt(startTime); + + // Calculate the slider velocity at the note's start time. + double currentSliderVelocity = calculateSliderVelocity(controlPointInfo, startTime, clockRate); + currentNoteObject.CurrentSliderVelocity = currentSliderVelocity; + + currentNoteObject.EffectiveBPM = currentControlPoint.BPM * currentSliderVelocity; + } + } + + /// + /// Calculates the slider velocity based on control point info and clock rate. + /// + private double calculateSliderVelocity(ControlPointInfo controlPointInfo, double startTime, double clockRate) + { + var activeEffectControlPoint = controlPointInfo.EffectPointAt(startTime); + return globalSliderVelocity * (activeEffectControlPoint.ScrollSpeed) * clockRate; + } + } +} diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/TaikoDifficultyHitObject.cs b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/TaikoDifficultyHitObject.cs index 4aaee50c18..e741e4c9e7 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/TaikoDifficultyHitObject.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/TaikoDifficultyHitObject.cs @@ -48,6 +48,16 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing /// public readonly TaikoDifficultyHitObjectColour Colour; + /// + /// The adjusted BPM of this hit object, based on its slider velocity and scroll speed. + /// + public double EffectiveBPM; + + /// + /// The current slider velocity of this hit object. + /// + public double CurrentSliderVelocity; + /// /// Creates a new difficulty hit object. /// diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Skills/Reading.cs b/osu.Game.Rulesets.Taiko/Difficulty/Skills/Reading.cs new file mode 100644 index 0000000000..9de058f289 --- /dev/null +++ b/osu.Game.Rulesets.Taiko/Difficulty/Skills/Reading.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.Game.Rulesets.Difficulty.Preprocessing; +using osu.Game.Rulesets.Difficulty.Skills; +using osu.Game.Rulesets.Mods; +using osu.Game.Rulesets.Taiko.Difficulty.Evaluators; +using osu.Game.Rulesets.Taiko.Difficulty.Preprocessing; +using osu.Game.Rulesets.Taiko.Objects; + +namespace osu.Game.Rulesets.Taiko.Difficulty.Skills +{ + /// + /// Calculates the reading coefficient of taiko difficulty. + /// + public class Reading : StrainDecaySkill + { + protected override double SkillMultiplier => 1.0; + protected override double StrainDecayBase => 0.4; + + private double currentStrain; + + public Reading(Mod[] mods) + : base(mods) + { + } + + protected override double StrainValueOf(DifficultyHitObject current) + { + // Drum Rolls and Swells are exempt. + if (current.BaseObject is not Hit) + { + return 0.0; + } + + var taikoObject = (TaikoDifficultyHitObject)current; + + currentStrain *= StrainDecayBase; + currentStrain += ReadingEvaluator.EvaluateDifficultyOf(taikoObject) * SkillMultiplier; + + return currentStrain; + } + } +} diff --git a/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyAttributes.cs b/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyAttributes.cs index 4a35c30e60..d3cdb379d5 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyAttributes.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyAttributes.cs @@ -28,6 +28,12 @@ namespace osu.Game.Rulesets.Taiko.Difficulty [JsonProperty("rhythm_difficulty")] public double RhythmDifficulty { get; set; } + /// + /// The difficulty corresponding to the reading skill. + /// + [JsonProperty("reading_difficulty")] + public double ReadingDifficulty { get; set; } + /// /// The difficulty corresponding to the colour skill. /// diff --git a/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs b/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs index 8f725d4f94..0d6ecb8d3e 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs @@ -13,6 +13,7 @@ using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.Taiko.Difficulty.Preprocessing; using osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Colour; +using osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Reading; using osu.Game.Rulesets.Taiko.Difficulty.Skills; using osu.Game.Rulesets.Taiko.Mods; using osu.Game.Rulesets.Taiko.Scoring; @@ -22,7 +23,8 @@ namespace osu.Game.Rulesets.Taiko.Difficulty public class TaikoDifficultyCalculator : DifficultyCalculator { private const double difficulty_multiplier = 0.084375; - private const double rhythm_skill_multiplier = 0.2 * difficulty_multiplier; + private const double rhythm_skill_multiplier = 0.200 * difficulty_multiplier; + private const double reading_skill_multiplier = 0.100 * difficulty_multiplier; private const double colour_skill_multiplier = 0.375 * difficulty_multiplier; private const double stamina_skill_multiplier = 0.375 * difficulty_multiplier; @@ -38,6 +40,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty return new Skill[] { new Rhythm(mods), + new Reading(mods), new Colour(mods), new Stamina(mods, false), new Stamina(mods, true) @@ -58,6 +61,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty var centreObjects = new List(); var rimObjects = new List(); var noteObjects = new List(); + EffectiveBPMPreprocessor bpmLoader = new EffectiveBPMPreprocessor(beatmap, noteObjects); // Generate TaikoDifficultyHitObjects from the beatmap's hit objects. for (int i = 2; i < beatmap.HitObjects.Count; i++) @@ -76,6 +80,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty } TaikoColourDifficultyPreprocessor.ProcessAndAssign(difficultyHitObjects); + bpmLoader.ProcessEffectiveBPM(beatmap.ControlPointInfo, clockRate); return difficultyHitObjects; } @@ -88,11 +93,13 @@ namespace osu.Game.Rulesets.Taiko.Difficulty bool isRelax = mods.Any(h => h is TaikoModRelax); Rhythm rhythm = (Rhythm)skills.First(x => x is Rhythm); + Reading reading = (Reading)skills.First(x => x is Reading); Colour colour = (Colour)skills.First(x => x is Colour); Stamina stamina = (Stamina)skills.First(x => x is Stamina); Stamina singleColourStamina = (Stamina)skills.Last(x => x is Stamina); double rhythmRating = rhythm.DifficultyValue() * rhythm_skill_multiplier; + double readingRating = reading.DifficultyValue() * reading_skill_multiplier; double colourRating = colour.DifficultyValue() * colour_skill_multiplier; double staminaRating = stamina.DifficultyValue() * stamina_skill_multiplier; double monoStaminaRating = singleColourStamina.DifficultyValue() * stamina_skill_multiplier; @@ -102,13 +109,13 @@ namespace osu.Game.Rulesets.Taiko.Difficulty double colourDifficultStrains = colour.CountTopWeightedStrains(); double staminaDifficultStrains = stamina.CountTopWeightedStrains(); - double combinedRating = combinedDifficultyValue(rhythm, colour, stamina, isRelax); + double combinedRating = combinedDifficultyValue(rhythm, reading, colour, stamina, isRelax); double starRating = rescale(combinedRating * 1.4); // Converts are penalised outside the scope of difficulty calculation, as our assumptions surrounding standard play-styles becomes out-of-scope. if (beatmap.BeatmapInfo.Ruleset.OnlineID == 0) { - starRating *= 0.925; + starRating *= 0.825; // For maps with relax, multiple inputs are more likely to be abused. if (isRelax) @@ -123,6 +130,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty StarRating = starRating, Mods = mods, RhythmDifficulty = rhythmRating, + ReadingDifficulty = readingRating, ColourDifficulty = colourRating, StaminaDifficulty = staminaRating, MonoStaminaFactor = monoStaminaFactor, @@ -144,17 +152,19 @@ namespace osu.Game.Rulesets.Taiko.Difficulty /// For each section, the peak strains of all separate skills are combined into a single peak strain for the section. /// The resulting partial rating of the beatmap is a weighted sum of the combined peaks (higher peaks are weighted more). /// - private double combinedDifficultyValue(Rhythm rhythm, Colour colour, Stamina stamina, bool isRelax) + private double combinedDifficultyValue(Rhythm rhythm, Reading reading, Colour colour, Stamina stamina, bool isRelax) { List peaks = new List(); - var colourPeaks = colour.GetCurrentStrainPeaks().ToList(); var rhythmPeaks = rhythm.GetCurrentStrainPeaks().ToList(); + var readingPeaks = reading.GetCurrentStrainPeaks().ToList(); + var colourPeaks = colour.GetCurrentStrainPeaks().ToList(); var staminaPeaks = stamina.GetCurrentStrainPeaks().ToList(); for (int i = 0; i < colourPeaks.Count; i++) { double rhythmPeak = rhythmPeaks[i] * rhythm_skill_multiplier; + double readingPeak = readingPeaks[i] * reading_skill_multiplier; double colourPeak = colourPeaks[i] * colour_skill_multiplier; double staminaPeak = staminaPeaks[i] * stamina_skill_multiplier; @@ -164,7 +174,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty staminaPeak /= 1.5; // Stamina difficulty is decreased with an increased available finger count. } - double peak = DifficultyCalculationUtils.Norm(2, DifficultyCalculationUtils.Norm(1.5, colourPeak, staminaPeak), rhythmPeak); + double peak = DifficultyCalculationUtils.Norm(2, DifficultyCalculationUtils.Norm(1.5, colourPeak, staminaPeak), rhythmPeak, readingPeak); // Sections with 0 strain are excluded to avoid worst-case time complexity of the following sort (e.g. /b/2351871). // These sections will not contribute to the difficulty. diff --git a/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceCalculator.cs b/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceCalculator.cs index a93f4c66ab..5da18e7963 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceCalculator.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceCalculator.cs @@ -87,9 +87,6 @@ namespace osu.Game.Rulesets.Taiko.Difficulty if (score.Mods.Any(m => m is ModHidden)) difficultyValue *= 1.025; - if (score.Mods.Any(m => m is ModHardRock)) - difficultyValue *= 1.10; - if (score.Mods.Any(m => m is ModFlashlight)) difficultyValue *= Math.Max(1, 1.050 - Math.Min(attributes.MonoStaminaFactor / 50, 1) * lengthBonus); From 1fcd953e4a55c8d6576e64c737fa05b19a40a829 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sat, 21 Dec 2024 20:17:27 +0900 Subject: [PATCH 0357/1275] 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 0358/1275] 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 0359/1275] 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 0360/1275] 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 0361/1275] 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 0362/1275] 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 0363/1275] 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 0364/1275] 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 0365/1275] 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 0366/1275] 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 6808a5a77cffdb5800fc6443823bcad80283f549 Mon Sep 17 00:00:00 2001 From: StanR Date: Sun, 22 Dec 2024 04:45:29 +0500 Subject: [PATCH 0367/1275] Change slider drop penalty to use actual number of difficult sliders, fix slider drop penalty being too lenient (#31055) * Change slider drop penalty to use actual number of difficult sliders, fix slider nerf being too lenient * Move cubing to performance calculation * Add separate list for slider strains * Rename difficulty atttribute * Rename attribute in perfcalc * Check if AimDifficultSliderCount is more than 0, code quality fixes * Add `AimDifficultSliderCount` to the list of databased attributes * Code quality --------- Co-authored-by: James Wilson --- .../Difficulty/OsuDifficultyAttributes.cs | 9 ++++ .../Difficulty/OsuDifficultyCalculator.cs | 3 +- .../Difficulty/OsuPerformanceCalculator.cs | 49 +++++++++---------- .../Difficulty/Skills/Aim.cs | 24 +++++++++ .../Difficulty/DifficultyAttributes.cs | 1 + 5 files changed, 60 insertions(+), 26 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyAttributes.cs b/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyAttributes.cs index a3c0209a08..3b9a23df23 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyAttributes.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyAttributes.cs @@ -8,6 +8,7 @@ using Newtonsoft.Json; using osu.Game.Beatmaps; using osu.Game.Rulesets.Difficulty; using osu.Game.Rulesets.Mods; +using osu.Game.Rulesets.Osu.Objects; namespace osu.Game.Rulesets.Osu.Difficulty { @@ -19,6 +20,12 @@ namespace osu.Game.Rulesets.Osu.Difficulty [JsonProperty("aim_difficulty")] public double AimDifficulty { get; set; } + /// + /// The number of s weighted by difficulty. + /// + [JsonProperty("aim_difficult_slider_count")] + public double AimDifficultSliderCount { get; set; } + /// /// The difficulty corresponding to the speed skill. /// @@ -109,6 +116,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty yield return (ATTRIB_ID_AIM_DIFFICULT_STRAIN_COUNT, AimDifficultStrainCount); yield return (ATTRIB_ID_SPEED_DIFFICULT_STRAIN_COUNT, SpeedDifficultStrainCount); yield return (ATTRIB_ID_SPEED_NOTE_COUNT, SpeedNoteCount); + yield return (ATTRIB_ID_AIM_DIFFICULT_SLIDER_COUNT, AimDifficultSliderCount); } public override void FromDatabaseAttributes(IReadOnlyDictionary values, IBeatmapOnlineInfo onlineInfo) @@ -125,6 +133,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty AimDifficultStrainCount = values[ATTRIB_ID_AIM_DIFFICULT_STRAIN_COUNT]; SpeedDifficultStrainCount = values[ATTRIB_ID_SPEED_DIFFICULT_STRAIN_COUNT]; SpeedNoteCount = values[ATTRIB_ID_SPEED_NOTE_COUNT]; + AimDifficultSliderCount = values[ATTRIB_ID_AIM_DIFFICULT_SLIDER_COUNT]; DrainRate = onlineInfo.DrainRate; HitCircleCount = onlineInfo.CircleCount; SliderCount = onlineInfo.SliderCount; diff --git a/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs b/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs index 575e03051c..ffdd4673e3 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs @@ -40,7 +40,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty double aimRatingNoSliders = Math.Sqrt(skills[1].DifficultyValue()) * difficulty_multiplier; double speedRating = Math.Sqrt(skills[2].DifficultyValue()) * difficulty_multiplier; double speedNotes = ((Speed)skills[2]).RelevantNoteCount(); - + double difficultSliders = ((Aim)skills[0]).GetDifficultSliders(); double flashlightRating = 0.0; if (mods.Any(h => h is OsuModFlashlight)) @@ -99,6 +99,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty StarRating = starRating, Mods = mods, AimDifficulty = aimRating, + AimDifficultSliderCount = difficultSliders, SpeedDifficulty = speedRating, SpeedNoteCount = speedNotes, FlashlightDifficulty = flashlightRating, diff --git a/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs b/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs index 31b00dba2b..3610845533 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs @@ -135,7 +135,30 @@ namespace osu.Game.Rulesets.Osu.Difficulty private double computeAimValue(ScoreInfo score, OsuDifficultyAttributes attributes) { - double aimValue = OsuStrainSkill.DifficultyToPerformance(attributes.AimDifficulty); + double aimDifficulty = attributes.AimDifficulty; + + if (attributes.SliderCount > 0 && attributes.AimDifficultSliderCount > 0) + { + double estimateImproperlyFollowedDifficultSliders; + + if (usingClassicSliderAccuracy) + { + // When the score is considered classic (regardless if it was made on old client or not) we consider all missing combo to be dropped difficult sliders + int maximumPossibleDroppedSliders = totalImperfectHits; + estimateImproperlyFollowedDifficultSliders = Math.Clamp(Math.Min(maximumPossibleDroppedSliders, attributes.MaxCombo - scoreMaxCombo), 0, attributes.AimDifficultSliderCount); + } + else + { + // We add tick misses here since they too mean that the player didn't follow the slider properly + // We however aren't adding misses here because missing slider heads has a harsh penalty by itself and doesn't mean that the rest of the slider wasn't followed properly + estimateImproperlyFollowedDifficultSliders = Math.Min(countSliderEndsDropped + countSliderTickMiss, attributes.AimDifficultSliderCount); + } + + double sliderNerfFactor = (1 - attributes.SliderFactor) * Math.Pow(1 - estimateImproperlyFollowedDifficultSliders / attributes.AimDifficultSliderCount, 3) + attributes.SliderFactor; + aimDifficulty *= sliderNerfFactor; + } + + double aimValue = OsuStrainSkill.DifficultyToPerformance(aimDifficulty); double lengthBonus = 0.95 + 0.4 * Math.Min(1.0, totalHits / 2000.0) + (totalHits > 2000 ? Math.Log10(totalHits / 2000.0) * 0.5 : 0.0); @@ -163,30 +186,6 @@ namespace osu.Game.Rulesets.Osu.Difficulty aimValue *= 1.0 + 0.04 * (12.0 - attributes.ApproachRate); } - // We assume 15% of sliders in a map are difficult since there's no way to tell from the performance calculator. - double estimateDifficultSliders = attributes.SliderCount * 0.15; - - if (attributes.SliderCount > 0) - { - double estimateImproperlyFollowedDifficultSliders; - - if (usingClassicSliderAccuracy) - { - // When the score is considered classic (regardless if it was made on old client or not) we consider all missing combo to be dropped difficult sliders - int maximumPossibleDroppedSliders = totalImperfectHits; - estimateImproperlyFollowedDifficultSliders = Math.Clamp(Math.Min(maximumPossibleDroppedSliders, attributes.MaxCombo - scoreMaxCombo), 0, estimateDifficultSliders); - } - else - { - // We add tick misses here since they too mean that the player didn't follow the slider properly - // We however aren't adding misses here because missing slider heads has a harsh penalty by itself and doesn't mean that the rest of the slider wasn't followed properly - estimateImproperlyFollowedDifficultSliders = Math.Clamp(countSliderEndsDropped + countSliderTickMiss, 0, estimateDifficultSliders); - } - - double sliderNerfFactor = (1 - attributes.SliderFactor) * Math.Pow(1 - estimateImproperlyFollowedDifficultSliders / estimateDifficultSliders, 3) + attributes.SliderFactor; - aimValue *= sliderNerfFactor; - } - aimValue *= accuracy; // It is important to consider accuracy difficulty when scaling with accuracy. aimValue *= 0.98 + Math.Pow(attributes.OverallDifficulty, 2) / 2500; diff --git a/osu.Game.Rulesets.Osu/Difficulty/Skills/Aim.cs b/osu.Game.Rulesets.Osu/Difficulty/Skills/Aim.cs index faf91e4652..400bc97fbc 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/Skills/Aim.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/Skills/Aim.cs @@ -2,9 +2,12 @@ // See the LICENCE file in the repository root for full licence text. using System; +using System.Collections.Generic; +using System.Linq; using osu.Game.Rulesets.Difficulty.Preprocessing; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Osu.Difficulty.Evaluators; +using osu.Game.Rulesets.Osu.Objects; namespace osu.Game.Rulesets.Osu.Difficulty.Skills { @@ -26,6 +29,8 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills private double skillMultiplier => 25.18; private double strainDecayBase => 0.15; + private readonly List sliderStrains = new List(); + private double strainDecay(double ms) => Math.Pow(strainDecayBase, ms / 1000); protected override double CalculateInitialStrain(double time, DifficultyHitObject current) => currentStrain * strainDecay(time - current.Previous(0).StartTime); @@ -35,7 +40,26 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills currentStrain *= strainDecay(current.DeltaTime); currentStrain += AimEvaluator.EvaluateDifficultyOf(current, withSliders) * skillMultiplier; + if (current.BaseObject is Slider) + { + sliderStrains.Add(currentStrain); + } + return currentStrain; } + + public double GetDifficultSliders() + { + if (sliderStrains.Count == 0) + return 0; + + double[] sortedStrains = sliderStrains.OrderDescending().ToArray(); + + double maxSliderStrain = sortedStrains.Max(); + if (maxSliderStrain == 0) + return 0; + + return sortedStrains.Sum(strain => 1.0 / (1.0 + Math.Exp(-(strain / maxSliderStrain * 12.0 - 6.0)))); + } } } diff --git a/osu.Game/Rulesets/Difficulty/DifficultyAttributes.cs b/osu.Game/Rulesets/Difficulty/DifficultyAttributes.cs index 7b6bc37a61..f5ed5a180b 100644 --- a/osu.Game/Rulesets/Difficulty/DifficultyAttributes.cs +++ b/osu.Game/Rulesets/Difficulty/DifficultyAttributes.cs @@ -30,6 +30,7 @@ namespace osu.Game.Rulesets.Difficulty protected const int ATTRIB_ID_AIM_DIFFICULT_STRAIN_COUNT = 25; protected const int ATTRIB_ID_OK_HIT_WINDOW = 27; protected const int ATTRIB_ID_MONO_STAMINA_FACTOR = 29; + protected const int ATTRIB_ID_AIM_DIFFICULT_SLIDER_COUNT = 31; /// /// The mods which were applied to the beatmap. From fa5922337da11583469482d26b8b4043badc8574 Mon Sep 17 00:00:00 2001 From: Plextora <71889427+Plextora@users.noreply.github.com> Date: Sat, 21 Dec 2024 21:17:03 -0500 Subject: [PATCH 0368/1275] Fail on slider tail miss option in Sudden Death --- osu.Game.Rulesets.Osu/Mods/OsuModSuddenDeath.cs | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModSuddenDeath.cs b/osu.Game.Rulesets.Osu/Mods/OsuModSuddenDeath.cs index e661610fe7..f781bf0b90 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModSuddenDeath.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModSuddenDeath.cs @@ -3,7 +3,12 @@ using System; using System.Linq; +using osu.Framework.Bindables; +using osu.Game.Configuration; +using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Mods; +using osu.Game.Rulesets.Osu.Objects; +using osu.Game.Rulesets.Scoring; namespace osu.Game.Rulesets.Osu.Mods { @@ -13,5 +18,16 @@ namespace osu.Game.Rulesets.Osu.Mods { typeof(OsuModTargetPractice), }).ToArray(); + + [SettingSource("Fail on slider tail miss", "Fail when missing on the end of a slider")] + public BindableBool SliderTailMiss { get; } = new BindableBool(); + + protected override bool FailCondition(HealthProcessor healthProcessor, JudgementResult result) + { + if (SliderTailMiss.Value && result.HitObject is SliderTailCircle && result.Type == HitResult.IgnoreMiss) + return true; + + return result.Type.AffectsCombo() && !result.IsHit; + } } } From 87697a72e333d1468a35d4a3fec388319cc16e2a Mon Sep 17 00:00:00 2001 From: Plextora <71889427+Plextora@users.noreply.github.com> Date: Sat, 21 Dec 2024 21:32:09 -0500 Subject: [PATCH 0369/1275] Rename to PFC mode --- osu.Game.Rulesets.Osu/Mods/OsuModSuddenDeath.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModSuddenDeath.cs b/osu.Game.Rulesets.Osu/Mods/OsuModSuddenDeath.cs index f781bf0b90..3a65ba3b10 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModSuddenDeath.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModSuddenDeath.cs @@ -19,7 +19,7 @@ namespace osu.Game.Rulesets.Osu.Mods typeof(OsuModTargetPractice), }).ToArray(); - [SettingSource("Fail on slider tail miss", "Fail when missing on the end of a slider")] + [SettingSource("PFC mode", "Fail when missing on a slider tail")] public BindableBool SliderTailMiss { get; } = new BindableBool(); protected override bool FailCondition(HealthProcessor healthProcessor, JudgementResult result) From 420c5577d3a8aef97af82158110211c72cf5f8aa Mon Sep 17 00:00:00 2001 From: Plextora <71889427+Plextora@users.noreply.github.com> Date: Sat, 21 Dec 2024 22:55:30 -0500 Subject: [PATCH 0370/1275] Rename option (again) --- osu.Game.Rulesets.Osu/Mods/OsuModSuddenDeath.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModSuddenDeath.cs b/osu.Game.Rulesets.Osu/Mods/OsuModSuddenDeath.cs index 3a65ba3b10..fb587a94ca 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModSuddenDeath.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModSuddenDeath.cs @@ -19,7 +19,7 @@ namespace osu.Game.Rulesets.Osu.Mods typeof(OsuModTargetPractice), }).ToArray(); - [SettingSource("PFC mode", "Fail when missing on a slider tail")] + [SettingSource("Fail when missing on a slider tail")] public BindableBool SliderTailMiss { get; } = new BindableBool(); protected override bool FailCondition(HealthProcessor healthProcessor, JudgementResult result) From 5c9278ee2f5c044e0b6565973497b559f620ab5e Mon Sep 17 00:00:00 2001 From: Plextora <71889427+Plextora@users.noreply.github.com> Date: Sat, 21 Dec 2024 22:56:42 -0500 Subject: [PATCH 0371/1275] One line return for FailCondition --- osu.Game.Rulesets.Osu/Mods/OsuModSuddenDeath.cs | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModSuddenDeath.cs b/osu.Game.Rulesets.Osu/Mods/OsuModSuddenDeath.cs index fb587a94ca..a90d44c473 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModSuddenDeath.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModSuddenDeath.cs @@ -22,12 +22,7 @@ namespace osu.Game.Rulesets.Osu.Mods [SettingSource("Fail when missing on a slider tail")] public BindableBool SliderTailMiss { get; } = new BindableBool(); - protected override bool FailCondition(HealthProcessor healthProcessor, JudgementResult result) - { - if (SliderTailMiss.Value && result.HitObject is SliderTailCircle && result.Type == HitResult.IgnoreMiss) - return true; - - return result.Type.AffectsCombo() && !result.IsHit; - } + protected override bool FailCondition(HealthProcessor healthProcessor, JudgementResult result) => ( + SliderTailMiss.Value && result.HitObject is SliderTailCircle && result.Type == HitResult.IgnoreMiss) || (result.Type.AffectsCombo() && !result.IsHit); } } From c24f690019fd1871a941abcbb5d70ca386387137 Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Sun, 22 Dec 2024 07:47:57 -0500 Subject: [PATCH 0372/1275] 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 0373/1275] 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 0374/1275] 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 0375/1275] 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 0376/1275] 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 047c448741a6c2ab038a04085ebab97048e8473d Mon Sep 17 00:00:00 2001 From: Plextora <71889427+Plextora@users.noreply.github.com> Date: Sun, 22 Dec 2024 12:09:27 -0500 Subject: [PATCH 0377/1275] Return base for default FailCondition --- osu.Game.Rulesets.Osu/Mods/OsuModSuddenDeath.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModSuddenDeath.cs b/osu.Game.Rulesets.Osu/Mods/OsuModSuddenDeath.cs index a90d44c473..ea32b4868a 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModSuddenDeath.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModSuddenDeath.cs @@ -22,7 +22,7 @@ namespace osu.Game.Rulesets.Osu.Mods [SettingSource("Fail when missing on a slider tail")] public BindableBool SliderTailMiss { get; } = new BindableBool(); - protected override bool FailCondition(HealthProcessor healthProcessor, JudgementResult result) => ( - SliderTailMiss.Value && result.HitObject is SliderTailCircle && result.Type == HitResult.IgnoreMiss) || (result.Type.AffectsCombo() && !result.IsHit); + protected override bool FailCondition(HealthProcessor healthProcessor, JudgementResult result) => + (SliderTailMiss.Value && result.HitObject is SliderTailCircle && result.Type == HitResult.IgnoreMiss) || base.FailCondition(healthProcessor, result); } } 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 0378/1275] 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 fd1cc34e3fd01747eb132c04b831a22429be7c99 Mon Sep 17 00:00:00 2001 From: Plextora <71889427+Plextora@users.noreply.github.com> Date: Sun, 22 Dec 2024 17:46:01 -0500 Subject: [PATCH 0379/1275] No more one line return for FailCondition --- osu.Game.Rulesets.Osu/Mods/OsuModSuddenDeath.cs | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModSuddenDeath.cs b/osu.Game.Rulesets.Osu/Mods/OsuModSuddenDeath.cs index ea32b4868a..73d0403e3f 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModSuddenDeath.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModSuddenDeath.cs @@ -22,7 +22,12 @@ namespace osu.Game.Rulesets.Osu.Mods [SettingSource("Fail when missing on a slider tail")] public BindableBool SliderTailMiss { get; } = new BindableBool(); - protected override bool FailCondition(HealthProcessor healthProcessor, JudgementResult result) => - (SliderTailMiss.Value && result.HitObject is SliderTailCircle && result.Type == HitResult.IgnoreMiss) || base.FailCondition(healthProcessor, result); + protected override bool FailCondition(HealthProcessor healthProcessor, JudgementResult result) + { + if (SliderTailMiss.Value && result.HitObject is SliderTailCircle && result.Type == HitResult.IgnoreMiss) + return true; + + return base.FailCondition(healthProcessor, result); + } } } From 1a7feeb4edab01db1ab6c9fa5c501b69456a78da Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 23 Dec 2024 14:39:07 +0900 Subject: [PATCH 0380/1275] 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 0381/1275] Fix more than obvious test failure Please run tests please run tests please run tests. --- .../BeatmapListing/BeatmapSearchGeneralFilterRow.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/osu.Game/Overlays/BeatmapListing/BeatmapSearchGeneralFilterRow.cs b/osu.Game/Overlays/BeatmapListing/BeatmapSearchGeneralFilterRow.cs index b525d8282e..e4c663ee13 100644 --- a/osu.Game/Overlays/BeatmapListing/BeatmapSearchGeneralFilterRow.cs +++ b/osu.Game/Overlays/BeatmapListing/BeatmapSearchGeneralFilterRow.cs @@ -125,9 +125,6 @@ namespace osu.Game.Overlays.BeatmapListing [Resolved] private OsuColour colours { get; set; } = null!; - [Resolved] - private OsuGame game { get; set; } = null!; - [Resolved] private OsuConfigManager config { get; set; } = null!; @@ -137,6 +134,9 @@ namespace osu.Game.Overlays.BeatmapListing [Resolved] private IDialogOverlay? dialogOverlay { get; set; } + [Resolved] + private OsuGame? game { get; set; } + protected override void LoadComplete() { base.LoadComplete(); @@ -148,7 +148,7 @@ namespace osu.Game.Overlays.BeatmapListing if (!Active.Value) disclaimerShown.Value = true; - if (game.HideUnlicensedContent) + if (game?.HideUnlicensedContent == true) { Enabled.Value = false; Active.Disabled = true; From 638d959c5cc3fdcdb6d070eb976191e2b6f734ec Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Mon, 23 Dec 2024 20:12:25 +0900 Subject: [PATCH 0382/1275] Initial support for free style selection --- osu.Game/Online/Rooms/PlaylistItem.cs | 5 +- .../Screens/OnlinePlay/Match/RoomSubScreen.cs | 118 ++++++++++++++++-- .../MultiplayerMatchStyleSelect.cs | 84 +++++++++++++ .../Multiplayer/MultiplayerMatchSubScreen.cs | 75 +++++++---- .../Select/Carousel/CarouselBeatmap.cs | 3 + osu.Game/Screens/Select/FilterControl.cs | 2 +- osu.Game/Screens/Select/FilterCriteria.cs | 1 + osu.Game/Screens/Select/SongSelect.cs | 10 +- 8 files changed, 252 insertions(+), 46 deletions(-) create mode 100644 osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchStyleSelect.cs diff --git a/osu.Game/Online/Rooms/PlaylistItem.cs b/osu.Game/Online/Rooms/PlaylistItem.cs index 3d829d1e4e..937bc40e9b 100644 --- a/osu.Game/Online/Rooms/PlaylistItem.cs +++ b/osu.Game/Online/Rooms/PlaylistItem.cs @@ -124,13 +124,14 @@ namespace osu.Game.Online.Rooms #endregion - public PlaylistItem With(Optional id = default, Optional beatmap = default, Optional playlistOrder = default) + public PlaylistItem With(Optional id = default, Optional beatmap = default, Optional playlistOrder = default, + Optional ruleset = default) { return new PlaylistItem(beatmap.GetOr(Beatmap)) { ID = id.GetOr(ID), OwnerID = OwnerID, - RulesetID = RulesetID, + RulesetID = ruleset.GetOr(RulesetID), Expired = Expired, PlaylistOrder = playlistOrder.GetOr(PlaylistOrder), PlayedAt = PlayedAt, diff --git a/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs b/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs index 4ef31c02c3..c9e0cbc1e9 100644 --- a/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs @@ -11,6 +11,7 @@ using osu.Framework.Audio; using osu.Framework.Audio.Sample; using osu.Framework.Bindables; using osu.Framework.Extensions.Color4Extensions; +using osu.Framework.Extensions.ObjectExtensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Cursor; @@ -36,6 +37,18 @@ namespace osu.Game.Screens.OnlinePlay.Match { public readonly Bindable SelectedItem = new Bindable(); + /// + /// When players are freely allowed to select their own gameplay style (selected item has a non-null beatmapset id), + /// a non-null value indicates a local beatmap selection from the same beatmapset as the selected item. + /// + public readonly Bindable DifficultyOverride = new Bindable(); + + /// + /// When players are freely allowed to select their own gameplay style (selected item has a non-null beatmapset id), + /// a non-null value indicates a local ruleset selection. + /// + public readonly Bindable RulesetOverride = new Bindable(); + public override bool? ApplyModTrackAdjustments => true; protected override BackgroundScreen CreateBackground() => new RoomBackgroundScreen(Room.Playlist.FirstOrDefault()) @@ -51,6 +64,17 @@ namespace osu.Game.Screens.OnlinePlay.Match /// protected Drawable? UserModsSection; + /// + /// A container that provides controls for selection of the user's difficulty override. + /// This will be shown/hidden automatically when applicable. + /// + protected Drawable? UserDifficultySection; + + /// + /// A container that will display the user's difficulty override. + /// + protected Container? UserStyleDisplayContainer; + private Sample? sampleStart; /// @@ -250,6 +274,8 @@ namespace osu.Game.Screens.OnlinePlay.Match SelectedItem.BindValueChanged(_ => Scheduler.AddOnce(selectedItemChanged)); UserMods.BindValueChanged(_ => Scheduler.AddOnce(UpdateMods)); + DifficultyOverride.BindValueChanged(_ => Scheduler.AddOnce(updateStyleOverride)); + RulesetOverride.BindValueChanged(_ => Scheduler.AddOnce(updateStyleOverride)); beatmapAvailabilityTracker.SelectedItem.BindTo(SelectedItem); beatmapAvailabilityTracker.Availability.BindValueChanged(_ => updateWorkingBeatmap()); @@ -383,7 +409,7 @@ namespace osu.Game.Screens.OnlinePlay.Match protected void StartPlay() { - if (SelectedItem.Value == null) + if (GetGameplayItem() is not PlaylistItem item) return; // User may be at song select or otherwise when the host starts gameplay. @@ -401,7 +427,7 @@ namespace osu.Game.Screens.OnlinePlay.Match // fallback is to allow this class to operate when there is no parent OnlineScreen (testing purposes). var targetScreen = (Screen?)ParentScreen ?? this; - targetScreen.Push(CreateGameplayScreen(SelectedItem.Value)); + targetScreen.Push(CreateGameplayScreen(item)); } /// @@ -413,11 +439,18 @@ namespace osu.Game.Screens.OnlinePlay.Match private void selectedItemChanged() { - updateWorkingBeatmap(); - if (SelectedItem.Value is not PlaylistItem selected) return; + if (selected.BeatmapSetId == null || selected.BeatmapSetId != DifficultyOverride.Value?.BeatmapSet.AsNonNull().OnlineID) + { + DifficultyOverride.Value = null; + RulesetOverride.Value = null; + } + + updateStyleOverride(); + updateWorkingBeatmap(); + var rulesetInstance = Rulesets.GetRuleset(selected.RulesetID)?.CreateInstance(); Debug.Assert(rulesetInstance != null); var allowedMods = selected.AllowedMods.Select(m => m.ToMod(rulesetInstance)); @@ -439,37 +472,96 @@ namespace osu.Game.Screens.OnlinePlay.Match UserModsSection?.Show(); UserModsSelectOverlay.IsValidMod = m => allowedMods.Any(a => a.GetType() == m.GetType()); } + + if (selected.BeatmapSetId == null) + UserDifficultySection?.Hide(); + else + UserDifficultySection?.Show(); } private void updateWorkingBeatmap() { - if (SelectedItem.Value == null || !this.IsCurrentScreen()) + if (GetGameplayItem() is not PlaylistItem item || !this.IsCurrentScreen()) return; - var beatmap = SelectedItem.Value?.Beatmap; - // Retrieve the corresponding local beatmap, since we can't directly use the playlist's beatmap info - var localBeatmap = beatmap == null ? null : beatmapManager.QueryBeatmap(b => b.OnlineID == beatmap.OnlineID); + var localBeatmap = beatmapManager.QueryBeatmap(b => b.OnlineID == item.Beatmap.OnlineID); UserModsSelectOverlay.Beatmap.Value = Beatmap.Value = beatmapManager.GetWorkingBeatmap(localBeatmap); } protected virtual void UpdateMods() { - if (SelectedItem.Value == null || !this.IsCurrentScreen()) + if (GetGameplayItem() is not PlaylistItem item || !this.IsCurrentScreen()) return; - var rulesetInstance = Rulesets.GetRuleset(SelectedItem.Value.RulesetID)?.CreateInstance(); + var rulesetInstance = Rulesets.GetRuleset(item.RulesetID)?.CreateInstance(); Debug.Assert(rulesetInstance != null); - Mods.Value = UserMods.Value.Concat(SelectedItem.Value.RequiredMods.Select(m => m.ToMod(rulesetInstance))).ToList(); + Mods.Value = UserMods.Value.Concat(item.RequiredMods.Select(m => m.ToMod(rulesetInstance))).ToList(); } - private void updateRuleset() + private void updateStyleOverride() { if (SelectedItem.Value == null || !this.IsCurrentScreen()) return; - Ruleset.Value = Rulesets.GetRuleset(SelectedItem.Value.RulesetID); + if (UserStyleDisplayContainer == null) + return; + + PlaylistItem gameplayItem = GetGameplayItem()!; + + if (UserStyleDisplayContainer.SingleOrDefault()?.Item.Equals(gameplayItem) == true) + return; + + UserStyleDisplayContainer.Child = new DrawableRoomPlaylistItem(gameplayItem) + { + AllowReordering = false, + AllowEditing = true, + RequestEdit = openStyleSelection + }; + } + + protected PlaylistItem? GetGameplayItem() + { + PlaylistItem? selectedItemWithOverride = SelectedItem.Value; + + if (selectedItemWithOverride?.BeatmapSetId == null) + return selectedItemWithOverride; + + // Sanity check. + if (DifficultyOverride.Value?.BeatmapSet?.OnlineID != selectedItemWithOverride.BeatmapSetId) + return selectedItemWithOverride; + + if (DifficultyOverride.Value != null) + selectedItemWithOverride = selectedItemWithOverride.With(beatmap: DifficultyOverride.Value); + + if (RulesetOverride.Value != null) + selectedItemWithOverride = selectedItemWithOverride.With(ruleset: RulesetOverride.Value.OnlineID); + + return selectedItemWithOverride; + } + + private void openStyleSelection(PlaylistItem item) + { + if (!this.IsCurrentScreen()) + return; + + this.Push(new MultiplayerMatchStyleSelect(Room, item, (beatmap, ruleset) => + { + if (SelectedItem.Value?.BeatmapSetId == null || SelectedItem.Value.BeatmapSetId != beatmap.BeatmapSet?.OnlineID) + return; + + DifficultyOverride.Value = beatmap; + RulesetOverride.Value = ruleset; + })); + } + + private void updateRuleset() + { + if (GetGameplayItem() is not PlaylistItem item || !this.IsCurrentScreen()) + return; + + Ruleset.Value = Rulesets.GetRuleset(item.RulesetID); } private void beginHandlingTrack() diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchStyleSelect.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchStyleSelect.cs new file mode 100644 index 0000000000..dc1393bf96 --- /dev/null +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchStyleSelect.cs @@ -0,0 +1,84 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using System.Collections.Generic; +using System.Linq; +using Humanizer; +using osu.Framework.Allocation; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Screens; +using osu.Game.Beatmaps; +using osu.Game.Online.Rooms; +using osu.Game.Rulesets; +using osu.Game.Screens.Select; +using osu.Game.Users; + +namespace osu.Game.Screens.OnlinePlay.Multiplayer +{ + public partial class MultiplayerMatchStyleSelect : SongSelect, IOnlinePlaySubScreen + { + public string ShortTitle => "style selection"; + + public override string Title => ShortTitle.Humanize(); + + public override bool AllowEditing => false; + + protected override UserActivity InitialActivity => new UserActivity.InLobby(room); + + private readonly Room room; + private readonly PlaylistItem item; + private readonly Action onSelect; + + public MultiplayerMatchStyleSelect(Room room, PlaylistItem item, Action onSelect) + { + this.room = room; + this.item = item; + this.onSelect = onSelect; + + Padding = new MarginPadding { Horizontal = HORIZONTAL_OVERFLOW_PADDING }; + } + + [BackgroundDependencyLoader] + private void load() + { + LeftArea.Padding = new MarginPadding { Top = Header.HEIGHT }; + } + + protected override FilterControl CreateFilterControl() => new DifficultySelectFilterControl(item); + + protected override IEnumerable<(FooterButton, OverlayContainer?)> CreateSongSelectFooterButtons() + { + // Required to create the drawable components. + base.CreateSongSelectFooterButtons(); + return Enumerable.Empty<(FooterButton, OverlayContainer?)>(); + } + + protected override BeatmapDetailArea CreateBeatmapDetailArea() => new PlayBeatmapDetailArea(); + + protected override bool OnStart() + { + onSelect(Beatmap.Value.BeatmapInfo, Ruleset.Value); + this.Exit(); + return true; + } + + private partial class DifficultySelectFilterControl : FilterControl + { + private readonly PlaylistItem item; + + public DifficultySelectFilterControl(PlaylistItem item) + { + this.item = item; + } + + public override FilterCriteria CreateCriteria() + { + var criteria = base.CreateCriteria(); + criteria.BeatmapSetId = item.BeatmapSetId; + return criteria; + } + } + } +} diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs index edc45dbf7c..d807fe8177 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs @@ -145,43 +145,66 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer SelectedItem = SelectedItem } }, - new[] + new Drawable[] { - UserModsSection = new FillFlowContainer + new Container { RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y, Margin = new MarginPadding { Top = 10 }, - Alpha = 0, - Children = new Drawable[] + Children = new[] { - new OverlinedHeader("Extra mods"), - new FillFlowContainer + UserModsSection = new FillFlowContainer { - AutoSizeAxes = Axes.Both, - Direction = FillDirection.Horizontal, - Spacing = new Vector2(10, 0), + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Alpha = 0, Children = new Drawable[] { - new UserModSelectButton + new OverlinedHeader("Extra mods"), + new FillFlowContainer { - Anchor = Anchor.CentreLeft, - Origin = Anchor.CentreLeft, - Width = 90, - Text = "Select", - Action = ShowUserModSelect, - }, - new ModDisplay - { - Anchor = Anchor.CentreLeft, - Origin = Anchor.CentreLeft, - Current = UserMods, - Scale = new Vector2(0.8f), + AutoSizeAxes = Axes.Both, + Direction = FillDirection.Horizontal, + Spacing = new Vector2(10, 0), + Children = new Drawable[] + { + new UserModSelectButton + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + Width = 90, + Text = "Select", + Action = ShowUserModSelect, + }, + new ModDisplay + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + Current = UserMods, + Scale = new Vector2(0.8f), + }, + } }, } }, + UserDifficultySection = new FillFlowContainer + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Alpha = 0, + Children = new Drawable[] + { + new OverlinedHeader("Difficulty"), + UserStyleDisplayContainer = new Container + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y + } + } + }, } - }, + } }, }, RowDimensions = new[] @@ -240,14 +263,14 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer protected override void UpdateMods() { - if (SelectedItem.Value == null || client.LocalUser == null || !this.IsCurrentScreen()) + if (GetGameplayItem() is not PlaylistItem item || client.LocalUser == null || !this.IsCurrentScreen()) return; // update local mods based on room's reported status for the local user (omitting the base call implementation). // this makes the server authoritative, and avoids the local user potentially setting mods that the server is not aware of (ie. if the match was started during the selection being changed). - var rulesetInstance = Rulesets.GetRuleset(SelectedItem.Value.RulesetID)?.CreateInstance(); + var rulesetInstance = Rulesets.GetRuleset(item.RulesetID)?.CreateInstance(); Debug.Assert(rulesetInstance != null); - Mods.Value = client.LocalUser.Mods.Select(m => m.ToMod(rulesetInstance)).Concat(SelectedItem.Value.RequiredMods.Select(m => m.ToMod(rulesetInstance))).ToList(); + Mods.Value = client.LocalUser.Mods.Select(m => m.ToMod(rulesetInstance)).Concat(item.RequiredMods.Select(m => m.ToMod(rulesetInstance))).ToList(); } [Resolved(canBeNull: true)] diff --git a/osu.Game/Screens/Select/Carousel/CarouselBeatmap.cs b/osu.Game/Screens/Select/Carousel/CarouselBeatmap.cs index c007fa29ed..95186e98d8 100644 --- a/osu.Game/Screens/Select/Carousel/CarouselBeatmap.cs +++ b/osu.Game/Screens/Select/Carousel/CarouselBeatmap.cs @@ -90,6 +90,9 @@ namespace osu.Game.Screens.Select.Carousel if (match && criteria.RulesetCriteria != null) match &= criteria.RulesetCriteria.Matches(BeatmapInfo, criteria); + if (match && criteria.BeatmapSetId != null) + match &= criteria.BeatmapSetId == BeatmapInfo.BeatmapSet?.OnlineID; + return match; } diff --git a/osu.Game/Screens/Select/FilterControl.cs b/osu.Game/Screens/Select/FilterControl.cs index b221296ba8..488f63accb 100644 --- a/osu.Game/Screens/Select/FilterControl.cs +++ b/osu.Game/Screens/Select/FilterControl.cs @@ -57,7 +57,7 @@ namespace osu.Game.Screens.Select [CanBeNull] private FilterCriteria currentCriteria; - public FilterCriteria CreateCriteria() + public virtual FilterCriteria CreateCriteria() { string query = searchTextBox.Text; diff --git a/osu.Game/Screens/Select/FilterCriteria.cs b/osu.Game/Screens/Select/FilterCriteria.cs index 76c0f769f0..63dbdfbed3 100644 --- a/osu.Game/Screens/Select/FilterCriteria.cs +++ b/osu.Game/Screens/Select/FilterCriteria.cs @@ -56,6 +56,7 @@ namespace osu.Game.Screens.Select public RulesetInfo? Ruleset; public IReadOnlyList? Mods; public bool AllowConvertedBeatmaps; + public int? BeatmapSetId; private string searchText = string.Empty; diff --git a/osu.Game/Screens/Select/SongSelect.cs b/osu.Game/Screens/Select/SongSelect.cs index 9ebd9c9846..c8d50436d9 100644 --- a/osu.Game/Screens/Select/SongSelect.cs +++ b/osu.Game/Screens/Select/SongSelect.cs @@ -216,11 +216,11 @@ namespace osu.Game.Screens.Select }, } }, - FilterControl = new FilterControl + FilterControl = CreateFilterControl().With(d => { - RelativeSizeAxes = Axes.X, - Height = FilterControl.HEIGHT, - }, + d.RelativeSizeAxes = Axes.X; + d.Height = FilterControl.HEIGHT; + }), new GridContainer // used for max width implementation { RelativeSizeAxes = Axes.Both, @@ -389,6 +389,8 @@ namespace osu.Game.Screens.Select SampleConfirm = audio.Samples.Get(@"SongSelect/confirm-selection"); } + protected virtual FilterControl CreateFilterControl() => new FilterControl(); + protected override void LoadComplete() { base.LoadComplete(); From 097828ded208d872bf886579741fe72197781f01 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 23 Dec 2024 22:07:42 +0900 Subject: [PATCH 0383/1275] 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 0384/1275] 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 0385/1275] 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 0386/1275] 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 0387/1275] 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 0388/1275] 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 0389/1275] 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 0390/1275] 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 0391/1275] 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 0392/1275] 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 0393/1275] 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 0394/1275] 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 0395/1275] 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 0396/1275] 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 0397/1275] 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 0398/1275] Fix adjusting volume using scroll wheel not working during intro --- osu.Game/Screens/Menu/IntroScreen.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/osu.Game/Screens/Menu/IntroScreen.cs b/osu.Game/Screens/Menu/IntroScreen.cs index 9885c061a9..c110c53df8 100644 --- a/osu.Game/Screens/Menu/IntroScreen.cs +++ b/osu.Game/Screens/Menu/IntroScreen.cs @@ -24,6 +24,7 @@ using osu.Game.Localisation; using osu.Game.Online.API; using osu.Game.Overlays; using osu.Game.Overlays.Notifications; +using osu.Game.Overlays.Volume; using osu.Game.Rulesets; using osu.Game.Screens.Backgrounds; using osu.Game.Skinning; @@ -174,6 +175,8 @@ namespace osu.Game.Screens.Menu return UsingThemedIntro = initialBeatmap != null; } + + AddInternal(new GlobalScrollAdjustsVolume()); } public override void OnEntering(ScreenTransitionEvent e) From 7777c447754a0bcfd64036681175712528c5d454 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Tue, 24 Dec 2024 17:57:59 +0900 Subject: [PATCH 0399/1275] Only allow selecting beatmaps within 30s length --- .../Screens/OnlinePlay/Match/RoomSubScreen.cs | 8 ++++---- .../Multiplayer/MultiplayerMatchStyleSelect.cs | 18 ++++++++++++++++++ 2 files changed, 22 insertions(+), 4 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs b/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs index c9e0cbc1e9..49144f9de5 100644 --- a/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs @@ -517,7 +517,7 @@ namespace osu.Game.Screens.OnlinePlay.Match { AllowReordering = false, AllowEditing = true, - RequestEdit = openStyleSelection + RequestEdit = _ => openStyleSelection() }; } @@ -541,12 +541,12 @@ namespace osu.Game.Screens.OnlinePlay.Match return selectedItemWithOverride; } - private void openStyleSelection(PlaylistItem item) + private void openStyleSelection() { - if (!this.IsCurrentScreen()) + if (SelectedItem.Value == null || !this.IsCurrentScreen()) return; - this.Push(new MultiplayerMatchStyleSelect(Room, item, (beatmap, ruleset) => + this.Push(new MultiplayerMatchStyleSelect(Room, SelectedItem.Value, (beatmap, ruleset) => { if (SelectedItem.Value?.BeatmapSetId == null || SelectedItem.Value.BeatmapSetId != beatmap.BeatmapSet?.OnlineID) return; diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchStyleSelect.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchStyleSelect.cs index dc1393bf96..19d8b96f2b 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchStyleSelect.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchStyleSelect.cs @@ -10,6 +10,7 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Screens; using osu.Game.Beatmaps; +using osu.Game.Database; using osu.Game.Online.Rooms; using osu.Game.Rulesets; using osu.Game.Screens.Select; @@ -67,16 +68,33 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer private partial class DifficultySelectFilterControl : FilterControl { private readonly PlaylistItem item; + private double itemLength; public DifficultySelectFilterControl(PlaylistItem item) { this.item = item; } + [BackgroundDependencyLoader] + private void load(RealmAccess realm) + { + int beatmapId = item.Beatmap.OnlineID; + itemLength = realm.Run(r => r.All().FirstOrDefault(b => b.OnlineID == beatmapId)?.Length ?? 0); + } + public override FilterCriteria CreateCriteria() { var criteria = base.CreateCriteria(); + + // Must be from the same set as the playlist item. criteria.BeatmapSetId = item.BeatmapSetId; + + // Must be within 30s of the playlist item. + criteria.Length.Min = itemLength - 30000; + criteria.Length.Max = itemLength + 30000; + criteria.Length.IsLowerInclusive = true; + criteria.Length.IsUpperInclusive = true; + return criteria; } } From 40486c4f38bfd60099c30fc3d20fcd148123c605 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Tue, 24 Dec 2024 18:04:36 +0900 Subject: [PATCH 0400/1275] Block beatmap presents in style select screen --- .../OnlinePlay/Multiplayer/MultiplayerMatchStyleSelect.cs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchStyleSelect.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchStyleSelect.cs index 19d8b96f2b..867579171d 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchStyleSelect.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchStyleSelect.cs @@ -18,7 +18,7 @@ using osu.Game.Users; namespace osu.Game.Screens.OnlinePlay.Multiplayer { - public partial class MultiplayerMatchStyleSelect : SongSelect, IOnlinePlaySubScreen + public partial class MultiplayerMatchStyleSelect : SongSelect, IOnlinePlaySubScreen, IHandlePresentBeatmap { public string ShortTitle => "style selection"; @@ -65,6 +65,11 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer return true; } + public void PresentBeatmap(WorkingBeatmap beatmap, RulesetInfo ruleset) + { + // This screen cannot present beatmaps. + } + private partial class DifficultySelectFilterControl : FilterControl { private readonly PlaylistItem item; From 3ddeaf8460476e8e8c8386e584addd8f9594d0d1 Mon Sep 17 00:00:00 2001 From: James Wilson Date: Tue, 24 Dec 2024 09:43:44 +0000 Subject: [PATCH 0401/1275] Use `lastAngle` when nerfing repeated angles on acute bonus (#31245) * Use `lastAngle` when nerfing repeated angles on acute bonus * Bump acute multiplier * Correct outdated wiggle bonus comment * Update test --------- Co-authored-by: StanR --- .../OsuDifficultyCalculatorTest.cs | 2 +- .../Difficulty/Evaluators/AimEvaluator.cs | 11 +++++------ 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/osu.Game.Rulesets.Osu.Tests/OsuDifficultyCalculatorTest.cs b/osu.Game.Rulesets.Osu.Tests/OsuDifficultyCalculatorTest.cs index 9798611488..c0a6d3a755 100644 --- a/osu.Game.Rulesets.Osu.Tests/OsuDifficultyCalculatorTest.cs +++ b/osu.Game.Rulesets.Osu.Tests/OsuDifficultyCalculatorTest.cs @@ -22,7 +22,7 @@ namespace osu.Game.Rulesets.Osu.Tests public void Test(double expectedStarRating, int expectedMaxCombo, string name) => base.Test(expectedStarRating, expectedMaxCombo, name); - [TestCase(9.4310274277499619d, 239, "diffcalc-test")] + [TestCase(9.6343245007055653d, 239, "diffcalc-test")] [TestCase(1.7550169162648608d, 54, "zero-length-sliders")] [TestCase(0.55231632896800109d, 4, "very-fast-slider")] public void TestClockRateAdjusted(double expectedStarRating, int expectedMaxCombo, string name) diff --git a/osu.Game.Rulesets.Osu/Difficulty/Evaluators/AimEvaluator.cs b/osu.Game.Rulesets.Osu/Difficulty/Evaluators/AimEvaluator.cs index c3270f25f8..fdf94719ed 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/Evaluators/AimEvaluator.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/Evaluators/AimEvaluator.cs @@ -12,7 +12,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators public static class AimEvaluator { private const double wide_angle_multiplier = 1.5; - private const double acute_angle_multiplier = 2.35; + private const double acute_angle_multiplier = 2.7; private const double slider_multiplier = 1.35; private const double velocity_change_multiplier = 0.75; private const double wiggle_multiplier = 1.02; @@ -75,7 +75,6 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators { double currAngle = osuCurrObj.Angle.Value; double lastAngle = osuLastObj.Angle.Value; - double lastLastAngle = osuLastLastObj.Angle.Value; // Rewarding angles, take the smaller velocity as base. double angleBonus = Math.Min(currVelocity, prevVelocity); @@ -90,11 +89,11 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators // Penalize wide angles if they're repeated, reducing the penalty as the lastAngle gets more acute. wideAngleBonus *= angleBonus * (1 - Math.Min(wideAngleBonus, Math.Pow(calcWideAngleBonus(lastAngle), 3))); - // Penalize acute angles if they're repeated, reducing the penalty as the lastLastAngle gets more obtuse. - acuteAngleBonus *= 0.03 + 0.97 * (1 - Math.Min(acuteAngleBonus, Math.Pow(calcAcuteAngleBonus(lastLastAngle), 3))); + // Penalize acute angles if they're repeated, reducing the penalty as the lastAngle gets more obtuse. + acuteAngleBonus *= 0.03 + 0.97 * (1 - Math.Min(acuteAngleBonus, Math.Pow(calcAcuteAngleBonus(lastAngle), 3))); - // Apply wiggle bonus for jumps that are [radius, 2*diameter] in distance, with < 110 angle and bpm > 150 - // https://www.desmos.com/calculator/iis7lgbppe + // Apply wiggle bonus for jumps that are [radius, 3*diameter] in distance, with < 110 angle + // https://www.desmos.com/calculator/dp0v0nvowc wiggleBonus = angleBonus * DifficultyCalculationUtils.Smootherstep(osuCurrObj.LazyJumpDistance, radius, diameter) * Math.Pow(DifficultyCalculationUtils.ReverseLerp(osuCurrObj.LazyJumpDistance, diameter * 3, diameter), 1.8) From 971ccb6a4e6a93b44e8bc17eb1ad577e334e6e6c Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Tue, 24 Dec 2024 19:05:50 +0900 Subject: [PATCH 0402/1275] Adjust namings --- ...rButtonFreePlay.cs => FooterButtonFreeStyle.cs} | 6 +++--- .../Screens/OnlinePlay/OnlinePlaySongSelect.cs | 14 +++++++------- .../OnlinePlay/Playlists/PlaylistsSongSelect.cs | 2 +- 3 files changed, 11 insertions(+), 11 deletions(-) rename osu.Game/Screens/OnlinePlay/{FooterButtonFreePlay.cs => FooterButtonFreeStyle.cs} (95%) diff --git a/osu.Game/Screens/OnlinePlay/FooterButtonFreePlay.cs b/osu.Game/Screens/OnlinePlay/FooterButtonFreeStyle.cs similarity index 95% rename from osu.Game/Screens/OnlinePlay/FooterButtonFreePlay.cs rename to osu.Game/Screens/OnlinePlay/FooterButtonFreeStyle.cs index bcc7bb787d..5edcddcb78 100644 --- a/osu.Game/Screens/OnlinePlay/FooterButtonFreePlay.cs +++ b/osu.Game/Screens/OnlinePlay/FooterButtonFreeStyle.cs @@ -15,7 +15,7 @@ using osu.Game.Screens.Select; namespace osu.Game.Screens.OnlinePlay { - public class FooterButtonFreePlay : FooterButton, IHasCurrentValue + public class FooterButtonFreeStyle : FooterButton, IHasCurrentValue { private readonly BindableWithCurrent current = new BindableWithCurrent(); @@ -33,7 +33,7 @@ namespace osu.Game.Screens.OnlinePlay [Resolved] private OsuColour colours { get; set; } = null!; - public FooterButtonFreePlay() + public FooterButtonFreeStyle() { // Overwrite any external behaviour as we delegate the main toggle action to a sub-button. base.Action = () => current.Value = !current.Value; @@ -71,7 +71,7 @@ namespace osu.Game.Screens.OnlinePlay SelectedColour = colours.Yellow; DeselectedColour = SelectedColour.Opacity(0.5f); - Text = @"freeplay"; + Text = @"freestyle"; } protected override void LoadComplete() diff --git a/osu.Game/Screens/OnlinePlay/OnlinePlaySongSelect.cs b/osu.Game/Screens/OnlinePlay/OnlinePlaySongSelect.cs index 1f1d259d0a..02f8c619a7 100644 --- a/osu.Game/Screens/OnlinePlay/OnlinePlaySongSelect.cs +++ b/osu.Game/Screens/OnlinePlay/OnlinePlaySongSelect.cs @@ -41,7 +41,7 @@ namespace osu.Game.Screens.OnlinePlay protected override UserActivity InitialActivity => new UserActivity.InLobby(room); protected readonly Bindable> FreeMods = new Bindable>(Array.Empty()); - protected readonly Bindable FreePlay = new Bindable(); + protected readonly Bindable FreeStyle = new Bindable(); private readonly Room room; private readonly PlaylistItem? initialItem; @@ -112,17 +112,17 @@ namespace osu.Game.Screens.OnlinePlay } if (initialItem.BeatmapSetId != null) - FreePlay.Value = true; + FreeStyle.Value = true; } Mods.BindValueChanged(onModsChanged); Ruleset.BindValueChanged(onRulesetChanged); - FreePlay.BindValueChanged(onFreePlayChanged, true); + FreeStyle.BindValueChanged(onFreeStyleChanged, true); freeModSelectOverlayRegistration = OverlayManager?.RegisterBlockingOverlay(freeModSelect); } - private void onFreePlayChanged(ValueChangedEvent enabled) + private void onFreeStyleChanged(ValueChangedEvent enabled) { if (enabled.NewValue) { @@ -162,7 +162,7 @@ namespace osu.Game.Screens.OnlinePlay RulesetID = Ruleset.Value.OnlineID, RequiredMods = Mods.Value.Select(m => new APIMod(m)).ToArray(), AllowedMods = FreeMods.Value.Select(m => new APIMod(m)).ToArray(), - BeatmapSetId = FreePlay.Value ? Beatmap.Value.BeatmapSetInfo.OnlineID : null + BeatmapSetId = FreeStyle.Value ? Beatmap.Value.BeatmapSetInfo.OnlineID : null }; return SelectItem(item); @@ -202,12 +202,12 @@ namespace osu.Game.Screens.OnlinePlay var baseButtons = base.CreateSongSelectFooterButtons().ToList(); freeModsFooterButton = new FooterButtonFreeMods(freeModSelect) { Current = FreeMods }; - var freePlayButton = new FooterButtonFreePlay { Current = FreePlay }; + var freeStyleButton = new FooterButtonFreeStyle { Current = FreeStyle }; baseButtons.InsertRange(baseButtons.FindIndex(b => b.Item1 is FooterButtonMods) + 1, new (FooterButton, OverlayContainer?)[] { (freeModsFooterButton, freeModSelect), - (freePlayButton, null) + (freeStyleButton, null) }); return baseButtons; diff --git a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsSongSelect.cs b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsSongSelect.cs index f9e014a727..a3b8a1575e 100644 --- a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsSongSelect.cs +++ b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsSongSelect.cs @@ -37,7 +37,7 @@ namespace osu.Game.Screens.OnlinePlay.Playlists private PlaylistItem createNewItem() => new PlaylistItem(Beatmap.Value.BeatmapInfo) { ID = room.Playlist.Count == 0 ? 0 : room.Playlist.Max(p => p.ID) + 1, - BeatmapSetId = FreePlay.Value ? Beatmap.Value.BeatmapSetInfo.OnlineID : null, + BeatmapSetId = FreeStyle.Value ? Beatmap.Value.BeatmapSetInfo.OnlineID : null, RulesetID = Ruleset.Value.OnlineID, RequiredMods = Mods.Value.Select(m => new APIMod(m)).ToArray(), AllowedMods = FreeMods.Value.Select(m => new APIMod(m)).ToArray(), From ac738f109ad4eb6ebf1790a26d031e3d8a738d85 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Tue, 24 Dec 2024 19:28:09 +0900 Subject: [PATCH 0403/1275] Add style selection to playlists screen --- .../Playlists/PlaylistsRoomSubScreen.cs | 70 +++++++++++++------ 1 file changed, 47 insertions(+), 23 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs index 9573155f5a..98667c16fb 100644 --- a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs @@ -171,39 +171,63 @@ namespace osu.Game.Screens.OnlinePlay.Playlists { new[] { - UserModsSection = new FillFlowContainer + new Container { RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y, - Alpha = 0, Margin = new MarginPadding { Bottom = 10 }, - Children = new Drawable[] + Children = new[] { - new OverlinedHeader("Extra mods"), - new FillFlowContainer + UserModsSection = new FillFlowContainer { - AutoSizeAxes = Axes.Both, - Direction = FillDirection.Horizontal, - Spacing = new Vector2(10, 0), + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Alpha = 0, + Margin = new MarginPadding { Bottom = 10 }, Children = new Drawable[] { - new UserModSelectButton + new OverlinedHeader("Extra mods"), + new FillFlowContainer { - Anchor = Anchor.CentreLeft, - Origin = Anchor.CentreLeft, - Width = 90, - Text = "Select", - Action = ShowUserModSelect, - }, - new ModDisplay - { - Anchor = Anchor.CentreLeft, - Origin = Anchor.CentreLeft, - Current = UserMods, - Scale = new Vector2(0.8f), - }, + AutoSizeAxes = Axes.Both, + Direction = FillDirection.Horizontal, + Spacing = new Vector2(10, 0), + Children = new Drawable[] + { + new UserModSelectButton + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + Width = 90, + Text = "Select", + Action = ShowUserModSelect, + }, + new ModDisplay + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + Current = UserMods, + Scale = new Vector2(0.8f), + }, + } + } } - } + }, + UserDifficultySection = new FillFlowContainer + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Alpha = 0, + Children = new Drawable[] + { + new OverlinedHeader("Difficulty"), + UserStyleDisplayContainer = new Container + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y + } + } + }, } }, }, From d8ff5bcacbb4460de8d51ff674b16f6a9aeba3b7 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Tue, 24 Dec 2024 19:39:56 +0900 Subject: [PATCH 0404/1275] Fix freemods button opening overlay unexpectedly --- osu.Game/Screens/OnlinePlay/OnlinePlaySongSelect.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/OnlinePlay/OnlinePlaySongSelect.cs b/osu.Game/Screens/OnlinePlay/OnlinePlaySongSelect.cs index 02f8c619a7..a91f43635b 100644 --- a/osu.Game/Screens/OnlinePlay/OnlinePlaySongSelect.cs +++ b/osu.Game/Screens/OnlinePlay/OnlinePlaySongSelect.cs @@ -206,7 +206,7 @@ namespace osu.Game.Screens.OnlinePlay baseButtons.InsertRange(baseButtons.FindIndex(b => b.Item1 is FooterButtonMods) + 1, new (FooterButton, OverlayContainer?)[] { - (freeModsFooterButton, freeModSelect), + (freeModsFooterButton, null), (freeStyleButton, null) }); From c88e906cb69bbc17c826fc1c9c0860cb64adc069 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Tue, 24 Dec 2024 19:40:06 +0900 Subject: [PATCH 0405/1275] Add some comments --- osu.Game/Online/Rooms/MultiplayerPlaylistItem.cs | 4 ++++ osu.Game/Online/Rooms/PlaylistItem.cs | 4 ++++ 2 files changed, 8 insertions(+) diff --git a/osu.Game/Online/Rooms/MultiplayerPlaylistItem.cs b/osu.Game/Online/Rooms/MultiplayerPlaylistItem.cs index 027d5b4a17..4a15fd9690 100644 --- a/osu.Game/Online/Rooms/MultiplayerPlaylistItem.cs +++ b/osu.Game/Online/Rooms/MultiplayerPlaylistItem.cs @@ -56,6 +56,10 @@ namespace osu.Game.Online.Rooms [Key(10)] public double StarRating { get; set; } + /// + /// A non-null value indicates "freestyle" mode where players are able to individually select + /// their own choice of beatmap (from the respective beatmap set) and ruleset to play in the room. + /// [Key(11)] public int? BeatmapSetID { get; set; } diff --git a/osu.Game/Online/Rooms/PlaylistItem.cs b/osu.Game/Online/Rooms/PlaylistItem.cs index 937bc40e9b..16c252befc 100644 --- a/osu.Game/Online/Rooms/PlaylistItem.cs +++ b/osu.Game/Online/Rooms/PlaylistItem.cs @@ -67,6 +67,10 @@ namespace osu.Game.Online.Rooms set => Beatmap = new APIBeatmap { OnlineID = value }; } + /// + /// A non-null value indicates "freestyle" mode where players are able to individually select + /// their own choice of beatmap (from the respective beatmap set) and ruleset to play in the room. + /// [JsonProperty("beatmapset_id")] public int? BeatmapSetId { get; set; } From b4f35f330ce215cd9aa7049d3ceb9e5e75fb2b8f Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Tue, 24 Dec 2024 20:13:35 +0900 Subject: [PATCH 0406/1275] Use online ruleset_id to build local score models --- osu.Game/Online/Rooms/MultiplayerScore.cs | 11 +++++++---- .../DailyChallenge/DailyChallengeLeaderboard.cs | 4 ++-- .../OnlinePlay/Playlists/PlaylistItemResultsScreen.cs | 2 +- 3 files changed, 10 insertions(+), 7 deletions(-) diff --git a/osu.Game/Online/Rooms/MultiplayerScore.cs b/osu.Game/Online/Rooms/MultiplayerScore.cs index faa66c571d..2adee26da3 100644 --- a/osu.Game/Online/Rooms/MultiplayerScore.cs +++ b/osu.Game/Online/Rooms/MultiplayerScore.cs @@ -77,11 +77,14 @@ namespace osu.Game.Online.Rooms [CanBeNull] public MultiplayerScoresAround ScoresAround { get; set; } - public ScoreInfo CreateScoreInfo(ScoreManager scoreManager, RulesetStore rulesets, PlaylistItem playlistItem, [NotNull] BeatmapInfo beatmap) + [JsonProperty("ruleset_id")] + public int RulesetId { get; set; } + + public ScoreInfo CreateScoreInfo(ScoreManager scoreManager, RulesetStore rulesets, [NotNull] BeatmapInfo beatmap) { - var ruleset = rulesets.GetRuleset(playlistItem.RulesetID); + var ruleset = rulesets.GetRuleset(RulesetId); if (ruleset == null) - throw new InvalidOperationException($"Couldn't create score with unknown ruleset: {playlistItem.RulesetID}"); + throw new InvalidOperationException($"Couldn't create score with unknown ruleset: {RulesetId}"); var rulesetInstance = ruleset.CreateInstance(); @@ -91,7 +94,7 @@ namespace osu.Game.Online.Rooms TotalScore = TotalScore, MaxCombo = MaxCombo, BeatmapInfo = beatmap, - Ruleset = rulesets.GetRuleset(playlistItem.RulesetID) ?? throw new InvalidOperationException($"Ruleset with ID of {playlistItem.RulesetID} not found locally"), + Ruleset = ruleset, Passed = Passed, Statistics = Statistics, MaximumStatistics = MaximumStatistics, diff --git a/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallengeLeaderboard.cs b/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallengeLeaderboard.cs index 9fe2b70a5a..4736ba28db 100644 --- a/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallengeLeaderboard.cs +++ b/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallengeLeaderboard.cs @@ -142,10 +142,10 @@ namespace osu.Game.Screens.OnlinePlay.DailyChallenge request.Success += req => Schedule(() => { - var best = req.Scores.Select(s => s.CreateScoreInfo(scoreManager, rulesets, playlistItem, beatmap.Value.BeatmapInfo)).ToArray(); + var best = req.Scores.Select(s => s.CreateScoreInfo(scoreManager, rulesets, beatmap.Value.BeatmapInfo)).ToArray(); userBestScore.Value = req.UserScore; - var userBest = userBestScore.Value?.CreateScoreInfo(scoreManager, rulesets, playlistItem, beatmap.Value.BeatmapInfo); + var userBest = userBestScore.Value?.CreateScoreInfo(scoreManager, rulesets, beatmap.Value.BeatmapInfo); cancellationTokenSource?.Cancel(); cancellationTokenSource = null; diff --git a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistItemResultsScreen.cs b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistItemResultsScreen.cs index 81ae51bd1b..13ef5d6f64 100644 --- a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistItemResultsScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistItemResultsScreen.cs @@ -189,7 +189,7 @@ namespace osu.Game.Screens.OnlinePlay.Playlists /// An optional pivot around which the scores were retrieved. protected virtual ScoreInfo[] PerformSuccessCallback(Action> callback, List scores, MultiplayerScores? pivot = null) { - var scoreInfos = scores.Select(s => s.CreateScoreInfo(ScoreManager, Rulesets, PlaylistItem, Beatmap.Value.BeatmapInfo)).OrderByTotalScore().ToArray(); + var scoreInfos = scores.Select(s => s.CreateScoreInfo(ScoreManager, Rulesets, Beatmap.Value.BeatmapInfo)).OrderByTotalScore().ToArray(); // Invoke callback to add the scores. Exclude the score provided to this screen since it's added already. callback.Invoke(scoreInfos.Where(s => s.OnlineID != Score?.OnlineID)); From a2dc16f8dffab2521b83d154cdcecb8d6baa48c1 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Tue, 24 Dec 2024 20:22:16 +0900 Subject: [PATCH 0407/1275] Fix inspection --- osu.Game/Screens/OnlinePlay/FooterButtonFreeStyle.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/OnlinePlay/FooterButtonFreeStyle.cs b/osu.Game/Screens/OnlinePlay/FooterButtonFreeStyle.cs index 5edcddcb78..cdfb73cee1 100644 --- a/osu.Game/Screens/OnlinePlay/FooterButtonFreeStyle.cs +++ b/osu.Game/Screens/OnlinePlay/FooterButtonFreeStyle.cs @@ -15,7 +15,7 @@ using osu.Game.Screens.Select; namespace osu.Game.Screens.OnlinePlay { - public class FooterButtonFreeStyle : FooterButton, IHasCurrentValue + public partial class FooterButtonFreeStyle : FooterButton, IHasCurrentValue { private readonly BindableWithCurrent current = new BindableWithCurrent(); From a407e3f3e04d5765d8678970c83e4fb13b04f513 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Wed, 25 Dec 2024 16:46:02 +0900 Subject: [PATCH 0408/1275] Fix co-variant array conversion --- osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs index 98667c16fb..48d50d727b 100644 --- a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs @@ -169,7 +169,7 @@ namespace osu.Game.Screens.OnlinePlay.Playlists RelativeSizeAxes = Axes.Both, Content = new[] { - new[] + new Drawable[] { new Container { From 95fe8d67e4fb899eec812e28a30528f145617caf Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Wed, 25 Dec 2024 16:51:50 +0900 Subject: [PATCH 0409/1275] Fix test --- .../Multiplayer/TestSceneMultiplayerMatchSubScreen.cs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSubScreen.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSubScreen.cs index 8ea52f8099..e95209f993 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSubScreen.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSubScreen.cs @@ -30,6 +30,7 @@ using osu.Game.Screens.OnlinePlay; using osu.Game.Screens.OnlinePlay.Match; using osu.Game.Screens.OnlinePlay.Multiplayer; using osu.Game.Screens.OnlinePlay.Multiplayer.Match; +using osu.Game.Screens.OnlinePlay.Multiplayer.Match.Playlist; using osu.Game.Screens.OnlinePlay.Multiplayer.Participants; using osu.Game.Tests.Beatmaps; using osu.Game.Tests.Resources; @@ -271,7 +272,10 @@ namespace osu.Game.Tests.Visual.Multiplayer AddUntilStep("last playlist item selected", () => { - var lastItem = this.ChildrenOfType().Single(p => p.Item.ID == MultiplayerClient.ServerAPIRoom?.Playlist.Last().ID); + var lastItem = this.ChildrenOfType() + .Single() + .ChildrenOfType() + .Single(p => p.Item.ID == MultiplayerClient.ServerAPIRoom?.Playlist.Last().ID); return lastItem.IsSelectedItem; }); } From 0093af8f5595bb28b8f39fc5faa2b96bf658ea5f Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Wed, 25 Dec 2024 22:24:21 +0900 Subject: [PATCH 0410/1275] Rewrite everything to better support spectator server messaging --- .../Online/Multiplayer/IMultiplayerClient.cs | 8 + .../Multiplayer/IMultiplayerRoomServer.cs | 7 + .../Online/Multiplayer/MultiplayerClient.cs | 21 ++ .../Online/Multiplayer/MultiplayerRoomUser.cs | 18 +- .../Multiplayer/OnlineMultiplayerClient.cs | 11 + .../Screens/OnlinePlay/Match/RoomSubScreen.cs | 228 +++++++++--------- .../MultiplayerMatchStyleSelect.cs | 130 +++++----- .../Multiplayer/MultiplayerMatchSubScreen.cs | 37 ++- .../OnlinePlay/OnlinePlayStyleSelect.cs | 98 ++++++++ .../Playlists/PlaylistsRoomStyleSelect.cs | 30 +++ .../Playlists/PlaylistsRoomSubScreen.cs | 14 +- .../Multiplayer/TestMultiplayerClient.cs | 17 ++ 12 files changed, 417 insertions(+), 202 deletions(-) create mode 100644 osu.Game/Screens/OnlinePlay/OnlinePlayStyleSelect.cs create mode 100644 osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomStyleSelect.cs diff --git a/osu.Game/Online/Multiplayer/IMultiplayerClient.cs b/osu.Game/Online/Multiplayer/IMultiplayerClient.cs index 0452d8b79c..adb9b92614 100644 --- a/osu.Game/Online/Multiplayer/IMultiplayerClient.cs +++ b/osu.Game/Online/Multiplayer/IMultiplayerClient.cs @@ -95,6 +95,14 @@ namespace osu.Game.Online.Multiplayer /// The new beatmap availability state of the user. Task UserBeatmapAvailabilityChanged(int userId, BeatmapAvailability beatmapAvailability); + /// + /// Signals that a user in this room changed their style. + /// + /// The ID of the user whose style changed. + /// The user's beatmap. + /// The user's ruleset. + Task UserStyleChanged(int userId, int? beatmapId, int? rulesetId); + /// /// Signals that a user in this room changed their local mods. /// diff --git a/osu.Game/Online/Multiplayer/IMultiplayerRoomServer.cs b/osu.Game/Online/Multiplayer/IMultiplayerRoomServer.cs index 55f00b447f..490973faa2 100644 --- a/osu.Game/Online/Multiplayer/IMultiplayerRoomServer.cs +++ b/osu.Game/Online/Multiplayer/IMultiplayerRoomServer.cs @@ -57,6 +57,13 @@ namespace osu.Game.Online.Multiplayer /// The proposed new beatmap availability state. Task ChangeBeatmapAvailability(BeatmapAvailability newBeatmapAvailability); + /// + /// Change the local user's style in the currently joined room. + /// + /// The beatmap. + /// The ruleset. + Task ChangeUserStyle(int? beatmapId, int? rulesetId); + /// /// Change the local user's mods in the currently joined room. /// diff --git a/osu.Game/Online/Multiplayer/MultiplayerClient.cs b/osu.Game/Online/Multiplayer/MultiplayerClient.cs index 998a34931d..a588ec4441 100644 --- a/osu.Game/Online/Multiplayer/MultiplayerClient.cs +++ b/osu.Game/Online/Multiplayer/MultiplayerClient.cs @@ -359,6 +359,8 @@ namespace osu.Game.Online.Multiplayer public abstract Task DisconnectInternal(); + public abstract Task ChangeUserStyle(int? beatmapId, int? rulesetId); + /// /// Change the local user's mods in the currently joined room. /// @@ -652,6 +654,25 @@ namespace osu.Game.Online.Multiplayer return Task.CompletedTask; } + public Task UserStyleChanged(int userId, int? beatmapId, int? rulesetId) + { + Scheduler.Add(() => + { + var user = Room?.Users.SingleOrDefault(u => u.UserID == userId); + + // errors here are not critical - user style is mostly for display. + if (user == null) + return; + + user.BeatmapId = beatmapId; + user.RulesetId = rulesetId; + + RoomUpdated?.Invoke(); + }, false); + + return Task.CompletedTask; + } + public Task UserModsChanged(int userId, IEnumerable mods) { Scheduler.Add(() => diff --git a/osu.Game/Online/Multiplayer/MultiplayerRoomUser.cs b/osu.Game/Online/Multiplayer/MultiplayerRoomUser.cs index f769b4c805..8142873fd5 100644 --- a/osu.Game/Online/Multiplayer/MultiplayerRoomUser.cs +++ b/osu.Game/Online/Multiplayer/MultiplayerRoomUser.cs @@ -22,9 +22,6 @@ namespace osu.Game.Online.Multiplayer [Key(1)] public MultiplayerUserState State { get; set; } = MultiplayerUserState.Idle; - [Key(4)] - public MatchUserState? MatchState { get; set; } - /// /// The availability state of the current beatmap. /// @@ -37,6 +34,21 @@ namespace osu.Game.Online.Multiplayer [Key(3)] public IEnumerable Mods { get; set; } = Enumerable.Empty(); + [Key(4)] + public MatchUserState? MatchState { get; set; } + + /// + /// Any ruleset applicable only to the local user. + /// + [Key(5)] + public int? RulesetId; + + /// + /// Any beatmap applicable only to the local user. + /// + [Key(6)] + public int? BeatmapId; + [IgnoreMember] public APIUser? User { get; set; } diff --git a/osu.Game/Online/Multiplayer/OnlineMultiplayerClient.cs b/osu.Game/Online/Multiplayer/OnlineMultiplayerClient.cs index 40436d730e..2660cd94e4 100644 --- a/osu.Game/Online/Multiplayer/OnlineMultiplayerClient.cs +++ b/osu.Game/Online/Multiplayer/OnlineMultiplayerClient.cs @@ -60,6 +60,7 @@ namespace osu.Game.Online.Multiplayer connection.On(nameof(IMultiplayerClient.GameplayStarted), ((IMultiplayerClient)this).GameplayStarted); connection.On(nameof(IMultiplayerClient.GameplayAborted), ((IMultiplayerClient)this).GameplayAborted); connection.On(nameof(IMultiplayerClient.ResultsReady), ((IMultiplayerClient)this).ResultsReady); + connection.On(nameof(IMultiplayerClient.UserStyleChanged), ((IMultiplayerClient)this).UserStyleChanged); connection.On>(nameof(IMultiplayerClient.UserModsChanged), ((IMultiplayerClient)this).UserModsChanged); connection.On(nameof(IMultiplayerClient.UserBeatmapAvailabilityChanged), ((IMultiplayerClient)this).UserBeatmapAvailabilityChanged); connection.On(nameof(IMultiplayerClient.MatchRoomStateChanged), ((IMultiplayerClient)this).MatchRoomStateChanged); @@ -186,6 +187,16 @@ namespace osu.Game.Online.Multiplayer return connection.InvokeAsync(nameof(IMultiplayerServer.ChangeBeatmapAvailability), newBeatmapAvailability); } + public override Task ChangeUserStyle(int? beatmapId, int? rulesetId) + { + if (!IsConnected.Value) + return Task.CompletedTask; + + Debug.Assert(connection != null); + + return connection.InvokeAsync(nameof(IMultiplayerServer.ChangeUserStyle), beatmapId, rulesetId); + } + public override Task ChangeUserMods(IEnumerable newMods) { if (!IsConnected.Value) diff --git a/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs b/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs index 49144f9de5..b51679ded6 100644 --- a/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs @@ -4,14 +4,12 @@ using System; using System.Collections.Generic; using System.ComponentModel; -using System.Diagnostics; using System.Linq; using osu.Framework.Allocation; using osu.Framework.Audio; using osu.Framework.Audio.Sample; using osu.Framework.Bindables; using osu.Framework.Extensions.Color4Extensions; -using osu.Framework.Extensions.ObjectExtensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Cursor; @@ -28,6 +26,7 @@ using osu.Game.Rulesets.Mods; using osu.Game.Screens.Menu; using osu.Game.Screens.OnlinePlay.Match.Components; using osu.Game.Screens.OnlinePlay.Multiplayer; +using osu.Game.Utils; using Container = osu.Framework.Graphics.Containers.Container; namespace osu.Game.Screens.OnlinePlay.Match @@ -37,18 +36,6 @@ namespace osu.Game.Screens.OnlinePlay.Match { public readonly Bindable SelectedItem = new Bindable(); - /// - /// When players are freely allowed to select their own gameplay style (selected item has a non-null beatmapset id), - /// a non-null value indicates a local beatmap selection from the same beatmapset as the selected item. - /// - public readonly Bindable DifficultyOverride = new Bindable(); - - /// - /// When players are freely allowed to select their own gameplay style (selected item has a non-null beatmapset id), - /// a non-null value indicates a local ruleset selection. - /// - public readonly Bindable RulesetOverride = new Bindable(); - public override bool? ApplyModTrackAdjustments => true; protected override BackgroundScreen CreateBackground() => new RoomBackgroundScreen(Room.Playlist.FirstOrDefault()) @@ -65,13 +52,13 @@ namespace osu.Game.Screens.OnlinePlay.Match protected Drawable? UserModsSection; /// - /// A container that provides controls for selection of the user's difficulty override. + /// A container that provides controls for selection of the user style. /// This will be shown/hidden automatically when applicable. /// - protected Drawable? UserDifficultySection; + protected Drawable? UserStyleSection; /// - /// A container that will display the user's difficulty override. + /// A container that will display the user's style. /// protected Container? UserStyleDisplayContainer; @@ -82,6 +69,18 @@ namespace osu.Game.Screens.OnlinePlay.Match /// protected readonly Bindable> UserMods = new Bindable>(Array.Empty()); + /// + /// When players are freely allowed to select their own gameplay style (selected item has a non-null beatmapset id), + /// a non-null value indicates a local beatmap selection from the same beatmapset as the selected item. + /// + public readonly Bindable UserBeatmap = new Bindable(); + + /// + /// When players are freely allowed to select their own gameplay style (selected item has a non-null beatmapset id), + /// a non-null value indicates a local ruleset selection. + /// + public readonly Bindable UserRuleset = new Bindable(); + [Resolved(CanBeNull = true)] private IOverlayManager? overlayManager { get; set; } @@ -272,13 +271,25 @@ namespace osu.Game.Screens.OnlinePlay.Match { base.LoadComplete(); - SelectedItem.BindValueChanged(_ => Scheduler.AddOnce(selectedItemChanged)); - UserMods.BindValueChanged(_ => Scheduler.AddOnce(UpdateMods)); - DifficultyOverride.BindValueChanged(_ => Scheduler.AddOnce(updateStyleOverride)); - RulesetOverride.BindValueChanged(_ => Scheduler.AddOnce(updateStyleOverride)); + SelectedItem.BindValueChanged(_ => Scheduler.AddOnce(OnSelectedItemChanged)); + + UserMods.BindValueChanged(_ => Scheduler.AddOnce(updateMods)); + + UserBeatmap.BindValueChanged(_ => Scheduler.AddOnce(() => + { + updateBeatmap(); + updateUserStyle(); + })); + + UserRuleset.BindValueChanged(_ => Scheduler.AddOnce(() => + { + updateUserMods(); + updateRuleset(); + updateUserStyle(); + })); beatmapAvailabilityTracker.SelectedItem.BindTo(SelectedItem); - beatmapAvailabilityTracker.Availability.BindValueChanged(_ => updateWorkingBeatmap()); + beatmapAvailabilityTracker.Availability.BindValueChanged(_ => updateBeatmap()); userModsSelectOverlayRegistration = overlayManager?.RegisterBlockingOverlay(UserModsSelectOverlay); @@ -347,7 +358,7 @@ namespace osu.Game.Screens.OnlinePlay.Match public override void OnSuspending(ScreenTransitionEvent e) { // Should be a noop in most cases, but let's ensure beyond doubt that the beatmap is in a correct state. - updateWorkingBeatmap(); + updateBeatmap(); onLeaving(); base.OnSuspending(e); @@ -356,10 +367,11 @@ namespace osu.Game.Screens.OnlinePlay.Match public override void OnResuming(ScreenTransitionEvent e) { base.OnResuming(e); - updateWorkingBeatmap(); + updateBeatmap(); beginHandlingTrack(); - Scheduler.AddOnce(UpdateMods); + Scheduler.AddOnce(updateMods); Scheduler.AddOnce(updateRuleset); + Scheduler.AddOnce(updateUserStyle); } protected bool ExitConfirmed { get; private set; } @@ -409,9 +421,13 @@ namespace osu.Game.Screens.OnlinePlay.Match protected void StartPlay() { - if (GetGameplayItem() is not PlaylistItem item) + if (SelectedItem.Value is not PlaylistItem item) return; + item = item.With( + ruleset: GetGameplayRuleset().OnlineID, + beatmap: new Optional(GetGameplayBeatmap())); + // User may be at song select or otherwise when the host starts gameplay. // Ensure that they first return to this screen, else global bindables (beatmap etc.) may be in a bad state. if (!this.IsCurrentScreen()) @@ -437,31 +453,26 @@ namespace osu.Game.Screens.OnlinePlay.Match /// The screen to enter. protected abstract Screen CreateGameplayScreen(PlaylistItem selectedItem); - private void selectedItemChanged() + protected void OnSelectedItemChanged() { - if (SelectedItem.Value is not PlaylistItem selected) + if (!this.IsCurrentScreen() || SelectedItem.Value is not PlaylistItem item) return; - if (selected.BeatmapSetId == null || selected.BeatmapSetId != DifficultyOverride.Value?.BeatmapSet.AsNonNull().OnlineID) + // Reset user style if no longer valid. + // Todo: In the future this can be made more lenient, such as allowing a non-null ruleset as the set changes. + if (item.BeatmapSetId == null || item.BeatmapSetId != UserBeatmap.Value?.BeatmapSet!.OnlineID) { - DifficultyOverride.Value = null; - RulesetOverride.Value = null; + UserBeatmap.Value = null; + UserRuleset.Value = null; } - updateStyleOverride(); - updateWorkingBeatmap(); - - var rulesetInstance = Rulesets.GetRuleset(selected.RulesetID)?.CreateInstance(); - Debug.Assert(rulesetInstance != null); - var allowedMods = selected.AllowedMods.Select(m => m.ToMod(rulesetInstance)); - - // Remove any user mods that are no longer allowed. - UserMods.Value = UserMods.Value.Where(m => allowedMods.Any(a => m.GetType() == a.GetType())).ToList(); - - UpdateMods(); + updateUserMods(); + updateBeatmap(); + updateMods(); updateRuleset(); + updateUserStyle(); - if (!selected.AllowedMods.Any()) + if (!item.AllowedMods.Any()) { UserModsSection?.Hide(); UserModsSelectOverlay.Hide(); @@ -470,100 +481,89 @@ namespace osu.Game.Screens.OnlinePlay.Match else { UserModsSection?.Show(); + + var rulesetInstance = GetGameplayRuleset().CreateInstance(); + var allowedMods = item.AllowedMods.Select(m => m.ToMod(rulesetInstance)); UserModsSelectOverlay.IsValidMod = m => allowedMods.Any(a => a.GetType() == m.GetType()); } - if (selected.BeatmapSetId == null) - UserDifficultySection?.Hide(); + if (item.BeatmapSetId == null) + UserStyleSection?.Hide(); else - UserDifficultySection?.Show(); + UserStyleSection?.Show(); } - private void updateWorkingBeatmap() + private void updateUserMods() { - if (GetGameplayItem() is not PlaylistItem item || !this.IsCurrentScreen()) + if (!this.IsCurrentScreen() || SelectedItem.Value is not PlaylistItem item) + return; + + // Remove any user mods that are no longer allowed. + var rulesetInstance = GetGameplayRuleset().CreateInstance(); + var allowedMods = item.AllowedMods.Select(m => m.ToMod(rulesetInstance)); + UserMods.Value = UserMods.Value.Where(m => allowedMods.Any(a => m.GetType() == a.GetType())).ToList(); + } + + private void updateBeatmap() + { + if (!this.IsCurrentScreen() || SelectedItem.Value is not PlaylistItem) return; // Retrieve the corresponding local beatmap, since we can't directly use the playlist's beatmap info - var localBeatmap = beatmapManager.QueryBeatmap(b => b.OnlineID == item.Beatmap.OnlineID); - - UserModsSelectOverlay.Beatmap.Value = Beatmap.Value = beatmapManager.GetWorkingBeatmap(localBeatmap); + int beatmapId = GetGameplayBeatmap().OnlineID; + var localBeatmap = beatmapManager.QueryBeatmap(b => b.OnlineID == beatmapId); + Beatmap.Value = beatmapManager.GetWorkingBeatmap(localBeatmap); + UserModsSelectOverlay.Beatmap.Value = Beatmap.Value; } - protected virtual void UpdateMods() + private void updateMods() { - if (GetGameplayItem() is not PlaylistItem item || !this.IsCurrentScreen()) + if (!this.IsCurrentScreen() || SelectedItem.Value is not PlaylistItem) return; - var rulesetInstance = Rulesets.GetRuleset(item.RulesetID)?.CreateInstance(); - Debug.Assert(rulesetInstance != null); - Mods.Value = UserMods.Value.Concat(item.RequiredMods.Select(m => m.ToMod(rulesetInstance))).ToList(); - } - - private void updateStyleOverride() - { - if (SelectedItem.Value == null || !this.IsCurrentScreen()) - return; - - if (UserStyleDisplayContainer == null) - return; - - PlaylistItem gameplayItem = GetGameplayItem()!; - - if (UserStyleDisplayContainer.SingleOrDefault()?.Item.Equals(gameplayItem) == true) - return; - - UserStyleDisplayContainer.Child = new DrawableRoomPlaylistItem(gameplayItem) - { - AllowReordering = false, - AllowEditing = true, - RequestEdit = _ => openStyleSelection() - }; - } - - protected PlaylistItem? GetGameplayItem() - { - PlaylistItem? selectedItemWithOverride = SelectedItem.Value; - - if (selectedItemWithOverride?.BeatmapSetId == null) - return selectedItemWithOverride; - - // Sanity check. - if (DifficultyOverride.Value?.BeatmapSet?.OnlineID != selectedItemWithOverride.BeatmapSetId) - return selectedItemWithOverride; - - if (DifficultyOverride.Value != null) - selectedItemWithOverride = selectedItemWithOverride.With(beatmap: DifficultyOverride.Value); - - if (RulesetOverride.Value != null) - selectedItemWithOverride = selectedItemWithOverride.With(ruleset: RulesetOverride.Value.OnlineID); - - return selectedItemWithOverride; - } - - private void openStyleSelection() - { - if (SelectedItem.Value == null || !this.IsCurrentScreen()) - return; - - this.Push(new MultiplayerMatchStyleSelect(Room, SelectedItem.Value, (beatmap, ruleset) => - { - if (SelectedItem.Value?.BeatmapSetId == null || SelectedItem.Value.BeatmapSetId != beatmap.BeatmapSet?.OnlineID) - return; - - DifficultyOverride.Value = beatmap; - RulesetOverride.Value = ruleset; - })); + var rulesetInstance = GetGameplayRuleset().CreateInstance(); + Mods.Value = GetGameplayMods().Select(m => m.ToMod(rulesetInstance)).ToArray(); } private void updateRuleset() { - if (GetGameplayItem() is not PlaylistItem item || !this.IsCurrentScreen()) + if (!this.IsCurrentScreen() || SelectedItem.Value is not PlaylistItem) return; - Ruleset.Value = Rulesets.GetRuleset(item.RulesetID); + Ruleset.Value = GetGameplayRuleset(); } + private void updateUserStyle() + { + if (!this.IsCurrentScreen() || SelectedItem.Value is not PlaylistItem) + return; + + if (UserStyleDisplayContainer != null) + { + PlaylistItem gameplayItem = SelectedItem.Value.With( + ruleset: GetGameplayRuleset().OnlineID, + beatmap: new Optional(GetGameplayBeatmap())); + + UserStyleDisplayContainer.Child = new DrawableRoomPlaylistItem(gameplayItem) + { + AllowReordering = false, + AllowEditing = true, + RequestEdit = _ => OpenStyleSelection() + }; + } + } + + protected virtual APIMod[] GetGameplayMods() + => UserMods.Value.Select(m => new APIMod(m)).Concat(SelectedItem.Value!.RequiredMods).ToArray(); + + protected virtual RulesetInfo GetGameplayRuleset() + => Rulesets.GetRuleset(UserRuleset.Value?.OnlineID ?? SelectedItem.Value!.RulesetID)!; + + protected virtual IBeatmapInfo GetGameplayBeatmap() + => UserBeatmap.Value ?? SelectedItem.Value!.Beatmap; + + protected abstract void OpenStyleSelection(); + private void beginHandlingTrack() { Beatmap.BindValueChanged(applyLoopingToTrack, true); diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchStyleSelect.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchStyleSelect.cs index 867579171d..3fe4926052 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchStyleSelect.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchStyleSelect.cs @@ -2,106 +2,88 @@ // See the LICENCE file in the repository root for full licence text. using System; -using System.Collections.Generic; -using System.Linq; -using Humanizer; using osu.Framework.Allocation; -using osu.Framework.Graphics; -using osu.Framework.Graphics.Containers; +using osu.Framework.Bindables; +using osu.Framework.Logging; using osu.Framework.Screens; -using osu.Game.Beatmaps; -using osu.Game.Database; +using osu.Game.Graphics.UserInterface; +using osu.Game.Online.Multiplayer; using osu.Game.Online.Rooms; -using osu.Game.Rulesets; -using osu.Game.Screens.Select; -using osu.Game.Users; namespace osu.Game.Screens.OnlinePlay.Multiplayer { - public partial class MultiplayerMatchStyleSelect : SongSelect, IOnlinePlaySubScreen, IHandlePresentBeatmap + public partial class MultiplayerMatchStyleSelect : OnlinePlayStyleSelect { - public string ShortTitle => "style selection"; + [Resolved] + private MultiplayerClient client { get; set; } = null!; - public override string Title => ShortTitle.Humanize(); + [Resolved] + private OngoingOperationTracker operationTracker { get; set; } = null!; - public override bool AllowEditing => false; + private readonly IBindable operationInProgress = new Bindable(); - protected override UserActivity InitialActivity => new UserActivity.InLobby(room); + private LoadingLayer loadingLayer = null!; + private IDisposable? selectionOperation; - private readonly Room room; - private readonly PlaylistItem item; - private readonly Action onSelect; - - public MultiplayerMatchStyleSelect(Room room, PlaylistItem item, Action onSelect) + public MultiplayerMatchStyleSelect(Room room, PlaylistItem item) + : base(room, item) { - this.room = room; - this.item = item; - this.onSelect = onSelect; - - Padding = new MarginPadding { Horizontal = HORIZONTAL_OVERFLOW_PADDING }; } [BackgroundDependencyLoader] private void load() { - LeftArea.Padding = new MarginPadding { Top = Header.HEIGHT }; + AddInternal(loadingLayer = new LoadingLayer(true)); } - protected override FilterControl CreateFilterControl() => new DifficultySelectFilterControl(item); - - protected override IEnumerable<(FooterButton, OverlayContainer?)> CreateSongSelectFooterButtons() + protected override void LoadComplete() { - // Required to create the drawable components. - base.CreateSongSelectFooterButtons(); - return Enumerable.Empty<(FooterButton, OverlayContainer?)>(); + base.LoadComplete(); + + operationInProgress.BindTo(operationTracker.InProgress); + operationInProgress.BindValueChanged(_ => updateLoadingLayer(), true); } - protected override BeatmapDetailArea CreateBeatmapDetailArea() => new PlayBeatmapDetailArea(); + private void updateLoadingLayer() + { + if (operationInProgress.Value) + loadingLayer.Show(); + else + loadingLayer.Hide(); + } protected override bool OnStart() { - onSelect(Beatmap.Value.BeatmapInfo, Ruleset.Value); - this.Exit(); + if (operationInProgress.Value) + { + Logger.Log($"{nameof(OnStart)} aborted due to {nameof(operationInProgress)}"); + return false; + } + + selectionOperation = operationTracker.BeginOperation(); + + client.ChangeUserStyle(Beatmap.Value.BeatmapInfo.OnlineID, Ruleset.Value.OnlineID) + .FireAndForget(onSuccess: () => + { + selectionOperation.Dispose(); + + Schedule(() => + { + // If an error or server side trigger occurred this screen may have already exited by external means. + if (this.IsCurrentScreen()) + this.Exit(); + }); + }, onError: _ => + { + selectionOperation.Dispose(); + + Schedule(() => + { + Carousel.AllowSelection = true; + }); + }); + return true; } - - public void PresentBeatmap(WorkingBeatmap beatmap, RulesetInfo ruleset) - { - // This screen cannot present beatmaps. - } - - private partial class DifficultySelectFilterControl : FilterControl - { - private readonly PlaylistItem item; - private double itemLength; - - public DifficultySelectFilterControl(PlaylistItem item) - { - this.item = item; - } - - [BackgroundDependencyLoader] - private void load(RealmAccess realm) - { - int beatmapId = item.Beatmap.OnlineID; - itemLength = realm.Run(r => r.All().FirstOrDefault(b => b.OnlineID == beatmapId)?.Length ?? 0); - } - - public override FilterCriteria CreateCriteria() - { - var criteria = base.CreateCriteria(); - - // Must be from the same set as the playlist item. - criteria.BeatmapSetId = item.BeatmapSetId; - - // Must be within 30s of the playlist item. - criteria.Length.Min = itemLength - 30000; - criteria.Length.Max = itemLength + 30000; - criteria.Length.IsLowerInclusive = true; - criteria.Length.IsUpperInclusive = true; - - return criteria; - } - } } } diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs index d807fe8177..edfb059c77 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs @@ -16,6 +16,8 @@ using osu.Game.Beatmaps; using osu.Game.Configuration; using osu.Game.Graphics.Cursor; using osu.Game.Online; +using osu.Game.Online.API; +using osu.Game.Online.API.Requests.Responses; using osu.Game.Online.Multiplayer; using osu.Game.Online.Rooms; using osu.Game.Overlays; @@ -188,7 +190,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer }, } }, - UserDifficultySection = new FillFlowContainer + UserStyleSection = new FillFlowContainer { RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y, @@ -251,6 +253,14 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer this.Push(new MultiplayerMatchSongSelect(Room, itemToEdit)); } + protected override void OpenStyleSelection() + { + if (!this.IsCurrentScreen() || SelectedItem.Value is not PlaylistItem item) + return; + + this.Push(new MultiplayerMatchStyleSelect(Room, item)); + } + protected override Drawable CreateFooter() => new MultiplayerMatchFooter { SelectedItem = SelectedItem @@ -261,16 +271,22 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer SelectedItem = SelectedItem }; - protected override void UpdateMods() + protected override APIMod[] GetGameplayMods() { - if (GetGameplayItem() is not PlaylistItem item || client.LocalUser == null || !this.IsCurrentScreen()) - return; + // Using the room's reported status makes the server authoritative. + return client.LocalUser?.Mods.Concat(SelectedItem.Value!.RequiredMods).ToArray()!; + } - // update local mods based on room's reported status for the local user (omitting the base call implementation). - // this makes the server authoritative, and avoids the local user potentially setting mods that the server is not aware of (ie. if the match was started during the selection being changed). - var rulesetInstance = Rulesets.GetRuleset(item.RulesetID)?.CreateInstance(); - Debug.Assert(rulesetInstance != null); - Mods.Value = client.LocalUser.Mods.Select(m => m.ToMod(rulesetInstance)).Concat(item.RequiredMods.Select(m => m.ToMod(rulesetInstance))).ToList(); + protected override RulesetInfo GetGameplayRuleset() + { + // Using the room's reported status makes the server authoritative. + return client.LocalUser?.RulesetId != null ? Rulesets.GetRuleset(client.LocalUser.RulesetId.Value)! : base.GetGameplayRuleset(); + } + + protected override IBeatmapInfo GetGameplayBeatmap() + { + // Using the room's reported status makes the server authoritative. + return client.LocalUser?.BeatmapId != null ? new APIBeatmap { OnlineID = client.LocalUser.BeatmapId.Value } : base.GetGameplayBeatmap(); } [Resolved(canBeNull: true)] @@ -376,7 +392,8 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer addItemButton.Alpha = localUserCanAddItem ? 1 : 0; - Scheduler.AddOnce(UpdateMods); + // Forcefully update the selected item so that the user state is applied. + Scheduler.AddOnce(OnSelectedItemChanged); Activity.Value = new UserActivity.InLobby(Room); } diff --git a/osu.Game/Screens/OnlinePlay/OnlinePlayStyleSelect.cs b/osu.Game/Screens/OnlinePlay/OnlinePlayStyleSelect.cs new file mode 100644 index 0000000000..89f2ffc883 --- /dev/null +++ b/osu.Game/Screens/OnlinePlay/OnlinePlayStyleSelect.cs @@ -0,0 +1,98 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using System.Collections.Generic; +using System.Linq; +using Humanizer; +using osu.Framework.Allocation; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Screens; +using osu.Game.Beatmaps; +using osu.Game.Database; +using osu.Game.Online.Rooms; +using osu.Game.Rulesets; +using osu.Game.Screens.Select; +using osu.Game.Users; + +namespace osu.Game.Screens.OnlinePlay +{ + public abstract partial class OnlinePlayStyleSelect : SongSelect, IOnlinePlaySubScreen, IHandlePresentBeatmap + { + public string ShortTitle => "style selection"; + + public override string Title => ShortTitle.Humanize(); + + public override bool AllowEditing => false; + + protected override UserActivity InitialActivity => new UserActivity.InLobby(room); + + private readonly Room room; + private readonly PlaylistItem item; + + protected OnlinePlayStyleSelect(Room room, PlaylistItem item) + { + this.room = room; + this.item = item; + + Padding = new MarginPadding { Horizontal = HORIZONTAL_OVERFLOW_PADDING }; + } + + [BackgroundDependencyLoader] + private void load() + { + LeftArea.Padding = new MarginPadding { Top = Header.HEIGHT }; + } + + protected override FilterControl CreateFilterControl() => new DifficultySelectFilterControl(item); + + protected override IEnumerable<(FooterButton, OverlayContainer?)> CreateSongSelectFooterButtons() + { + // Required to create the drawable components. + base.CreateSongSelectFooterButtons(); + return Enumerable.Empty<(FooterButton, OverlayContainer?)>(); + } + + protected override BeatmapDetailArea CreateBeatmapDetailArea() => new PlayBeatmapDetailArea(); + + public void PresentBeatmap(WorkingBeatmap beatmap, RulesetInfo ruleset) + { + // This screen cannot present beatmaps. + } + + private partial class DifficultySelectFilterControl : FilterControl + { + private readonly PlaylistItem item; + private double itemLength; + + public DifficultySelectFilterControl(PlaylistItem item) + { + this.item = item; + } + + [BackgroundDependencyLoader] + private void load(RealmAccess realm) + { + int beatmapId = item.Beatmap.OnlineID; + itemLength = realm.Run(r => r.All().FirstOrDefault(b => b.OnlineID == beatmapId)?.Length ?? 0); + } + + public override FilterCriteria CreateCriteria() + { + var criteria = base.CreateCriteria(); + + // Must be from the same set as the playlist item. + criteria.BeatmapSetId = item.BeatmapSetId; + + // Must be within 30s of the playlist item. + criteria.Length.Min = itemLength - 30000; + criteria.Length.Max = itemLength + 30000; + criteria.Length.IsLowerInclusive = true; + criteria.Length.IsUpperInclusive = true; + + return criteria; + } + } + } +} diff --git a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomStyleSelect.cs b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomStyleSelect.cs new file mode 100644 index 0000000000..f3d868b0de --- /dev/null +++ b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomStyleSelect.cs @@ -0,0 +1,30 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Bindables; +using osu.Framework.Screens; +using osu.Game.Beatmaps; +using osu.Game.Online.Rooms; +using osu.Game.Rulesets; + +namespace osu.Game.Screens.OnlinePlay.Playlists +{ + public partial class PlaylistsRoomStyleSelect : OnlinePlayStyleSelect + { + public new readonly Bindable Beatmap = new Bindable(); + public new readonly Bindable Ruleset = new Bindable(); + + public PlaylistsRoomStyleSelect(Room room, PlaylistItem item) + : base(room, item) + { + } + + protected override bool OnStart() + { + Beatmap.Value = base.Beatmap.Value.BeatmapInfo; + Ruleset.Value = base.Ruleset.Value; + this.Exit(); + return true; + } + } +} diff --git a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs index 48d50d727b..b941bbd290 100644 --- a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs @@ -213,7 +213,7 @@ namespace osu.Game.Screens.OnlinePlay.Playlists } } }, - UserDifficultySection = new FillFlowContainer + UserStyleSection = new FillFlowContainer { RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y, @@ -299,6 +299,18 @@ namespace osu.Game.Screens.OnlinePlay.Playlists }, }; + protected override void OpenStyleSelection() + { + if (!this.IsCurrentScreen() || SelectedItem.Value is not PlaylistItem item) + return; + + this.Push(new PlaylistsRoomStyleSelect(Room, item) + { + Beatmap = { BindTarget = UserBeatmap }, + Ruleset = { BindTarget = UserRuleset } + }); + } + private void updatePollingRate() { selectionPollingComponent.TimeBetweenPolls.Value = isIdle.Value ? 30000 : 5000; diff --git a/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs b/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs index 4d812abf11..3abef523cd 100644 --- a/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs +++ b/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs @@ -335,6 +335,23 @@ namespace osu.Game.Tests.Visual.Multiplayer return Task.CompletedTask; } + public override Task ChangeUserStyle(int? beatmapId, int? rulesetId) + { + ChangeUserStyle(api.LocalUser.Value.Id, beatmapId, rulesetId); + return Task.CompletedTask; + } + + public void ChangeUserStyle(int userId, int? beatmapId, int? rulesetId) + { + Debug.Assert(ServerRoom != null); + + var user = ServerRoom.Users.Single(u => u.UserID == userId); + user.BeatmapId = beatmapId; + user.RulesetId = rulesetId; + + ((IMultiplayerClient)this).UserStyleChanged(userId, beatmapId, rulesetId); + } + public void ChangeUserMods(int userId, IEnumerable newMods) => ChangeUserMods(userId, newMods.Select(m => new APIMod(m))); From c3aa9d6f8a495f4ef592767ddab579f8c232ce5b Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Wed, 25 Dec 2024 23:30:24 +0900 Subject: [PATCH 0411/1275] Display user style in participant panel --- .../TestSceneMultiplayerParticipantsList.cs | 27 +++++ .../Participants/ParticipantPanel.cs | 105 +++++++++++++++++- 2 files changed, 128 insertions(+), 4 deletions(-) diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerParticipantsList.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerParticipantsList.cs index d88741ec0c..238a716f91 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerParticipantsList.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerParticipantsList.cs @@ -308,6 +308,33 @@ namespace osu.Game.Tests.Visual.Multiplayer AddStep("set state: locally available", () => MultiplayerClient.ChangeUserBeatmapAvailability(0, BeatmapAvailability.LocallyAvailable())); } + [Test] + public void TestUserWithStyle() + { + AddStep("add users", () => + { + MultiplayerClient.AddUser(new APIUser + { + Id = 0, + Username = "User 0", + RulesetsStatistics = new Dictionary + { + { + Ruleset.Value.ShortName, + new UserStatistics { GlobalRank = RNG.Next(1, 100000), } + } + }, + CoverUrl = @"https://osu.ppy.sh/images/headers/profile-covers/c3.jpg", + }); + + MultiplayerClient.ChangeUserStyle(0, 259, 2); + }); + + AddStep("set beatmap locally available", () => MultiplayerClient.ChangeUserBeatmapAvailability(0, BeatmapAvailability.LocallyAvailable())); + AddStep("change user style to beatmap: 258, ruleset: 1", () => MultiplayerClient.ChangeUserStyle(0, 258, 1)); + AddStep("change user style to beatmap: null, ruleset: null", () => MultiplayerClient.ChangeUserStyle(0, null, null)); + } + [Test] public void TestModOverlap() { diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Participants/ParticipantPanel.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Participants/ParticipantPanel.cs index 7e42b18240..64c4648125 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Participants/ParticipantPanel.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Participants/ParticipantPanel.cs @@ -4,6 +4,8 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Threading; +using System.Threading.Tasks; using osu.Framework.Allocation; using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Extensions.ObjectExtensions; @@ -14,6 +16,9 @@ using osu.Framework.Graphics.Cursor; using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.UserInterface; +using osu.Framework.Logging; +using osu.Game.Beatmaps.Drawables; +using osu.Game.Database; using osu.Game.Graphics; using osu.Game.Graphics.Sprites; using osu.Game.Graphics.UserInterface; @@ -47,6 +52,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Participants private SpriteIcon crown = null!; private OsuSpriteText userRankText = null!; + private StyleDisplayIcon userStyleDisplay = null!; private ModDisplay userModsDisplay = null!; private StateDisplay userStateDisplay = null!; @@ -149,16 +155,20 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Participants } } }, - new Container + new FillFlowContainer { Anchor = Anchor.CentreRight, Origin = Anchor.CentreRight, AutoSizeAxes = Axes.Both, Margin = new MarginPadding { Right = 70 }, - Child = userModsDisplay = new ModDisplay + Children = new Drawable[] { - Scale = new Vector2(0.5f), - ExpansionMode = ExpansionMode.AlwaysContracted, + userStyleDisplay = new StyleDisplayIcon(), + userModsDisplay = new ModDisplay + { + Scale = new Vector2(0.5f), + ExpansionMode = ExpansionMode.AlwaysContracted, + } } }, userStateDisplay = new StateDisplay @@ -208,9 +218,20 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Participants userStateDisplay.UpdateStatus(User.State, User.BeatmapAvailability); if ((User.BeatmapAvailability.State == DownloadState.LocallyAvailable) && (User.State != MultiplayerUserState.Spectating)) + { userModsDisplay.FadeIn(fade_time); + userStyleDisplay.FadeIn(fade_time); + } else + { userModsDisplay.FadeOut(fade_time); + userStyleDisplay.FadeOut(fade_time); + } + + if (User.BeatmapId == null && User.RulesetId == null) + userStyleDisplay.Style = null; + else + userStyleDisplay.Style = (User.BeatmapId ?? currentItem?.BeatmapID ?? 0, User.RulesetId ?? currentItem?.RulesetID ?? 0); kickButton.Alpha = client.IsHost && !User.Equals(client.LocalUser) ? 1 : 0; crown.Alpha = client.Room.Host?.Equals(User) == true ? 1 : 0; @@ -284,5 +305,81 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Participants IconHoverColour = colours.Red; } } + + private partial class StyleDisplayIcon : CompositeComponent + { + [Resolved] + private BeatmapLookupCache beatmapLookupCache { get; set; } = null!; + + [Resolved] + private RulesetStore rulesets { get; set; } = null!; + + public StyleDisplayIcon() + { + AutoSizeAxes = Axes.Both; + } + + private (int beatmap, int ruleset)? style; + + public (int beatmap, int ruleset)? Style + { + get => style; + set + { + if (style == value) + return; + + style = value; + Scheduler.Add(refresh); + } + } + + private CancellationTokenSource? cancellationSource; + + private void refresh() + { + cancellationSource?.Cancel(); + cancellationSource?.Dispose(); + cancellationSource = null; + + if (Style == null) + { + ClearInternal(); + return; + } + + cancellationSource = new CancellationTokenSource(); + CancellationToken token = cancellationSource.Token; + + int localBeatmap = Style.Value.beatmap; + int localRuleset = Style.Value.ruleset; + + Task.Run(async () => + { + try + { + var beatmap = await beatmapLookupCache.GetBeatmapAsync(localBeatmap, token).ConfigureAwait(false); + if (beatmap == null) + return; + + Schedule(() => + { + if (token.IsCancellationRequested) + return; + + InternalChild = new DifficultyIcon(beatmap, rulesets.GetRuleset(localRuleset)) + { + Size = new Vector2(20), + TooltipType = DifficultyIconTooltipType.Extended, + }; + }); + } + catch (Exception e) + { + Logger.Log($"Error while populating participant style icon {e}"); + } + }, token); + } + } } } From e7c272b8b9278e706baf9305c8ff92548c22ff32 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Wed, 25 Dec 2024 23:39:01 +0900 Subject: [PATCH 0412/1275] Don't display on matching beatmap/ruleset --- .../OnlinePlay/Multiplayer/Participants/ParticipantPanel.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Participants/ParticipantPanel.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Participants/ParticipantPanel.cs index 64c4648125..a2657019a3 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Participants/ParticipantPanel.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Participants/ParticipantPanel.cs @@ -228,7 +228,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Participants userStyleDisplay.FadeOut(fade_time); } - if (User.BeatmapId == null && User.RulesetId == null) + if ((User.BeatmapId == null && User.RulesetId == null) || (User.BeatmapId == currentItem?.BeatmapID && User.RulesetId == currentItem?.RulesetID)) userStyleDisplay.Style = null; else userStyleDisplay.Style = (User.BeatmapId ?? currentItem?.BeatmapID ?? 0, User.RulesetId ?? currentItem?.RulesetID ?? 0); From 6579b055618f375e06437f05ff70f612316e72a6 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Wed, 25 Dec 2024 23:45:36 +0900 Subject: [PATCH 0413/1275] Remove unused usings --- osu.Game/Screens/OnlinePlay/OnlinePlayStyleSelect.cs | 2 -- 1 file changed, 2 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/OnlinePlayStyleSelect.cs b/osu.Game/Screens/OnlinePlay/OnlinePlayStyleSelect.cs index 89f2ffc883..029ca68e36 100644 --- a/osu.Game/Screens/OnlinePlay/OnlinePlayStyleSelect.cs +++ b/osu.Game/Screens/OnlinePlay/OnlinePlayStyleSelect.cs @@ -1,14 +1,12 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using System; using System.Collections.Generic; using System.Linq; using Humanizer; using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; -using osu.Framework.Screens; using osu.Game.Beatmaps; using osu.Game.Database; using osu.Game.Online.Rooms; From 1f60adbaf144ab77dbc211f14c1a2ede46e6bf74 Mon Sep 17 00:00:00 2001 From: kongehund <63306696+kongehund@users.noreply.github.com> Date: Thu, 26 Dec 2024 00:35:21 +0100 Subject: [PATCH 0414/1275] 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 0415/1275] 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 0416/1275] 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 0417/1275] 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 0418/1275] 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 0419/1275] 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 0420/1275] Add failing test case --- .../Beatmaps/IO/LegacyBeatmapExporterTest.cs | 24 ++++++++++++++++++ .../Archives/fractional-coordinates.olz | Bin 0 -> 556 bytes 2 files changed, 24 insertions(+) create mode 100644 osu.Game.Tests/Resources/Archives/fractional-coordinates.olz diff --git a/osu.Game.Tests/Beatmaps/IO/LegacyBeatmapExporterTest.cs b/osu.Game.Tests/Beatmaps/IO/LegacyBeatmapExporterTest.cs index 8a95d26782..cf498c7856 100644 --- a/osu.Game.Tests/Beatmaps/IO/LegacyBeatmapExporterTest.cs +++ b/osu.Game.Tests/Beatmaps/IO/LegacyBeatmapExporterTest.cs @@ -11,6 +11,7 @@ using osu.Framework.Testing; using osu.Game.Beatmaps; using osu.Game.Database; using osu.Game.IO.Archives; +using osu.Game.Rulesets.Objects.Types; using osu.Game.Tests.Resources; using osu.Game.Tests.Visual; using MemoryStream = System.IO.MemoryStream; @@ -50,6 +51,29 @@ namespace osu.Game.Tests.Beatmaps.IO AddAssert("hit object is snapped", () => beatmap.Beatmap.HitObjects[0].StartTime, () => Is.EqualTo(28519).Within(0.001)); } + [Test] + public void TestFractionalObjectCoordinatesRounded() + { + IWorkingBeatmap beatmap = null!; + MemoryStream outStream = null!; + + // Ensure importer encoding is correct + AddStep("import beatmap", () => beatmap = importBeatmapFromArchives(@"fractional-coordinates.olz")); + AddAssert("hit object has fractional position", () => ((IHasYPosition)beatmap.Beatmap.HitObjects[1]).Y, () => Is.EqualTo(383.99997).Within(0.00001)); + + // Ensure exporter legacy conversion is correct + AddStep("export", () => + { + outStream = new MemoryStream(); + + new LegacyBeatmapExporter(LocalStorage) + .ExportToStream((BeatmapSetInfo)beatmap.BeatmapInfo.BeatmapSet!, outStream, null); + }); + + AddStep("import beatmap again", () => beatmap = importBeatmapFromStream(outStream)); + AddAssert("hit object is snapped", () => ((IHasYPosition)beatmap.Beatmap.HitObjects[1]).Y, () => Is.EqualTo(384).Within(0.00001)); + } + [Test] public void TestExportStability() { diff --git a/osu.Game.Tests/Resources/Archives/fractional-coordinates.olz b/osu.Game.Tests/Resources/Archives/fractional-coordinates.olz new file mode 100644 index 0000000000000000000000000000000000000000..5c5af368c8b95fe76c9f45f0dfbc5f1e73a126a5 GIT binary patch literal 556 zcmWIWW@Zs#;9%fjunnIb$p8h{7#SF(7!(-NiV~AcGV}8ib99sQ^NUh4^Abx^i}mu0 zOG86=8Q9gU^3rvy^3tuU^3qEyxEUB(UNAE-fQirv2ZIh72(-Pg?BaXwr@mD8=skgu z8H|ZK#&R-zigzD*%__OHrDOfGgYKV1eYpCPi*CI6cmLV_C%->?o30IC#IQbOSJJW9 zhaS$DFr9tEf|)ladPXnUwY?!lG)1mBGvMk)*9azUF{95KH#1%c_(hk5%sIBW!zWbR zccI_jd7D>>O=v#3$NjmAN1lYeo}-dSUD*jQ(Fc!&dKmYHzvr%8-6$&^UUQ5|Y8_)r z-W0p{qL*EtwU%9xoTmCIY{uulGv>x;1MCcw^Sw@pm~LP8<6`>jbGDv;KCZJ^nY~t` z{%`KdR?|z`+R3^CTfTBEnqAj>Ow)IAphWCK-A|8~p6xBX-e_{RFJS&JX0a=u3^u4| ziT+mC;FoUQE62BTzS@=kzZYI9F4c?5>Py>O&-`wNip;Nz7T?-eE*3tXy5)YjCf6N@ zm8?p$*iP0zVGr Date: Fri, 27 Dec 2024 10:56:52 +0100 Subject: [PATCH 0421/1275] 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 0422/1275] 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 0423/1275] 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 0424/1275] 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 0425/1275] 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 824497d82c6f86eebf6421b1cdcf25beaf39f881 Mon Sep 17 00:00:00 2001 From: Jay Lawton Date: Fri, 27 Dec 2024 23:30:30 +1000 Subject: [PATCH 0426/1275] Rewrite of the `Rhythm` Skill within osu!taiko (#31284) * implement bell curve into diffcalcutils * remove unneeded attributes * implement new rhythm skill * change dho variables * update dho rhythm * interval interface * implement rhythmevaluator * evenhitobjects * evenpatterns * evenrhythm * change attribute ordering * initial balancing * change naming to Same instead of Even * remove attribute bump for display * Fix diffcalc tests --------- Co-authored-by: StanR --- .../TaikoDifficultyCalculatorTest.cs | 8 +- .../Difficulty/Evaluators/RhythmEvaluator.cs | 149 +++++++++++++++++ .../Preprocessing/Rhythm/Data/SamePatterns.cs | 55 ++++++ .../Preprocessing/Rhythm/Data/SameRhythm.cs | 73 ++++++++ .../Rhythm/Data/SameRhythmHitObjects.cs | 94 +++++++++++ .../Preprocessing/Rhythm/IHasInterval.cs | 13 ++ .../Rhythm/TaikoDifficultyHitObjectRhythm.cs | 79 ++++++++- .../Preprocessing/TaikoDifficultyHitObject.cs | 51 ++---- .../Difficulty/Skills/Rhythm.cs | 157 ++---------------- .../Difficulty/TaikoDifficultyAttributes.cs | 28 ++-- .../Difficulty/TaikoDifficultyCalculator.cs | 20 ++- .../Utils/DifficultyCalculationUtils.cs | 10 ++ 12 files changed, 520 insertions(+), 217 deletions(-) create mode 100644 osu.Game.Rulesets.Taiko/Difficulty/Evaluators/RhythmEvaluator.cs create mode 100644 osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/Data/SamePatterns.cs create mode 100644 osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/Data/SameRhythm.cs create mode 100644 osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/Data/SameRhythmHitObjects.cs create mode 100644 osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/IHasInterval.cs diff --git a/osu.Game.Rulesets.Taiko.Tests/TaikoDifficultyCalculatorTest.cs b/osu.Game.Rulesets.Taiko.Tests/TaikoDifficultyCalculatorTest.cs index 09d6540f72..ba247c68d4 100644 --- a/osu.Game.Rulesets.Taiko.Tests/TaikoDifficultyCalculatorTest.cs +++ b/osu.Game.Rulesets.Taiko.Tests/TaikoDifficultyCalculatorTest.cs @@ -14,13 +14,13 @@ namespace osu.Game.Rulesets.Taiko.Tests { protected override string ResourceAssembly => "osu.Game.Rulesets.Taiko"; - [TestCase(3.0920212594351191d, 200, "diffcalc-test")] - [TestCase(3.0920212594351191d, 200, "diffcalc-test-strong")] + [TestCase(3.0950934814938953d, 200, "diffcalc-test")] + [TestCase(3.0950934814938953d, 200, "diffcalc-test-strong")] public void Test(double expectedStarRating, int expectedMaxCombo, string name) => base.Test(expectedStarRating, expectedMaxCombo, name); - [TestCase(4.0789820318081444d, 200, "diffcalc-test")] - [TestCase(4.0789820318081444d, 200, "diffcalc-test-strong")] + [TestCase(4.0839365008715403d, 200, "diffcalc-test")] + [TestCase(4.0839365008715403d, 200, "diffcalc-test-strong")] public void TestClockRateAdjusted(double expectedStarRating, int expectedMaxCombo, string name) => Test(expectedStarRating, expectedMaxCombo, name, new TaikoModDoubleTime()); diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Evaluators/RhythmEvaluator.cs b/osu.Game.Rulesets.Taiko/Difficulty/Evaluators/RhythmEvaluator.cs new file mode 100644 index 0000000000..3a294f7123 --- /dev/null +++ b/osu.Game.Rulesets.Taiko/Difficulty/Evaluators/RhythmEvaluator.cs @@ -0,0 +1,149 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using System.Collections.Generic; +using osu.Game.Rulesets.Difficulty.Preprocessing; +using osu.Game.Rulesets.Difficulty.Utils; +using osu.Game.Rulesets.Taiko.Difficulty.Preprocessing; +using osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Rhythm; +using osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Rhythm.Data; + +namespace osu.Game.Rulesets.Taiko.Difficulty.Evaluators +{ + public class RhythmEvaluator + { + /// + /// Multiplier for a given denominator term. + /// + private static double termPenalty(double ratio, int denominator, double power, double multiplier) + { + return -multiplier * Math.Pow(Math.Cos(denominator * Math.PI * ratio), power); + } + + /// + /// Calculates the difficulty of a given ratio using a combination of periodic penalties and bonuses. + /// + private static double ratioDifficulty(double ratio, int terms = 8) + { + double difficulty = 0; + + for (int i = 1; i <= terms; ++i) + { + difficulty += termPenalty(ratio, i, 2, 1); + } + + difficulty += terms; + + // Give bonus to near-1 ratios + difficulty += DifficultyCalculationUtils.BellCurve(ratio, 1, 0.7); + + // Penalize ratios that are VERY near 1 + difficulty -= DifficultyCalculationUtils.BellCurve(ratio, 1, 0.5); + + return difficulty / Math.Sqrt(8); + } + + /// + /// Determines if the changes in hit object intervals is consistent based on a given threshold. + /// + private static double repeatedIntervalPenalty(SameRhythmHitObjects sameRhythmHitObjects, double hitWindow, double threshold = 0.1) + { + double longIntervalPenalty = sameInterval(sameRhythmHitObjects, 3); + + double shortIntervalPenalty = sameRhythmHitObjects.Children.Count < 6 + ? sameInterval(sameRhythmHitObjects, 4) + : 1.0; // Returns a non-penalty if there are 6 or more notes within an interval. + + // Scale penalties dynamically based on hit object duration relative to hitWindow. + double penaltyScaling = Math.Max(1 - sameRhythmHitObjects.Duration / (hitWindow * 2), 0.5); + + return Math.Min(longIntervalPenalty, shortIntervalPenalty) * penaltyScaling; + + double sameInterval(SameRhythmHitObjects startObject, int intervalCount) + { + List intervals = new List(); + var currentObject = startObject; + + for (int i = 0; i < intervalCount && currentObject != null; i++) + { + intervals.Add(currentObject.HitObjectInterval); + currentObject = currentObject.Previous; + } + + intervals.RemoveAll(interval => interval == null); + + if (intervals.Count < intervalCount) + return 1.0; // No penalty if there aren't enough valid intervals. + + for (int i = 0; i < intervals.Count; i++) + { + for (int j = i + 1; j < intervals.Count; j++) + { + double ratio = intervals[i]!.Value / intervals[j]!.Value; + if (Math.Abs(1 - ratio) <= threshold) // If any two intervals are similar, apply a penalty. + return 0.3; + } + } + + return 1.0; // No penalty if all intervals are different. + } + } + + private static double evaluateDifficultyOf(SameRhythmHitObjects sameRhythmHitObjects, double hitWindow) + { + double intervalDifficulty = ratioDifficulty(sameRhythmHitObjects.HitObjectIntervalRatio); + double? previousInterval = sameRhythmHitObjects.Previous?.HitObjectInterval; + + // If a previous interval exists and there are multiple hit objects in the sequence: + if (previousInterval != null && sameRhythmHitObjects.Children.Count > 1) + { + double expectedDurationFromPrevious = (double)previousInterval * sameRhythmHitObjects.Children.Count; + double durationDifference = sameRhythmHitObjects.Duration - expectedDurationFromPrevious; + + if (durationDifference > 0) + { + intervalDifficulty *= DifficultyCalculationUtils.Logistic( + durationDifference / hitWindow, + midpointOffset: 0.7, + multiplier: 1.5, + maxValue: 1); + } + } + + // Apply consistency penalty. + intervalDifficulty *= repeatedIntervalPenalty(sameRhythmHitObjects, hitWindow); + + // Penalise patterns that can be hit within a single hit window. + intervalDifficulty *= DifficultyCalculationUtils.Logistic( + sameRhythmHitObjects.Duration / hitWindow, + midpointOffset: 0.6, + multiplier: 1, + maxValue: 1); + + return Math.Pow(intervalDifficulty, 0.75); + } + + private static double evaluateDifficultyOf(SamePatterns samePatterns) + { + return ratioDifficulty(samePatterns.IntervalRatio); + } + + /// + /// Evaluate the difficulty of a hitobject considering its interval change. + /// + public static double EvaluateDifficultyOf(DifficultyHitObject hitObject, double hitWindow) + { + TaikoDifficultyHitObjectRhythm rhythm = ((TaikoDifficultyHitObject)hitObject).Rhythm; + double difficulty = 0.0d; + + if (rhythm.SameRhythmHitObjects?.FirstHitObject == hitObject) // Difficulty for SameRhythmHitObjects + difficulty += evaluateDifficultyOf(rhythm.SameRhythmHitObjects, hitWindow); + + if (rhythm.SamePatterns?.FirstHitObject == hitObject) // Difficulty for SamePatterns + difficulty += 0.5 * evaluateDifficultyOf(rhythm.SamePatterns); + + return difficulty; + } + } +} diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/Data/SamePatterns.cs b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/Data/SamePatterns.cs new file mode 100644 index 0000000000..50839c4561 --- /dev/null +++ b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/Data/SamePatterns.cs @@ -0,0 +1,55 @@ +// 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; + +namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Rhythm.Data +{ + /// + /// Represents grouped by their 's interval. + /// + public class SamePatterns : SameRhythm + { + public SamePatterns? Previous { get; private set; } + + /// + /// The between children within this group. + /// If there is only one child, this will have the value of the first child's . + /// + public double ChildrenInterval => Children.Count > 1 ? Children[1].Interval : Children[0].Interval; + + /// + /// The ratio of between this and the previous . In the + /// case where there is no previous , this will have a value of 1. + /// + public double IntervalRatio => ChildrenInterval / Previous?.ChildrenInterval ?? 1.0d; + + public TaikoDifficultyHitObject FirstHitObject => Children[0].FirstHitObject; + + public IEnumerable AllHitObjects => Children.SelectMany(child => child.Children); + + private SamePatterns(SamePatterns? previous, List data, ref int i) + : base(data, ref i, 5) + { + Previous = previous; + + foreach (TaikoDifficultyHitObject hitObject in AllHitObjects) + { + hitObject.Rhythm.SamePatterns = this; + } + } + + public static void GroupPatterns(List data) + { + List samePatterns = new List(); + + // Index does not need to be incremented, as it is handled within the SameRhythm constructor. + for (int i = 0; i < data.Count;) + { + SamePatterns? previous = samePatterns.Count > 0 ? samePatterns[^1] : null; + samePatterns.Add(new SamePatterns(previous, data, ref i)); + } + } + } +} diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/Data/SameRhythm.cs b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/Data/SameRhythm.cs new file mode 100644 index 0000000000..b1ca22595b --- /dev/null +++ b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/Data/SameRhythm.cs @@ -0,0 +1,73 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using System.Collections.Generic; + +namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Rhythm.Data +{ + /// + /// A base class for grouping s by their interval. In edges where an interval change + /// occurs, the is added to the group with the smaller interval. + /// + public abstract class SameRhythm + where ChildType : IHasInterval + { + public IReadOnlyList Children { get; private set; } + + /// + /// Determines if the intervals between two child objects are within a specified margin of error, + /// indicating that the intervals are effectively "flat" or consistent. + /// + private bool isFlat(ChildType current, ChildType previous, double marginOfError) + { + return Math.Abs(current.Interval - previous.Interval) <= marginOfError; + } + + /// + /// Create a new from a list of s, and add + /// them to the list until the end of the group. + /// + /// The list of s. + /// + /// Index in to start adding children. This will be modified and should be passed into + /// the next 's constructor. + /// + /// + /// The margin of error for the interval, within of which no interval change is considered to have occured. + /// + protected SameRhythm(List data, ref int i, double marginOfError) + { + List children = new List(); + Children = children; + children.Add(data[i]); + i++; + + for (; i < data.Count - 1; i++) + { + // An interval change occured, add the current data if the next interval is larger. + if (!isFlat(data[i], data[i + 1], marginOfError)) + { + if (data[i + 1].Interval > data[i].Interval + marginOfError) + { + children.Add(data[i]); + i++; + } + + return; + } + + // No interval change occured + children.Add(data[i]); + } + + // Check if the last two objects in the data form a "flat" rhythm pattern within the specified margin of error. + // If true, add the current object to the group and increment the index to process the next object. + if (data.Count > 2 && isFlat(data[^1], data[^2], marginOfError)) + { + children.Add(data[i]); + i++; + } + } + } +} diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/Data/SameRhythmHitObjects.cs b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/Data/SameRhythmHitObjects.cs new file mode 100644 index 0000000000..0ccc6da026 --- /dev/null +++ b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/Data/SameRhythmHitObjects.cs @@ -0,0 +1,94 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Collections.Generic; +using osu.Game.Rulesets.Difficulty.Preprocessing; + +namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Rhythm.Data +{ + /// + /// Represents a group of s with no rhythm variation. + /// + public class SameRhythmHitObjects : SameRhythm, IHasInterval + { + public TaikoDifficultyHitObject FirstHitObject => Children[0]; + + public SameRhythmHitObjects? Previous; + + /// + /// of the first hit object. + /// + public double StartTime => Children[0].StartTime; + + /// + /// The interval between the first and final hit object within this group. + /// + public double Duration => Children[^1].StartTime - Children[0].StartTime; + + /// + /// The interval in ms of each hit object in this . This is only defined if there is + /// more than two hit objects in this . + /// + public double? HitObjectInterval; + + /// + /// The ratio of between this and the previous . In the + /// case where one or both of the is undefined, this will have a value of 1. + /// + public double HitObjectIntervalRatio = 1; + + /// + /// The interval between the of this and the previous . + /// + public double Interval { get; private set; } = double.PositiveInfinity; + + public SameRhythmHitObjects(SameRhythmHitObjects? previous, List data, ref int i) + : base(data, ref i, 5) + { + Previous = previous; + + foreach (var hitObject in Children) + { + hitObject.Rhythm.SameRhythmHitObjects = this; + + // Pass the HitObjectInterval to each child. + hitObject.HitObjectInterval = HitObjectInterval; + } + + calculateIntervals(); + } + + public static List GroupHitObjects(List data) + { + List flatPatterns = new List(); + + // Index does not need to be incremented, as it is handled within SameRhythm's constructor. + for (int i = 0; i < data.Count;) + { + SameRhythmHitObjects? previous = flatPatterns.Count > 0 ? flatPatterns[^1] : null; + flatPatterns.Add(new SameRhythmHitObjects(previous, data, ref i)); + } + + return flatPatterns; + } + + private void calculateIntervals() + { + // Calculate the average interval between hitobjects, or null if there are fewer than two. + HitObjectInterval = Children.Count < 2 ? null : (Children[^1].StartTime - Children[0].StartTime) / (Children.Count - 1); + + // If both the current and previous intervals are available, calculate the ratio. + if (Previous?.HitObjectInterval != null && HitObjectInterval != null) + { + HitObjectIntervalRatio = HitObjectInterval.Value / Previous.HitObjectInterval.Value; + } + + if (Previous == null) + { + return; + } + + Interval = StartTime - Previous.StartTime; + } + } +} diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/IHasInterval.cs b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/IHasInterval.cs new file mode 100644 index 0000000000..8f3917cbde --- /dev/null +++ b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/IHasInterval.cs @@ -0,0 +1,13 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Rhythm +{ + /// + /// The interface for hitobjects that provide an interval value. + /// + public interface IHasInterval + { + double Interval { get; } + } +} diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/TaikoDifficultyHitObjectRhythm.cs b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/TaikoDifficultyHitObjectRhythm.cs index a273d7e2ea..beb7bfe5f6 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/TaikoDifficultyHitObjectRhythm.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/TaikoDifficultyHitObjectRhythm.cs @@ -1,35 +1,98 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System; +using System.Linq; +using osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Rhythm.Data; + namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Rhythm { /// - /// Represents a rhythm change in a taiko map. + /// Stores rhythm data for a . /// public class TaikoDifficultyHitObjectRhythm { /// - /// The difficulty multiplier associated with this rhythm change. + /// The group of hit objects with consistent rhythm that this object belongs to. /// - public readonly double Difficulty; + public SameRhythmHitObjects? SameRhythmHitObjects; /// - /// The ratio of current - /// to previous for the rhythm change. + /// The larger pattern of rhythm groups that this object is part of. + /// + public SamePatterns? SamePatterns; + + /// + /// The ratio of current + /// to previous for the rhythm change. /// A above 1 indicates a slow-down; a below 1 indicates a speed-up. /// public readonly double Ratio; + /// + /// List of most common rhythm changes in taiko maps. Based on how each object's interval compares to the previous object. + /// + /// + /// The general guidelines for the values are: + /// + /// rhythm changes with ratio closer to 1 (that are not 1) are harder to play, + /// speeding up is generally harder than slowing down (with exceptions of rhythm changes requiring a hand switch). + /// + /// + private static readonly TaikoDifficultyHitObjectRhythm[] common_rhythms = + { + new TaikoDifficultyHitObjectRhythm(1, 1), + new TaikoDifficultyHitObjectRhythm(2, 1), + new TaikoDifficultyHitObjectRhythm(1, 2), + new TaikoDifficultyHitObjectRhythm(3, 1), + new TaikoDifficultyHitObjectRhythm(1, 3), + new TaikoDifficultyHitObjectRhythm(3, 2), + new TaikoDifficultyHitObjectRhythm(2, 3), + new TaikoDifficultyHitObjectRhythm(5, 4), + new TaikoDifficultyHitObjectRhythm(4, 5) + }; + + /// + /// Initialises a new instance of s, + /// calculating the closest rhythm change and its associated difficulty for the current hit object. + /// + /// The current being processed. + public TaikoDifficultyHitObjectRhythm(TaikoDifficultyHitObject current) + { + var previous = current.Previous(0); + + if (previous == null) + { + Ratio = 1; + return; + } + + TaikoDifficultyHitObjectRhythm closestRhythm = getClosestRhythm(current.DeltaTime, previous.DeltaTime); + Ratio = closestRhythm.Ratio; + } + /// /// Creates an object representing a rhythm change. /// /// The numerator for . /// The denominator for - /// The difficulty multiplier associated with this rhythm change. - public TaikoDifficultyHitObjectRhythm(int numerator, int denominator, double difficulty) + private TaikoDifficultyHitObjectRhythm(int numerator, int denominator) { Ratio = numerator / (double)denominator; - Difficulty = difficulty; + } + + /// + /// Determines the closest rhythm change from that matches the timing ratio + /// between the current and previous intervals. + /// + /// The time difference between the current hit object and the previous one. + /// The time difference between the previous hit object and the one before it. + /// The closest matching rhythm from . + private TaikoDifficultyHitObjectRhythm getClosestRhythm(double currentDeltaTime, double previousDeltaTime) + { + double ratio = currentDeltaTime / previousDeltaTime; + return common_rhythms.OrderBy(x => Math.Abs(x.Ratio - ratio)).First(); } } } + diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/TaikoDifficultyHitObject.cs b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/TaikoDifficultyHitObject.cs index e741e4c9e7..dfcd08ed94 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/TaikoDifficultyHitObject.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/TaikoDifficultyHitObject.cs @@ -1,7 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using System; using System.Collections.Generic; using System.Linq; using osu.Game.Rulesets.Difficulty.Preprocessing; @@ -15,7 +14,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing /// /// Represents a single hit object in taiko difficulty calculation. /// - public class TaikoDifficultyHitObject : DifficultyHitObject + public class TaikoDifficultyHitObject : DifficultyHitObject, IHasInterval { /// /// The list of all of the same colour as this in the beatmap. @@ -42,6 +41,11 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing /// public readonly TaikoDifficultyHitObjectRhythm Rhythm; + /// + /// The interval between this hit object and the surrounding hit objects in its rhythm group. + /// + public double? HitObjectInterval { get; set; } + /// /// Colour data for this hit object. This is used by colour evaluator to calculate colour difficulty, but can be used /// by other skills in the future. @@ -58,6 +62,8 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing /// public double CurrentSliderVelocity; + public double Interval => DeltaTime; + /// /// Creates a new difficulty hit object. /// @@ -81,7 +87,9 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing // Create the Colour object, its properties should be filled in by TaikoDifficultyPreprocessor Colour = new TaikoDifficultyHitObjectColour(); - Rhythm = getClosestRhythm(lastObject, lastLastObject, clockRate); + + // Create a Rhythm object, its properties are filled in by TaikoDifficultyHitObjectRhythm + Rhythm = new TaikoDifficultyHitObjectRhythm(this); switch ((hitObject as Hit)?.Type) { @@ -105,43 +113,6 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing } } - /// - /// List of most common rhythm changes in taiko maps. - /// - /// - /// The general guidelines for the values are: - /// - /// rhythm changes with ratio closer to 1 (that are not 1) are harder to play, - /// speeding up is generally harder than slowing down (with exceptions of rhythm changes requiring a hand switch). - /// - /// - private static readonly TaikoDifficultyHitObjectRhythm[] common_rhythms = - { - new TaikoDifficultyHitObjectRhythm(1, 1, 0.0), - new TaikoDifficultyHitObjectRhythm(2, 1, 0.3), - new TaikoDifficultyHitObjectRhythm(1, 2, 0.5), - new TaikoDifficultyHitObjectRhythm(3, 1, 0.3), - new TaikoDifficultyHitObjectRhythm(1, 3, 0.35), - new TaikoDifficultyHitObjectRhythm(3, 2, 0.6), // purposefully higher (requires hand switch in full alternating gameplay style) - new TaikoDifficultyHitObjectRhythm(2, 3, 0.4), - new TaikoDifficultyHitObjectRhythm(5, 4, 0.5), - new TaikoDifficultyHitObjectRhythm(4, 5, 0.7) - }; - - /// - /// Returns the closest rhythm change from required to hit this object. - /// - /// The gameplay preceding this one. - /// The gameplay preceding . - /// The rate of the gameplay clock. - private TaikoDifficultyHitObjectRhythm getClosestRhythm(HitObject lastObject, HitObject lastLastObject, double clockRate) - { - double prevLength = (lastObject.StartTime - lastLastObject.StartTime) / clockRate; - double ratio = DeltaTime / prevLength; - - return common_rhythms.OrderBy(x => Math.Abs(x.Ratio - ratio)).First(); - } - public TaikoDifficultyHitObject? PreviousMono(int backwardsIndex) => monoDifficultyHitObjects?.ElementAtOrDefault(MonoIndex - (backwardsIndex + 1)); public TaikoDifficultyHitObject? NextMono(int forwardsIndex) => monoDifficultyHitObjects?.ElementAtOrDefault(MonoIndex + (forwardsIndex + 1)); diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Skills/Rhythm.cs b/osu.Game.Rulesets.Taiko/Difficulty/Skills/Rhythm.cs index e76af13686..4fe1ea693e 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Skills/Rhythm.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Skills/Rhythm.cs @@ -1,13 +1,11 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using System; using osu.Game.Rulesets.Difficulty.Preprocessing; using osu.Game.Rulesets.Difficulty.Skills; +using osu.Game.Rulesets.Difficulty.Utils; using osu.Game.Rulesets.Mods; -using osu.Game.Rulesets.Taiko.Difficulty.Preprocessing; -using osu.Game.Rulesets.Taiko.Objects; -using osu.Game.Utils; +using osu.Game.Rulesets.Taiko.Difficulty.Evaluators; namespace osu.Game.Rulesets.Taiko.Difficulty.Skills { @@ -16,158 +14,25 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Skills /// public class Rhythm : StrainDecaySkill { - protected override double SkillMultiplier => 10; - protected override double StrainDecayBase => 0; + protected override double SkillMultiplier => 1.0; + protected override double StrainDecayBase => 0.4; - /// - /// The note-based decay for rhythm strain. - /// - /// - /// is not used here, as it's time- and not note-based. - /// - private const double strain_decay = 0.96; + private readonly double greatHitWindow; - /// - /// Maximum number of entries in . - /// - private const int rhythm_history_max_length = 8; - - /// - /// Contains the last changes in note sequence rhythms. - /// - private readonly LimitedCapacityQueue rhythmHistory = new LimitedCapacityQueue(rhythm_history_max_length); - - /// - /// Contains the rolling rhythm strain. - /// Used to apply per-note decay. - /// - private double currentStrain; - - /// - /// Number of notes since the last rhythm change has taken place. - /// - private int notesSinceRhythmChange; - - public Rhythm(Mod[] mods) + public Rhythm(Mod[] mods, double greatHitWindow) : base(mods) { + this.greatHitWindow = greatHitWindow; } protected override double StrainValueOf(DifficultyHitObject current) { - // drum rolls and swells are exempt. - if (!(current.BaseObject is Hit)) - { - resetRhythmAndStrain(); - return 0.0; - } + double difficulty = RhythmEvaluator.EvaluateDifficultyOf(current, greatHitWindow); - currentStrain *= strain_decay; + // To prevent abuse of exceedingly long intervals between awkward rhythms, we penalise its difficulty. + difficulty *= DifficultyCalculationUtils.Logistic(current.DeltaTime, 350, -1 / 25.0, 0.5) + 0.5; - TaikoDifficultyHitObject hitObject = (TaikoDifficultyHitObject)current; - notesSinceRhythmChange += 1; - - // rhythm difficulty zero (due to rhythm not changing) => no rhythm strain. - if (hitObject.Rhythm.Difficulty == 0.0) - { - return 0.0; - } - - double objectStrain = hitObject.Rhythm.Difficulty; - - objectStrain *= repetitionPenalties(hitObject); - objectStrain *= patternLengthPenalty(notesSinceRhythmChange); - objectStrain *= speedPenalty(hitObject.DeltaTime); - - // careful - needs to be done here since calls above read this value - notesSinceRhythmChange = 0; - - currentStrain += objectStrain; - return currentStrain; - } - - /// - /// Returns a penalty to apply to the current hit object caused by repeating rhythm changes. - /// - /// - /// Repetitions of more recent patterns are associated with a higher penalty. - /// - /// The current hit object being considered. - private double repetitionPenalties(TaikoDifficultyHitObject hitObject) - { - double penalty = 1; - - rhythmHistory.Enqueue(hitObject); - - for (int mostRecentPatternsToCompare = 2; mostRecentPatternsToCompare <= rhythm_history_max_length / 2; mostRecentPatternsToCompare++) - { - for (int start = rhythmHistory.Count - mostRecentPatternsToCompare - 1; start >= 0; start--) - { - if (!samePattern(start, mostRecentPatternsToCompare)) - continue; - - int notesSince = hitObject.Index - rhythmHistory[start].Index; - penalty *= repetitionPenalty(notesSince); - break; - } - } - - return penalty; - } - - /// - /// Determines whether the rhythm change pattern starting at is a repeat of any of the - /// . - /// - private bool samePattern(int start, int mostRecentPatternsToCompare) - { - for (int i = 0; i < mostRecentPatternsToCompare; i++) - { - if (rhythmHistory[start + i].Rhythm != rhythmHistory[rhythmHistory.Count - mostRecentPatternsToCompare + i].Rhythm) - return false; - } - - return true; - } - - /// - /// Calculates a single rhythm repetition penalty. - /// - /// Number of notes since the last repetition of a rhythm change. - private static double repetitionPenalty(int notesSince) => Math.Min(1.0, 0.032 * notesSince); - - /// - /// Calculates a penalty based on the number of notes since the last rhythm change. - /// Both rare and frequent rhythm changes are penalised. - /// - /// Number of notes since the last rhythm change. - private static double patternLengthPenalty(int patternLength) - { - double shortPatternPenalty = Math.Min(0.15 * patternLength, 1.0); - double longPatternPenalty = Math.Clamp(2.5 - 0.15 * patternLength, 0.0, 1.0); - return Math.Min(shortPatternPenalty, longPatternPenalty); - } - - /// - /// Calculates a penalty for objects that do not require alternating hands. - /// - /// Time (in milliseconds) since the last hit object. - private double speedPenalty(double deltaTime) - { - if (deltaTime < 80) return 1; - if (deltaTime < 210) return Math.Max(0, 1.4 - 0.005 * deltaTime); - - resetRhythmAndStrain(); - return 0.0; - } - - /// - /// Resets the rolling strain value and counter. - /// - private void resetRhythmAndStrain() - { - currentStrain = 0.0; - notesSinceRhythmChange = 0; + return difficulty; } } } diff --git a/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyAttributes.cs b/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyAttributes.cs index d3cdb379d5..ef729e1f07 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyAttributes.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyAttributes.cs @@ -10,18 +10,6 @@ namespace osu.Game.Rulesets.Taiko.Difficulty { public class TaikoDifficultyAttributes : DifficultyAttributes { - /// - /// The difficulty corresponding to the stamina skill. - /// - [JsonProperty("stamina_difficulty")] - public double StaminaDifficulty { get; set; } - - /// - /// The ratio of stamina difficulty from mono-color (single colour) streams to total stamina difficulty. - /// - [JsonProperty("mono_stamina_factor")] - public double MonoStaminaFactor { get; set; } - /// /// The difficulty corresponding to the rhythm skill. /// @@ -40,8 +28,20 @@ namespace osu.Game.Rulesets.Taiko.Difficulty [JsonProperty("colour_difficulty")] public double ColourDifficulty { get; set; } - [JsonProperty("rhythm_difficult_strains")] - public double RhythmTopStrains { get; set; } + /// + /// The difficulty corresponding to the stamina skill. + /// + [JsonProperty("stamina_difficulty")] + public double StaminaDifficulty { get; set; } + + /// + /// The ratio of stamina difficulty from mono-color (single colour) streams to total stamina difficulty. + /// + [JsonProperty("mono_stamina_factor")] + public double MonoStaminaFactor { get; set; } + + [JsonProperty("reading_difficult_strains")] + public double ReadingTopStrains { get; set; } [JsonProperty("colour_difficult_strains")] public double ColourTopStrains { get; set; } diff --git a/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs b/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs index 0d6ecb8d3e..f8ff6f6065 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs @@ -14,6 +14,7 @@ using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.Taiko.Difficulty.Preprocessing; using osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Colour; using osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Reading; +using osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Rhythm.Data; using osu.Game.Rulesets.Taiko.Difficulty.Skills; using osu.Game.Rulesets.Taiko.Mods; using osu.Game.Rulesets.Taiko.Scoring; @@ -23,7 +24,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty public class TaikoDifficultyCalculator : DifficultyCalculator { private const double difficulty_multiplier = 0.084375; - private const double rhythm_skill_multiplier = 0.200 * difficulty_multiplier; + private const double rhythm_skill_multiplier = 1.24 * difficulty_multiplier; private const double reading_skill_multiplier = 0.100 * difficulty_multiplier; private const double colour_skill_multiplier = 0.375 * difficulty_multiplier; private const double stamina_skill_multiplier = 0.375 * difficulty_multiplier; @@ -37,9 +38,12 @@ namespace osu.Game.Rulesets.Taiko.Difficulty protected override Skill[] CreateSkills(IBeatmap beatmap, Mod[] mods, double clockRate) { + HitWindows hitWindows = new HitWindows(); + hitWindows.SetDifficulty(beatmap.Difficulty.OverallDifficulty); + return new Skill[] { - new Rhythm(mods), + new Rhythm(mods, hitWindows.WindowFor(HitResult.Great) / clockRate), new Reading(mods), new Colour(mods), new Stamina(mods, false), @@ -57,6 +61,9 @@ namespace osu.Game.Rulesets.Taiko.Difficulty protected override IEnumerable CreateDifficultyHitObjects(IBeatmap beatmap, double clockRate) { + var hitWindows = new HitWindows(); + hitWindows.SetDifficulty(beatmap.Difficulty.OverallDifficulty); + var difficultyHitObjects = new List(); var centreObjects = new List(); var rimObjects = new List(); @@ -79,7 +86,10 @@ namespace osu.Game.Rulesets.Taiko.Difficulty )); } + var groupedHitObjects = SameRhythmHitObjects.GroupHitObjects(noteObjects); + TaikoColourDifficultyPreprocessor.ProcessAndAssign(difficultyHitObjects); + SamePatterns.GroupPatterns(groupedHitObjects); bpmLoader.ProcessEffectiveBPM(beatmap.ControlPointInfo, clockRate); return difficultyHitObjects; @@ -105,8 +115,8 @@ namespace osu.Game.Rulesets.Taiko.Difficulty double monoStaminaRating = singleColourStamina.DifficultyValue() * stamina_skill_multiplier; double monoStaminaFactor = staminaRating == 0 ? 1 : Math.Pow(monoStaminaRating / staminaRating, 5); - double rhythmDifficultStrains = rhythm.CountTopWeightedStrains(); double colourDifficultStrains = colour.CountTopWeightedStrains(); + double readingDifficultStrains = reading.CountTopWeightedStrains(); double staminaDifficultStrains = stamina.CountTopWeightedStrains(); double combinedRating = combinedDifficultyValue(rhythm, reading, colour, stamina, isRelax); @@ -134,9 +144,9 @@ namespace osu.Game.Rulesets.Taiko.Difficulty ColourDifficulty = colourRating, StaminaDifficulty = staminaRating, MonoStaminaFactor = monoStaminaFactor, - StaminaTopStrains = staminaDifficultStrains, - RhythmTopStrains = rhythmDifficultStrains, + ReadingTopStrains = readingDifficultStrains, ColourTopStrains = colourDifficultStrains, + StaminaTopStrains = staminaDifficultStrains, GreatHitWindow = hitWindows.WindowFor(HitResult.Great) / clockRate, OkHitWindow = hitWindows.WindowFor(HitResult.Ok) / clockRate, MaxCombo = beatmap.GetMaxCombo(), diff --git a/osu.Game/Rulesets/Difficulty/Utils/DifficultyCalculationUtils.cs b/osu.Game/Rulesets/Difficulty/Utils/DifficultyCalculationUtils.cs index 055d8a458b..497a1f8234 100644 --- a/osu.Game/Rulesets/Difficulty/Utils/DifficultyCalculationUtils.cs +++ b/osu.Game/Rulesets/Difficulty/Utils/DifficultyCalculationUtils.cs @@ -56,6 +56,16 @@ namespace osu.Game.Rulesets.Difficulty.Utils /// The p-norm of the vector. public static double Norm(double p, params double[] values) => Math.Pow(values.Sum(x => Math.Pow(x, p)), 1 / p); + /// + /// Calculates a Gaussian-based bell curve function (https://en.wikipedia.org/wiki/Gaussian_function) + /// + /// Value to calculate the function for + /// The mean (center) of the bell curve + /// The width (spread) of the curve + /// Multiplier to adjust the curve's height + /// The output of the bell curve function of + public static double BellCurve(double x, double mean, double width, double multiplier = 1.0) => multiplier * Math.Exp(Math.E * -(Math.Pow(x - mean, 2) / Math.Pow(width, 2))); + /// /// Smootherstep function (https://en.wikipedia.org/wiki/Smoothstep#Variations) /// 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 0427/1275] 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 0428/1275] 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 988ed374ae82528991f37516ee40098d2adf1af4 Mon Sep 17 00:00:00 2001 From: James Wilson Date: Sun, 29 Dec 2024 19:29:57 +0000 Subject: [PATCH 0429/1275] Add basic difficulty & performance calculation for Autopilot mod on osu! ruleset (#21211) * Set speed distance to 0 * Reduce speed & flashlight, remove aim * Remove speed AR bonus * cleanup autopilot mod check in `SpeedEvaluator` * further decrease speed rating for extra hand availability * Pass all mods to the speed evaluator, zero out distance bonus instead of distance --------- Co-authored-by: tsunyoku Co-authored-by: StanR --- .../Difficulty/Evaluators/SpeedEvaluator.cs | 9 ++++++++- .../Difficulty/OsuDifficultyCalculator.cs | 6 ++++++ .../Difficulty/OsuPerformanceCalculator.cs | 6 ++++++ osu.Game.Rulesets.Osu/Difficulty/Skills/Speed.cs | 2 +- 4 files changed, 21 insertions(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Difficulty/Evaluators/SpeedEvaluator.cs b/osu.Game.Rulesets.Osu/Difficulty/Evaluators/SpeedEvaluator.cs index a5f6468f17..e5e9769081 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/Evaluators/SpeedEvaluator.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/Evaluators/SpeedEvaluator.cs @@ -2,9 +2,13 @@ // See the LICENCE file in the repository root for full licence text. using System; +using System.Collections.Generic; +using System.Linq; using osu.Game.Rulesets.Difficulty.Preprocessing; using osu.Game.Rulesets.Difficulty.Utils; +using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Osu.Difficulty.Preprocessing; +using osu.Game.Rulesets.Osu.Mods; using osu.Game.Rulesets.Osu.Objects; namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators @@ -24,7 +28,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators /// and how easily they can be cheesed. /// /// - public static double EvaluateDifficultyOf(DifficultyHitObject current) + public static double EvaluateDifficultyOf(DifficultyHitObject current, IReadOnlyList mods) { if (current.BaseObject is Spinner) return 0; @@ -56,6 +60,9 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators // Max distance bonus is 1 * `distance_multiplier` at single_spacing_threshold double distanceBonus = Math.Pow(distance / single_spacing_threshold, 3.95) * distance_multiplier; + if (mods.OfType().Any()) + distanceBonus = 0; + // Base difficulty with all bonuses double difficulty = (1 + speedBonus + distanceBonus) * 1000 / strainTime; diff --git a/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs b/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs index ffdd4673e3..d0f23735c3 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs @@ -63,6 +63,12 @@ namespace osu.Game.Rulesets.Osu.Difficulty speedRating = 0.0; flashlightRating *= 0.7; } + else if (mods.Any(h => h is OsuModAutopilot)) + { + speedRating *= 0.5; + aimRating = 0.0; + flashlightRating *= 0.4; + } double baseAimPerformance = OsuStrainSkill.DifficultyToPerformance(aimRating); double baseSpeedPerformance = OsuStrainSkill.DifficultyToPerformance(speedRating); diff --git a/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs b/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs index 3610845533..df418fb3f8 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs @@ -135,6 +135,9 @@ namespace osu.Game.Rulesets.Osu.Difficulty private double computeAimValue(ScoreInfo score, OsuDifficultyAttributes attributes) { + if (score.Mods.Any(h => h is OsuModAutopilot)) + return 0.0; + double aimDifficulty = attributes.AimDifficulty; if (attributes.SliderCount > 0 && attributes.AimDifficultSliderCount > 0) @@ -211,6 +214,9 @@ namespace osu.Game.Rulesets.Osu.Difficulty if (attributes.ApproachRate > 10.33) approachRateFactor = 0.3 * (attributes.ApproachRate - 10.33); + if (score.Mods.Any(h => h is OsuModAutopilot)) + approachRateFactor = 0.0; + speedValue *= 1.0 + approachRateFactor * lengthBonus; // Buff for longer maps with high AR. if (score.Mods.Any(m => m is OsuModBlinds)) diff --git a/osu.Game.Rulesets.Osu/Difficulty/Skills/Speed.cs b/osu.Game.Rulesets.Osu/Difficulty/Skills/Speed.cs index d2c4bbb618..5dae9a9fc5 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/Skills/Speed.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/Skills/Speed.cs @@ -35,7 +35,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills protected override double StrainValueAt(DifficultyHitObject current) { currentStrain *= strainDecay(((OsuDifficultyHitObject)current).StrainTime); - currentStrain += SpeedEvaluator.EvaluateDifficultyOf(current) * skillMultiplier; + currentStrain += SpeedEvaluator.EvaluateDifficultyOf(current, Mods) * skillMultiplier; currentRhythm = RhythmEvaluator.EvaluateDifficultyOf(current); 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 0430/1275] 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 0431/1275] 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 0432/1275] Use return value instead of field to force weight position update --- osu.Game/Screens/Edit/Timing/MetronomeDisplay.cs | 16 +++++----------- 1 file changed, 5 insertions(+), 11 deletions(-) diff --git a/osu.Game/Screens/Edit/Timing/MetronomeDisplay.cs b/osu.Game/Screens/Edit/Timing/MetronomeDisplay.cs index 553eacab46..58d461b3a5 100644 --- a/osu.Game/Screens/Edit/Timing/MetronomeDisplay.cs +++ b/osu.Game/Screens/Edit/Timing/MetronomeDisplay.cs @@ -238,9 +238,7 @@ namespace osu.Game.Screens.Edit.Timing private bool spedUp; - private bool divisorChanged; - - private void updateDivisor() + private bool updateDivisor() { int divisor = 1; @@ -248,12 +246,12 @@ namespace osu.Game.Screens.Edit.Timing divisor = beatDivisor.Value % 3 == 0 ? 3 : 2; if (divisor == Divisor) - return; - - divisorChanged = true; + return false; Divisor = divisor; metronomeTick.Divisor = divisor; + + return true; } protected override void LoadComplete() @@ -274,9 +272,7 @@ namespace osu.Game.Screens.Edit.Timing timingPoint = BeatSyncSource.ControlPoints.TimingPointAt(BeatSyncSource.Clock.CurrentTime); - updateDivisor(); - - if (beatLength != timingPoint.BeatLength || divisorChanged) + if (updateDivisor() || beatLength != timingPoint.BeatLength) { beatLength = timingPoint.BeatLength; @@ -312,8 +308,6 @@ namespace osu.Game.Screens.Edit.Timing latchDelegate = Schedule(() => sampleLatch?.Play()); } } - - divisorChanged = false; } protected override void OnNewBeat(int beatIndex, TimingControlPoint timingPoint, EffectControlPoint effectPoint, ChannelAmplitudes amplitudes) From 7563a18c7fdcc40c33a1ef0e0ab5342ba8e879d1 Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Tue, 24 Dec 2024 09:23:52 -0500 Subject: [PATCH 0433/1275] Allow locking orientation on iOS in certain circumstances --- osu.Game/OsuGame.cs | 12 ++++ osu.Game/Rulesets/UI/DrawableRuleset.cs | 5 ++ osu.Game/Screens/IOsuScreen.cs | 10 ++++ osu.Game/Screens/OsuScreen.cs | 2 + osu.Game/Screens/Play/Player.cs | 2 + osu.iOS/AppDelegate.cs | 49 +++++++++++++++- osu.iOS/IOSOrientationHandler.cs | 76 +++++++++++++++++++++++++ osu.iOS/OsuGameIOS.cs | 12 ++++ 8 files changed, 167 insertions(+), 1 deletion(-) create mode 100644 osu.iOS/IOSOrientationHandler.cs diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs index 06e30e3fab..4352eb2a71 100644 --- a/osu.Game/OsuGame.cs +++ b/osu.Game/OsuGame.cs @@ -174,6 +174,16 @@ namespace osu.Game /// public readonly IBindable OverlayActivationMode = new Bindable(); + /// + /// On mobile devices, this specifies whether the device should be set and locked to portrait orientation. + /// + /// + /// Implementations can be viewed in mobile projects. + /// + public IBindable RequiresPortraitOrientation => requiresPortraitOrientation; + + private readonly Bindable requiresPortraitOrientation = new BindableBool(); + /// /// Whether the back button is currently displayed. /// @@ -1623,6 +1633,8 @@ namespace osu.Game GlobalCursorDisplay.MenuCursor.HideCursorOnNonMouseInput = newOsuScreen.HideMenuCursorOnNonMouseInput; + requiresPortraitOrientation.Value = newOsuScreen.RequiresPortraitOrientation; + if (newOsuScreen.HideOverlaysOnEnter) CloseAllOverlays(); else diff --git a/osu.Game/Rulesets/UI/DrawableRuleset.cs b/osu.Game/Rulesets/UI/DrawableRuleset.cs index ebd84fd91b..13d4b67132 100644 --- a/osu.Game/Rulesets/UI/DrawableRuleset.cs +++ b/osu.Game/Rulesets/UI/DrawableRuleset.cs @@ -577,6 +577,11 @@ namespace osu.Game.Rulesets.UI /// public virtual bool AllowGameplayOverlays => true; + /// + /// On mobile devices, this specifies whether this ruleset requires the device to be in portrait orientation. + /// + public virtual bool RequiresPortraitOrientation => false; + /// /// Sets a replay to be used, overriding local input. /// diff --git a/osu.Game/Screens/IOsuScreen.cs b/osu.Game/Screens/IOsuScreen.cs index 9e474ed0c6..8b3ff4306f 100644 --- a/osu.Game/Screens/IOsuScreen.cs +++ b/osu.Game/Screens/IOsuScreen.cs @@ -61,6 +61,16 @@ namespace osu.Game.Screens /// bool HideMenuCursorOnNonMouseInput { get; } + /// + /// On mobile devices, this specifies whether this requires the device to be in portrait orientation. + /// + /// + /// By default, all screens in the game display in landscape orientation. + /// Setting this to true will display this screen in portrait orientation instead, + /// and switch back to landscape when transitioning back to a regular non-portrait screen. + /// + bool RequiresPortraitOrientation { get; } + /// /// Whether overlays should be able to be opened when this screen is current. /// diff --git a/osu.Game/Screens/OsuScreen.cs b/osu.Game/Screens/OsuScreen.cs index ab66241a77..e1d1ac38da 100644 --- a/osu.Game/Screens/OsuScreen.cs +++ b/osu.Game/Screens/OsuScreen.cs @@ -47,6 +47,8 @@ namespace osu.Game.Screens public virtual bool HideMenuCursorOnNonMouseInput => false; + public virtual bool RequiresPortraitOrientation => false; + /// /// The initial overlay activation mode to use when this screen is entered for the first time. /// diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs index 228b77b780..e50f97f912 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -68,6 +68,8 @@ namespace osu.Game.Screens.Play public override bool HideMenuCursorOnNonMouseInput => true; + public override bool RequiresPortraitOrientation => DrawableRuleset.RequiresPortraitOrientation; + protected override OverlayActivation InitialOverlayActivationMode => OverlayActivation.UserTriggered; // We are managing our own adjustments (see OnEntering/OnExiting). diff --git a/osu.iOS/AppDelegate.cs b/osu.iOS/AppDelegate.cs index e88b39f710..5d309f2fc1 100644 --- a/osu.iOS/AppDelegate.cs +++ b/osu.iOS/AppDelegate.cs @@ -1,14 +1,61 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System; using Foundation; using osu.Framework.iOS; +using UIKit; namespace osu.iOS { [Register("AppDelegate")] public class AppDelegate : GameApplicationDelegate { - protected override Framework.Game CreateGame() => new OsuGameIOS(); + private UIInterfaceOrientationMask? defaultOrientationsMask; + private UIInterfaceOrientationMask? orientations; + + /// + /// The current orientation the game is displayed in. + /// + public UIInterfaceOrientation CurrentOrientation => Host.Window.UIWindow.WindowScene!.InterfaceOrientation; + + /// + /// Controls the orientations allowed for the device to rotate to, overriding the default allowed orientations. + /// + public UIInterfaceOrientationMask? Orientations + { + get => orientations; + set + { + if (orientations == value) + return; + + orientations = value; + + if (OperatingSystem.IsIOSVersionAtLeast(16)) + Host.Window.ViewController.SetNeedsUpdateOfSupportedInterfaceOrientations(); + else + UIViewController.AttemptRotationToDeviceOrientation(); + } + } + + protected override Framework.Game CreateGame() => new OsuGameIOS(this); + + public override UIInterfaceOrientationMask GetSupportedInterfaceOrientations(UIApplication application, UIWindow forWindow) + { + if (orientations != null) + return orientations.Value; + + if (defaultOrientationsMask == null) + { + defaultOrientationsMask = 0; + var defaultOrientations = (NSArray)NSBundle.MainBundle.ObjectForInfoDictionary("UISupportedInterfaceOrientations"); + + foreach (var value in defaultOrientations.ToArray()) + defaultOrientationsMask |= Enum.Parse(value.ToString().Replace("UIInterfaceOrientation", string.Empty)); + } + + return defaultOrientationsMask.Value; + } } } diff --git a/osu.iOS/IOSOrientationHandler.cs b/osu.iOS/IOSOrientationHandler.cs new file mode 100644 index 0000000000..9b60497be8 --- /dev/null +++ b/osu.iOS/IOSOrientationHandler.cs @@ -0,0 +1,76 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Allocation; +using osu.Framework.Bindables; +using osu.Framework.Graphics; +using osu.Game; +using osu.Game.Screens.Play; +using UIKit; + +namespace osu.iOS +{ + public partial class IOSOrientationHandler : Component + { + private readonly AppDelegate appDelegate; + + [Resolved] + private OsuGame game { get; set; } = null!; + + [Resolved] + private ILocalUserPlayInfo localUserPlayInfo { get; set; } = null!; + + private IBindable requiresPortraitOrientation = null!; + private IBindable localUserPlaying = null!; + + public IOSOrientationHandler(AppDelegate appDelegate) + { + this.appDelegate = appDelegate; + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + requiresPortraitOrientation = game.RequiresPortraitOrientation.GetBoundCopy(); + requiresPortraitOrientation.BindValueChanged(_ => updateOrientations()); + + localUserPlaying = localUserPlayInfo.PlayingState.GetBoundCopy(); + localUserPlaying.BindValueChanged(_ => updateOrientations()); + + updateOrientations(); + } + + private void updateOrientations() + { + UIInterfaceOrientation currentOrientation = appDelegate.CurrentOrientation; + bool lockCurrentOrientation = localUserPlaying.Value == LocalUserPlayingState.Playing; + bool lockToPortrait = requiresPortraitOrientation.Value; + bool isPhone = UIDevice.CurrentDevice.UserInterfaceIdiom == UIUserInterfaceIdiom.Phone; + + if (lockCurrentOrientation) + { + if (lockToPortrait && !currentOrientation.IsPortrait()) + currentOrientation = UIInterfaceOrientation.Portrait; + else if (!lockToPortrait && currentOrientation.IsPortrait() && isPhone) + currentOrientation = UIInterfaceOrientation.LandscapeRight; + + appDelegate.Orientations = (UIInterfaceOrientationMask)(1 << (int)currentOrientation); + return; + } + + if (lockToPortrait) + { + UIInterfaceOrientationMask portraitOrientations = UIInterfaceOrientationMask.Portrait; + + if (!isPhone) + portraitOrientations |= UIInterfaceOrientationMask.PortraitUpsideDown; + + appDelegate.Orientations = portraitOrientations; + return; + } + + appDelegate.Orientations = null; + } + } +} diff --git a/osu.iOS/OsuGameIOS.cs b/osu.iOS/OsuGameIOS.cs index a9ca1778a0..6a3d0d0ba4 100644 --- a/osu.iOS/OsuGameIOS.cs +++ b/osu.iOS/OsuGameIOS.cs @@ -15,10 +15,22 @@ namespace osu.iOS { public partial class OsuGameIOS : OsuGame { + private readonly AppDelegate appDelegate; public override Version AssemblyVersion => new Version(NSBundle.MainBundle.InfoDictionary["CFBundleVersion"].ToString()); public override bool HideUnlicensedContent => true; + public OsuGameIOS(AppDelegate appDelegate) + { + this.appDelegate = appDelegate; + } + + protected override void LoadComplete() + { + base.LoadComplete(); + Add(new IOSOrientationHandler(appDelegate)); + } + protected override UpdateManager CreateUpdateManager() => new MobileUpdateNotifier(); protected override BatteryInfo CreateBatteryInfo() => new IOSBatteryInfo(); From aa67f87fe95af769c66e5329b30212d07b8e3ebd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 30 Dec 2024 09:42:24 +0100 Subject: [PATCH 0434/1275] 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 0435/1275] 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 0436/1275] 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 0437/1275] 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 0438/1275] 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 0439/1275] 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 0440/1275] 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 0441/1275] 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 0442/1275] 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 0443/1275] 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 0444/1275] 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 0445/1275] 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 0446/1275] 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 0447/1275] 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 0448/1275] 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 0449/1275] Fix code quality --- .../Visual/Editing/TestSceneEditorTestGameplay.cs | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/osu.Game.Tests/Visual/Editing/TestSceneEditorTestGameplay.cs b/osu.Game.Tests/Visual/Editing/TestSceneEditorTestGameplay.cs index 21c414cc21..60781d6f0a 100644 --- a/osu.Game.Tests/Visual/Editing/TestSceneEditorTestGameplay.cs +++ b/osu.Game.Tests/Visual/Editing/TestSceneEditorTestGameplay.cs @@ -7,12 +7,10 @@ using System; using System.Linq; using NUnit.Framework; using osu.Framework.Allocation; -using osu.Framework.Bindables; using osu.Framework.Extensions; using osu.Framework.Screens; using osu.Framework.Testing; using osu.Game.Beatmaps; -using osu.Game.Configuration; using osu.Game.Rulesets; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Osu; @@ -43,14 +41,6 @@ namespace osu.Game.Tests.Visual.Editing private BeatmapSetInfo importedBeatmapSet; - private Bindable editorDim; - - [BackgroundDependencyLoader] - private void load(OsuConfigManager config) - { - editorDim = config.GetBindable(OsuSetting.EditorDim); - } - public override void SetUpSteps() { AddStep("import test beatmap", () => importedBeatmapSet = BeatmapImportHelper.LoadOszIntoOsu(game).GetResultSafely()); From 9d08bc2b50d9e5b80f38f0ebad2b72c6f3855361 Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Sat, 28 Dec 2024 19:22:45 -0500 Subject: [PATCH 0450/1275] Improve osu!mania gameplay scaling on portrait orientation --- .../UI/DrawableManiaRuleset.cs | 2 + .../UI/ManiaPlayfieldAdjustmentContainer.cs | 51 ++++++++++++++++++- 2 files changed, 51 insertions(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Mania/UI/DrawableManiaRuleset.cs b/osu.Game.Rulesets.Mania/UI/DrawableManiaRuleset.cs index d173ae4143..136b172a59 100644 --- a/osu.Game.Rulesets.Mania/UI/DrawableManiaRuleset.cs +++ b/osu.Game.Rulesets.Mania/UI/DrawableManiaRuleset.cs @@ -51,6 +51,8 @@ namespace osu.Game.Rulesets.Mania.UI public IEnumerable BarLines; + public override bool RequiresPortraitOrientation => Beatmap.Stages.Count == 1; + protected override bool RelativeScaleBeatLengths => true; protected new ManiaRulesetConfigManager Config => (ManiaRulesetConfigManager)base.Config; diff --git a/osu.Game.Rulesets.Mania/UI/ManiaPlayfieldAdjustmentContainer.cs b/osu.Game.Rulesets.Mania/UI/ManiaPlayfieldAdjustmentContainer.cs index 1183b616f5..d7cb211d4a 100644 --- a/osu.Game.Rulesets.Mania/UI/ManiaPlayfieldAdjustmentContainer.cs +++ b/osu.Game.Rulesets.Mania/UI/ManiaPlayfieldAdjustmentContainer.cs @@ -1,17 +1,64 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System; +using osu.Framework.Allocation; using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; using osu.Game.Rulesets.UI; +using osuTK; namespace osu.Game.Rulesets.Mania.UI { public partial class ManiaPlayfieldAdjustmentContainer : PlayfieldAdjustmentContainer { + protected override Container Content { get; } + + private readonly DrawSizePreservingFillContainer scalingContainer; + public ManiaPlayfieldAdjustmentContainer() { - Anchor = Anchor.Centre; - Origin = Anchor.Centre; + InternalChild = scalingContainer = new DrawSizePreservingFillContainer + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + RelativeSizeAxes = Axes.Both, + Child = Content = new Container + { + RelativeSizeAxes = Axes.Both, + } + }; + } + + [Resolved] + private DrawableManiaRuleset drawableManiaRuleset { get; set; } = null!; + + protected override void Update() + { + base.Update(); + + float aspectRatio = DrawWidth / DrawHeight; + bool isPortrait = aspectRatio < 4 / 3f; + + if (isPortrait && drawableManiaRuleset.Beatmap.Stages.Count == 1) + { + // Scale playfield up by 25% to become playable on mobile devices, + // and leave a 10% horizontal gap if the playfield is scaled down due to being too wide. + const float base_scale = 1.25f; + const float base_width = 768f / base_scale; + const float side_gap = 0.9f; + + scalingContainer.Strategy = DrawSizePreservationStrategy.Maximum; + float stageWidth = drawableManiaRuleset.Playfield.Stages[0].DrawWidth; + scalingContainer.TargetDrawSize = new Vector2(1024, base_width * Math.Max(stageWidth / aspectRatio / (base_width * side_gap), 1f)); + } + else + { + scalingContainer.Strategy = DrawSizePreservationStrategy.Minimum; + scalingContainer.Scale = new Vector2(1f); + scalingContainer.Size = new Vector2(1f); + scalingContainer.TargetDrawSize = new Vector2(1024, 768); + } } } } From d7e4038f4ae75645a6f074e7c49c9265ac9f04e2 Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Sun, 29 Dec 2024 23:54:04 -0500 Subject: [PATCH 0451/1275] Keep game in portrait mode when restarting --- osu.Game/Screens/Play/PlayerLoader.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/osu.Game/Screens/Play/PlayerLoader.cs b/osu.Game/Screens/Play/PlayerLoader.cs index 837974a8f2..b258de0e9e 100644 --- a/osu.Game/Screens/Play/PlayerLoader.cs +++ b/osu.Game/Screens/Play/PlayerLoader.cs @@ -54,6 +54,9 @@ namespace osu.Game.Screens.Play public override bool? AllowGlobalTrackControl => false; + // this makes the game stay in portrait mode when restarting gameplay rather than switching back to landscape. + public override bool RequiresPortraitOrientation => CurrentPlayer?.RequiresPortraitOrientation == true; + public override float BackgroundParallaxAmount => quickRestart ? 0 : 1; // Here because IsHovered will not update unless we do so. From 0cd7f1b2d4f138443260042cb04ca6cbf2988184 Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Mon, 30 Dec 2024 15:04:21 -0500 Subject: [PATCH 0452/1275] Abstractify orientation handling and add Android support --- osu.Android/AndroidOrientationManager.cs | 39 ++++++++++ osu.Android/GameplayScreenRotationLocker.cs | 34 --------- osu.Android/OsuGameActivity.cs | 6 +- osu.Android/OsuGameAndroid.cs | 2 +- osu.Game/Mobile/GameOrientation.cs | 34 +++++++++ osu.Game/Mobile/OrientationManager.cs | 84 +++++++++++++++++++++ osu.iOS/IOSOrientationHandler.cs | 76 ------------------- osu.iOS/IOSOrientationManager.cs | 41 ++++++++++ osu.iOS/OsuGameIOS.cs | 2 +- 9 files changed, 204 insertions(+), 114 deletions(-) create mode 100644 osu.Android/AndroidOrientationManager.cs delete mode 100644 osu.Android/GameplayScreenRotationLocker.cs create mode 100644 osu.Game/Mobile/GameOrientation.cs create mode 100644 osu.Game/Mobile/OrientationManager.cs delete mode 100644 osu.iOS/IOSOrientationHandler.cs create mode 100644 osu.iOS/IOSOrientationManager.cs diff --git a/osu.Android/AndroidOrientationManager.cs b/osu.Android/AndroidOrientationManager.cs new file mode 100644 index 0000000000..76d2fc24cb --- /dev/null +++ b/osu.Android/AndroidOrientationManager.cs @@ -0,0 +1,39 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using Android.Content.PM; +using Android.Content.Res; +using osu.Framework.Allocation; +using osu.Game.Mobile; + +namespace osu.Android +{ + public partial class AndroidOrientationManager : OrientationManager + { + [Resolved] + private OsuGameActivity gameActivity { get; set; } = null!; + + protected override bool IsCurrentOrientationPortrait => gameActivity.Resources!.Configuration!.Orientation == Orientation.Portrait; + protected override bool IsTablet => gameActivity.IsTablet; + + protected override void SetAllowedOrientations(GameOrientation? orientation) + => gameActivity.RequestedOrientation = orientation == null ? gameActivity.DefaultOrientation : toScreenOrientation(orientation.Value); + + private static ScreenOrientation toScreenOrientation(GameOrientation orientation) + { + if (orientation == GameOrientation.Locked) + return ScreenOrientation.Locked; + + if (orientation == GameOrientation.Portrait) + return ScreenOrientation.Portrait; + + if (orientation == GameOrientation.Landscape) + return ScreenOrientation.Landscape; + + if (orientation == GameOrientation.FullPortrait) + return ScreenOrientation.SensorPortrait; + + return ScreenOrientation.SensorLandscape; + } + } +} diff --git a/osu.Android/GameplayScreenRotationLocker.cs b/osu.Android/GameplayScreenRotationLocker.cs deleted file mode 100644 index 42583b5dc2..0000000000 --- a/osu.Android/GameplayScreenRotationLocker.cs +++ /dev/null @@ -1,34 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -using Android.Content.PM; -using osu.Framework.Allocation; -using osu.Framework.Bindables; -using osu.Framework.Graphics; -using osu.Game.Screens.Play; - -namespace osu.Android -{ - public partial class GameplayScreenRotationLocker : Component - { - private IBindable localUserPlaying = null!; - - [Resolved] - private OsuGameActivity gameActivity { get; set; } = null!; - - [BackgroundDependencyLoader] - private void load(ILocalUserPlayInfo localUserPlayInfo) - { - localUserPlaying = localUserPlayInfo.PlayingState.GetBoundCopy(); - localUserPlaying.BindValueChanged(updateLock, true); - } - - private void updateLock(ValueChangedEvent userPlaying) - { - gameActivity.RunOnUiThread(() => - { - gameActivity.RequestedOrientation = userPlaying.NewValue == LocalUserPlayingState.Playing ? ScreenOrientation.Locked : gameActivity.DefaultOrientation; - }); - } - } -} diff --git a/osu.Android/OsuGameActivity.cs b/osu.Android/OsuGameActivity.cs index bbee491d90..b3717791da 100644 --- a/osu.Android/OsuGameActivity.cs +++ b/osu.Android/OsuGameActivity.cs @@ -50,6 +50,8 @@ namespace osu.Android /// Adjusted on startup to match expected UX for the current device type (phone/tablet). public ScreenOrientation DefaultOrientation = ScreenOrientation.Unspecified; + public bool IsTablet { get; private set; } + private OsuGameAndroid game = null!; protected override Framework.Game CreateGame() => game = new OsuGameAndroid(this); @@ -76,9 +78,9 @@ namespace osu.Android WindowManager.DefaultDisplay.GetSize(displaySize); #pragma warning restore CA1422 float smallestWidthDp = Math.Min(displaySize.X, displaySize.Y) / Resources.DisplayMetrics.Density; - bool isTablet = smallestWidthDp >= 600f; + IsTablet = smallestWidthDp >= 600f; - RequestedOrientation = DefaultOrientation = isTablet ? ScreenOrientation.FullUser : ScreenOrientation.SensorLandscape; + RequestedOrientation = DefaultOrientation = IsTablet ? ScreenOrientation.FullUser : ScreenOrientation.SensorLandscape; // Currently (SDK 6.0.200), BundleAssemblies is not runnable for net6-android. // The assembly files are not available as files either after native AOT. diff --git a/osu.Android/OsuGameAndroid.cs b/osu.Android/OsuGameAndroid.cs index ffab7dd86d..4143c8cae6 100644 --- a/osu.Android/OsuGameAndroid.cs +++ b/osu.Android/OsuGameAndroid.cs @@ -71,7 +71,7 @@ namespace osu.Android protected override void LoadComplete() { base.LoadComplete(); - LoadComponentAsync(new GameplayScreenRotationLocker(), Add); + LoadComponentAsync(new AndroidOrientationManager(), Add); } public override void SetHost(GameHost host) diff --git a/osu.Game/Mobile/GameOrientation.cs b/osu.Game/Mobile/GameOrientation.cs new file mode 100644 index 0000000000..0022c8fefb --- /dev/null +++ b/osu.Game/Mobile/GameOrientation.cs @@ -0,0 +1,34 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +namespace osu.Game.Mobile +{ + public enum GameOrientation + { + /// + /// Lock the game orientation. + /// + Locked, + + /// + /// Display the game in regular portrait orientation. + /// + Portrait, + + /// + /// Display the game in landscape-right orientation. + /// + Landscape, + + /// + /// Display the game in landscape-right/landscape-left orientations. + /// + FullLandscape, + + /// + /// Display the game in portrait/portrait-upside-down orientations. + /// This is exclusive to tablet mobile devices. + /// + FullPortrait, + } +} diff --git a/osu.Game/Mobile/OrientationManager.cs b/osu.Game/Mobile/OrientationManager.cs new file mode 100644 index 0000000000..b78bf8e760 --- /dev/null +++ b/osu.Game/Mobile/OrientationManager.cs @@ -0,0 +1,84 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Allocation; +using osu.Framework.Bindables; +using osu.Framework.Graphics; +using osu.Game.Screens.Play; + +namespace osu.Game.Mobile +{ + /// + /// A that manages the device orientations a game can display in. + /// + public abstract partial class OrientationManager : Component + { + /// + /// Whether the current orientation of the game is portrait. + /// + protected abstract bool IsCurrentOrientationPortrait { get; } + + /// + /// Whether the mobile device is considered a tablet. + /// + protected abstract bool IsTablet { get; } + + [Resolved] + private OsuGame game { get; set; } = null!; + + [Resolved] + private ILocalUserPlayInfo localUserPlayInfo { get; set; } = null!; + + private IBindable requiresPortraitOrientation = null!; + private IBindable localUserPlaying = null!; + + protected override void LoadComplete() + { + base.LoadComplete(); + + requiresPortraitOrientation = game.RequiresPortraitOrientation.GetBoundCopy(); + requiresPortraitOrientation.BindValueChanged(_ => updateOrientations()); + + localUserPlaying = localUserPlayInfo.PlayingState.GetBoundCopy(); + localUserPlaying.BindValueChanged(_ => updateOrientations()); + + updateOrientations(); + } + + private void updateOrientations() + { + bool lockCurrentOrientation = localUserPlaying.Value == LocalUserPlayingState.Playing; + bool lockToPortrait = requiresPortraitOrientation.Value; + + if (lockCurrentOrientation) + { + if (lockToPortrait && !IsCurrentOrientationPortrait) + SetAllowedOrientations(GameOrientation.Portrait); + else if (!lockToPortrait && IsCurrentOrientationPortrait && !IsTablet) + SetAllowedOrientations(GameOrientation.Landscape); + else + SetAllowedOrientations(GameOrientation.Locked); + + return; + } + + if (lockToPortrait) + { + if (IsTablet) + SetAllowedOrientations(GameOrientation.FullPortrait); + else + SetAllowedOrientations(GameOrientation.Portrait); + + return; + } + + SetAllowedOrientations(null); + } + + /// + /// Sets the allowed orientations the device can rotate to. + /// + /// The allowed orientations, or null to return back to default. + protected abstract void SetAllowedOrientations(GameOrientation? orientation); + } +} diff --git a/osu.iOS/IOSOrientationHandler.cs b/osu.iOS/IOSOrientationHandler.cs deleted file mode 100644 index 9b60497be8..0000000000 --- a/osu.iOS/IOSOrientationHandler.cs +++ /dev/null @@ -1,76 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -using osu.Framework.Allocation; -using osu.Framework.Bindables; -using osu.Framework.Graphics; -using osu.Game; -using osu.Game.Screens.Play; -using UIKit; - -namespace osu.iOS -{ - public partial class IOSOrientationHandler : Component - { - private readonly AppDelegate appDelegate; - - [Resolved] - private OsuGame game { get; set; } = null!; - - [Resolved] - private ILocalUserPlayInfo localUserPlayInfo { get; set; } = null!; - - private IBindable requiresPortraitOrientation = null!; - private IBindable localUserPlaying = null!; - - public IOSOrientationHandler(AppDelegate appDelegate) - { - this.appDelegate = appDelegate; - } - - protected override void LoadComplete() - { - base.LoadComplete(); - - requiresPortraitOrientation = game.RequiresPortraitOrientation.GetBoundCopy(); - requiresPortraitOrientation.BindValueChanged(_ => updateOrientations()); - - localUserPlaying = localUserPlayInfo.PlayingState.GetBoundCopy(); - localUserPlaying.BindValueChanged(_ => updateOrientations()); - - updateOrientations(); - } - - private void updateOrientations() - { - UIInterfaceOrientation currentOrientation = appDelegate.CurrentOrientation; - bool lockCurrentOrientation = localUserPlaying.Value == LocalUserPlayingState.Playing; - bool lockToPortrait = requiresPortraitOrientation.Value; - bool isPhone = UIDevice.CurrentDevice.UserInterfaceIdiom == UIUserInterfaceIdiom.Phone; - - if (lockCurrentOrientation) - { - if (lockToPortrait && !currentOrientation.IsPortrait()) - currentOrientation = UIInterfaceOrientation.Portrait; - else if (!lockToPortrait && currentOrientation.IsPortrait() && isPhone) - currentOrientation = UIInterfaceOrientation.LandscapeRight; - - appDelegate.Orientations = (UIInterfaceOrientationMask)(1 << (int)currentOrientation); - return; - } - - if (lockToPortrait) - { - UIInterfaceOrientationMask portraitOrientations = UIInterfaceOrientationMask.Portrait; - - if (!isPhone) - portraitOrientations |= UIInterfaceOrientationMask.PortraitUpsideDown; - - appDelegate.Orientations = portraitOrientations; - return; - } - - appDelegate.Orientations = null; - } - } -} diff --git a/osu.iOS/IOSOrientationManager.cs b/osu.iOS/IOSOrientationManager.cs new file mode 100644 index 0000000000..6d5bb990c2 --- /dev/null +++ b/osu.iOS/IOSOrientationManager.cs @@ -0,0 +1,41 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Game.Mobile; +using UIKit; + +namespace osu.iOS +{ + public partial class IOSOrientationManager : OrientationManager + { + private readonly AppDelegate appDelegate; + + protected override bool IsCurrentOrientationPortrait => appDelegate.CurrentOrientation.IsPortrait(); + protected override bool IsTablet => UIDevice.CurrentDevice.UserInterfaceIdiom == UIUserInterfaceIdiom.Pad; + + public IOSOrientationManager(AppDelegate appDelegate) + { + this.appDelegate = appDelegate; + } + + protected override void SetAllowedOrientations(GameOrientation? orientation) + => appDelegate.Orientations = orientation == null ? null : toUIInterfaceOrientationMask(orientation.Value); + + private UIInterfaceOrientationMask toUIInterfaceOrientationMask(GameOrientation orientation) + { + if (orientation == GameOrientation.Locked) + return (UIInterfaceOrientationMask)(1 << (int)appDelegate.CurrentOrientation); + + if (orientation == GameOrientation.Portrait) + return UIInterfaceOrientationMask.Portrait; + + if (orientation == GameOrientation.Landscape) + return UIInterfaceOrientationMask.LandscapeRight; + + if (orientation == GameOrientation.FullPortrait) + return UIInterfaceOrientationMask.Portrait | UIInterfaceOrientationMask.PortraitUpsideDown; + + return UIInterfaceOrientationMask.Landscape; + } + } +} diff --git a/osu.iOS/OsuGameIOS.cs b/osu.iOS/OsuGameIOS.cs index 6a3d0d0ba4..ed47a1e8b8 100644 --- a/osu.iOS/OsuGameIOS.cs +++ b/osu.iOS/OsuGameIOS.cs @@ -28,7 +28,7 @@ namespace osu.iOS protected override void LoadComplete() { base.LoadComplete(); - Add(new IOSOrientationHandler(appDelegate)); + LoadComponentAsync(new IOSOrientationManager(appDelegate), Add); } protected override UpdateManager CreateUpdateManager() => new MobileUpdateNotifier(); From 1e08b3dbdac1ed07fd56c0d55d83ce200053c336 Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Sun, 29 Dec 2024 23:33:32 -0500 Subject: [PATCH 0453/1275] Make mania judgements relative to the hit target position This improves display in portrait screen, where the stage is scaled up. --- .../Mods/ManiaModWithPlayfieldCover.cs | 2 +- .../Skinning/Argon/ArgonJudgementPiece.cs | 2 +- .../Skinning/Legacy/LegacyManiaJudgementPiece.cs | 12 +++++------- .../UI/Components/ColumnHitObjectArea.cs | 2 +- ...bjectArea.cs => HitPositionPaddedContainer.cs} | 15 ++++----------- .../UI/DrawableManiaJudgement.cs | 3 +++ osu.Game.Rulesets.Mania/UI/Stage.cs | 12 ++++++------ 7 files changed, 21 insertions(+), 27 deletions(-) rename osu.Game.Rulesets.Mania/UI/Components/{HitObjectArea.cs => HitPositionPaddedContainer.cs} (74%) diff --git a/osu.Game.Rulesets.Mania/Mods/ManiaModWithPlayfieldCover.cs b/osu.Game.Rulesets.Mania/Mods/ManiaModWithPlayfieldCover.cs index 864ef6c3d6..1bc16112c5 100644 --- a/osu.Game.Rulesets.Mania/Mods/ManiaModWithPlayfieldCover.cs +++ b/osu.Game.Rulesets.Mania/Mods/ManiaModWithPlayfieldCover.cs @@ -34,7 +34,7 @@ namespace osu.Game.Rulesets.Mania.Mods foreach (Column column in maniaPlayfield.Stages.SelectMany(stage => stage.Columns)) { - HitObjectContainer hoc = column.HitObjectArea.HitObjectContainer; + HitObjectContainer hoc = column.HitObjectContainer; Container hocParent = (Container)hoc.Parent!; hocParent.Remove(hoc, false); diff --git a/osu.Game.Rulesets.Mania/Skinning/Argon/ArgonJudgementPiece.cs b/osu.Game.Rulesets.Mania/Skinning/Argon/ArgonJudgementPiece.cs index 0052fd8b78..a1c81d3a6a 100644 --- a/osu.Game.Rulesets.Mania/Skinning/Argon/ArgonJudgementPiece.cs +++ b/osu.Game.Rulesets.Mania/Skinning/Argon/ArgonJudgementPiece.cs @@ -19,7 +19,7 @@ namespace osu.Game.Rulesets.Mania.Skinning.Argon { public partial class ArgonJudgementPiece : TextJudgementPiece, IAnimatableJudgement { - private const float judgement_y_position = 160; + private const float judgement_y_position = -180f; private RingExplosion? ringExplosion; diff --git a/osu.Game.Rulesets.Mania/Skinning/Legacy/LegacyManiaJudgementPiece.cs b/osu.Game.Rulesets.Mania/Skinning/Legacy/LegacyManiaJudgementPiece.cs index d21a8cd140..4b0cc482d9 100644 --- a/osu.Game.Rulesets.Mania/Skinning/Legacy/LegacyManiaJudgementPiece.cs +++ b/osu.Game.Rulesets.Mania/Skinning/Legacy/LegacyManiaJudgementPiece.cs @@ -7,7 +7,6 @@ using osu.Framework.Graphics.Animations; using osu.Framework.Graphics.Containers; using osu.Framework.Utils; using osu.Game.Rulesets.Judgements; -using osu.Game.Rulesets.Mania.UI; using osu.Game.Rulesets.Scoring; using osu.Game.Skinning; @@ -23,7 +22,7 @@ namespace osu.Game.Rulesets.Mania.Skinning.Legacy this.result = result; this.animation = animation; - Anchor = Anchor.Centre; + Anchor = Anchor.BottomCentre; Origin = Anchor.Centre; AutoSizeAxes = Axes.Both; @@ -32,12 +31,11 @@ namespace osu.Game.Rulesets.Mania.Skinning.Legacy [BackgroundDependencyLoader] private void load(ISkinSource skin) { - float? scorePosition = skin.GetManiaSkinConfig(LegacyManiaSkinConfigurationLookups.ScorePosition)?.Value; + float hitPosition = skin.GetManiaSkinConfig(LegacyManiaSkinConfigurationLookups.HitPosition)?.Value ?? 0; + float scorePosition = skin.GetManiaSkinConfig(LegacyManiaSkinConfigurationLookups.ScorePosition)?.Value ?? 0; - if (scorePosition != null) - scorePosition -= Stage.HIT_TARGET_POSITION + 150; - - Y = scorePosition ?? 0; + float absoluteHitPosition = 480f * LegacyManiaSkinConfiguration.POSITION_SCALE_FACTOR - hitPosition; + Y = scorePosition - absoluteHitPosition; InternalChild = animation.With(d => { diff --git a/osu.Game.Rulesets.Mania/UI/Components/ColumnHitObjectArea.cs b/osu.Game.Rulesets.Mania/UI/Components/ColumnHitObjectArea.cs index 91e0f2c19b..2d719ef764 100644 --- a/osu.Game.Rulesets.Mania/UI/Components/ColumnHitObjectArea.cs +++ b/osu.Game.Rulesets.Mania/UI/Components/ColumnHitObjectArea.cs @@ -9,7 +9,7 @@ using osu.Game.Skinning; namespace osu.Game.Rulesets.Mania.UI.Components { - public partial class ColumnHitObjectArea : HitObjectArea + public partial class ColumnHitObjectArea : HitPositionPaddedContainer { public readonly Container Explosions; diff --git a/osu.Game.Rulesets.Mania/UI/Components/HitObjectArea.cs b/osu.Game.Rulesets.Mania/UI/Components/HitPositionPaddedContainer.cs similarity index 74% rename from osu.Game.Rulesets.Mania/UI/Components/HitObjectArea.cs rename to osu.Game.Rulesets.Mania/UI/Components/HitPositionPaddedContainer.cs index 2ad6e4f076..f591102f6c 100644 --- a/osu.Game.Rulesets.Mania/UI/Components/HitObjectArea.cs +++ b/osu.Game.Rulesets.Mania/UI/Components/HitPositionPaddedContainer.cs @@ -1,29 +1,22 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; -using osu.Framework.Graphics.Containers; using osu.Game.Rulesets.Mania.Skinning; -using osu.Game.Rulesets.UI; using osu.Game.Rulesets.UI.Scrolling; using osu.Game.Skinning; namespace osu.Game.Rulesets.Mania.UI.Components { - public partial class HitObjectArea : SkinReloadableDrawable + public partial class HitPositionPaddedContainer : SkinReloadableDrawable { protected readonly IBindable Direction = new Bindable(); - public readonly HitObjectContainer HitObjectContainer; - public HitObjectArea(HitObjectContainer hitObjectContainer) + public HitPositionPaddedContainer(Drawable child) { - InternalChild = new Container - { - RelativeSizeAxes = Axes.Both, - Child = HitObjectContainer = hitObjectContainer - }; + InternalChild = child; } [BackgroundDependencyLoader] diff --git a/osu.Game.Rulesets.Mania/UI/DrawableManiaJudgement.cs b/osu.Game.Rulesets.Mania/UI/DrawableManiaJudgement.cs index 9f25a44e21..5b87c74bbe 100644 --- a/osu.Game.Rulesets.Mania/UI/DrawableManiaJudgement.cs +++ b/osu.Game.Rulesets.Mania/UI/DrawableManiaJudgement.cs @@ -15,9 +15,12 @@ namespace osu.Game.Rulesets.Mania.UI private partial class DefaultManiaJudgementPiece : DefaultJudgementPiece { + private const float judgement_y_position = -180f; + public DefaultManiaJudgementPiece(HitResult result) : base(result) { + Y = judgement_y_position; } protected override void LoadComplete() diff --git a/osu.Game.Rulesets.Mania/UI/Stage.cs b/osu.Game.Rulesets.Mania/UI/Stage.cs index 9fb77a4995..2d73e7bcbe 100644 --- a/osu.Game.Rulesets.Mania/UI/Stage.cs +++ b/osu.Game.Rulesets.Mania/UI/Stage.cs @@ -103,7 +103,7 @@ namespace osu.Game.Rulesets.Mania.UI Width = 1366, // Bar lines should only be masked on the vertical axis BypassAutoSizeAxes = Axes.Both, Masking = true, - Child = barLineContainer = new HitObjectArea(HitObjectContainer) + Child = barLineContainer = new HitPositionPaddedContainer(HitObjectContainer) { Name = "Bar lines", Anchor = Anchor.TopCentre, @@ -119,12 +119,12 @@ namespace osu.Game.Rulesets.Mania.UI { RelativeSizeAxes = Axes.Both }, - judgements = new JudgementContainer + new HitPositionPaddedContainer(judgements = new JudgementContainer + { + RelativeSizeAxes = Axes.Both, + }) { - Anchor = Anchor.TopCentre, - Origin = Anchor.Centre, RelativeSizeAxes = Axes.Both, - Y = HIT_TARGET_POSITION + 150 }, topLevelContainer = new Container { RelativeSizeAxes = Axes.Both } } @@ -218,7 +218,7 @@ namespace osu.Game.Rulesets.Mania.UI { j.Apply(result, judgedObject); - j.Anchor = Anchor.Centre; + j.Anchor = Anchor.BottomCentre; j.Origin = Anchor.Centre; })!); } From bea61d24835e31af9821bce4e96e1cfd33c9f988 Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Tue, 31 Dec 2024 12:28:04 -0500 Subject: [PATCH 0454/1275] Replace `ManiaTouchInputArea` with touchable columns --- .../TestSceneManiaTouchInput.cs | 68 ++++++ .../TestSceneManiaTouchInputArea.cs | 49 ----- osu.Game.Rulesets.Mania/UI/Column.cs | 24 +++ .../UI/DrawableManiaRuleset.cs | 2 - .../UI/ManiaTouchInputArea.cs | 199 ------------------ 5 files changed, 92 insertions(+), 250 deletions(-) create mode 100644 osu.Game.Rulesets.Mania.Tests/TestSceneManiaTouchInput.cs delete mode 100644 osu.Game.Rulesets.Mania.Tests/TestSceneManiaTouchInputArea.cs delete mode 100644 osu.Game.Rulesets.Mania/UI/ManiaTouchInputArea.cs diff --git a/osu.Game.Rulesets.Mania.Tests/TestSceneManiaTouchInput.cs b/osu.Game.Rulesets.Mania.Tests/TestSceneManiaTouchInput.cs new file mode 100644 index 0000000000..dc95cd9ca0 --- /dev/null +++ b/osu.Game.Rulesets.Mania.Tests/TestSceneManiaTouchInput.cs @@ -0,0 +1,68 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Linq; +using NUnit.Framework; +using osu.Framework.Input; +using osu.Framework.Testing; +using osu.Game.Rulesets.Mania.UI; +using osu.Game.Tests.Visual; + +namespace osu.Game.Rulesets.Mania.Tests +{ + public partial class TestSceneManiaTouchInput : PlayerTestScene + { + protected override Ruleset CreatePlayerRuleset() => new ManiaRuleset(); + + [Test] + public void TestTouchInput() + { + for (int i = 0; i < 4; i++) + { + int index = i; + + AddStep($"touch column {index}", () => InputManager.BeginTouch(new Touch(TouchSource.Touch1, getColumn(index).ScreenSpaceDrawQuad.Centre))); + + AddAssert("action sent", + () => this.ChildrenOfType().SelectMany(m => m.KeyBindingContainer.PressedActions), + () => Does.Contain(getColumn(index).Action.Value)); + + AddStep($"release column {index}", () => InputManager.EndTouch(new Touch(TouchSource.Touch1, getColumn(index).ScreenSpaceDrawQuad.Centre))); + + AddAssert("action released", + () => this.ChildrenOfType().SelectMany(m => m.KeyBindingContainer.PressedActions), + () => Does.Not.Contain(getColumn(index).Action.Value)); + } + } + + [Test] + public void TestOneColumnMultipleTouches() + { + AddStep("touch column 0", () => InputManager.BeginTouch(new Touch(TouchSource.Touch1, getColumn(0).ScreenSpaceDrawQuad.Centre))); + + AddAssert("action sent", + () => this.ChildrenOfType().SelectMany(m => m.KeyBindingContainer.PressedActions), + () => Does.Contain(getColumn(0).Action.Value)); + + AddStep("touch another finger", () => InputManager.BeginTouch(new Touch(TouchSource.Touch2, getColumn(0).ScreenSpaceDrawQuad.Centre))); + + AddAssert("action still pressed", + () => this.ChildrenOfType().SelectMany(m => m.KeyBindingContainer.PressedActions), + () => Does.Contain(getColumn(0).Action.Value)); + + AddStep("release first finger", () => InputManager.EndTouch(new Touch(TouchSource.Touch1, getColumn(0).ScreenSpaceDrawQuad.Centre))); + + AddAssert("action still pressed", + () => this.ChildrenOfType().SelectMany(m => m.KeyBindingContainer.PressedActions), + () => Does.Contain(getColumn(0).Action.Value)); + + AddStep("release second finger", () => InputManager.EndTouch(new Touch(TouchSource.Touch2, getColumn(0).ScreenSpaceDrawQuad.Centre))); + + AddAssert("action released", + () => this.ChildrenOfType().SelectMany(m => m.KeyBindingContainer.PressedActions), + () => Does.Not.Contain(getColumn(0).Action.Value)); + } + + private Column getColumn(int index) => this.ChildrenOfType().ElementAt(index); + } +} diff --git a/osu.Game.Rulesets.Mania.Tests/TestSceneManiaTouchInputArea.cs b/osu.Game.Rulesets.Mania.Tests/TestSceneManiaTouchInputArea.cs deleted file mode 100644 index 30c0113bff..0000000000 --- a/osu.Game.Rulesets.Mania.Tests/TestSceneManiaTouchInputArea.cs +++ /dev/null @@ -1,49 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -using System.Linq; -using NUnit.Framework; -using osu.Framework.Graphics.Containers; -using osu.Framework.Input; -using osu.Framework.Testing; -using osu.Game.Rulesets.Mania.UI; -using osu.Game.Tests.Visual; - -namespace osu.Game.Rulesets.Mania.Tests -{ - public partial class TestSceneManiaTouchInputArea : PlayerTestScene - { - protected override Ruleset CreatePlayerRuleset() => new ManiaRuleset(); - - [Test] - public void TestTouchAreaNotInitiallyVisible() - { - AddAssert("touch area not visible", () => getTouchOverlay()?.State.Value == Visibility.Hidden); - } - - [Test] - public void TestPressReceptors() - { - AddAssert("touch area not visible", () => getTouchOverlay()?.State.Value == Visibility.Hidden); - - for (int i = 0; i < 4; i++) - { - int index = i; - - AddStep($"touch receptor {index}", () => InputManager.BeginTouch(new Touch(TouchSource.Touch1, getReceptor(index).ScreenSpaceDrawQuad.Centre))); - - AddAssert("action sent", - () => this.ChildrenOfType().SelectMany(m => m.KeyBindingContainer.PressedActions), - () => Does.Contain(getReceptor(index).Action.Value)); - - AddStep($"release receptor {index}", () => InputManager.EndTouch(new Touch(TouchSource.Touch1, getReceptor(index).ScreenSpaceDrawQuad.Centre))); - - AddAssert("touch area visible", () => getTouchOverlay()?.State.Value == Visibility.Visible); - } - } - - private ManiaTouchInputArea? getTouchOverlay() => this.ChildrenOfType().SingleOrDefault(); - - private ManiaTouchInputArea.ColumnInputReceptor getReceptor(int index) => this.ChildrenOfType().ElementAt(index); - } -} diff --git a/osu.Game.Rulesets.Mania/UI/Column.cs b/osu.Game.Rulesets.Mania/UI/Column.cs index c05a8f2a29..99d952ef1f 100644 --- a/osu.Game.Rulesets.Mania/UI/Column.cs +++ b/osu.Game.Rulesets.Mania/UI/Column.cs @@ -180,5 +180,29 @@ namespace osu.Game.Rulesets.Mania.UI public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) // This probably shouldn't exist as is, but the columns in the stage are separated by a 1px border => DrawRectangle.Inflate(new Vector2(Stage.COLUMN_SPACING / 2, 0)).Contains(ToLocalSpace(screenSpacePos)); + + #region Touch Input + + [Resolved(canBeNull: true)] + private ManiaInputManager maniaInputManager { get; set; } + + private int touchActivationCount; + + protected override bool OnTouchDown(TouchDownEvent e) + { + maniaInputManager.KeyBindingContainer.TriggerPressed(Action.Value); + touchActivationCount++; + return true; + } + + protected override void OnTouchUp(TouchUpEvent e) + { + touchActivationCount--; + + if (touchActivationCount == 0) + maniaInputManager.KeyBindingContainer.TriggerReleased(Action.Value); + } + + #endregion } } diff --git a/osu.Game.Rulesets.Mania/UI/DrawableManiaRuleset.cs b/osu.Game.Rulesets.Mania/UI/DrawableManiaRuleset.cs index 136b172a59..65841af5de 100644 --- a/osu.Game.Rulesets.Mania/UI/DrawableManiaRuleset.cs +++ b/osu.Game.Rulesets.Mania/UI/DrawableManiaRuleset.cs @@ -112,8 +112,6 @@ namespace osu.Game.Rulesets.Mania.UI configScrollSpeed.BindValueChanged(speed => TargetTimeRange = ComputeScrollTime(speed.NewValue)); TimeRange.Value = TargetTimeRange = currentTimeRange = ComputeScrollTime(configScrollSpeed.Value); - - KeyBindingInputManager.Add(new ManiaTouchInputArea()); } protected override void AdjustScrollSpeed(int amount) => configScrollSpeed.Value += amount; diff --git a/osu.Game.Rulesets.Mania/UI/ManiaTouchInputArea.cs b/osu.Game.Rulesets.Mania/UI/ManiaTouchInputArea.cs deleted file mode 100644 index 8c4a71cf24..0000000000 --- a/osu.Game.Rulesets.Mania/UI/ManiaTouchInputArea.cs +++ /dev/null @@ -1,199 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -using System.Collections.Generic; -using osu.Framework.Allocation; -using osu.Framework.Bindables; -using osu.Framework.Graphics; -using osu.Framework.Graphics.Containers; -using osu.Framework.Graphics.Shapes; -using osu.Framework.Input.Events; -using osu.Game.Configuration; -using osuTK; - -namespace osu.Game.Rulesets.Mania.UI -{ - /// - /// An overlay that captures and displays osu!mania mouse and touch input. - /// - public partial class ManiaTouchInputArea : VisibilityContainer - { - // visibility state affects our child. we always want to handle input. - public override bool PropagatePositionalInputSubTree => true; - public override bool PropagateNonPositionalInputSubTree => true; - - [SettingSource("Spacing", "The spacing between receptors.")] - public BindableFloat Spacing { get; } = new BindableFloat(10) - { - Precision = 1, - MinValue = 0, - MaxValue = 100, - }; - - [SettingSource("Opacity", "The receptor opacity.")] - public BindableFloat Opacity { get; } = new BindableFloat(1) - { - Precision = 0.1f, - MinValue = 0, - MaxValue = 1 - }; - - [Resolved] - private DrawableManiaRuleset drawableRuleset { get; set; } = null!; - - private GridContainer gridContainer = null!; - - public ManiaTouchInputArea() - { - Anchor = Anchor.BottomCentre; - Origin = Anchor.BottomCentre; - - RelativeSizeAxes = Axes.Both; - Height = 0.5f; - } - - [BackgroundDependencyLoader] - private void load() - { - List receptorGridContent = new List(); - List receptorGridDimensions = new List(); - - bool first = true; - - foreach (var stage in drawableRuleset.Playfield.Stages) - { - foreach (var column in stage.Columns) - { - if (!first) - { - receptorGridContent.Add(new Gutter { Spacing = { BindTarget = Spacing } }); - receptorGridDimensions.Add(new Dimension(GridSizeMode.AutoSize)); - } - - receptorGridContent.Add(new ColumnInputReceptor { Action = { BindTarget = column.Action } }); - receptorGridDimensions.Add(new Dimension()); - - first = false; - } - } - - InternalChild = gridContainer = new GridContainer - { - RelativeSizeAxes = Axes.Both, - AlwaysPresent = true, - Content = new[] { receptorGridContent.ToArray() }, - ColumnDimensions = receptorGridDimensions.ToArray() - }; - } - - protected override void LoadComplete() - { - base.LoadComplete(); - Opacity.BindValueChanged(o => Alpha = o.NewValue, true); - } - - protected override bool OnKeyDown(KeyDownEvent e) - { - // Hide whenever the keyboard is used. - Hide(); - return false; - } - - protected override bool OnTouchDown(TouchDownEvent e) - { - Show(); - return true; - } - - protected override void PopIn() - { - gridContainer.FadeIn(500, Easing.OutQuint); - } - - protected override void PopOut() - { - gridContainer.FadeOut(300); - } - - public partial class ColumnInputReceptor : CompositeDrawable - { - public readonly IBindable Action = new Bindable(); - - private readonly Box highlightOverlay; - - [Resolved] - private ManiaInputManager? inputManager { get; set; } - - private bool isPressed; - - public ColumnInputReceptor() - { - RelativeSizeAxes = Axes.Both; - - InternalChildren = new Drawable[] - { - new Container - { - RelativeSizeAxes = Axes.Both, - Masking = true, - CornerRadius = 10, - Children = new Drawable[] - { - new Box - { - RelativeSizeAxes = Axes.Both, - Alpha = 0.15f, - }, - highlightOverlay = new Box - { - RelativeSizeAxes = Axes.Both, - Alpha = 0, - Blending = BlendingParameters.Additive, - } - } - } - }; - } - - protected override bool OnTouchDown(TouchDownEvent e) - { - updateButton(true); - return false; // handled by parent container to show overlay. - } - - protected override void OnTouchUp(TouchUpEvent e) - { - updateButton(false); - } - - private void updateButton(bool press) - { - if (press == isPressed) - return; - - isPressed = press; - - if (press) - { - inputManager?.KeyBindingContainer.TriggerPressed(Action.Value); - highlightOverlay.FadeTo(0.1f, 80, Easing.OutQuint); - } - else - { - inputManager?.KeyBindingContainer.TriggerReleased(Action.Value); - highlightOverlay.FadeTo(0, 400, Easing.OutQuint); - } - } - } - - private partial class Gutter : Drawable - { - public readonly IBindable Spacing = new Bindable(); - - public Gutter() - { - Spacing.BindValueChanged(s => Size = new Vector2(s.NewValue)); - } - } - } -} From 64e557d00f98728e5a67d84c3158a8a11478c168 Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Tue, 31 Dec 2024 20:01:21 -0500 Subject: [PATCH 0455/1275] Simplify portrait check --- osu.Game.Rulesets.Mania/UI/ManiaPlayfieldAdjustmentContainer.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Mania/UI/ManiaPlayfieldAdjustmentContainer.cs b/osu.Game.Rulesets.Mania/UI/ManiaPlayfieldAdjustmentContainer.cs index d7cb211d4a..f7c4850a94 100644 --- a/osu.Game.Rulesets.Mania/UI/ManiaPlayfieldAdjustmentContainer.cs +++ b/osu.Game.Rulesets.Mania/UI/ManiaPlayfieldAdjustmentContainer.cs @@ -38,7 +38,7 @@ namespace osu.Game.Rulesets.Mania.UI base.Update(); float aspectRatio = DrawWidth / DrawHeight; - bool isPortrait = aspectRatio < 4 / 3f; + bool isPortrait = aspectRatio < 1f; if (isPortrait && drawableManiaRuleset.Beatmap.Stages.Count == 1) { From 3ac2d90f19a1da783a45f721fdf4d9046dfe3886 Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Tue, 31 Dec 2024 20:44:50 -0500 Subject: [PATCH 0456/1275] Add explanatory note --- osu.iOS/Info.plist | 2 ++ 1 file changed, 2 insertions(+) diff --git a/osu.iOS/Info.plist b/osu.iOS/Info.plist index 02f8462fbc..70747fc9c8 100644 --- a/osu.iOS/Info.plist +++ b/osu.iOS/Info.plist @@ -157,6 +157,8 @@ public.app-category.music-games LSSupportsOpeningDocumentsInPlace + GCSupportsGameMode From e5713e52392066a1430ebce460d07d8af01ad29f Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Tue, 31 Dec 2024 21:31:52 -0500 Subject: [PATCH 0457/1275] Fix triangles judgement mispositioned on a miss Similar to mania's `ArgonJudgementPiece`. --- .../UI/DrawableManiaJudgement.cs | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Mania/UI/DrawableManiaJudgement.cs b/osu.Game.Rulesets.Mania/UI/DrawableManiaJudgement.cs index 5b87c74bbe..a1dabd66bc 100644 --- a/osu.Game.Rulesets.Mania/UI/DrawableManiaJudgement.cs +++ b/osu.Game.Rulesets.Mania/UI/DrawableManiaJudgement.cs @@ -6,6 +6,7 @@ using osu.Framework.Graphics; using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Scoring; +using osuTK; namespace osu.Game.Rulesets.Mania.UI { @@ -35,8 +36,20 @@ namespace osu.Game.Rulesets.Mania.UI switch (Result) { case HitResult.None: + this.FadeOutFromOne(800); + break; + case HitResult.Miss: - base.PlayAnimation(); + this.ScaleTo(1.6f); + this.ScaleTo(1, 100, Easing.In); + + this.MoveToY(judgement_y_position); + this.MoveToOffset(new Vector2(0, 100), 800, Easing.InQuint); + + this.RotateTo(0); + this.RotateTo(40, 800, Easing.InQuint); + + this.FadeOutFromOne(800); break; default: From c221a0c9f93c20949f26459405cfcc5047a39e0b Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Wed, 1 Jan 2025 01:43:43 -0500 Subject: [PATCH 0458/1275] Improve UI scale on iOS devices --- osu.Game/Graphics/Containers/ScalingContainer.cs | 6 ++++++ osu.Game/OsuGame.cs | 5 +++++ osu.iOS/OsuGameIOS.cs | 3 +++ 3 files changed, 14 insertions(+) diff --git a/osu.Game/Graphics/Containers/ScalingContainer.cs b/osu.Game/Graphics/Containers/ScalingContainer.cs index c47aba2f0c..ac76c0546b 100644 --- a/osu.Game/Graphics/Containers/ScalingContainer.cs +++ b/osu.Game/Graphics/Containers/ScalingContainer.cs @@ -3,6 +3,7 @@ #nullable disable +using JetBrains.Annotations; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; @@ -99,6 +100,10 @@ namespace osu.Game.Graphics.Containers this.applyUIScale = applyUIScale; } + [Resolved(canBeNull: true)] + [CanBeNull] + private OsuGame game { get; set; } + [BackgroundDependencyLoader] private void load(OsuConfigManager osuConfig) { @@ -111,6 +116,7 @@ namespace osu.Game.Graphics.Containers protected override void Update() { + TargetDrawSize = new Vector2(1024, 1024 / (game?.BaseAspectRatio ?? 1f)); Scale = new Vector2(CurrentScale); Size = new Vector2(1 / CurrentScale); diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs index c20536a1ec..5227400694 100644 --- a/osu.Game/OsuGame.cs +++ b/osu.Game/OsuGame.cs @@ -831,6 +831,11 @@ namespace osu.Game protected virtual UpdateManager CreateUpdateManager() => new UpdateManager(); + /// + /// The base aspect ratio to use in all s. + /// + protected internal virtual float BaseAspectRatio => 4f / 3f; + protected override Container CreateScalingContainer() => new ScalingContainer(ScalingMode.Everything); #region Beatmap progression diff --git a/osu.iOS/OsuGameIOS.cs b/osu.iOS/OsuGameIOS.cs index a9ca1778a0..b3d9be04a1 100644 --- a/osu.iOS/OsuGameIOS.cs +++ b/osu.iOS/OsuGameIOS.cs @@ -10,6 +10,7 @@ using osu.Framework.Platform; using osu.Game; using osu.Game.Updater; using osu.Game.Utils; +using UIKit; namespace osu.iOS { @@ -19,6 +20,8 @@ namespace osu.iOS public override bool HideUnlicensedContent => true; + protected override float BaseAspectRatio => (float)(UIScreen.MainScreen.Bounds.Width / UIScreen.MainScreen.Bounds.Height); + protected override UpdateManager CreateUpdateManager() => new MobileUpdateNotifier(); protected override BatteryInfo CreateBatteryInfo() => new IOSBatteryInfo(); From 1211f6cf4cfc7a214e026e584ba6f704ea3471e9 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 2 Jan 2025 13:06:34 +0900 Subject: [PATCH 0459/1275] 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 0460/1275] 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 0461/1275] 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 0462/1275] 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 0463/1275] 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 0464/1275] 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 0465/1275] 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 0466/1275] 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 0467/1275] 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 0468/1275] 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 0469/1275] 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 0470/1275] 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 0471/1275] 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 0472/1275] 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 0473/1275] 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 0474/1275] 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 0475/1275] 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 0476/1275] Ensure intro files exist in storage Guards against user interdiction. See [https://discord.com/channels/188630481301012481/1097318920991559880/1324765503012601927](recent) but not only case of this occurring. --- osu.Game/Screens/Menu/IntroScreen.cs | 24 +++++++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/osu.Game/Screens/Menu/IntroScreen.cs b/osu.Game/Screens/Menu/IntroScreen.cs index c110c53df8..7b23cc7538 100644 --- a/osu.Game/Screens/Menu/IntroScreen.cs +++ b/osu.Game/Screens/Menu/IntroScreen.cs @@ -20,6 +20,7 @@ using osu.Game.Audio; using osu.Game.Beatmaps; using osu.Game.Configuration; using osu.Game.Database; +using osu.Game.Extensions; using osu.Game.Localisation; using osu.Game.Online.API; using osu.Game.Overlays; @@ -170,7 +171,14 @@ namespace osu.Game.Screens.Menu if (s.Beatmaps.Count == 0) return; - initialBeatmap = beatmaps.GetWorkingBeatmap(s.Beatmaps.First()); + var working = beatmaps.GetWorkingBeatmap(s.Beatmaps.First()); + + // Ensure files area actually present on disk. + // This is to handle edge cases like users deleting files outside the game and breaking the world. + if (!hasAllFiles(working)) + return; + + initialBeatmap = working; }); return UsingThemedIntro = initialBeatmap != null; @@ -188,6 +196,20 @@ namespace osu.Game.Screens.Menu [Resolved] private INotificationOverlay notifications { get; set; } + private bool hasAllFiles(WorkingBeatmap working) + { + foreach (var f in working.BeatmapSetInfo.Files) + { + using (var str = working.GetStream(f.File.GetStoragePath())) + { + if (str == null) + return false; + } + } + + return true; + } + private void ensureEventuallyArrivingAtMenu() { // This intends to handle the case where an intro may get stuck. From 21389820c5f415dab2db00530a860f1eb93ee270 Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Sat, 4 Jan 2025 02:35:48 -0500 Subject: [PATCH 0477/1275] Fix player no longer handling non-loaded beatmaps --- osu.Game/Screens/Play/Player.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs index e50f97f912..02a8a6d2cc 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -68,7 +68,7 @@ namespace osu.Game.Screens.Play public override bool HideMenuCursorOnNonMouseInput => true; - public override bool RequiresPortraitOrientation => DrawableRuleset.RequiresPortraitOrientation; + public override bool RequiresPortraitOrientation => DrawableRuleset?.RequiresPortraitOrientation == true; protected override OverlayActivation InitialOverlayActivationMode => OverlayActivation.UserTriggered; From a241d1f5032f453d7a83e0b6fb0a8502bd42e431 Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Sat, 4 Jan 2025 02:36:06 -0500 Subject: [PATCH 0478/1275] Fix `DrawableManiaRuleset` not cached as itself in subtypes i.e. editor mania ruleset --- osu.Game.Rulesets.Mania/UI/DrawableManiaRuleset.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Mania/UI/DrawableManiaRuleset.cs b/osu.Game.Rulesets.Mania/UI/DrawableManiaRuleset.cs index 65841af5de..d6794d0b4f 100644 --- a/osu.Game.Rulesets.Mania/UI/DrawableManiaRuleset.cs +++ b/osu.Game.Rulesets.Mania/UI/DrawableManiaRuleset.cs @@ -32,7 +32,7 @@ using osu.Game.Skinning; namespace osu.Game.Rulesets.Mania.UI { - [Cached] + [Cached(typeof(DrawableManiaRuleset))] public partial class DrawableManiaRuleset : DrawableScrollingRuleset { /// From 37da72d764896b6678738bf9ea175b8a3ae2bed5 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sun, 5 Jan 2025 00:32:06 +0900 Subject: [PATCH 0479/1275] 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 0480/1275] 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 0481/1275] 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 0482/1275] 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 76ac11ff593bafc32a99a92368f79c94dac2f512 Mon Sep 17 00:00:00 2001 From: StanR Date: Mon, 6 Jan 2025 20:08:14 +0500 Subject: [PATCH 0483/1275] Fix angle bonuses calculating repetition incorrectly, apply distance scaling to wide bonus (#31320) * Fix angle bonuses calculating repetition incorrectly, apply distance scaling to wide bonus * Buff speed to compensate for streams losing pp * Adjust speed multiplier * Adjust wide scaling * Fix tests --- .../OsuDifficultyCalculatorTest.cs | 18 ++++++++--------- .../Difficulty/Evaluators/AimEvaluator.cs | 20 ++++++++++--------- .../Difficulty/Evaluators/SpeedEvaluator.cs | 2 +- .../Difficulty/Skills/Speed.cs | 2 +- 4 files changed, 22 insertions(+), 20 deletions(-) diff --git a/osu.Game.Rulesets.Osu.Tests/OsuDifficultyCalculatorTest.cs b/osu.Game.Rulesets.Osu.Tests/OsuDifficultyCalculatorTest.cs index c0a6d3a755..842a34aaa8 100644 --- a/osu.Game.Rulesets.Osu.Tests/OsuDifficultyCalculatorTest.cs +++ b/osu.Game.Rulesets.Osu.Tests/OsuDifficultyCalculatorTest.cs @@ -15,22 +15,22 @@ namespace osu.Game.Rulesets.Osu.Tests { protected override string ResourceAssembly => "osu.Game.Rulesets.Osu.Tests"; - [TestCase(6.718709884850683d, 239, "diffcalc-test")] - [TestCase(1.4485749025771304d, 54, "zero-length-sliders")] - [TestCase(0.42630400627180914d, 4, "very-fast-slider")] + [TestCase(6.7153612142198682d, 239, "diffcalc-test")] + [TestCase(1.4484916289194889d, 54, "zero-length-sliders")] + [TestCase(0.42912495021837549d, 4, "very-fast-slider")] [TestCase(0.14143808967817237d, 2, "nan-slider")] public void Test(double expectedStarRating, int expectedMaxCombo, string name) => base.Test(expectedStarRating, expectedMaxCombo, name); - [TestCase(9.6343245007055653d, 239, "diffcalc-test")] - [TestCase(1.7550169162648608d, 54, "zero-length-sliders")] - [TestCase(0.55231632896800109d, 4, "very-fast-slider")] + [TestCase(9.6358837846598835d, 239, "diffcalc-test")] + [TestCase(1.754888327422514d, 54, "zero-length-sliders")] + [TestCase(0.55601568006454294d, 4, "very-fast-slider")] public void TestClockRateAdjusted(double expectedStarRating, int expectedMaxCombo, string name) => Test(expectedStarRating, expectedMaxCombo, name, new OsuModDoubleTime()); - [TestCase(6.718709884850683d, 239, "diffcalc-test")] - [TestCase(1.4485749025771304d, 54, "zero-length-sliders")] - [TestCase(0.42630400627180914d, 4, "very-fast-slider")] + [TestCase(6.7153612142198682d, 239, "diffcalc-test")] + [TestCase(1.4484916289194889d, 54, "zero-length-sliders")] + [TestCase(0.42912495021837549d, 4, "very-fast-slider")] public void TestClassicMod(double expectedStarRating, int expectedMaxCombo, string name) => Test(expectedStarRating, expectedMaxCombo, name, new OsuModClassic()); diff --git a/osu.Game.Rulesets.Osu/Difficulty/Evaluators/AimEvaluator.cs b/osu.Game.Rulesets.Osu/Difficulty/Evaluators/AimEvaluator.cs index fdf94719ed..cff2eae357 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/Evaluators/AimEvaluator.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/Evaluators/AimEvaluator.cs @@ -80,17 +80,19 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators double angleBonus = Math.Min(currVelocity, prevVelocity); wideAngleBonus = calcWideAngleBonus(currAngle); + acuteAngleBonus = calcAcuteAngleBonus(currAngle); + + // Penalize angle repetition. + wideAngleBonus *= 1 - Math.Min(wideAngleBonus, Math.Pow(calcWideAngleBonus(lastAngle), 3)); + acuteAngleBonus *= 0.03 + 0.97 * (1 - Math.Min(acuteAngleBonus, Math.Pow(calcAcuteAngleBonus(lastAngle), 3))); + + // Apply full wide angle bonus for distance more than one diameter + wideAngleBonus *= angleBonus * DifficultyCalculationUtils.Smootherstep(osuCurrObj.LazyJumpDistance, 0, diameter); // Apply acute angle bonus for BPM above 300 1/2 and distance more than one diameter - acuteAngleBonus = calcAcuteAngleBonus(currAngle) * - angleBonus * - DifficultyCalculationUtils.Smootherstep(DifficultyCalculationUtils.MillisecondsToBPM(osuCurrObj.StrainTime, 2), 300, 400) * - DifficultyCalculationUtils.Smootherstep(osuCurrObj.LazyJumpDistance, diameter, diameter * 2); - - // Penalize wide angles if they're repeated, reducing the penalty as the lastAngle gets more acute. - wideAngleBonus *= angleBonus * (1 - Math.Min(wideAngleBonus, Math.Pow(calcWideAngleBonus(lastAngle), 3))); - // Penalize acute angles if they're repeated, reducing the penalty as the lastAngle gets more obtuse. - acuteAngleBonus *= 0.03 + 0.97 * (1 - Math.Min(acuteAngleBonus, Math.Pow(calcAcuteAngleBonus(lastAngle), 3))); + acuteAngleBonus *= angleBonus * + DifficultyCalculationUtils.Smootherstep(DifficultyCalculationUtils.MillisecondsToBPM(osuCurrObj.StrainTime, 2), 300, 400) * + DifficultyCalculationUtils.Smootherstep(osuCurrObj.LazyJumpDistance, diameter, diameter * 2); // Apply wiggle bonus for jumps that are [radius, 3*diameter] in distance, with < 110 angle // https://www.desmos.com/calculator/dp0v0nvowc diff --git a/osu.Game.Rulesets.Osu/Difficulty/Evaluators/SpeedEvaluator.cs b/osu.Game.Rulesets.Osu/Difficulty/Evaluators/SpeedEvaluator.cs index e5e9769081..769220ece0 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/Evaluators/SpeedEvaluator.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/Evaluators/SpeedEvaluator.cs @@ -18,7 +18,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators private const double single_spacing_threshold = OsuDifficultyHitObject.NORMALISED_DIAMETER * 1.25; // 1.25 circles distance between centers private const double min_speed_bonus = 200; // 200 BPM 1/4th private const double speed_balancing_factor = 40; - private const double distance_multiplier = 0.94; + private const double distance_multiplier = 0.9; /// /// Evaluates the difficulty of tapping the current object, based on: diff --git a/osu.Game.Rulesets.Osu/Difficulty/Skills/Speed.cs b/osu.Game.Rulesets.Osu/Difficulty/Skills/Speed.cs index 5dae9a9fc5..f2e2c2ec5f 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/Skills/Speed.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/Skills/Speed.cs @@ -15,7 +15,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills /// public class Speed : OsuStrainSkill { - private double skillMultiplier => 1.430; + private double skillMultiplier => 1.45; private double strainDecayBase => 0.3; private double currentStrain; From e8dc09f5bc66642b21e0a2bae8645f20904870d2 Mon Sep 17 00:00:00 2001 From: Andrei Zavatski Date: Tue, 7 Jan 2025 00:36:58 +0300 Subject: [PATCH 0484/1275] 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 0485/1275] 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 0486/1275] 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 0487/1275] 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 0488/1275] 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 0489/1275] 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 0490/1275] 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 0491/1275] 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 0492/1275] 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 0493/1275] 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 0494/1275] 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 0495/1275] 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 0496/1275] 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 0497/1275] 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 0498/1275] 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 0499/1275] 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 0500/1275] 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 0501/1275] 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 0502/1275] 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 0503/1275] 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 4095b2662bc67da4e3eeb90da0d747b2cc135dcb Mon Sep 17 00:00:00 2001 From: Jay Lawton Date: Tue, 7 Jan 2025 21:36:56 +1000 Subject: [PATCH 0504/1275] Add `consistentRatioPenalty` to the `Colour` skill. (#31285) * fix colour * review fix Co-authored-by: StanR * remove cancelled out operand * increase nerf, adjust tests * fix automated spacing issues * up penalty * adjust tests * apply review changes * fix nullable hell --------- Co-authored-by: StanR --- .../TaikoDifficultyCalculatorTest.cs | 8 +-- .../Difficulty/Evaluators/ColourEvaluator.cs | 54 ++++++++++++++++++- 2 files changed, 57 insertions(+), 5 deletions(-) diff --git a/osu.Game.Rulesets.Taiko.Tests/TaikoDifficultyCalculatorTest.cs b/osu.Game.Rulesets.Taiko.Tests/TaikoDifficultyCalculatorTest.cs index ba247c68d4..de3bec5fcf 100644 --- a/osu.Game.Rulesets.Taiko.Tests/TaikoDifficultyCalculatorTest.cs +++ b/osu.Game.Rulesets.Taiko.Tests/TaikoDifficultyCalculatorTest.cs @@ -14,13 +14,13 @@ namespace osu.Game.Rulesets.Taiko.Tests { protected override string ResourceAssembly => "osu.Game.Rulesets.Taiko"; - [TestCase(3.0950934814938953d, 200, "diffcalc-test")] - [TestCase(3.0950934814938953d, 200, "diffcalc-test-strong")] + [TestCase(2.837609165845338d, 200, "diffcalc-test")] + [TestCase(2.837609165845338d, 200, "diffcalc-test-strong")] public void Test(double expectedStarRating, int expectedMaxCombo, string name) => base.Test(expectedStarRating, expectedMaxCombo, name); - [TestCase(4.0839365008715403d, 200, "diffcalc-test")] - [TestCase(4.0839365008715403d, 200, "diffcalc-test-strong")] + [TestCase(3.8005218640444949, 200, "diffcalc-test")] + [TestCase(3.8005218640444949, 200, "diffcalc-test-strong")] public void TestClockRateAdjusted(double expectedStarRating, int expectedMaxCombo, string name) => Test(expectedStarRating, expectedMaxCombo, name, new TaikoModDoubleTime()); diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Evaluators/ColourEvaluator.cs b/osu.Game.Rulesets.Taiko/Difficulty/Evaluators/ColourEvaluator.cs index 25428c8b2f..3ff5b87fb6 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Evaluators/ColourEvaluator.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Evaluators/ColourEvaluator.cs @@ -36,18 +36,70 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Evaluators return 2 * (1 - DifficultyCalculationUtils.Logistic(exponent: Math.E * repeatingHitPattern.RepetitionInterval - 2 * Math.E)); } + /// + /// Calculates a consistency penalty based on the number of consecutive consistent intervals, + /// considering the delta time between each colour sequence. + /// + /// The current hitObject to consider. + /// The allowable margin of error for determining whether ratios are consistent. + /// The maximum objects to check per count of consistent ratio. + private static double consistentRatioPenalty(TaikoDifficultyHitObject hitObject, double threshold = 0.01, int maxObjectsToCheck = 64) + { + int consistentRatioCount = 0; + double totalRatioCount = 0.0; + + TaikoDifficultyHitObject current = hitObject; + + for (int i = 0; i < maxObjectsToCheck; i++) + { + // Break if there is no valid previous object + if (current.Index <= 1) + break; + + var previousHitObject = (TaikoDifficultyHitObject)current.Previous(1); + + double currentRatio = current.Rhythm.Ratio; + double previousRatio = previousHitObject.Rhythm.Ratio; + + // A consistent interval is defined as the percentage difference between the two rhythmic ratios with the margin of error. + if (Math.Abs(1 - currentRatio / previousRatio) <= threshold) + { + consistentRatioCount++; + totalRatioCount += currentRatio; + break; + } + + // Move to the previous object + current = previousHitObject; + } + + // Ensure no division by zero + double ratioPenalty = 1 - totalRatioCount / (consistentRatioCount + 1) * 0.80; + + return ratioPenalty; + } + + /// + /// Evaluate the difficulty of the first hitobject within a colour streak. + /// public static double EvaluateDifficultyOf(DifficultyHitObject hitObject) { - TaikoDifficultyHitObjectColour colour = ((TaikoDifficultyHitObject)hitObject).Colour; + var taikoObject = (TaikoDifficultyHitObject)hitObject; + TaikoDifficultyHitObjectColour colour = taikoObject.Colour; double difficulty = 0.0d; if (colour.MonoStreak?.FirstHitObject == hitObject) // Difficulty for MonoStreak difficulty += EvaluateDifficultyOf(colour.MonoStreak); + if (colour.AlternatingMonoPattern?.FirstHitObject == hitObject) // Difficulty for AlternatingMonoPattern difficulty += EvaluateDifficultyOf(colour.AlternatingMonoPattern); + if (colour.RepeatingHitPattern?.FirstHitObject == hitObject) // Difficulty for RepeatingHitPattern difficulty += EvaluateDifficultyOf(colour.RepeatingHitPattern); + double consistencyPenalty = consistentRatioPenalty(taikoObject); + difficulty *= consistencyPenalty; + return difficulty; } } From 3b58d5e43565e9b16b94667972ba968dbea36ba1 Mon Sep 17 00:00:00 2001 From: StanR Date: Tue, 7 Jan 2025 17:49:55 +0500 Subject: [PATCH 0505/1275] Clamp OD in performance calculation to fix negative OD gaining pp (#31447) Co-authored-by: James Wilson --- .../Difficulty/OsuPerformanceCalculator.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs b/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs index df418fb3f8..5cf7a56d8a 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs @@ -191,7 +191,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty aimValue *= accuracy; // It is important to consider accuracy difficulty when scaling with accuracy. - aimValue *= 0.98 + Math.Pow(attributes.OverallDifficulty, 2) / 2500; + aimValue *= 0.98 + Math.Pow(Math.Max(0, attributes.OverallDifficulty), 2) / 2500; return aimValue; } @@ -238,7 +238,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty double relevantAccuracy = attributes.SpeedNoteCount == 0 ? 0 : (relevantCountGreat * 6.0 + relevantCountOk * 2.0 + relevantCountMeh) / (attributes.SpeedNoteCount * 6.0); // Scale the speed value with accuracy and OD. - speedValue *= (0.95 + Math.Pow(attributes.OverallDifficulty, 2) / 750) * Math.Pow((accuracy + relevantAccuracy) / 2.0, (14.5 - attributes.OverallDifficulty) / 2); + speedValue *= (0.95 + Math.Pow(Math.Max(0, attributes.OverallDifficulty), 2) / 750) * Math.Pow((accuracy + relevantAccuracy) / 2.0, (14.5 - attributes.OverallDifficulty) / 2); // Scale the speed value with # of 50s to punish doubletapping. speedValue *= Math.Pow(0.99, countMeh < totalHits / 500.0 ? 0 : countMeh - totalHits / 500.0); @@ -305,7 +305,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty // Scale the flashlight value with accuracy _slightly_. flashlightValue *= 0.5 + accuracy / 2.0; // It is important to also consider accuracy difficulty when doing that. - flashlightValue *= 0.98 + Math.Pow(attributes.OverallDifficulty, 2) / 2500; + flashlightValue *= 0.98 + Math.Pow(Math.Max(0, attributes.OverallDifficulty), 2) / 2500; return flashlightValue; } 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 0506/1275] 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 0507/1275] 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 0508/1275] 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 0509/1275] 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 0510/1275] 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 0511/1275] 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 0512/1275] 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 0513/1275] 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 0514/1275] Only update combo information when any changes happened --- .../Screens/Edit/EditorBeatmapProcessor.cs | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/osu.Game/Screens/Edit/EditorBeatmapProcessor.cs b/osu.Game/Screens/Edit/EditorBeatmapProcessor.cs index 8108f51ad1..957c1d0969 100644 --- a/osu.Game/Screens/Edit/EditorBeatmapProcessor.cs +++ b/osu.Game/Screens/Edit/EditorBeatmapProcessor.cs @@ -111,20 +111,29 @@ namespace osu.Game.Screens.Edit int currentBreak = 0; - for (int i = 0; i < Beatmap.HitObjects.Count; ++i) - { - var hitObject = Beatmap.HitObjects[i]; + IHasComboInformation? lastObj = null; + bool comboInformationUpdateRequired = false; + foreach (var hitObject in Beatmap.HitObjects) + { if (hitObject is not IHasComboInformation hasCombo) continue; if (currentBreak < breakEnds.Count && hitObject.StartTime >= breakEnds[currentBreak]) { - hasCombo.NewCombo = true; + if (!hasCombo.NewCombo) + { + hasCombo.NewCombo = true; + comboInformationUpdateRequired = true; + } + currentBreak += 1; } - hasCombo.UpdateComboInformation(i > 0 ? Beatmap.HitObjects[i - 1] as IHasComboInformation : null); + if (comboInformationUpdateRequired) + hasCombo.UpdateComboInformation(lastObj); + + lastObj = hasCombo; } } } From 9c05837b3a36e26b4cbe6cdb6b364b03d99b585c Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Wed, 8 Jan 2025 18:45:35 +0900 Subject: [PATCH 0515/1275] Change to using a 'FreeStyle' boolean --- .../Online/Rooms/MultiplayerPlaylistItem.cs | 5 +-- osu.Game/Online/Rooms/PlaylistItem.cs | 18 ++++---- .../Screens/OnlinePlay/Match/RoomSubScreen.cs | 41 ++++++------------- .../Multiplayer/MultiplayerMatchSongSelect.cs | 4 +- .../Multiplayer/MultiplayerMatchSubScreen.cs | 3 ++ .../OnlinePlay/OnlinePlaySongSelect.cs | 5 +-- .../OnlinePlay/OnlinePlayStyleSelect.cs | 13 ++++-- .../Playlists/PlaylistsRoomSubScreen.cs | 8 ++++ .../Playlists/PlaylistsSongSelect.cs | 2 +- 9 files changed, 49 insertions(+), 50 deletions(-) diff --git a/osu.Game/Online/Rooms/MultiplayerPlaylistItem.cs b/osu.Game/Online/Rooms/MultiplayerPlaylistItem.cs index 4a15fd9690..4dfb3b389d 100644 --- a/osu.Game/Online/Rooms/MultiplayerPlaylistItem.cs +++ b/osu.Game/Online/Rooms/MultiplayerPlaylistItem.cs @@ -57,11 +57,10 @@ namespace osu.Game.Online.Rooms public double StarRating { get; set; } /// - /// A non-null value indicates "freestyle" mode where players are able to individually select - /// their own choice of beatmap (from the respective beatmap set) and ruleset to play in the room. + /// Indicates whether participants in the room are able to pick their own choice of beatmap difficulty and ruleset. /// [Key(11)] - public int? BeatmapSetID { get; set; } + public bool FreeStyle { get; set; } [SerializationConstructor] public MultiplayerPlaylistItem() diff --git a/osu.Game/Online/Rooms/PlaylistItem.cs b/osu.Game/Online/Rooms/PlaylistItem.cs index 16c252befc..e8725b6792 100644 --- a/osu.Game/Online/Rooms/PlaylistItem.cs +++ b/osu.Game/Online/Rooms/PlaylistItem.cs @@ -68,11 +68,10 @@ namespace osu.Game.Online.Rooms } /// - /// A non-null value indicates "freestyle" mode where players are able to individually select - /// their own choice of beatmap (from the respective beatmap set) and ruleset to play in the room. + /// Indicates whether participants in the room are able to pick their own choice of beatmap difficulty and ruleset. /// - [JsonProperty("beatmapset_id")] - public int? BeatmapSetId { get; set; } + [JsonProperty("freestyle")] + public bool FreeStyle { get; set; } /// /// A beatmap representing this playlist item. @@ -108,7 +107,7 @@ namespace osu.Game.Online.Rooms PlayedAt = item.PlayedAt; RequiredMods = item.RequiredMods.ToArray(); AllowedMods = item.AllowedMods.ToArray(); - BeatmapSetId = item.BeatmapSetID; + FreeStyle = item.FreeStyle; } public void MarkInvalid() => valid.Value = false; @@ -128,8 +127,7 @@ namespace osu.Game.Online.Rooms #endregion - public PlaylistItem With(Optional id = default, Optional beatmap = default, Optional playlistOrder = default, - Optional ruleset = default) + public PlaylistItem With(Optional id = default, Optional beatmap = default, Optional playlistOrder = default, Optional ruleset = default) { return new PlaylistItem(beatmap.GetOr(Beatmap)) { @@ -141,19 +139,19 @@ namespace osu.Game.Online.Rooms PlayedAt = PlayedAt, AllowedMods = AllowedMods, RequiredMods = RequiredMods, + FreeStyle = FreeStyle, valid = { Value = Valid.Value }, - BeatmapSetId = BeatmapSetId }; } public bool Equals(PlaylistItem? other) => ID == other?.ID && Beatmap.OnlineID == other.Beatmap.OnlineID - && BeatmapSetId == other.BeatmapSetId && RulesetID == other.RulesetID && Expired == other.Expired && PlaylistOrder == other.PlaylistOrder && AllowedMods.SequenceEqual(other.AllowedMods) - && RequiredMods.SequenceEqual(other.RequiredMods); + && RequiredMods.SequenceEqual(other.RequiredMods) + && FreeStyle == other.FreeStyle; } } diff --git a/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs b/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs index b51679ded6..ec2ed90eca 100644 --- a/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs @@ -272,21 +272,9 @@ namespace osu.Game.Screens.OnlinePlay.Match base.LoadComplete(); SelectedItem.BindValueChanged(_ => Scheduler.AddOnce(OnSelectedItemChanged)); - - UserMods.BindValueChanged(_ => Scheduler.AddOnce(updateMods)); - - UserBeatmap.BindValueChanged(_ => Scheduler.AddOnce(() => - { - updateBeatmap(); - updateUserStyle(); - })); - - UserRuleset.BindValueChanged(_ => Scheduler.AddOnce(() => - { - updateUserMods(); - updateRuleset(); - updateUserStyle(); - })); + UserMods.BindValueChanged(_ => Scheduler.AddOnce(OnSelectedItemChanged)); + UserBeatmap.BindValueChanged(_ => Scheduler.AddOnce(OnSelectedItemChanged)); + UserRuleset.BindValueChanged(_ => Scheduler.AddOnce(OnSelectedItemChanged)); beatmapAvailabilityTracker.SelectedItem.BindTo(SelectedItem); beatmapAvailabilityTracker.Availability.BindValueChanged(_ => updateBeatmap()); @@ -458,14 +446,6 @@ namespace osu.Game.Screens.OnlinePlay.Match if (!this.IsCurrentScreen() || SelectedItem.Value is not PlaylistItem item) return; - // Reset user style if no longer valid. - // Todo: In the future this can be made more lenient, such as allowing a non-null ruleset as the set changes. - if (item.BeatmapSetId == null || item.BeatmapSetId != UserBeatmap.Value?.BeatmapSet!.OnlineID) - { - UserBeatmap.Value = null; - UserRuleset.Value = null; - } - updateUserMods(); updateBeatmap(); updateMods(); @@ -487,10 +467,10 @@ namespace osu.Game.Screens.OnlinePlay.Match UserModsSelectOverlay.IsValidMod = m => allowedMods.Any(a => a.GetType() == m.GetType()); } - if (item.BeatmapSetId == null) - UserStyleSection?.Hide(); - else + if (item.FreeStyle) UserStyleSection?.Show(); + else + UserStyleSection?.Hide(); } private void updateUserMods() @@ -499,8 +479,13 @@ namespace osu.Game.Screens.OnlinePlay.Match return; // Remove any user mods that are no longer allowed. - var rulesetInstance = GetGameplayRuleset().CreateInstance(); - var allowedMods = item.AllowedMods.Select(m => m.ToMod(rulesetInstance)); + Ruleset rulesetInstance = GetGameplayRuleset().CreateInstance(); + Mod[] allowedMods = item.AllowedMods.Select(m => m.ToMod(rulesetInstance)).ToArray(); + Mod[] newUserMods = UserMods.Value.Where(m => allowedMods.Any(a => m.GetType() == a.GetType())).ToArray(); + + if (newUserMods.SequenceEqual(UserMods.Value)) + return; + UserMods.Value = UserMods.Value.Where(m => allowedMods.Any(a => m.GetType() == a.GetType())).ToList(); } diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSongSelect.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSongSelect.cs index 9f9e6349a6..5754bcb963 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSongSelect.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSongSelect.cs @@ -83,11 +83,11 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer { ID = itemToEdit?.ID ?? 0, BeatmapID = item.Beatmap.OnlineID, - BeatmapSetID = item.BeatmapSetId, BeatmapChecksum = item.Beatmap.MD5Hash, RulesetID = item.RulesetID, RequiredMods = item.RequiredMods.ToArray(), - AllowedMods = item.AllowedMods.ToArray() + AllowedMods = item.AllowedMods.ToArray(), + FreeStyle = item.FreeStyle }; Task task = itemToEdit != null ? client.EditPlaylistItem(multiplayerItem) : client.AddPlaylistItem(multiplayerItem); diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs index edfb059c77..34a1eb70a9 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs @@ -403,7 +403,10 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer private void updateCurrentItem() { Debug.Assert(client.Room != null); + SelectedItem.Value = Room.Playlist.SingleOrDefault(i => i.ID == client.Room.Settings.PlaylistItemId); + UserBeatmap.Value = client.LocalUser?.BeatmapId == null ? null : UserBeatmap.Value; + UserRuleset.Value = client.LocalUser?.RulesetId == null ? null : UserRuleset.Value; } private void handleRoomLost() => Schedule(() => diff --git a/osu.Game/Screens/OnlinePlay/OnlinePlaySongSelect.cs b/osu.Game/Screens/OnlinePlay/OnlinePlaySongSelect.cs index a91f43635b..9df01ead42 100644 --- a/osu.Game/Screens/OnlinePlay/OnlinePlaySongSelect.cs +++ b/osu.Game/Screens/OnlinePlay/OnlinePlaySongSelect.cs @@ -111,8 +111,7 @@ namespace osu.Game.Screens.OnlinePlay FreeMods.Value = initialItem.AllowedMods.Select(m => m.ToMod(rulesetInstance)).ToArray(); } - if (initialItem.BeatmapSetId != null) - FreeStyle.Value = true; + FreeStyle.Value = initialItem.FreeStyle; } Mods.BindValueChanged(onModsChanged); @@ -162,7 +161,7 @@ namespace osu.Game.Screens.OnlinePlay RulesetID = Ruleset.Value.OnlineID, RequiredMods = Mods.Value.Select(m => new APIMod(m)).ToArray(), AllowedMods = FreeMods.Value.Select(m => new APIMod(m)).ToArray(), - BeatmapSetId = FreeStyle.Value ? Beatmap.Value.BeatmapSetInfo.OnlineID : null + FreeStyle = FreeStyle.Value }; return SelectItem(item); diff --git a/osu.Game/Screens/OnlinePlay/OnlinePlayStyleSelect.cs b/osu.Game/Screens/OnlinePlay/OnlinePlayStyleSelect.cs index 029ca68e36..d1fcf94152 100644 --- a/osu.Game/Screens/OnlinePlay/OnlinePlayStyleSelect.cs +++ b/osu.Game/Screens/OnlinePlay/OnlinePlayStyleSelect.cs @@ -63,6 +63,7 @@ namespace osu.Game.Screens.OnlinePlay { private readonly PlaylistItem item; private double itemLength; + private int beatmapSetId; public DifficultySelectFilterControl(PlaylistItem item) { @@ -72,8 +73,14 @@ namespace osu.Game.Screens.OnlinePlay [BackgroundDependencyLoader] private void load(RealmAccess realm) { - int beatmapId = item.Beatmap.OnlineID; - itemLength = realm.Run(r => r.All().FirstOrDefault(b => b.OnlineID == beatmapId)?.Length ?? 0); + realm.Run(r => + { + int beatmapId = item.Beatmap.OnlineID; + BeatmapInfo? beatmap = r.All().FirstOrDefault(b => b.OnlineID == beatmapId); + + itemLength = beatmap?.Length ?? 0; + beatmapSetId = beatmap?.BeatmapSet?.OnlineID ?? 0; + }); } public override FilterCriteria CreateCriteria() @@ -81,7 +88,7 @@ namespace osu.Game.Screens.OnlinePlay var criteria = base.CreateCriteria(); // Must be from the same set as the playlist item. - criteria.BeatmapSetId = item.BeatmapSetId; + criteria.BeatmapSetId = beatmapSetId; // Must be within 30s of the playlist item. criteria.Length.Min = itemLength - 30000; diff --git a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs index b941bbd290..eaadfb6507 100644 --- a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs @@ -67,6 +67,7 @@ namespace osu.Game.Screens.OnlinePlay.Playlists { base.LoadComplete(); + SelectedItem.BindValueChanged(onSelectedItemChanged, true); isIdle.BindValueChanged(_ => updatePollingRate(), true); Room.PropertyChanged += onRoomPropertyChanged; @@ -75,6 +76,13 @@ namespace osu.Game.Screens.OnlinePlay.Playlists updateRoomPlaylist(); } + private void onSelectedItemChanged(ValueChangedEvent item) + { + // Simplest for now. + UserBeatmap.Value = null; + UserRuleset.Value = null; + } + private void onRoomPropertyChanged(object? sender, PropertyChangedEventArgs e) { switch (e.PropertyName) diff --git a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsSongSelect.cs b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsSongSelect.cs index a3b8a1575e..abf80c0d44 100644 --- a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsSongSelect.cs +++ b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsSongSelect.cs @@ -37,10 +37,10 @@ namespace osu.Game.Screens.OnlinePlay.Playlists private PlaylistItem createNewItem() => new PlaylistItem(Beatmap.Value.BeatmapInfo) { ID = room.Playlist.Count == 0 ? 0 : room.Playlist.Max(p => p.ID) + 1, - BeatmapSetId = FreeStyle.Value ? Beatmap.Value.BeatmapSetInfo.OnlineID : null, RulesetID = Ruleset.Value.OnlineID, RequiredMods = Mods.Value.Select(m => new APIMod(m)).ToArray(), AllowedMods = FreeMods.Value.Select(m => new APIMod(m)).ToArray(), + FreeStyle = FreeStyle.Value }; } } From be33addae16f589dda941d27d2e49a25ec61d0bd Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Wed, 8 Jan 2025 18:57:22 +0900 Subject: [PATCH 0516/1275] Fix possible null reference --- .../Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs index 34a1eb70a9..b5fe8bf631 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs @@ -274,7 +274,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer protected override APIMod[] GetGameplayMods() { // Using the room's reported status makes the server authoritative. - return client.LocalUser?.Mods.Concat(SelectedItem.Value!.RequiredMods).ToArray()!; + return client.LocalUser?.Mods != null ? client.LocalUser.Mods.Concat(SelectedItem.Value!.RequiredMods).ToArray() : base.GetGameplayMods(); } protected override RulesetInfo GetGameplayRuleset() From 392bb5718cbbab3a2b3738d460ea3cbbc4d46885 Mon Sep 17 00:00:00 2001 From: StanR Date: Wed, 8 Jan 2025 15:03:22 +0500 Subject: [PATCH 0517/1275] Simplify angle bonus formula (#31449) * Simplify angle bonus formula * Simplify further * Simplify acute too * Tests --- .../OsuDifficultyCalculatorTest.cs | 6 +++--- .../Difficulty/Evaluators/AimEvaluator.cs | 4 ++-- .../Difficulty/Utils/DifficultyCalculationUtils.cs | 13 +++++++++++++ 3 files changed, 18 insertions(+), 5 deletions(-) diff --git a/osu.Game.Rulesets.Osu.Tests/OsuDifficultyCalculatorTest.cs b/osu.Game.Rulesets.Osu.Tests/OsuDifficultyCalculatorTest.cs index 842a34aaa8..fbd865df47 100644 --- a/osu.Game.Rulesets.Osu.Tests/OsuDifficultyCalculatorTest.cs +++ b/osu.Game.Rulesets.Osu.Tests/OsuDifficultyCalculatorTest.cs @@ -15,20 +15,20 @@ namespace osu.Game.Rulesets.Osu.Tests { protected override string ResourceAssembly => "osu.Game.Rulesets.Osu.Tests"; - [TestCase(6.7153612142198682d, 239, "diffcalc-test")] + [TestCase(6.7230435389286045d, 239, "diffcalc-test")] [TestCase(1.4484916289194889d, 54, "zero-length-sliders")] [TestCase(0.42912495021837549d, 4, "very-fast-slider")] [TestCase(0.14143808967817237d, 2, "nan-slider")] public void Test(double expectedStarRating, int expectedMaxCombo, string name) => base.Test(expectedStarRating, expectedMaxCombo, name); - [TestCase(9.6358837846598835d, 239, "diffcalc-test")] + [TestCase(9.6468019709446171d, 239, "diffcalc-test")] [TestCase(1.754888327422514d, 54, "zero-length-sliders")] [TestCase(0.55601568006454294d, 4, "very-fast-slider")] public void TestClockRateAdjusted(double expectedStarRating, int expectedMaxCombo, string name) => Test(expectedStarRating, expectedMaxCombo, name, new OsuModDoubleTime()); - [TestCase(6.7153612142198682d, 239, "diffcalc-test")] + [TestCase(6.7230435389286045d, 239, "diffcalc-test")] [TestCase(1.4484916289194889d, 54, "zero-length-sliders")] [TestCase(0.42912495021837549d, 4, "very-fast-slider")] public void TestClassicMod(double expectedStarRating, int expectedMaxCombo, string name) diff --git a/osu.Game.Rulesets.Osu/Difficulty/Evaluators/AimEvaluator.cs b/osu.Game.Rulesets.Osu/Difficulty/Evaluators/AimEvaluator.cs index cff2eae357..8c41240a24 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/Evaluators/AimEvaluator.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/Evaluators/AimEvaluator.cs @@ -142,8 +142,8 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators return aimStrain; } - private static double calcWideAngleBonus(double angle) => Math.Pow(Math.Sin(3.0 / 4 * (Math.Min(5.0 / 6 * Math.PI, Math.Max(Math.PI / 6, angle)) - Math.PI / 6)), 2); + private static double calcWideAngleBonus(double angle) => DifficultyCalculationUtils.Smoothstep(angle, double.DegreesToRadians(30), double.DegreesToRadians(150)); - private static double calcAcuteAngleBonus(double angle) => 1 - calcWideAngleBonus(angle); + private static double calcAcuteAngleBonus(double angle) => DifficultyCalculationUtils.Smoothstep(angle, double.DegreesToRadians(150), double.DegreesToRadians(30)); } } diff --git a/osu.Game/Rulesets/Difficulty/Utils/DifficultyCalculationUtils.cs b/osu.Game/Rulesets/Difficulty/Utils/DifficultyCalculationUtils.cs index 497a1f8234..aeccf2fd55 100644 --- a/osu.Game/Rulesets/Difficulty/Utils/DifficultyCalculationUtils.cs +++ b/osu.Game/Rulesets/Difficulty/Utils/DifficultyCalculationUtils.cs @@ -66,6 +66,19 @@ namespace osu.Game.Rulesets.Difficulty.Utils /// The output of the bell curve function of public static double BellCurve(double x, double mean, double width, double multiplier = 1.0) => multiplier * Math.Exp(Math.E * -(Math.Pow(x - mean, 2) / Math.Pow(width, 2))); + /// + /// Smoothstep function (https://en.wikipedia.org/wiki/Smoothstep) + /// + /// Value to calculate the function for + /// Value at which function returns 0 + /// Value at which function returns 1 + public static double Smoothstep(double x, double start, double end) + { + x = Math.Clamp((x - start) / (end - start), 0.0, 1.0); + + return x * x * (3.0 - 2.0 * x); + } + /// /// Smootherstep function (https://en.wikipedia.org/wiki/Smoothstep#Variations) /// 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 0518/1275] 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 0519/1275] 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 0520/1275] 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 0521/1275] 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 0522/1275] 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 0523/1275] 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 0524/1275] 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 0525/1275] 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 0526/1275] 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 0527/1275] 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 0528/1275] 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 db58ec864569889a17952149ff85a05d28a07133 Mon Sep 17 00:00:00 2001 From: StanR Date: Thu, 9 Jan 2025 14:57:48 +0500 Subject: [PATCH 0529/1275] Apply a bunch of balancing changes to aim (#31456) * Apply a bunch of balancing changes to aim * Update tests --------- Co-authored-by: James Wilson --- .../OsuDifficultyCalculatorTest.cs | 18 +++++++++--------- .../Difficulty/Evaluators/AimEvaluator.cs | 8 ++++---- .../Difficulty/Skills/Speed.cs | 2 +- 3 files changed, 14 insertions(+), 14 deletions(-) diff --git a/osu.Game.Rulesets.Osu.Tests/OsuDifficultyCalculatorTest.cs b/osu.Game.Rulesets.Osu.Tests/OsuDifficultyCalculatorTest.cs index fbd865df47..9af5051f45 100644 --- a/osu.Game.Rulesets.Osu.Tests/OsuDifficultyCalculatorTest.cs +++ b/osu.Game.Rulesets.Osu.Tests/OsuDifficultyCalculatorTest.cs @@ -15,22 +15,22 @@ namespace osu.Game.Rulesets.Osu.Tests { protected override string ResourceAssembly => "osu.Game.Rulesets.Osu.Tests"; - [TestCase(6.7230435389286045d, 239, "diffcalc-test")] - [TestCase(1.4484916289194889d, 54, "zero-length-sliders")] - [TestCase(0.42912495021837549d, 4, "very-fast-slider")] + [TestCase(6.6860329680488437d, 239, "diffcalc-test")] + [TestCase(1.4485740324170036d, 54, "zero-length-sliders")] + [TestCase(0.43052813047866129d, 4, "very-fast-slider")] [TestCase(0.14143808967817237d, 2, "nan-slider")] public void Test(double expectedStarRating, int expectedMaxCombo, string name) => base.Test(expectedStarRating, expectedMaxCombo, name); - [TestCase(9.6468019709446171d, 239, "diffcalc-test")] - [TestCase(1.754888327422514d, 54, "zero-length-sliders")] - [TestCase(0.55601568006454294d, 4, "very-fast-slider")] + [TestCase(9.6300773538770041d, 239, "diffcalc-test")] + [TestCase(1.7550155729445993d, 54, "zero-length-sliders")] + [TestCase(0.55785578988249407d, 4, "very-fast-slider")] public void TestClockRateAdjusted(double expectedStarRating, int expectedMaxCombo, string name) => Test(expectedStarRating, expectedMaxCombo, name, new OsuModDoubleTime()); - [TestCase(6.7230435389286045d, 239, "diffcalc-test")] - [TestCase(1.4484916289194889d, 54, "zero-length-sliders")] - [TestCase(0.42912495021837549d, 4, "very-fast-slider")] + [TestCase(6.6860329680488437d, 239, "diffcalc-test")] + [TestCase(1.4485740324170036d, 54, "zero-length-sliders")] + [TestCase(0.43052813047866129d, 4, "very-fast-slider")] public void TestClassicMod(double expectedStarRating, int expectedMaxCombo, string name) => Test(expectedStarRating, expectedMaxCombo, name, new OsuModClassic()); diff --git a/osu.Game.Rulesets.Osu/Difficulty/Evaluators/AimEvaluator.cs b/osu.Game.Rulesets.Osu/Difficulty/Evaluators/AimEvaluator.cs index 8c41240a24..e279ed889a 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/Evaluators/AimEvaluator.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/Evaluators/AimEvaluator.cs @@ -12,7 +12,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators public static class AimEvaluator { private const double wide_angle_multiplier = 1.5; - private const double acute_angle_multiplier = 2.7; + private const double acute_angle_multiplier = 2.6; private const double slider_multiplier = 1.35; private const double velocity_change_multiplier = 0.75; private const double wiggle_multiplier = 1.02; @@ -84,7 +84,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators // Penalize angle repetition. wideAngleBonus *= 1 - Math.Min(wideAngleBonus, Math.Pow(calcWideAngleBonus(lastAngle), 3)); - acuteAngleBonus *= 0.03 + 0.97 * (1 - Math.Min(acuteAngleBonus, Math.Pow(calcAcuteAngleBonus(lastAngle), 3))); + acuteAngleBonus *= 0.1 + 0.9 * (1 - Math.Min(acuteAngleBonus, Math.Pow(calcAcuteAngleBonus(lastAngle), 3))); // Apply full wide angle bonus for distance more than one diameter wideAngleBonus *= angleBonus * DifficultyCalculationUtils.Smootherstep(osuCurrObj.LazyJumpDistance, 0, diameter); @@ -142,8 +142,8 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators return aimStrain; } - private static double calcWideAngleBonus(double angle) => DifficultyCalculationUtils.Smoothstep(angle, double.DegreesToRadians(30), double.DegreesToRadians(150)); + private static double calcWideAngleBonus(double angle) => DifficultyCalculationUtils.Smoothstep(angle, double.DegreesToRadians(40), double.DegreesToRadians(140)); - private static double calcAcuteAngleBonus(double angle) => DifficultyCalculationUtils.Smoothstep(angle, double.DegreesToRadians(150), double.DegreesToRadians(30)); + private static double calcAcuteAngleBonus(double angle) => DifficultyCalculationUtils.Smoothstep(angle, double.DegreesToRadians(140), double.DegreesToRadians(40)); } } diff --git a/osu.Game.Rulesets.Osu/Difficulty/Skills/Speed.cs b/osu.Game.Rulesets.Osu/Difficulty/Skills/Speed.cs index f2e2c2ec5f..bdeea0e918 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/Skills/Speed.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/Skills/Speed.cs @@ -15,7 +15,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills /// public class Speed : OsuStrainSkill { - private double skillMultiplier => 1.45; + private double skillMultiplier => 1.46; private double strainDecayBase => 0.3; private double currentStrain; 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 0530/1275] 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 b21c6457b1a1febd004d508e3597815b64a2a6d4 Mon Sep 17 00:00:00 2001 From: Givikap120 <89256026+Givikap120@users.noreply.github.com> Date: Thu, 9 Jan 2025 15:27:54 +0200 Subject: [PATCH 0531/1275] Punish speed PP for scores with high deviation (#30907) --- .../Difficulty/OsuDifficultyAttributes.cs | 31 ++++- .../Difficulty/OsuDifficultyCalculator.cs | 5 + .../Difficulty/OsuPerformanceAttributes.cs | 3 + .../Difficulty/OsuPerformanceCalculator.cs | 119 +++++++++++++++++- .../Difficulty/DifficultyAttributes.cs | 1 + 5 files changed, 149 insertions(+), 10 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyAttributes.cs b/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyAttributes.cs index 3b9a23df23..395f581b65 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyAttributes.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyAttributes.cs @@ -62,21 +62,33 @@ namespace osu.Game.Rulesets.Osu.Difficulty /// /// The perceived approach rate inclusive of rate-adjusting mods (DT/HT/etc). /// - /// - /// Rate-adjusting mods don't directly affect the approach rate difficulty value, but have a perceived effect as a result of adjusting audio timing. - /// [JsonProperty("approach_rate")] public double ApproachRate { get; set; } /// /// The perceived overall difficulty inclusive of rate-adjusting mods (DT/HT/etc). /// - /// - /// Rate-adjusting mods don't directly affect the overall difficulty value, but have a perceived effect as a result of adjusting audio timing. - /// [JsonProperty("overall_difficulty")] public double OverallDifficulty { get; set; } + /// + /// The perceived hit window for a GREAT hit inclusive of rate-adjusting mods (DT/HT/etc). + /// + [JsonProperty("great_hit_window")] + public double GreatHitWindow { get; set; } + + /// + /// The perceived hit window for an OK hit inclusive of rate-adjusting mods (DT/HT/etc). + /// + [JsonProperty("ok_hit_window")] + public double OkHitWindow { get; set; } + + /// + /// The perceived hit window for a MEH hit inclusive of rate-adjusting mods (DT/HT/etc). + /// + [JsonProperty("meh_hit_window")] + public double MehHitWindow { get; set; } + /// /// The beatmap's drain rate. This doesn't scale with rate-adjusting mods. /// @@ -107,6 +119,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty yield return (ATTRIB_ID_OVERALL_DIFFICULTY, OverallDifficulty); yield return (ATTRIB_ID_APPROACH_RATE, ApproachRate); yield return (ATTRIB_ID_DIFFICULTY, StarRating); + yield return (ATTRIB_ID_GREAT_HIT_WINDOW, GreatHitWindow); if (ShouldSerializeFlashlightDifficulty()) yield return (ATTRIB_ID_FLASHLIGHT, FlashlightDifficulty); @@ -117,6 +130,9 @@ namespace osu.Game.Rulesets.Osu.Difficulty yield return (ATTRIB_ID_SPEED_DIFFICULT_STRAIN_COUNT, SpeedDifficultStrainCount); yield return (ATTRIB_ID_SPEED_NOTE_COUNT, SpeedNoteCount); yield return (ATTRIB_ID_AIM_DIFFICULT_SLIDER_COUNT, AimDifficultSliderCount); + + yield return (ATTRIB_ID_OK_HIT_WINDOW, OkHitWindow); + yield return (ATTRIB_ID_MEH_HIT_WINDOW, MehHitWindow); } public override void FromDatabaseAttributes(IReadOnlyDictionary values, IBeatmapOnlineInfo onlineInfo) @@ -128,12 +144,15 @@ namespace osu.Game.Rulesets.Osu.Difficulty OverallDifficulty = values[ATTRIB_ID_OVERALL_DIFFICULTY]; ApproachRate = values[ATTRIB_ID_APPROACH_RATE]; StarRating = values[ATTRIB_ID_DIFFICULTY]; + GreatHitWindow = values[ATTRIB_ID_GREAT_HIT_WINDOW]; FlashlightDifficulty = values.GetValueOrDefault(ATTRIB_ID_FLASHLIGHT); SliderFactor = values[ATTRIB_ID_SLIDER_FACTOR]; AimDifficultStrainCount = values[ATTRIB_ID_AIM_DIFFICULT_STRAIN_COUNT]; SpeedDifficultStrainCount = values[ATTRIB_ID_SPEED_DIFFICULT_STRAIN_COUNT]; SpeedNoteCount = values[ATTRIB_ID_SPEED_NOTE_COUNT]; AimDifficultSliderCount = values[ATTRIB_ID_AIM_DIFFICULT_SLIDER_COUNT]; + OkHitWindow = values[ATTRIB_ID_OK_HIT_WINDOW]; + MehHitWindow = values[ATTRIB_ID_MEH_HIT_WINDOW]; DrainRate = onlineInfo.DrainRate; HitCircleCount = onlineInfo.CircleCount; SliderCount = onlineInfo.SliderCount; diff --git a/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs b/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs index d0f23735c3..5a61ea586a 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs @@ -99,6 +99,8 @@ namespace osu.Game.Rulesets.Osu.Difficulty hitWindows.SetDifficulty(beatmap.Difficulty.OverallDifficulty); double hitWindowGreat = hitWindows.WindowFor(HitResult.Great) / clockRate; + double hitWindowOk = hitWindows.WindowFor(HitResult.Ok) / clockRate; + double hitWindowMeh = hitWindows.WindowFor(HitResult.Meh) / clockRate; OsuDifficultyAttributes attributes = new OsuDifficultyAttributes { @@ -114,6 +116,9 @@ namespace osu.Game.Rulesets.Osu.Difficulty SpeedDifficultStrainCount = speedDifficultyStrainCount, ApproachRate = preempt > 1200 ? (1800 - preempt) / 120 : (1200 - preempt) / 150 + 5, OverallDifficulty = (80 - hitWindowGreat) / 6, + GreatHitWindow = hitWindowGreat, + OkHitWindow = hitWindowOk, + MehHitWindow = hitWindowMeh, DrainRate = drainRate, MaxCombo = beatmap.GetMaxCombo(), HitCircleCount = hitCirclesCount, diff --git a/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceAttributes.cs b/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceAttributes.cs index 0aeaf7669f..de4491a31b 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceAttributes.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceAttributes.cs @@ -24,6 +24,9 @@ namespace osu.Game.Rulesets.Osu.Difficulty [JsonProperty("effective_miss_count")] public double EffectiveMissCount { get; set; } + [JsonProperty("speed_deviation")] + public double? SpeedDeviation { get; set; } + public override IEnumerable GetAttributesForDisplay() { foreach (var attribute in base.GetAttributesForDisplay()) diff --git a/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs b/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs index 5cf7a56d8a..91cd270966 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs @@ -9,6 +9,7 @@ using osu.Game.Rulesets.Osu.Difficulty.Skills; using osu.Game.Rulesets.Osu.Mods; using osu.Game.Rulesets.Scoring; using osu.Game.Scoring; +using osu.Game.Utils; namespace osu.Game.Rulesets.Osu.Difficulty { @@ -40,6 +41,8 @@ namespace osu.Game.Rulesets.Osu.Difficulty /// private double effectiveMissCount; + private double? speedDeviation; + public OsuPerformanceCalculator() : base(new OsuRuleset()) { @@ -110,10 +113,13 @@ namespace osu.Game.Rulesets.Osu.Difficulty effectiveMissCount = Math.Min(effectiveMissCount + countOk * okMultiplier + countMeh * mehMultiplier, totalHits); } + speedDeviation = calculateSpeedDeviation(osuAttributes); + double aimValue = computeAimValue(score, osuAttributes); double speedValue = computeSpeedValue(score, osuAttributes); double accuracyValue = computeAccuracyValue(score, osuAttributes); double flashlightValue = computeFlashlightValue(score, osuAttributes); + double totalValue = Math.Pow( Math.Pow(aimValue, 1.1) + @@ -129,6 +135,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty Accuracy = accuracyValue, Flashlight = flashlightValue, EffectiveMissCount = effectiveMissCount, + SpeedDeviation = speedDeviation, Total = totalValue }; } @@ -198,7 +205,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty private double computeSpeedValue(ScoreInfo score, OsuDifficultyAttributes attributes) { - if (score.Mods.Any(h => h is OsuModRelax)) + if (score.Mods.Any(h => h is OsuModRelax) || speedDeviation == null) return 0.0; double speedValue = OsuStrainSkill.DifficultyToPerformance(attributes.SpeedDifficulty); @@ -230,6 +237,9 @@ namespace osu.Game.Rulesets.Osu.Difficulty speedValue *= 1.0 + 0.04 * (12.0 - attributes.ApproachRate); } + double speedHighDeviationMultiplier = calculateSpeedHighDeviationNerf(attributes); + speedValue *= speedHighDeviationMultiplier; + // Calculate accuracy assuming the worst case scenario double relevantTotalDiff = totalHits - attributes.SpeedNoteCount; double relevantCountGreat = Math.Max(0, countGreat - relevantTotalDiff); @@ -240,9 +250,6 @@ namespace osu.Game.Rulesets.Osu.Difficulty // Scale the speed value with accuracy and OD. speedValue *= (0.95 + Math.Pow(Math.Max(0, attributes.OverallDifficulty), 2) / 750) * Math.Pow((accuracy + relevantAccuracy) / 2.0, (14.5 - attributes.OverallDifficulty) / 2); - // Scale the speed value with # of 50s to punish doubletapping. - speedValue *= Math.Pow(0.99, countMeh < totalHits / 500.0 ? 0 : countMeh - totalHits / 500.0); - return speedValue; } @@ -310,12 +317,116 @@ namespace osu.Game.Rulesets.Osu.Difficulty return flashlightValue; } + /// + /// Estimates player's deviation on speed notes using , assuming worst-case. + /// Treats all speed notes as hit circles. + /// + private double? calculateSpeedDeviation(OsuDifficultyAttributes attributes) + { + if (totalSuccessfulHits == 0) + return null; + + // Calculate accuracy assuming the worst case scenario + double speedNoteCount = attributes.SpeedNoteCount; + speedNoteCount += (totalHits - attributes.SpeedNoteCount) * 0.1; + + // Assume worst case: all mistakes were on speed notes + double relevantCountMiss = Math.Min(countMiss, speedNoteCount); + double relevantCountMeh = Math.Min(countMeh, speedNoteCount - relevantCountMiss); + double relevantCountOk = Math.Min(countOk, speedNoteCount - relevantCountMiss - relevantCountMeh); + double relevantCountGreat = Math.Max(0, speedNoteCount - relevantCountMiss - relevantCountMeh - relevantCountOk); + + return calculateDeviation(attributes, relevantCountGreat, relevantCountOk, relevantCountMeh, relevantCountMiss); + } + + /// + /// Estimates the player's tap deviation based on the OD, given number of greats, oks, mehs and misses, + /// assuming the player's mean hit error is 0. The estimation is consistent in that two SS scores on the same map with the same settings + /// will always return the same deviation. Misses are ignored because they are usually due to misaiming. + /// Greats and oks are assumed to follow a normal distribution, whereas mehs are assumed to follow a uniform distribution. + /// + private double? calculateDeviation(OsuDifficultyAttributes attributes, double relevantCountGreat, double relevantCountOk, double relevantCountMeh, double relevantCountMiss) + { + if (relevantCountGreat + relevantCountOk + relevantCountMeh <= 0) + return null; + + double objectCount = relevantCountGreat + relevantCountOk + relevantCountMeh + relevantCountMiss; + + double hitWindowGreat = attributes.GreatHitWindow; + double hitWindowOk = attributes.OkHitWindow; + double hitWindowMeh = attributes.MehHitWindow; + + // The probability that a player hits a circle is unknown, but we can estimate it to be + // the number of greats on circles divided by the number of circles, and then add one + // to the number of circles as a bias correction. + double n = Math.Max(1, objectCount - relevantCountMiss - relevantCountMeh); + const double z = 2.32634787404; // 99% critical value for the normal distribution (one-tailed). + + // Proportion of greats hit on circles, ignoring misses and 50s. + double p = relevantCountGreat / n; + + // We can be 99% confident that p is at least this value. + double pLowerBound = (n * p + z * z / 2) / (n + z * z) - z / (n + z * z) * Math.Sqrt(n * p * (1 - p) + z * z / 4); + + // Compute the deviation assuming greats and oks are normally distributed, and mehs are uniformly distributed. + // Begin with greats and oks first. Ignoring mehs, we can be 99% confident that the deviation is not higher than: + double deviation = hitWindowGreat / (Math.Sqrt(2) * SpecialFunctions.ErfInv(pLowerBound)); + + double randomValue = Math.Sqrt(2 / Math.PI) * hitWindowOk * Math.Exp(-0.5 * Math.Pow(hitWindowOk / deviation, 2)) + / (deviation * SpecialFunctions.Erf(hitWindowOk / (Math.Sqrt(2) * deviation))); + + deviation *= Math.Sqrt(1 - randomValue); + + // Value deviation approach as greatCount approaches 0 + double limitValue = hitWindowOk / Math.Sqrt(3); + + // If precision is not enough to compute true deviation - use limit value + if (pLowerBound == 0 || randomValue >= 1 || deviation > limitValue) + deviation = limitValue; + + // Then compute the variance for mehs. + double mehVariance = (hitWindowMeh * hitWindowMeh + hitWindowOk * hitWindowMeh + hitWindowOk * hitWindowOk) / 3; + + // Find the total deviation. + deviation = Math.Sqrt(((relevantCountGreat + relevantCountOk) * Math.Pow(deviation, 2) + relevantCountMeh * mehVariance) / (relevantCountGreat + relevantCountOk + relevantCountMeh)); + + return deviation; + } + + // Calculates multiplier for speed to account for improper tapping based on the deviation and speed difficulty + // https://www.desmos.com/calculator/dmogdhzofn + private double calculateSpeedHighDeviationNerf(OsuDifficultyAttributes attributes) + { + if (speedDeviation == null) + return 0; + + double speedValue = OsuStrainSkill.DifficultyToPerformance(attributes.SpeedDifficulty); + + // Decides a point where the PP value achieved compared to the speed deviation is assumed to be tapped improperly. Any PP above this point is considered "excess" speed difficulty. + // This is used to cause PP above the cutoff to scale logarithmically towards the original speed value thus nerfing the value. + double excessSpeedDifficultyCutoff = 100 + 220 * Math.Pow(22 / speedDeviation.Value, 6.5); + + if (speedValue <= excessSpeedDifficultyCutoff) + return 1.0; + + const double scale = 50; + double adjustedSpeedValue = scale * (Math.Log((speedValue - excessSpeedDifficultyCutoff) / scale + 1) + excessSpeedDifficultyCutoff / scale); + + // 200 UR and less are considered tapped correctly to ensure that normal scores will be punished as little as possible + double lerp = 1 - Math.Clamp((speedDeviation.Value - 20) / (24 - 20), 0, 1); + adjustedSpeedValue = double.Lerp(adjustedSpeedValue, speedValue, lerp); + + return adjustedSpeedValue / speedValue; + } + // Miss penalty assumes that a player will miss on the hardest parts of a map, // so we use the amount of relatively difficult sections to adjust miss penalty // to make it more punishing on maps with lower amount of hard sections. private double calculateMissPenalty(double missCount, double difficultStrainCount) => 0.96 / ((missCount / (4 * Math.Pow(Math.Log(difficultStrainCount), 0.94))) + 1); private double getComboScalingFactor(OsuDifficultyAttributes attributes) => attributes.MaxCombo <= 0 ? 1.0 : Math.Min(Math.Pow(scoreMaxCombo, 0.8) / Math.Pow(attributes.MaxCombo, 0.8), 1.0); + private int totalHits => countGreat + countOk + countMeh + countMiss; + private int totalSuccessfulHits => countGreat + countOk + countMeh; private int totalImperfectHits => countOk + countMeh + countMiss; } } diff --git a/osu.Game/Rulesets/Difficulty/DifficultyAttributes.cs b/osu.Game/Rulesets/Difficulty/DifficultyAttributes.cs index f5ed5a180b..1d6cee043b 100644 --- a/osu.Game/Rulesets/Difficulty/DifficultyAttributes.cs +++ b/osu.Game/Rulesets/Difficulty/DifficultyAttributes.cs @@ -31,6 +31,7 @@ namespace osu.Game.Rulesets.Difficulty protected const int ATTRIB_ID_OK_HIT_WINDOW = 27; protected const int ATTRIB_ID_MONO_STAMINA_FACTOR = 29; protected const int ATTRIB_ID_AIM_DIFFICULT_SLIDER_COUNT = 31; + protected const int ATTRIB_ID_MEH_HIT_WINDOW = 33; /// /// The mods which were applied to the beatmap. From 253b9cbbdd3ef5a3e78ec4401a44096315874956 Mon Sep 17 00:00:00 2001 From: Susko3 Date: Thu, 9 Jan 2025 16:51:52 +0000 Subject: [PATCH 0532/1275] 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 0533/1275] 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 0534/1275] 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 0535/1275] 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 0536/1275] 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 0537/1275] 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 0538/1275] 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 0539/1275] 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 0540/1275] 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 0541/1275] 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 0542/1275] Update readme with updated mobile release information --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 6043497181..32c43995f4 100644 --- a/README.md +++ b/README.md @@ -37,7 +37,7 @@ You can also generally download a version for your current device from the [osu! If your platform is unsupported or not listed above, there is still a chance you can run the release or manually build it by following the instructions below. -**For iOS/iPadOS users**: The iOS testflight link fills up very fast (Apple has a hard limit of 10,000 users). We reset it occasionally. Please do not ask about this. Check back regularly for link resets or follow [peppy](https://twitter.com/ppy) on twitter for announcements. Our goal is to get the game on mobile app stores in early 2024. +**For iOS/iPadOS users**: The iOS testflight link fills up very fast (Apple has a hard limit of 10,000 users). We reset it occasionally. Please do not ask about this. Check back regularly for link resets or follow [peppy](https://twitter.com/ppy) on twitter for announcements. Our goal is to get the game on mobile app stores very soon to se don't have to live with this limitation. ## Developing a custom ruleset From f71869610292ff7be0025f149cb92664e7809aea Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Sun, 12 Jan 2025 02:34:36 -0500 Subject: [PATCH 0543/1275] Allow landscape orientation on tablet devices in osu!mania --- osu.Game/Mobile/OrientationManager.cs | 19 ++++++++++--------- osu.Game/OsuGame.cs | 3 ++- osu.Game/Screens/IOsuScreen.cs | 5 +++-- 3 files changed, 15 insertions(+), 12 deletions(-) diff --git a/osu.Game/Mobile/OrientationManager.cs b/osu.Game/Mobile/OrientationManager.cs index b78bf8e760..0f9b56d434 100644 --- a/osu.Game/Mobile/OrientationManager.cs +++ b/osu.Game/Mobile/OrientationManager.cs @@ -48,27 +48,28 @@ namespace osu.Game.Mobile private void updateOrientations() { bool lockCurrentOrientation = localUserPlaying.Value == LocalUserPlayingState.Playing; - bool lockToPortrait = requiresPortraitOrientation.Value; + bool lockToPortraitOnPhone = requiresPortraitOrientation.Value; if (lockCurrentOrientation) { - if (lockToPortrait && !IsCurrentOrientationPortrait) + if (!IsTablet && lockToPortraitOnPhone && !IsCurrentOrientationPortrait) SetAllowedOrientations(GameOrientation.Portrait); - else if (!lockToPortrait && IsCurrentOrientationPortrait && !IsTablet) + else if (!IsTablet && !lockToPortraitOnPhone && IsCurrentOrientationPortrait) SetAllowedOrientations(GameOrientation.Landscape); else + { + // if the orientation is already portrait/landscape according to the game's specifications, + // then use Locked instead of Portrait/Landscape to handle the case where the device is + // in landscape-left or reverse-portrait. SetAllowedOrientations(GameOrientation.Locked); + } return; } - if (lockToPortrait) + if (!IsTablet && lockToPortraitOnPhone) { - if (IsTablet) - SetAllowedOrientations(GameOrientation.FullPortrait); - else - SetAllowedOrientations(GameOrientation.Portrait); - + SetAllowedOrientations(GameOrientation.Portrait); return; } diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs index e72d106928..0d725bf07c 100644 --- a/osu.Game/OsuGame.cs +++ b/osu.Game/OsuGame.cs @@ -174,7 +174,8 @@ namespace osu.Game public readonly IBindable OverlayActivationMode = new Bindable(); /// - /// On mobile devices, this specifies whether the device should be set and locked to portrait orientation. + /// On mobile phones, this specifies whether the device should be set and locked to portrait orientation. + /// Tablet devices are unaffected by this property. /// /// /// Implementations can be viewed in mobile projects. diff --git a/osu.Game/Screens/IOsuScreen.cs b/osu.Game/Screens/IOsuScreen.cs index 8b3ff4306f..0fd7299115 100644 --- a/osu.Game/Screens/IOsuScreen.cs +++ b/osu.Game/Screens/IOsuScreen.cs @@ -62,10 +62,11 @@ namespace osu.Game.Screens bool HideMenuCursorOnNonMouseInput { get; } /// - /// On mobile devices, this specifies whether this requires the device to be in portrait orientation. + /// On mobile phones, this specifies whether this requires the device to be in portrait orientation. + /// Tablet devices are unaffected by this property. /// /// - /// By default, all screens in the game display in landscape orientation. + /// By default, all screens in the game display in landscape orientation on phones. /// Setting this to true will display this screen in portrait orientation instead, /// and switch back to landscape when transitioning back to a regular non-portrait screen. /// From dfbc93c3dc99653bb221bc07e3647402505bb676 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sun, 12 Jan 2025 19:16:53 +0900 Subject: [PATCH 0544/1275] 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 0545/1275] 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 0546/1275] Create game as soon as possible --- osu.Android/OsuGameActivity.cs | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/osu.Android/OsuGameActivity.cs b/osu.Android/OsuGameActivity.cs index fe11672767..42065e61fd 100644 --- a/osu.Android/OsuGameActivity.cs +++ b/osu.Android/OsuGameActivity.cs @@ -49,9 +49,23 @@ namespace osu.Android /// Adjusted on startup to match expected UX for the current device type (phone/tablet). public ScreenOrientation DefaultOrientation = ScreenOrientation.Unspecified; - private OsuGameAndroid game = null!; + private readonly OsuGameAndroid game; - protected override Framework.Game CreateGame() => game = new OsuGameAndroid(this); + private bool gameCreated; + + protected override Framework.Game CreateGame() + { + if (gameCreated) + throw new InvalidOperationException("Framework tried to create a game twice."); + + gameCreated = true; + return game; + } + + public OsuGameActivity() + { + game = new OsuGameAndroid(this); + } protected override void OnCreate(Bundle? savedInstanceState) { From c1ac27d65894b4418c9d700ec87972728f0c26d9 Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Sun, 12 Jan 2025 22:56:28 -0500 Subject: [PATCH 0547/1275] Fix failing tests - Caches `DrawableRuleset` in editor compose screen for mania playfield adjustment container (because it's used to wrap the blueprint container as well) - Fixes `ManiaModWithPlayfieldCover` performing a no-longer-correct direct cast with a naive-but-working approach. --- .../Mods/ManiaModWithPlayfieldCover.cs | 4 ++-- .../Components/HitPositionPaddedContainer.cs | 10 +++++++++ .../UI/DrawableManiaRuleset.cs | 1 - .../UI/ManiaPlayfieldAdjustmentContainer.cs | 4 +++- .../Edit/DrawableEditorRulesetWrapper.cs | 22 +++++++++---------- osu.Game/Rulesets/Edit/HitObjectComposer.cs | 1 + 6 files changed, 27 insertions(+), 15 deletions(-) diff --git a/osu.Game.Rulesets.Mania/Mods/ManiaModWithPlayfieldCover.cs b/osu.Game.Rulesets.Mania/Mods/ManiaModWithPlayfieldCover.cs index 1bc16112c5..b6e6ee7481 100644 --- a/osu.Game.Rulesets.Mania/Mods/ManiaModWithPlayfieldCover.cs +++ b/osu.Game.Rulesets.Mania/Mods/ManiaModWithPlayfieldCover.cs @@ -5,9 +5,9 @@ using System; using System.Linq; using osu.Framework.Bindables; using osu.Framework.Graphics; -using osu.Framework.Graphics.Containers; using osu.Game.Rulesets.Mania.Objects; using osu.Game.Rulesets.Mania.UI; +using osu.Game.Rulesets.Mania.UI.Components; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.UI; @@ -35,7 +35,7 @@ namespace osu.Game.Rulesets.Mania.Mods foreach (Column column in maniaPlayfield.Stages.SelectMany(stage => stage.Columns)) { HitObjectContainer hoc = column.HitObjectContainer; - Container hocParent = (Container)hoc.Parent!; + ColumnHitObjectArea hocParent = (ColumnHitObjectArea)hoc.Parent!; hocParent.Remove(hoc, false); hocParent.Add(CreateCover(hoc).With(c => diff --git a/osu.Game.Rulesets.Mania/UI/Components/HitPositionPaddedContainer.cs b/osu.Game.Rulesets.Mania/UI/Components/HitPositionPaddedContainer.cs index f591102f6c..f550e3b241 100644 --- a/osu.Game.Rulesets.Mania/UI/Components/HitPositionPaddedContainer.cs +++ b/osu.Game.Rulesets.Mania/UI/Components/HitPositionPaddedContainer.cs @@ -19,6 +19,16 @@ namespace osu.Game.Rulesets.Mania.UI.Components InternalChild = child; } + internal void Add(Drawable drawable) + { + base.AddInternal(drawable); + } + + internal void Remove(Drawable drawable, bool disposeImmediately = true) + { + base.RemoveInternal(drawable, disposeImmediately); + } + [BackgroundDependencyLoader] private void load(IScrollingInfo scrollingInfo) { diff --git a/osu.Game.Rulesets.Mania/UI/DrawableManiaRuleset.cs b/osu.Game.Rulesets.Mania/UI/DrawableManiaRuleset.cs index d6794d0b4f..a186d9aa7d 100644 --- a/osu.Game.Rulesets.Mania/UI/DrawableManiaRuleset.cs +++ b/osu.Game.Rulesets.Mania/UI/DrawableManiaRuleset.cs @@ -32,7 +32,6 @@ using osu.Game.Skinning; namespace osu.Game.Rulesets.Mania.UI { - [Cached(typeof(DrawableManiaRuleset))] public partial class DrawableManiaRuleset : DrawableScrollingRuleset { /// diff --git a/osu.Game.Rulesets.Mania/UI/ManiaPlayfieldAdjustmentContainer.cs b/osu.Game.Rulesets.Mania/UI/ManiaPlayfieldAdjustmentContainer.cs index f7c4850a94..b0203643b0 100644 --- a/osu.Game.Rulesets.Mania/UI/ManiaPlayfieldAdjustmentContainer.cs +++ b/osu.Game.Rulesets.Mania/UI/ManiaPlayfieldAdjustmentContainer.cs @@ -31,7 +31,7 @@ namespace osu.Game.Rulesets.Mania.UI } [Resolved] - private DrawableManiaRuleset drawableManiaRuleset { get; set; } = null!; + private DrawableRuleset drawableRuleset { get; set; } = null!; protected override void Update() { @@ -40,6 +40,8 @@ namespace osu.Game.Rulesets.Mania.UI float aspectRatio = DrawWidth / DrawHeight; bool isPortrait = aspectRatio < 1f; + var drawableManiaRuleset = (DrawableManiaRuleset)drawableRuleset; + if (isPortrait && drawableManiaRuleset.Beatmap.Stages.Count == 1) { // Scale playfield up by 25% to become playable on mobile devices, diff --git a/osu.Game/Rulesets/Edit/DrawableEditorRulesetWrapper.cs b/osu.Game/Rulesets/Edit/DrawableEditorRulesetWrapper.cs index 174b278d89..573eb8c42f 100644 --- a/osu.Game/Rulesets/Edit/DrawableEditorRulesetWrapper.cs +++ b/osu.Game/Rulesets/Edit/DrawableEditorRulesetWrapper.cs @@ -19,16 +19,16 @@ namespace osu.Game.Rulesets.Edit internal partial class DrawableEditorRulesetWrapper : CompositeDrawable where TObject : HitObject { - public Playfield Playfield => drawableRuleset.Playfield; + public Playfield Playfield => DrawableRuleset.Playfield; - private readonly DrawableRuleset drawableRuleset; + public readonly DrawableRuleset DrawableRuleset; [Resolved] private EditorBeatmap beatmap { get; set; } = null!; public DrawableEditorRulesetWrapper(DrawableRuleset drawableRuleset) { - this.drawableRuleset = drawableRuleset; + DrawableRuleset = drawableRuleset; RelativeSizeAxes = Axes.Both; @@ -38,7 +38,7 @@ namespace osu.Game.Rulesets.Edit [BackgroundDependencyLoader] private void load() { - drawableRuleset.FrameStablePlayback = false; + DrawableRuleset.FrameStablePlayback = false; Playfield.DisplayJudgements.Value = false; } @@ -67,27 +67,27 @@ namespace osu.Game.Rulesets.Edit private void regenerateAutoplay() { - var autoplayMod = drawableRuleset.Mods.OfType().Single(); - drawableRuleset.SetReplayScore(autoplayMod.CreateScoreFromReplayData(drawableRuleset.Beatmap, drawableRuleset.Mods)); + var autoplayMod = DrawableRuleset.Mods.OfType().Single(); + DrawableRuleset.SetReplayScore(autoplayMod.CreateScoreFromReplayData(DrawableRuleset.Beatmap, DrawableRuleset.Mods)); } private void addHitObject(HitObject hitObject) { - drawableRuleset.AddHitObject((TObject)hitObject); - drawableRuleset.Playfield.PostProcess(); + DrawableRuleset.AddHitObject((TObject)hitObject); + DrawableRuleset.Playfield.PostProcess(); } private void removeHitObject(HitObject hitObject) { - drawableRuleset.RemoveHitObject((TObject)hitObject); - drawableRuleset.Playfield.PostProcess(); + DrawableRuleset.RemoveHitObject((TObject)hitObject); + DrawableRuleset.Playfield.PostProcess(); } public override bool PropagatePositionalInputSubTree => false; public override bool PropagateNonPositionalInputSubTree => false; - public PlayfieldAdjustmentContainer CreatePlayfieldAdjustmentContainer() => drawableRuleset.CreatePlayfieldAdjustmentContainer(); + public PlayfieldAdjustmentContainer CreatePlayfieldAdjustmentContainer() => DrawableRuleset.CreatePlayfieldAdjustmentContainer(); protected override void Dispose(bool isDisposing) { diff --git a/osu.Game/Rulesets/Edit/HitObjectComposer.cs b/osu.Game/Rulesets/Edit/HitObjectComposer.cs index 9f277b6190..8cc7072582 100644 --- a/osu.Game/Rulesets/Edit/HitObjectComposer.cs +++ b/osu.Game/Rulesets/Edit/HitObjectComposer.cs @@ -132,6 +132,7 @@ namespace osu.Game.Rulesets.Edit if (DrawableRuleset is IDrawableScrollingRuleset scrollingRuleset) dependencies.CacheAs(scrollingRuleset.ScrollingInfo); + dependencies.CacheAs(drawableRulesetWrapper.DrawableRuleset); dependencies.CacheAs(Playfield); InternalChildren = new[] From 4774d9c9ae2652ceb002444dfcc37d176cdbfa45 Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Sun, 12 Jan 2025 22:56:39 -0500 Subject: [PATCH 0548/1275] Fix mania fade in test not actually testing the mod --- .../Mods/TestSceneManiaModFadeIn.cs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/osu.Game.Rulesets.Mania.Tests/Mods/TestSceneManiaModFadeIn.cs b/osu.Game.Rulesets.Mania.Tests/Mods/TestSceneManiaModFadeIn.cs index f403d67377..7b8156c74f 100644 --- a/osu.Game.Rulesets.Mania.Tests/Mods/TestSceneManiaModFadeIn.cs +++ b/osu.Game.Rulesets.Mania.Tests/Mods/TestSceneManiaModFadeIn.cs @@ -26,7 +26,7 @@ namespace osu.Game.Rulesets.Mania.Tests.Mods { CreateModTest(new ModTestData { - Mod = new ManiaModHidden(), + Mod = new ManiaModFadeIn(), PassCondition = () => checkCoverage(ManiaModHidden.MIN_COVERAGE) }); } @@ -36,7 +36,7 @@ namespace osu.Game.Rulesets.Mania.Tests.Mods { CreateModTest(new ModTestData { - Mod = new ManiaModHidden(), + Mod = new ManiaModFadeIn(), PassCondition = () => checkCoverage(ManiaModHidden.MIN_COVERAGE) }); @@ -48,7 +48,7 @@ namespace osu.Game.Rulesets.Mania.Tests.Mods { CreateModTest(new ModTestData { - Mod = new ManiaModHidden(), + Mod = new ManiaModFadeIn(), PassCondition = () => checkCoverage(ManiaModHidden.MAX_COVERAGE) }); @@ -60,7 +60,7 @@ namespace osu.Game.Rulesets.Mania.Tests.Mods { CreateModTest(new ModTestData { - Mod = new ManiaModHidden(), + Mod = new ManiaModFadeIn(), PassCondition = () => checkCoverage(ManiaModHidden.MAX_COVERAGE) }); @@ -73,7 +73,7 @@ namespace osu.Game.Rulesets.Mania.Tests.Mods { CreateModTest(new ModTestData { - Mod = new ManiaModHidden(), + Mod = new ManiaModFadeIn(), CreateBeatmap = () => new Beatmap { HitObjects = Enumerable.Range(1, 100).Select(i => (HitObject)new Note { StartTime = 1000 + 200 * i }).ToList(), From fc069e060c69599285dcba82c657b2568c399674 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 13 Jan 2025 12:38:28 +0100 Subject: [PATCH 0549/1275] 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 0550/1275] 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 0551/1275] 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 0552/1275] 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 0553/1275] 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 0554/1275] 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 0555/1275] 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 0556/1275] 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 0557/1275] 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 0558/1275] 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 0559/1275] 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 0560/1275] 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 0561/1275] 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 0562/1275] Split out `BeatmapCarousel` classes and drop `V2` suffix --- .../SongSelect/TestSceneBeatmapCarouselV2.cs | 4 +- osu.Game/Screens/SelectV2/BeatmapCarousel.cs | 99 +++++++ .../SelectV2/BeatmapCarouselFilterGrouping.cs | 40 +++ .../SelectV2/BeatmapCarouselFilterSorting.cs | 28 ++ .../Screens/SelectV2/BeatmapCarouselItem.cs | 36 +++ .../Screens/SelectV2/BeatmapCarouselPanel.cs | 96 +++++++ .../Screens/SelectV2/BeatmapCarouselV2.cs | 257 ------------------ 7 files changed, 301 insertions(+), 259 deletions(-) create mode 100644 osu.Game/Screens/SelectV2/BeatmapCarousel.cs create mode 100644 osu.Game/Screens/SelectV2/BeatmapCarouselFilterGrouping.cs create mode 100644 osu.Game/Screens/SelectV2/BeatmapCarouselFilterSorting.cs create mode 100644 osu.Game/Screens/SelectV2/BeatmapCarouselItem.cs create mode 100644 osu.Game/Screens/SelectV2/BeatmapCarouselPanel.cs delete mode 100644 osu.Game/Screens/SelectV2/BeatmapCarouselV2.cs diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2.cs index dde4ef88bd..6d54e13b6f 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2.cs @@ -33,7 +33,7 @@ namespace osu.Game.Tests.Visual.SongSelect private BeatmapStore store; private OsuTextFlowContainer stats = null!; - private BeatmapCarouselV2 carousel = null!; + private BeatmapCarousel carousel = null!; private OsuScrollContainer scroll => carousel.ChildrenOfType().Single(); @@ -92,7 +92,7 @@ namespace osu.Game.Tests.Visual.SongSelect }, new Drawable[] { - carousel = new BeatmapCarouselV2 + carousel = new BeatmapCarousel { Anchor = Anchor.Centre, Origin = Anchor.Centre, diff --git a/osu.Game/Screens/SelectV2/BeatmapCarousel.cs b/osu.Game/Screens/SelectV2/BeatmapCarousel.cs new file mode 100644 index 0000000000..3c431a6003 --- /dev/null +++ b/osu.Game/Screens/SelectV2/BeatmapCarousel.cs @@ -0,0 +1,99 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using System.Collections.Generic; +using System.Collections.Specialized; +using System.Linq; +using System.Threading; +using osu.Framework.Allocation; +using osu.Framework.Bindables; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Pooling; +using osu.Game.Beatmaps; +using osu.Game.Database; +using osu.Game.Screens.Select; +using osuTK.Graphics; + +namespace osu.Game.Screens.SelectV2 +{ + [Cached] + public partial class BeatmapCarousel : Carousel + { + private IBindableList detachedBeatmaps = null!; + + private readonly DrawablePool carouselPanelPool = new DrawablePool(100); + + public BeatmapCarousel() + { + DebounceDelay = 100; + DistanceOffscreenToPreload = 100; + + Filters = new ICarouselFilter[] + { + new BeatmapCarouselFilterSorting(), + new BeatmapCarouselFilterGrouping(), + }; + + AddInternal(carouselPanelPool); + } + + [BackgroundDependencyLoader] + private void load(BeatmapStore beatmapStore, CancellationToken? cancellationToken) + { + detachedBeatmaps = beatmapStore.GetBeatmapSets(cancellationToken); + detachedBeatmaps.BindCollectionChanged(beatmapSetsChanged, true); + } + + protected override Drawable GetDrawableForDisplay(CarouselItem item) + { + var drawable = carouselPanelPool.Get(); + drawable.FlashColour(Color4.Red, 2000); + + return drawable; + } + + protected override CarouselItem CreateCarouselItemForModel(BeatmapInfo model) => new BeatmapCarouselItem(model); + + private void beatmapSetsChanged(object? beatmaps, NotifyCollectionChangedEventArgs changed) + { + // TODO: moving management of BeatmapInfo tracking to BeatmapStore might be something we want to consider. + // right now we are managing this locally which is a bit of added overhead. + IEnumerable? newBeatmapSets = changed.NewItems?.Cast(); + IEnumerable? beatmapSetInfos = changed.OldItems?.Cast(); + + switch (changed.Action) + { + case NotifyCollectionChangedAction.Add: + Items.AddRange(newBeatmapSets!.SelectMany(s => s.Beatmaps)); + break; + + case NotifyCollectionChangedAction.Remove: + + foreach (var set in beatmapSetInfos!) + { + foreach (var beatmap in set.Beatmaps) + Items.RemoveAll(i => i is BeatmapInfo bi && beatmap.Equals(bi)); + } + + break; + + case NotifyCollectionChangedAction.Move: + case NotifyCollectionChangedAction.Replace: + throw new NotImplementedException(); + + case NotifyCollectionChangedAction.Reset: + Items.Clear(); + break; + } + } + + public FilterCriteria Criteria { get; private set; } = new FilterCriteria(); + + public void Filter(FilterCriteria criteria) + { + Criteria = criteria; + QueueFilter(); + } + } +} diff --git a/osu.Game/Screens/SelectV2/BeatmapCarouselFilterGrouping.cs b/osu.Game/Screens/SelectV2/BeatmapCarouselFilterGrouping.cs new file mode 100644 index 0000000000..ee4b9ddb69 --- /dev/null +++ b/osu.Game/Screens/SelectV2/BeatmapCarouselFilterGrouping.cs @@ -0,0 +1,40 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using osu.Game.Beatmaps; + +namespace osu.Game.Screens.SelectV2 +{ + public class BeatmapCarouselFilterGrouping : ICarouselFilter + { + public async Task> Run(IEnumerable items, CancellationToken cancellationToken) => await Task.Run(() => + { + // TODO: perform grouping based on FilterCriteria + + CarouselItem? lastItem = null; + + var newItems = new List(items.Count()); + + foreach (var item in items) + { + cancellationToken.ThrowIfCancellationRequested(); + + if (item.Model is BeatmapInfo b1) + { + // Add set header + if (lastItem == null || (lastItem.Model is BeatmapInfo b2 && b2.BeatmapSet!.OnlineID != b1.BeatmapSet!.OnlineID)) + newItems.Add(new BeatmapCarouselItem(b1.BeatmapSet!)); + } + + newItems.Add(item); + lastItem = item; + } + + return newItems; + }, cancellationToken).ConfigureAwait(false); + } +} diff --git a/osu.Game/Screens/SelectV2/BeatmapCarouselFilterSorting.cs b/osu.Game/Screens/SelectV2/BeatmapCarouselFilterSorting.cs new file mode 100644 index 0000000000..a2fd774cf0 --- /dev/null +++ b/osu.Game/Screens/SelectV2/BeatmapCarouselFilterSorting.cs @@ -0,0 +1,28 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using osu.Game.Beatmaps; + +namespace osu.Game.Screens.SelectV2 +{ + public class BeatmapCarouselFilterSorting : ICarouselFilter + { + public async Task> Run(IEnumerable items, CancellationToken cancellationToken) => await Task.Run(() => + { + return items.OrderDescending(Comparer.Create((a, b) => + { + if (a.Model is BeatmapInfo ab && b.Model is BeatmapInfo bb) + return ab.OnlineID.CompareTo(bb.OnlineID); + + if (a is BeatmapCarouselItem aItem && b is BeatmapCarouselItem bItem) + return aItem.ID.CompareTo(bItem.ID); + + return 0; + })); + }, cancellationToken).ConfigureAwait(false); + } +} diff --git a/osu.Game/Screens/SelectV2/BeatmapCarouselItem.cs b/osu.Game/Screens/SelectV2/BeatmapCarouselItem.cs new file mode 100644 index 0000000000..adb5a19875 --- /dev/null +++ b/osu.Game/Screens/SelectV2/BeatmapCarouselItem.cs @@ -0,0 +1,36 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using osu.Game.Beatmaps; +using osu.Game.Database; + +namespace osu.Game.Screens.SelectV2 +{ + public class BeatmapCarouselItem : CarouselItem + { + public readonly Guid ID; + + public override float DrawHeight => Model is BeatmapInfo ? 40 : 80; + + public BeatmapCarouselItem(object model) + : base(model) + { + ID = (Model as IHasGuidPrimaryKey)?.ID ?? Guid.NewGuid(); + } + + public override string? ToString() + { + switch (Model) + { + case BeatmapInfo bi: + return $"Difficulty: {bi.DifficultyName} ({bi.StarRating:N1}*)"; + + case BeatmapSetInfo si: + return $"{si.Metadata}"; + } + + return Model.ToString(); + } + } +} diff --git a/osu.Game/Screens/SelectV2/BeatmapCarouselPanel.cs b/osu.Game/Screens/SelectV2/BeatmapCarouselPanel.cs new file mode 100644 index 0000000000..a64d16a984 --- /dev/null +++ b/osu.Game/Screens/SelectV2/BeatmapCarouselPanel.cs @@ -0,0 +1,96 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Diagnostics; +using osu.Framework.Allocation; +using osu.Framework.Bindables; +using osu.Framework.Extensions.Color4Extensions; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Pooling; +using osu.Framework.Graphics.Shapes; +using osu.Framework.Input.Events; +using osu.Game.Beatmaps; +using osu.Game.Graphics.Sprites; +using osuTK; +using osuTK.Graphics; + +namespace osu.Game.Screens.SelectV2 +{ + public partial class BeatmapCarouselPanel : PoolableDrawable, ICarouselPanel + { + [Resolved] + private BeatmapCarousel carousel { get; set; } = null!; + + public CarouselItem? Item + { + get => item; + set + { + item = value; + + selected.UnbindBindings(); + + if (item != null) + selected.BindTo(item.Selected); + } + } + + private readonly BindableBool selected = new BindableBool(); + private CarouselItem? item; + + [BackgroundDependencyLoader] + private void load() + { + selected.BindValueChanged(value => + { + if (value.NewValue) + { + BorderThickness = 5; + BorderColour = Color4.Pink; + } + else + { + BorderThickness = 0; + } + }); + } + + protected override void FreeAfterUse() + { + base.FreeAfterUse(); + Item = null; + } + + protected override void PrepareForUse() + { + base.PrepareForUse(); + + Debug.Assert(Item != null); + + Size = new Vector2(500, Item.DrawHeight); + Masking = true; + + InternalChildren = new Drawable[] + { + new Box + { + Colour = (Item.Model is BeatmapInfo ? Color4.Aqua : Color4.Yellow).Darken(5), + RelativeSizeAxes = Axes.Both, + }, + new OsuSpriteText + { + Text = Item.ToString() ?? string.Empty, + Padding = new MarginPadding(5), + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + } + }; + } + + protected override bool OnClick(ClickEvent e) + { + carousel.CurrentSelection = Item!.Model; + return true; + } + } +} diff --git a/osu.Game/Screens/SelectV2/BeatmapCarouselV2.cs b/osu.Game/Screens/SelectV2/BeatmapCarouselV2.cs deleted file mode 100644 index 23954da3a1..0000000000 --- a/osu.Game/Screens/SelectV2/BeatmapCarouselV2.cs +++ /dev/null @@ -1,257 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -using System; -using System.Collections.Generic; -using System.Collections.Specialized; -using System.Diagnostics; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using osu.Framework.Allocation; -using osu.Framework.Bindables; -using osu.Framework.Extensions.Color4Extensions; -using osu.Framework.Graphics; -using osu.Framework.Graphics.Pooling; -using osu.Framework.Graphics.Shapes; -using osu.Framework.Input.Events; -using osu.Game.Beatmaps; -using osu.Game.Database; -using osu.Game.Graphics.Sprites; -using osu.Game.Screens.Select; -using osuTK; -using osuTK.Graphics; - -namespace osu.Game.Screens.SelectV2 -{ - [Cached] - public partial class BeatmapCarouselV2 : Carousel - { - private IBindableList detachedBeatmaps = null!; - - private readonly DrawablePool carouselPanelPool = new DrawablePool(100); - - public BeatmapCarouselV2() - { - DebounceDelay = 100; - DistanceOffscreenToPreload = 100; - - Filters = new ICarouselFilter[] - { - new Sorter(), - new Grouper(), - }; - - AddInternal(carouselPanelPool); - } - - [BackgroundDependencyLoader] - private void load(BeatmapStore beatmapStore, CancellationToken? cancellationToken) - { - detachedBeatmaps = beatmapStore.GetBeatmapSets(cancellationToken); - detachedBeatmaps.BindCollectionChanged(beatmapSetsChanged, true); - } - - protected override Drawable GetDrawableForDisplay(CarouselItem item) - { - var drawable = carouselPanelPool.Get(); - drawable.FlashColour(Color4.Red, 2000); - - return drawable; - } - - protected override CarouselItem CreateCarouselItemForModel(BeatmapInfo model) => new BeatmapCarouselItem(model); - - private void beatmapSetsChanged(object? beatmaps, NotifyCollectionChangedEventArgs changed) - { - // TODO: moving management of BeatmapInfo tracking to BeatmapStore might be something we want to consider. - // right now we are managing this locally which is a bit of added overhead. - IEnumerable? newBeatmapSets = changed.NewItems?.Cast(); - IEnumerable? beatmapSetInfos = changed.OldItems?.Cast(); - - switch (changed.Action) - { - case NotifyCollectionChangedAction.Add: - Items.AddRange(newBeatmapSets!.SelectMany(s => s.Beatmaps)); - break; - - case NotifyCollectionChangedAction.Remove: - - foreach (var set in beatmapSetInfos!) - { - foreach (var beatmap in set.Beatmaps) - Items.RemoveAll(i => i is BeatmapInfo bi && beatmap.Equals(bi)); - } - - break; - - case NotifyCollectionChangedAction.Move: - case NotifyCollectionChangedAction.Replace: - throw new NotImplementedException(); - - case NotifyCollectionChangedAction.Reset: - Items.Clear(); - break; - } - } - - public FilterCriteria Criteria { get; private set; } = new FilterCriteria(); - - public void Filter(FilterCriteria criteria) - { - Criteria = criteria; - QueueFilter(); - } - } - - public partial class BeatmapCarouselPanel : PoolableDrawable, ICarouselPanel - { - [Resolved] - private BeatmapCarouselV2 carousel { get; set; } = null!; - - public CarouselItem? Item - { - get => item; - set - { - item = value; - - selected.UnbindBindings(); - - if (item != null) - selected.BindTo(item.Selected); - } - } - - private readonly BindableBool selected = new BindableBool(); - private CarouselItem? item; - - [BackgroundDependencyLoader] - private void load() - { - selected.BindValueChanged(value => - { - if (value.NewValue) - { - BorderThickness = 5; - BorderColour = Color4.Pink; - } - else - { - BorderThickness = 0; - } - }); - } - - protected override void FreeAfterUse() - { - base.FreeAfterUse(); - Item = null; - } - - protected override void PrepareForUse() - { - base.PrepareForUse(); - - Debug.Assert(Item != null); - - Size = new Vector2(500, Item.DrawHeight); - Masking = true; - - InternalChildren = new Drawable[] - { - new Box - { - Colour = (Item.Model is BeatmapInfo ? Color4.Aqua : Color4.Yellow).Darken(5), - RelativeSizeAxes = Axes.Both, - }, - new OsuSpriteText - { - Text = Item.ToString() ?? string.Empty, - Padding = new MarginPadding(5), - Anchor = Anchor.CentreLeft, - Origin = Anchor.CentreLeft, - } - }; - } - - protected override bool OnClick(ClickEvent e) - { - carousel.CurrentSelection = Item!.Model; - return true; - } - } - - public class BeatmapCarouselItem : CarouselItem - { - public readonly Guid ID; - - public override float DrawHeight => Model is BeatmapInfo ? 40 : 80; - - public BeatmapCarouselItem(object model) - : base(model) - { - ID = (Model as IHasGuidPrimaryKey)?.ID ?? Guid.NewGuid(); - } - - public override string? ToString() - { - switch (Model) - { - case BeatmapInfo bi: - return $"Difficulty: {bi.DifficultyName} ({bi.StarRating:N1}*)"; - - case BeatmapSetInfo si: - return $"{si.Metadata}"; - } - - return Model.ToString(); - } - } - - public class Grouper : ICarouselFilter - { - public async Task> Run(IEnumerable items, CancellationToken cancellationToken) => await Task.Run(() => - { - // TODO: perform grouping based on FilterCriteria - - CarouselItem? lastItem = null; - - var newItems = new List(items.Count()); - - foreach (var item in items) - { - cancellationToken.ThrowIfCancellationRequested(); - - if (item.Model is BeatmapInfo b1) - { - // Add set header - if (lastItem == null || (lastItem.Model is BeatmapInfo b2 && b2.BeatmapSet!.OnlineID != b1.BeatmapSet!.OnlineID)) - newItems.Add(new BeatmapCarouselItem(b1.BeatmapSet!)); - } - - newItems.Add(item); - lastItem = item; - } - - return newItems; - }, cancellationToken).ConfigureAwait(false); - } - - public class Sorter : ICarouselFilter - { - public async Task> Run(IEnumerable items, CancellationToken cancellationToken) => await Task.Run(() => - { - return items.OrderDescending(Comparer.Create((a, b) => - { - if (a.Model is BeatmapInfo ab && b.Model is BeatmapInfo bb) - return ab.OnlineID.CompareTo(bb.OnlineID); - - if (a is BeatmapCarouselItem aItem && b is BeatmapCarouselItem bItem) - return aItem.ID.CompareTo(bItem.ID); - - return 0; - })); - }, cancellationToken).ConfigureAwait(false); - } -} From b0c0c98c5dff7ac92e67d5f25a0c20749568adda Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 14 Jan 2025 11:19:17 +0100 Subject: [PATCH 0563/1275] Refetch local metadata cache if corruption is detected Addresses one of the points in https://github.com/ppy/osu/issues/31496. Not going to lie, this is mostly best-effort stuff (while the refetch is happening, metadata lookups using the local source *will* fail), but I see this as a marginal scenario anyways. --- .../LocalCachedBeatmapMetadataSource.cs | 25 +++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/osu.Game/Beatmaps/LocalCachedBeatmapMetadataSource.cs b/osu.Game/Beatmaps/LocalCachedBeatmapMetadataSource.cs index 66fad6c8d8..7495805cff 100644 --- a/osu.Game/Beatmaps/LocalCachedBeatmapMetadataSource.cs +++ b/osu.Game/Beatmaps/LocalCachedBeatmapMetadataSource.cs @@ -114,6 +114,15 @@ namespace osu.Game.Beatmaps } } } + catch (SqliteException sqliteException) when (sqliteException.SqliteErrorCode == 11 || sqliteException.SqliteErrorCode == 26) // SQLITE_CORRUPT, SQLITE_NOTADB + { + // only attempt purge & refetch if there is no other refetch in progress + if (cacheDownloadRequest == null) + { + tryPurgeCache(); + prepareLocalCache(); + } + } catch (Exception ex) { logForModel(beatmapInfo.BeatmapSet, $@"Cached local retrieval for {beatmapInfo} failed with {ex}."); @@ -125,6 +134,22 @@ namespace osu.Game.Beatmaps return false; } + private void tryPurgeCache() + { + log(@"Local metadata cache is corrupted; attempting purge."); + + try + { + File.Delete(storage.GetFullPath(cache_database_name)); + } + catch (Exception ex) + { + log($@"Failed to purge local metadata cache: {ex}"); + } + + log(@"Local metadata cache purged due to corruption."); + } + private SqliteConnection getConnection() => new SqliteConnection(string.Concat(@"Data Source=", storage.GetFullPath(@"online.db", true))); From 7e8a80a0e5e812a30df71687e91952def018aeeb Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 14 Jan 2025 19:37:28 +0900 Subject: [PATCH 0564/1275] 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 0565/1275] 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 0566/1275] 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 0567/1275] 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 0568/1275] 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 c53188cf450bf5eb9efb903e5e295b7435971386 Mon Sep 17 00:00:00 2001 From: StanR Date: Tue, 14 Jan 2025 18:18:02 +0500 Subject: [PATCH 0569/1275] Use total deviation to scale accuracy on aim, general aim buff (#31498) * Make aim accuracy scaling harsher * Use deviation-based scaling * Bring the balancing multiplier down * Adjust multipliers, fix incorrect deviation when using slider accuracy * Adjust multipliers * Update osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceAttributes.cs Co-authored-by: James Wilson * Change high speed deviation threshold to 22-27 instead of 20-24 * Update tests --------- Co-authored-by: James Wilson --- .../OsuDifficultyCalculatorTest.cs | 12 ++-- .../Difficulty/Evaluators/AimEvaluator.cs | 2 +- .../Difficulty/OsuPerformanceAttributes.cs | 3 + .../Difficulty/OsuPerformanceCalculator.cs | 57 +++++++++++++++++-- .../Difficulty/Skills/Aim.cs | 2 +- 5 files changed, 63 insertions(+), 13 deletions(-) diff --git a/osu.Game.Rulesets.Osu.Tests/OsuDifficultyCalculatorTest.cs b/osu.Game.Rulesets.Osu.Tests/OsuDifficultyCalculatorTest.cs index 9af5051f45..a68d9dad39 100644 --- a/osu.Game.Rulesets.Osu.Tests/OsuDifficultyCalculatorTest.cs +++ b/osu.Game.Rulesets.Osu.Tests/OsuDifficultyCalculatorTest.cs @@ -15,21 +15,21 @@ namespace osu.Game.Rulesets.Osu.Tests { protected override string ResourceAssembly => "osu.Game.Rulesets.Osu.Tests"; - [TestCase(6.6860329680488437d, 239, "diffcalc-test")] - [TestCase(1.4485740324170036d, 54, "zero-length-sliders")] + [TestCase(6.7443067697205539d, 239, "diffcalc-test")] + [TestCase(1.4630292101418947d, 54, "zero-length-sliders")] [TestCase(0.43052813047866129d, 4, "very-fast-slider")] [TestCase(0.14143808967817237d, 2, "nan-slider")] public void Test(double expectedStarRating, int expectedMaxCombo, string name) => base.Test(expectedStarRating, expectedMaxCombo, name); - [TestCase(9.6300773538770041d, 239, "diffcalc-test")] - [TestCase(1.7550155729445993d, 54, "zero-length-sliders")] + [TestCase(9.7058844423552308d, 239, "diffcalc-test")] + [TestCase(1.7724929629205366d, 54, "zero-length-sliders")] [TestCase(0.55785578988249407d, 4, "very-fast-slider")] public void TestClockRateAdjusted(double expectedStarRating, int expectedMaxCombo, string name) => Test(expectedStarRating, expectedMaxCombo, name, new OsuModDoubleTime()); - [TestCase(6.6860329680488437d, 239, "diffcalc-test")] - [TestCase(1.4485740324170036d, 54, "zero-length-sliders")] + [TestCase(6.7443067697205539d, 239, "diffcalc-test")] + [TestCase(1.4630292101418947d, 54, "zero-length-sliders")] [TestCase(0.43052813047866129d, 4, "very-fast-slider")] public void TestClassicMod(double expectedStarRating, int expectedMaxCombo, string name) => Test(expectedStarRating, expectedMaxCombo, name, new OsuModClassic()); diff --git a/osu.Game.Rulesets.Osu/Difficulty/Evaluators/AimEvaluator.cs b/osu.Game.Rulesets.Osu/Difficulty/Evaluators/AimEvaluator.cs index e279ed889a..858ce673ee 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/Evaluators/AimEvaluator.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/Evaluators/AimEvaluator.cs @@ -84,7 +84,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators // Penalize angle repetition. wideAngleBonus *= 1 - Math.Min(wideAngleBonus, Math.Pow(calcWideAngleBonus(lastAngle), 3)); - acuteAngleBonus *= 0.1 + 0.9 * (1 - Math.Min(acuteAngleBonus, Math.Pow(calcAcuteAngleBonus(lastAngle), 3))); + acuteAngleBonus *= 0.09 + 0.91 * (1 - Math.Min(acuteAngleBonus, Math.Pow(calcAcuteAngleBonus(lastAngle), 3))); // Apply full wide angle bonus for distance more than one diameter wideAngleBonus *= angleBonus * DifficultyCalculationUtils.Smootherstep(osuCurrObj.LazyJumpDistance, 0, diameter); diff --git a/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceAttributes.cs b/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceAttributes.cs index de4491a31b..9c30c0f7c7 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceAttributes.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceAttributes.cs @@ -24,6 +24,9 @@ namespace osu.Game.Rulesets.Osu.Difficulty [JsonProperty("effective_miss_count")] public double EffectiveMissCount { get; set; } + [JsonProperty("total_deviation")] + public double? TotalDeviation { get; set; } + [JsonProperty("speed_deviation")] public double? SpeedDeviation { get; set; } diff --git a/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs b/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs index 91cd270966..a03e3fd6ef 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs @@ -5,6 +5,7 @@ using System; using System.Collections.Generic; using System.Linq; using osu.Game.Rulesets.Difficulty; +using osu.Game.Rulesets.Difficulty.Utils; using osu.Game.Rulesets.Osu.Difficulty.Skills; using osu.Game.Rulesets.Osu.Mods; using osu.Game.Rulesets.Scoring; @@ -41,6 +42,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty /// private double effectiveMissCount; + private double? totalDeviation; private double? speedDeviation; public OsuPerformanceCalculator() @@ -113,6 +115,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty effectiveMissCount = Math.Min(effectiveMissCount + countOk * okMultiplier + countMeh * mehMultiplier, totalHits); } + totalDeviation = calculateTotalDeviation(osuAttributes); speedDeviation = calculateSpeedDeviation(osuAttributes); double aimValue = computeAimValue(score, osuAttributes); @@ -135,6 +138,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty Accuracy = accuracyValue, Flashlight = flashlightValue, EffectiveMissCount = effectiveMissCount, + TotalDeviation = totalDeviation, SpeedDeviation = speedDeviation, Total = totalValue }; @@ -145,6 +149,9 @@ namespace osu.Game.Rulesets.Osu.Difficulty if (score.Mods.Any(h => h is OsuModAutopilot)) return 0.0; + if (totalDeviation == null) + return 0; + double aimDifficulty = attributes.AimDifficulty; if (attributes.SliderCount > 0 && attributes.AimDifficultSliderCount > 0) @@ -196,9 +203,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty aimValue *= 1.0 + 0.04 * (12.0 - attributes.ApproachRate); } - aimValue *= accuracy; - // It is important to consider accuracy difficulty when scaling with accuracy. - aimValue *= 0.98 + Math.Pow(Math.Max(0, attributes.OverallDifficulty), 2) / 2500; + aimValue *= SpecialFunctions.Erf(25.0 / (Math.Sqrt(2) * totalDeviation.Value)); return aimValue; } @@ -317,6 +322,48 @@ namespace osu.Game.Rulesets.Osu.Difficulty return flashlightValue; } + /// + /// Using estimates player's deviation on accuracy objects. + /// Returns deviation for circles and sliders if score was set with slideracc. + /// Returns the min between deviation of circles and deviation on circles and sliders (assuming slider hits are 50s), if score was set without slideracc. + /// + private double? calculateTotalDeviation(OsuDifficultyAttributes attributes) + { + if (totalSuccessfulHits == 0) + return null; + + int accuracyObjectCount = attributes.HitCircleCount; + + if (!usingClassicSliderAccuracy) + accuracyObjectCount += attributes.SliderCount; + + // Assume worst case: all mistakes was on accuracy objects + int relevantCountMiss = Math.Min(countMiss, accuracyObjectCount); + int relevantCountMeh = Math.Min(countMeh, accuracyObjectCount - relevantCountMiss); + int relevantCountOk = Math.Min(countOk, accuracyObjectCount - relevantCountMiss - relevantCountMeh); + int relevantCountGreat = Math.Max(0, accuracyObjectCount - relevantCountMiss - relevantCountMeh - relevantCountOk); + + // Calculate deviation on accuracy objects + double? deviation = calculateDeviation(attributes, relevantCountGreat, relevantCountOk, relevantCountMeh, relevantCountMiss); + if (deviation == null) + return null; + + if (!usingClassicSliderAccuracy) + return deviation.Value; + + // If score was set without slider accuracy - also compute deviation with sliders + // Assume that all hits was 50s + int totalCountWithSliders = attributes.HitCircleCount + attributes.SliderCount; + int missCountWithSliders = Math.Min(totalCountWithSliders, countMiss); + int hitCountWithSliders = totalCountWithSliders - missCountWithSliders; + + double hitProbabilityWithSliders = hitCountWithSliders / (totalCountWithSliders + 1.0); + double deviationWithSliders = attributes.MehHitWindow / (Math.Sqrt(2) * SpecialFunctions.ErfInv(hitProbabilityWithSliders)); + + // Min is needed for edgecase maps with 1 circle and 999 sliders, as deviation on sliders can be lower in this case + return Math.Min(deviation.Value, deviationWithSliders); + } + /// /// Estimates player's deviation on speed notes using , assuming worst-case. /// Treats all speed notes as hit circles. @@ -412,8 +459,8 @@ namespace osu.Game.Rulesets.Osu.Difficulty const double scale = 50; double adjustedSpeedValue = scale * (Math.Log((speedValue - excessSpeedDifficultyCutoff) / scale + 1) + excessSpeedDifficultyCutoff / scale); - // 200 UR and less are considered tapped correctly to ensure that normal scores will be punished as little as possible - double lerp = 1 - Math.Clamp((speedDeviation.Value - 20) / (24 - 20), 0, 1); + // 220 UR and less are considered tapped correctly to ensure that normal scores will be punished as little as possible + double lerp = 1 - DifficultyCalculationUtils.ReverseLerp(speedDeviation.Value, 22.0, 27.0); adjustedSpeedValue = double.Lerp(adjustedSpeedValue, speedValue, lerp); return adjustedSpeedValue / speedValue; diff --git a/osu.Game.Rulesets.Osu/Difficulty/Skills/Aim.cs b/osu.Game.Rulesets.Osu/Difficulty/Skills/Aim.cs index 400bc97fbc..69211b610f 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/Skills/Aim.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/Skills/Aim.cs @@ -26,7 +26,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills private double currentStrain; - private double skillMultiplier => 25.18; + private double skillMultiplier => 25.7; private double strainDecayBase => 0.15; private readonly List sliderStrains = new List(); From 20108e3b74084692b34643d4e61124b079c0aa44 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Tue, 14 Jan 2025 23:44:14 +0900 Subject: [PATCH 0570/1275] 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 0571/1275] 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 6cf15e3e5a2f5aa0df42886a60367eb2f184fe30 Mon Sep 17 00:00:00 2001 From: James Wilson Date: Tue, 14 Jan 2025 18:27:25 +0000 Subject: [PATCH 0572/1275] Remove problematic total deviation scaling, rebalance aim (#31515) * Remove problematic total deviation scaling, rebalance aim * Fix tests --- .../OsuDifficultyCalculatorTest.cs | 12 ++--- .../Difficulty/Evaluators/AimEvaluator.cs | 2 +- .../Difficulty/OsuPerformanceAttributes.cs | 3 -- .../Difficulty/OsuPerformanceCalculator.cs | 52 ++----------------- .../Difficulty/Skills/Aim.cs | 2 +- 5 files changed, 11 insertions(+), 60 deletions(-) diff --git a/osu.Game.Rulesets.Osu.Tests/OsuDifficultyCalculatorTest.cs b/osu.Game.Rulesets.Osu.Tests/OsuDifficultyCalculatorTest.cs index a68d9dad39..7cf5b0529f 100644 --- a/osu.Game.Rulesets.Osu.Tests/OsuDifficultyCalculatorTest.cs +++ b/osu.Game.Rulesets.Osu.Tests/OsuDifficultyCalculatorTest.cs @@ -15,21 +15,21 @@ namespace osu.Game.Rulesets.Osu.Tests { protected override string ResourceAssembly => "osu.Game.Rulesets.Osu.Tests"; - [TestCase(6.7443067697205539d, 239, "diffcalc-test")] - [TestCase(1.4630292101418947d, 54, "zero-length-sliders")] + [TestCase(6.7331304290522747d, 239, "diffcalc-test")] + [TestCase(1.4602604078137214d, 54, "zero-length-sliders")] [TestCase(0.43052813047866129d, 4, "very-fast-slider")] [TestCase(0.14143808967817237d, 2, "nan-slider")] public void Test(double expectedStarRating, int expectedMaxCombo, string name) => base.Test(expectedStarRating, expectedMaxCombo, name); - [TestCase(9.7058844423552308d, 239, "diffcalc-test")] - [TestCase(1.7724929629205366d, 54, "zero-length-sliders")] + [TestCase(9.6779397290273756d, 239, "diffcalc-test")] + [TestCase(1.7691451263718989d, 54, "zero-length-sliders")] [TestCase(0.55785578988249407d, 4, "very-fast-slider")] public void TestClockRateAdjusted(double expectedStarRating, int expectedMaxCombo, string name) => Test(expectedStarRating, expectedMaxCombo, name, new OsuModDoubleTime()); - [TestCase(6.7443067697205539d, 239, "diffcalc-test")] - [TestCase(1.4630292101418947d, 54, "zero-length-sliders")] + [TestCase(6.7331304290522747d, 239, "diffcalc-test")] + [TestCase(1.4602604078137214d, 54, "zero-length-sliders")] [TestCase(0.43052813047866129d, 4, "very-fast-slider")] public void TestClassicMod(double expectedStarRating, int expectedMaxCombo, string name) => Test(expectedStarRating, expectedMaxCombo, name, new OsuModClassic()); diff --git a/osu.Game.Rulesets.Osu/Difficulty/Evaluators/AimEvaluator.cs b/osu.Game.Rulesets.Osu/Difficulty/Evaluators/AimEvaluator.cs index 858ce673ee..9a5533e536 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/Evaluators/AimEvaluator.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/Evaluators/AimEvaluator.cs @@ -84,7 +84,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators // Penalize angle repetition. wideAngleBonus *= 1 - Math.Min(wideAngleBonus, Math.Pow(calcWideAngleBonus(lastAngle), 3)); - acuteAngleBonus *= 0.09 + 0.91 * (1 - Math.Min(acuteAngleBonus, Math.Pow(calcAcuteAngleBonus(lastAngle), 3))); + acuteAngleBonus *= 0.08 + 0.92 * (1 - Math.Min(acuteAngleBonus, Math.Pow(calcAcuteAngleBonus(lastAngle), 3))); // Apply full wide angle bonus for distance more than one diameter wideAngleBonus *= angleBonus * DifficultyCalculationUtils.Smootherstep(osuCurrObj.LazyJumpDistance, 0, diameter); diff --git a/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceAttributes.cs b/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceAttributes.cs index 9c30c0f7c7..de4491a31b 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceAttributes.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceAttributes.cs @@ -24,9 +24,6 @@ namespace osu.Game.Rulesets.Osu.Difficulty [JsonProperty("effective_miss_count")] public double EffectiveMissCount { get; set; } - [JsonProperty("total_deviation")] - public double? TotalDeviation { get; set; } - [JsonProperty("speed_deviation")] public double? SpeedDeviation { get; set; } diff --git a/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs b/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs index a03e3fd6ef..7013ee55c4 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs @@ -42,7 +42,6 @@ namespace osu.Game.Rulesets.Osu.Difficulty /// private double effectiveMissCount; - private double? totalDeviation; private double? speedDeviation; public OsuPerformanceCalculator() @@ -115,7 +114,6 @@ namespace osu.Game.Rulesets.Osu.Difficulty effectiveMissCount = Math.Min(effectiveMissCount + countOk * okMultiplier + countMeh * mehMultiplier, totalHits); } - totalDeviation = calculateTotalDeviation(osuAttributes); speedDeviation = calculateSpeedDeviation(osuAttributes); double aimValue = computeAimValue(score, osuAttributes); @@ -138,7 +136,6 @@ namespace osu.Game.Rulesets.Osu.Difficulty Accuracy = accuracyValue, Flashlight = flashlightValue, EffectiveMissCount = effectiveMissCount, - TotalDeviation = totalDeviation, SpeedDeviation = speedDeviation, Total = totalValue }; @@ -149,9 +146,6 @@ namespace osu.Game.Rulesets.Osu.Difficulty if (score.Mods.Any(h => h is OsuModAutopilot)) return 0.0; - if (totalDeviation == null) - return 0; - double aimDifficulty = attributes.AimDifficulty; if (attributes.SliderCount > 0 && attributes.AimDifficultSliderCount > 0) @@ -203,7 +197,9 @@ namespace osu.Game.Rulesets.Osu.Difficulty aimValue *= 1.0 + 0.04 * (12.0 - attributes.ApproachRate); } - aimValue *= SpecialFunctions.Erf(25.0 / (Math.Sqrt(2) * totalDeviation.Value)); + aimValue *= accuracy; + // It is important to consider accuracy difficulty when scaling with accuracy. + aimValue *= 0.98 + Math.Pow(Math.Max(0, attributes.OverallDifficulty), 2) / 2500; return aimValue; } @@ -322,48 +318,6 @@ namespace osu.Game.Rulesets.Osu.Difficulty return flashlightValue; } - /// - /// Using estimates player's deviation on accuracy objects. - /// Returns deviation for circles and sliders if score was set with slideracc. - /// Returns the min between deviation of circles and deviation on circles and sliders (assuming slider hits are 50s), if score was set without slideracc. - /// - private double? calculateTotalDeviation(OsuDifficultyAttributes attributes) - { - if (totalSuccessfulHits == 0) - return null; - - int accuracyObjectCount = attributes.HitCircleCount; - - if (!usingClassicSliderAccuracy) - accuracyObjectCount += attributes.SliderCount; - - // Assume worst case: all mistakes was on accuracy objects - int relevantCountMiss = Math.Min(countMiss, accuracyObjectCount); - int relevantCountMeh = Math.Min(countMeh, accuracyObjectCount - relevantCountMiss); - int relevantCountOk = Math.Min(countOk, accuracyObjectCount - relevantCountMiss - relevantCountMeh); - int relevantCountGreat = Math.Max(0, accuracyObjectCount - relevantCountMiss - relevantCountMeh - relevantCountOk); - - // Calculate deviation on accuracy objects - double? deviation = calculateDeviation(attributes, relevantCountGreat, relevantCountOk, relevantCountMeh, relevantCountMiss); - if (deviation == null) - return null; - - if (!usingClassicSliderAccuracy) - return deviation.Value; - - // If score was set without slider accuracy - also compute deviation with sliders - // Assume that all hits was 50s - int totalCountWithSliders = attributes.HitCircleCount + attributes.SliderCount; - int missCountWithSliders = Math.Min(totalCountWithSliders, countMiss); - int hitCountWithSliders = totalCountWithSliders - missCountWithSliders; - - double hitProbabilityWithSliders = hitCountWithSliders / (totalCountWithSliders + 1.0); - double deviationWithSliders = attributes.MehHitWindow / (Math.Sqrt(2) * SpecialFunctions.ErfInv(hitProbabilityWithSliders)); - - // Min is needed for edgecase maps with 1 circle and 999 sliders, as deviation on sliders can be lower in this case - return Math.Min(deviation.Value, deviationWithSliders); - } - /// /// Estimates player's deviation on speed notes using , assuming worst-case. /// Treats all speed notes as hit circles. diff --git a/osu.Game.Rulesets.Osu/Difficulty/Skills/Aim.cs b/osu.Game.Rulesets.Osu/Difficulty/Skills/Aim.cs index 69211b610f..f04b679b73 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/Skills/Aim.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/Skills/Aim.cs @@ -26,7 +26,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills private double currentStrain; - private double skillMultiplier => 25.7; + private double skillMultiplier => 25.6; private double strainDecayBase => 0.15; private readonly List sliderStrains = new List(); From 5bed7c22e351a60a9ae22b2f736da4871646911d Mon Sep 17 00:00:00 2001 From: Natelytle <92956514+Natelytle@users.noreply.github.com> Date: Tue, 14 Jan 2025 14:12:08 -0500 Subject: [PATCH 0573/1275] Remove lower cap on deviation without misses (#31499) --- .../Difficulty/TaikoPerformanceCalculator.cs | 48 ++++--------------- 1 file changed, 8 insertions(+), 40 deletions(-) diff --git a/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceCalculator.cs b/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceCalculator.cs index 5da18e7963..4933c9dee6 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceCalculator.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceCalculator.cs @@ -123,53 +123,21 @@ namespace osu.Game.Rulesets.Taiko.Difficulty /// private double? computeDeviationUpperBound(TaikoDifficultyAttributes attributes) { - if (totalSuccessfulHits == 0 || attributes.GreatHitWindow <= 0) + if (countGreat == 0 || attributes.GreatHitWindow <= 0) return null; - double h300 = attributes.GreatHitWindow; - double h100 = attributes.OkHitWindow; - const double z = 2.32634787404; // 99% critical value for the normal distribution (one-tailed). - double? deviationGreatWindow = calcDeviationGreatWindow(); - double? deviationGoodWindow = calcDeviationGoodWindow(); + double n = totalHits; - return deviationGreatWindow is null ? deviationGoodWindow : Math.Min(deviationGreatWindow.Value, deviationGoodWindow!.Value); + // Proportion of greats hit. + double p = countGreat / n; - // The upper bound on deviation, calculated with the ratio of 300s to objects, and the great hit window. - double? calcDeviationGreatWindow() - { - if (countGreat == 0) return null; + // We can be 99% confident that p is at least this value. + double pLowerBound = (n * p + z * z / 2) / (n + z * z) - z / (n + z * z) * Math.Sqrt(n * p * (1 - p) + z * z / 4); - double n = totalHits; - - // Proportion of greats hit. - double p = countGreat / n; - - // We can be 99% confident that p is at least this value. - double pLowerBound = (n * p + z * z / 2) / (n + z * z) - z / (n + z * z) * Math.Sqrt(n * p * (1 - p) + z * z / 4); - - // We can be 99% confident that the deviation is not higher than: - return h300 / (Math.Sqrt(2) * SpecialFunctions.ErfInv(pLowerBound)); - } - - // The upper bound on deviation, calculated with the ratio of 300s + 100s to objects, and the good hit window. - // This will return a lower value than the first method when the number of 100s is high, but the miss count is low. - double? calcDeviationGoodWindow() - { - if (totalSuccessfulHits == 0) return null; - - double n = totalHits; - - // Proportion of greats + goods hit. - double p = Math.Max(0, totalSuccessfulHits - 0.0005 * countOk) / n; - - // We can be 99% confident that p is at least this value. - double pLowerBound = (n * p + z * z / 2) / (n + z * z) - z / (n + z * z) * Math.Sqrt(n * p * (1 - p) + z * z / 4); - - // We can be 99% confident that the deviation is not higher than: - return h100 / (Math.Sqrt(2) * SpecialFunctions.ErfInv(pLowerBound)); - } + // We can be 99% confident that the deviation is not higher than: + return attributes.GreatHitWindow / (Math.Sqrt(2) * SpecialFunctions.ErfInv(pLowerBound)); } private int totalHits => countGreat + countOk + countMeh + countMiss; From 208824e9f47de863860ac8a010cae9deabb0f20b Mon Sep 17 00:00:00 2001 From: Andrei Zavatski Date: Tue, 14 Jan 2025 21:40:14 +0300 Subject: [PATCH 0574/1275] 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 0575/1275] 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 0576/1275] 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 0577/1275] Fix substring of `GetHashCode` potentially failing --- osu.Game/Screens/SelectV2/Carousel.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/SelectV2/Carousel.cs b/osu.Game/Screens/SelectV2/Carousel.cs index 12f520d6c4..aeab6a96d0 100644 --- a/osu.Game/Screens/SelectV2/Carousel.cs +++ b/osu.Game/Screens/SelectV2/Carousel.cs @@ -206,7 +206,7 @@ namespace osu.Game.Screens.SelectV2 updateSelection(); - void log(string text) => Logger.Log($"Carousel[op {cts.GetHashCode().ToString().Substring(0, 5)}] {stopwatch.ElapsedMilliseconds} ms: {text}"); + void log(string text) => Logger.Log($"Carousel[op {cts.GetHashCode().ToString()}] {stopwatch.ElapsedMilliseconds} ms: {text}"); } private async Task updateYPositions(IEnumerable carouselItems, CancellationToken cancellationToken) => await Task.Run(() => From 8985a387344b91ce8ec48da0bfc183db67b14b4f Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Wed, 15 Jan 2025 16:53:55 +0900 Subject: [PATCH 0578/1275] Display up-to-date online status in user panels --- .../Visual/Online/TestSceneUserPanel.cs | 221 +++++++++--------- osu.Game/Online/Metadata/MetadataClient.cs | 16 ++ .../Dashboard/CurrentlyOnlineDisplay.cs | 59 ++--- osu.Game/Users/ExtendedUserPanel.cs | 105 +++++---- 4 files changed, 202 insertions(+), 199 deletions(-) diff --git a/osu.Game.Tests/Visual/Online/TestSceneUserPanel.cs b/osu.Game.Tests/Visual/Online/TestSceneUserPanel.cs index 4c2e47d336..b4dafd3107 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneUserPanel.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneUserPanel.cs @@ -4,17 +4,18 @@ using System; using NUnit.Framework; using osu.Framework.Allocation; -using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Utils; using osu.Game.Beatmaps; using osu.Game.Online; using osu.Game.Online.API.Requests.Responses; +using osu.Game.Online.Metadata; using osu.Game.Overlays; using osu.Game.Rulesets; using osu.Game.Scoring; using osu.Game.Tests.Beatmaps; +using osu.Game.Tests.Visual.Metadata; using osu.Game.Users; using osuTK; @@ -23,144 +24,138 @@ namespace osu.Game.Tests.Visual.Online [TestFixture] public partial class TestSceneUserPanel : OsuTestScene { - private readonly Bindable activity = new Bindable(); - private readonly Bindable status = new Bindable(); - - private UserGridPanel boundPanel1 = null!; - private TestUserListPanel boundPanel2 = null!; - [Cached] private readonly OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Purple); - [Cached(typeof(LocalUserStatisticsProvider))] - private readonly TestUserStatisticsProvider statisticsProvider = new TestUserStatisticsProvider(); - [Resolved] private IRulesetStore rulesetStore { get; set; } = null!; + private TestUserStatisticsProvider statisticsProvider = null!; + private TestMetadataClient metadataClient = null!; + private TestUserListPanel panel = null!; + [SetUp] public void SetUp() => Schedule(() => { - activity.Value = null; - status.Value = null; - - Remove(statisticsProvider, false); - Clear(); - Add(statisticsProvider); - - Add(new FillFlowContainer + Child = new DependencyProvidingContainer { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - AutoSizeAxes = Axes.Y, - RelativeSizeAxes = Axes.X, - Spacing = new Vector2(10f), + RelativeSizeAxes = Axes.Both, + CachedDependencies = + [ + (typeof(LocalUserStatisticsProvider), statisticsProvider = new TestUserStatisticsProvider()), + (typeof(MetadataClient), metadataClient = new TestMetadataClient()) + ], Children = new Drawable[] { - new UserBrickPanel(new APIUser + statisticsProvider, + metadataClient, + new FillFlowContainer { - Username = @"flyte", - Id = 3103765, - CoverUrl = @"https://assets.ppy.sh/user-cover-presets/1/df28696b58541a9e67f6755918951d542d93bdf1da41720fcca2fd2c1ea8cf51.jpeg", - }), - new UserBrickPanel(new APIUser - { - Username = @"peppy", - Id = 2, - Colour = "99EB47", - CoverUrl = @"https://assets.ppy.sh/user-profile-covers/8195163/4a8e2ad5a02a2642b631438cfa6c6bd7e2f9db289be881cb27df18331f64144c.jpeg", - }), - new UserGridPanel(new APIUser - { - Username = @"flyte", - Id = 3103765, - CountryCode = CountryCode.JP, - CoverUrl = @"https://assets.ppy.sh/user-cover-presets/1/df28696b58541a9e67f6755918951d542d93bdf1da41720fcca2fd2c1ea8cf51.jpeg", - IsOnline = true - }) { Width = 300 }, - boundPanel1 = new UserGridPanel(new APIUser - { - Username = @"peppy", - Id = 2, - CountryCode = CountryCode.AU, - CoverUrl = @"https://assets.ppy.sh/user-profile-covers/8195163/4a8e2ad5a02a2642b631438cfa6c6bd7e2f9db289be881cb27df18331f64144c.jpeg", - IsSupporter = true, - SupportLevel = 3, - }) { Width = 300 }, - boundPanel2 = new TestUserListPanel(new APIUser - { - Username = @"Evast", - Id = 8195163, - CountryCode = CountryCode.BY, - CoverUrl = @"https://assets.ppy.sh/user-profile-covers/8195163/4a8e2ad5a02a2642b631438cfa6c6bd7e2f9db289be881cb27df18331f64144c.jpeg", - IsOnline = false, - LastVisit = DateTimeOffset.Now - }), - new UserRankPanel(new APIUser - { - Username = @"flyte", - Id = 3103765, - CountryCode = CountryCode.JP, - CoverUrl = @"https://assets.ppy.sh/user-cover-presets/1/df28696b58541a9e67f6755918951d542d93bdf1da41720fcca2fd2c1ea8cf51.jpeg", - Statistics = new UserStatistics { GlobalRank = 12345, CountryRank = 1234 } - }) { Width = 300 }, - new UserRankPanel(new APIUser - { - Username = @"peppy", - Id = 2, - Colour = "99EB47", - CountryCode = CountryCode.AU, - CoverUrl = @"https://assets.ppy.sh/user-profile-covers/8195163/4a8e2ad5a02a2642b631438cfa6c6bd7e2f9db289be881cb27df18331f64144c.jpeg", - Statistics = new UserStatistics { GlobalRank = null, CountryRank = null } - }) { Width = 300 } + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + AutoSizeAxes = Axes.Y, + RelativeSizeAxes = Axes.X, + Spacing = new Vector2(10f), + Children = new Drawable[] + { + new UserBrickPanel(new APIUser + { + Username = @"flyte", + Id = 3103765, + CoverUrl = @"https://assets.ppy.sh/user-cover-presets/1/df28696b58541a9e67f6755918951d542d93bdf1da41720fcca2fd2c1ea8cf51.jpeg", + }), + new UserBrickPanel(new APIUser + { + Username = @"peppy", + Id = 2, + Colour = "99EB47", + CoverUrl = @"https://assets.ppy.sh/user-profile-covers/8195163/4a8e2ad5a02a2642b631438cfa6c6bd7e2f9db289be881cb27df18331f64144c.jpeg", + }), + new UserGridPanel(new APIUser + { + Username = @"flyte", + Id = 3103765, + CountryCode = CountryCode.JP, + CoverUrl = @"https://assets.ppy.sh/user-cover-presets/1/df28696b58541a9e67f6755918951d542d93bdf1da41720fcca2fd2c1ea8cf51.jpeg", + IsOnline = true + }) { Width = 300 }, + new UserGridPanel(new APIUser + { + Username = @"peppy", + Id = 2, + CountryCode = CountryCode.AU, + CoverUrl = @"https://assets.ppy.sh/user-profile-covers/8195163/4a8e2ad5a02a2642b631438cfa6c6bd7e2f9db289be881cb27df18331f64144c.jpeg", + IsSupporter = true, + SupportLevel = 3, + }) { Width = 300 }, + panel = new TestUserListPanel(new APIUser + { + Username = @"peppy", + Id = 2, + CountryCode = CountryCode.AU, + CoverUrl = @"https://assets.ppy.sh/user-profile-covers/8195163/4a8e2ad5a02a2642b631438cfa6c6bd7e2f9db289be881cb27df18331f64144c.jpeg", + LastVisit = DateTimeOffset.Now + }), + new UserRankPanel(new APIUser + { + Username = @"flyte", + Id = 3103765, + CountryCode = CountryCode.JP, + CoverUrl = @"https://assets.ppy.sh/user-cover-presets/1/df28696b58541a9e67f6755918951d542d93bdf1da41720fcca2fd2c1ea8cf51.jpeg", + Statistics = new UserStatistics { GlobalRank = 12345, CountryRank = 1234 } + }) { Width = 300 }, + new UserRankPanel(new APIUser + { + Username = @"peppy", + Id = 2, + Colour = "99EB47", + CountryCode = CountryCode.AU, + CoverUrl = @"https://assets.ppy.sh/user-profile-covers/8195163/4a8e2ad5a02a2642b631438cfa6c6bd7e2f9db289be881cb27df18331f64144c.jpeg", + Statistics = new UserStatistics { GlobalRank = null, CountryRank = null } + }) { Width = 300 } + } + } } - }); + }; - boundPanel1.Status.BindTo(status); - boundPanel1.Activity.BindTo(activity); - - boundPanel2.Status.BindTo(status); - boundPanel2.Activity.BindTo(activity); + metadataClient.BeginWatchingUserPresence(); }); [Test] public void TestUserStatus() { - AddStep("online", () => status.Value = UserStatus.Online); - AddStep("do not disturb", () => status.Value = UserStatus.DoNotDisturb); - AddStep("offline", () => status.Value = UserStatus.Offline); - AddStep("null status", () => status.Value = null); + AddStep("online", () => setPresence(UserStatus.Online, null)); + AddStep("do not disturb", () => setPresence(UserStatus.DoNotDisturb, null)); + AddStep("offline", () => setPresence(UserStatus.Offline, null)); } [Test] public void TestUserActivity() { - AddStep("set online status", () => status.Value = UserStatus.Online); - - AddStep("idle", () => activity.Value = null); - AddStep("watching replay", () => activity.Value = new UserActivity.WatchingReplay(createScore(@"nats"))); - AddStep("spectating user", () => activity.Value = new UserActivity.SpectatingUser(createScore(@"mrekk"))); - AddStep("solo (osu!)", () => activity.Value = soloGameStatusForRuleset(0)); - AddStep("solo (osu!taiko)", () => activity.Value = soloGameStatusForRuleset(1)); - AddStep("solo (osu!catch)", () => activity.Value = soloGameStatusForRuleset(2)); - AddStep("solo (osu!mania)", () => activity.Value = soloGameStatusForRuleset(3)); - AddStep("choosing", () => activity.Value = new UserActivity.ChoosingBeatmap()); - AddStep("editing beatmap", () => activity.Value = new UserActivity.EditingBeatmap(new BeatmapInfo())); - AddStep("modding beatmap", () => activity.Value = new UserActivity.ModdingBeatmap(new BeatmapInfo())); - AddStep("testing beatmap", () => activity.Value = new UserActivity.TestingBeatmap(new BeatmapInfo())); + AddStep("idle", () => setPresence(UserStatus.Online, null)); + AddStep("watching replay", () => setPresence(UserStatus.Online, new UserActivity.WatchingReplay(createScore(@"nats")))); + AddStep("spectating user", () => setPresence(UserStatus.Online, new UserActivity.SpectatingUser(createScore(@"mrekk")))); + AddStep("solo (osu!)", () => setPresence(UserStatus.Online, soloGameStatusForRuleset(0))); + AddStep("solo (osu!taiko)", () => setPresence(UserStatus.Online, soloGameStatusForRuleset(1))); + AddStep("solo (osu!catch)", () => setPresence(UserStatus.Online, soloGameStatusForRuleset(2))); + AddStep("solo (osu!mania)", () => setPresence(UserStatus.Online, soloGameStatusForRuleset(3))); + AddStep("choosing", () => setPresence(UserStatus.Online, new UserActivity.ChoosingBeatmap())); + AddStep("editing beatmap", () => setPresence(UserStatus.Online, new UserActivity.EditingBeatmap(new BeatmapInfo()))); + AddStep("modding beatmap", () => setPresence(UserStatus.Online, new UserActivity.ModdingBeatmap(new BeatmapInfo()))); + AddStep("testing beatmap", () => setPresence(UserStatus.Online, new UserActivity.TestingBeatmap(new BeatmapInfo()))); } [Test] public void TestUserActivityChange() { - AddAssert("visit message is visible", () => boundPanel2.LastVisitMessage.IsPresent); - AddStep("set online status", () => status.Value = UserStatus.Online); - AddAssert("visit message is not visible", () => !boundPanel2.LastVisitMessage.IsPresent); - AddStep("set choosing activity", () => activity.Value = new UserActivity.ChoosingBeatmap()); - AddStep("set offline status", () => status.Value = UserStatus.Offline); - AddAssert("visit message is visible", () => boundPanel2.LastVisitMessage.IsPresent); - AddStep("set online status", () => status.Value = UserStatus.Online); - AddAssert("visit message is not visible", () => !boundPanel2.LastVisitMessage.IsPresent); + AddAssert("visit message is visible", () => panel.LastVisitMessage.IsPresent); + AddStep("set online status", () => setPresence(UserStatus.Online, null)); + AddAssert("visit message is not visible", () => !panel.LastVisitMessage.IsPresent); + AddStep("set choosing activity", () => setPresence(UserStatus.Online, new UserActivity.ChoosingBeatmap())); + AddStep("set offline status", () => setPresence(UserStatus.Offline, null)); + AddAssert("visit message is visible", () => panel.LastVisitMessage.IsPresent); + AddStep("set online status", () => setPresence(UserStatus.Online, null)); + AddAssert("visit message is not visible", () => !panel.LastVisitMessage.IsPresent); } [Test] @@ -185,6 +180,14 @@ namespace osu.Game.Tests.Visual.Online AddStep("set statistics to empty", () => statisticsProvider.UpdateStatistics(new UserStatistics(), Ruleset.Value)); } + private void setPresence(UserStatus status, UserActivity? activity) + { + if (status == UserStatus.Offline) + metadataClient.UserPresenceUpdated(panel.User.OnlineID, null); + else + metadataClient.UserPresenceUpdated(panel.User.OnlineID, new UserPresence { Status = status, Activity = activity }); + } + private UserActivity soloGameStatusForRuleset(int rulesetId) => new UserActivity.InSoloGame(new BeatmapInfo(), rulesetStore.GetRuleset(rulesetId)!); private ScoreInfo createScore(string name) => new ScoreInfo(new TestBeatmap(Ruleset.Value).BeatmapInfo) diff --git a/osu.Game/Online/Metadata/MetadataClient.cs b/osu.Game/Online/Metadata/MetadataClient.cs index 6578f70f74..8f1fe0641f 100644 --- a/osu.Game/Online/Metadata/MetadataClient.cs +++ b/osu.Game/Online/Metadata/MetadataClient.cs @@ -47,6 +47,22 @@ namespace osu.Game.Online.Metadata /// public abstract IBindableDictionary FriendStates { get; } + /// + /// Attempts to retrieve the presence of a user. + /// + /// The user ID. + /// The user presence, or null if not available or the user's offline. + public UserPresence? GetPresence(int userId) + { + if (FriendStates.TryGetValue(userId, out UserPresence presence)) + return presence; + + if (UserStates.TryGetValue(userId, out presence)) + return presence; + + return null; + } + /// public abstract Task UpdateActivity(UserActivity? activity); diff --git a/osu.Game/Overlays/Dashboard/CurrentlyOnlineDisplay.cs b/osu.Game/Overlays/Dashboard/CurrentlyOnlineDisplay.cs index 2ca548fdf5..ef07f4538c 100644 --- a/osu.Game/Overlays/Dashboard/CurrentlyOnlineDisplay.cs +++ b/osu.Game/Overlays/Dashboard/CurrentlyOnlineDisplay.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System.Collections.Generic; using System.Collections.Specialized; using System.Diagnostics; @@ -40,17 +38,20 @@ namespace osu.Game.Overlays.Dashboard private readonly IBindableDictionary onlineUsers = new BindableDictionary(); private readonly Dictionary userPanels = new Dictionary(); - private SearchContainer userFlow; - private BasicSearchTextBox searchTextBox; + private SearchContainer userFlow = null!; + private BasicSearchTextBox searchTextBox = null!; [Resolved] - private IAPIProvider api { get; set; } + private IAPIProvider api { get; set; } = null!; [Resolved] - private SpectatorClient spectatorClient { get; set; } + private SpectatorClient spectatorClient { get; set; } = null!; [Resolved] - private MetadataClient metadataClient { get; set; } + private MetadataClient metadataClient { get; set; } = null!; + + [Resolved] + private UserLookupCache users { get; set; } = null!; [BackgroundDependencyLoader] private void load(OverlayColourProvider colourProvider) @@ -99,9 +100,6 @@ namespace osu.Game.Overlays.Dashboard searchTextBox.Current.ValueChanged += text => userFlow.SearchTerm = text.NewValue; } - [Resolved] - private UserLookupCache users { get; set; } - protected override void LoadComplete() { base.LoadComplete(); @@ -120,7 +118,7 @@ namespace osu.Game.Overlays.Dashboard searchTextBox.TakeFocus(); } - private void onUserUpdated(object sender, NotifyDictionaryChangedEventArgs e) => Schedule(() => + private void onUserUpdated(object? sender, NotifyDictionaryChangedEventArgs e) => Schedule(() => { switch (e.Action) { @@ -133,38 +131,13 @@ namespace osu.Game.Overlays.Dashboard users.GetUserAsync(userId).ContinueWith(task => { - APIUser user = task.GetResultSafely(); - - if (user == null) - return; - - Schedule(() => - { - userFlow.Add(userPanels[userId] = createUserPanel(user).With(p => - { - p.Status.Value = onlineUsers.GetValueOrDefault(userId).Status; - p.Activity.Value = onlineUsers.GetValueOrDefault(userId).Activity; - })); - }); + if (task.GetResultSafely() is APIUser user) + Schedule(() => userFlow.Add(userPanels[userId] = createUserPanel(user))); }); } break; - case NotifyDictionaryChangedAction.Replace: - Debug.Assert(e.NewItems != null); - - foreach (var kvp in e.NewItems) - { - if (userPanels.TryGetValue(kvp.Key, out var panel)) - { - panel.Activity.Value = kvp.Value.Activity; - panel.Status.Value = kvp.Value.Status; - } - } - - break; - case NotifyDictionaryChangedAction.Remove: Debug.Assert(e.OldItems != null); @@ -179,7 +152,7 @@ namespace osu.Game.Overlays.Dashboard } }); - private void onPlayingUsersChanged(object sender, NotifyCollectionChangedEventArgs e) + private void onPlayingUsersChanged(object? sender, NotifyCollectionChangedEventArgs e) { switch (e.Action) { @@ -219,9 +192,6 @@ namespace osu.Game.Overlays.Dashboard { public readonly APIUser User; - public readonly Bindable Status = new Bindable(); - public readonly Bindable Activity = new Bindable(); - public BindableBool CanSpectate { get; } = new BindableBool(); public IEnumerable FilterTerms { get; } @@ -268,10 +238,7 @@ namespace osu.Game.Overlays.Dashboard { RelativeSizeAxes = Axes.X, Anchor = Anchor.TopCentre, - Origin = Anchor.TopCentre, - // this is SHOCKING - Activity = { BindTarget = Activity }, - Status = { BindTarget = Status }, + Origin = Anchor.TopCentre }, new PurpleRoundedButton { diff --git a/osu.Game/Users/ExtendedUserPanel.cs b/osu.Game/Users/ExtendedUserPanel.cs index e33fb7a44e..eb1115e296 100644 --- a/osu.Game/Users/ExtendedUserPanel.cs +++ b/osu.Game/Users/ExtendedUserPanel.cs @@ -1,10 +1,8 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - +using System; using osu.Framework.Allocation; -using osu.Framework.Bindables; using osu.Framework.Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; @@ -14,44 +12,56 @@ using osu.Game.Graphics.Sprites; using osu.Game.Users.Drawables; using osu.Framework.Input.Events; using osu.Framework.Localisation; +using osu.Game.Online.API; using osu.Game.Online.API.Requests.Responses; +using osu.Game.Online.Metadata; namespace osu.Game.Users { public abstract partial class ExtendedUserPanel : UserPanel { - public readonly Bindable Status = new Bindable(); + protected TextFlowContainer LastVisitMessage { get; private set; } = null!; - public readonly IBindable Activity = new Bindable(); + private StatusIcon statusIcon = null!; + private StatusText statusMessage = null!; - protected TextFlowContainer LastVisitMessage { get; private set; } + [Resolved] + private MetadataClient? metadata { get; set; } - private StatusIcon statusIcon; - private StatusText statusMessage; + [Resolved] + private IAPIProvider? api { get; set; } + + private UserStatus? lastStatus; + private UserActivity? lastActivity; + private DateTimeOffset? lastVisit; protected ExtendedUserPanel(APIUser user) : base(user) { + lastVisit = user.LastVisit; } [BackgroundDependencyLoader] private void load() { BorderColour = ColourProvider?.Light1 ?? Colours.GreyVioletLighter; - - Status.ValueChanged += status => displayStatus(status.NewValue, Activity.Value); - Activity.ValueChanged += activity => displayStatus(Status.Value, activity.NewValue); } protected override void LoadComplete() { base.LoadComplete(); - Status.TriggerChange(); + updatePresence(); // Colour should be applied immediately on first load. statusIcon.FinishTransforms(); } + protected override void Update() + { + base.Update(); + updatePresence(); + } + protected Container CreateStatusIcon() => statusIcon = new StatusIcon(); protected FillFlowContainer CreateStatusMessage(bool rightAlignedChildren) @@ -70,15 +80,6 @@ namespace osu.Game.Users text.Origin = alignment; text.AutoSizeAxes = Axes.Both; text.Alpha = 0; - - if (User.LastVisit.HasValue) - { - text.AddText(@"Last seen "); - text.AddText(new DrawableDate(User.LastVisit.Value, italic: false) - { - Shadow = false - }); - } })); statusContainer.Add(statusMessage = new StatusText @@ -91,37 +92,53 @@ namespace osu.Game.Users return statusContainer; } - private void displayStatus(UserStatus? status, UserActivity activity = null) + private void updatePresence() { - if (status != null) + UserPresence? presence; + + if (User.Equals(api?.LocalUser.Value)) + presence = new UserPresence { Status = api.Status.Value, Activity = api.Activity.Value }; + else + presence = metadata?.GetPresence(User.OnlineID); + + UserStatus status = presence?.Status ?? UserStatus.Offline; + UserActivity? activity = presence?.Activity; + + if (status == lastStatus && activity == lastActivity) + return; + + if (status == UserStatus.Offline && lastVisit != null) { - LastVisitMessage.FadeTo(status == UserStatus.Offline && User.LastVisit.HasValue ? 1 : 0); - - // Set status message based on activity (if we have one) and status is not offline - if (activity != null && status != UserStatus.Offline) + LastVisitMessage.FadeTo(1); + LastVisitMessage.Clear(); + LastVisitMessage.AddText(@"Last seen "); + LastVisitMessage.AddText(new DrawableDate(lastVisit.Value, italic: false) { - statusMessage.Text = activity.GetStatus(); - statusMessage.TooltipText = activity.GetDetails(); - statusIcon.FadeColour(activity.GetAppropriateColour(Colours), 500, Easing.OutQuint); - return; - } + Shadow = false + }); + } + else + LastVisitMessage.FadeTo(0); - // Otherwise use only status + // Set status message based on activity (if we have one) and status is not offline + if (activity != null && status != UserStatus.Offline) + { + statusMessage.Text = activity.GetStatus(); + statusMessage.TooltipText = activity.GetDetails() ?? string.Empty; + statusIcon.FadeColour(activity.GetAppropriateColour(Colours), 500, Easing.OutQuint); + } + + // Otherwise use only status + else + { statusMessage.Text = status.GetLocalisableDescription(); statusMessage.TooltipText = string.Empty; - statusIcon.FadeColour(status.Value.GetAppropriateColour(Colours), 500, Easing.OutQuint); - - return; + statusIcon.FadeColour(status.GetAppropriateColour(Colours), 500, Easing.OutQuint); } - // Fallback to web status if local one is null - if (User.IsOnline) - { - Status.Value = UserStatus.Online; - return; - } - - Status.Value = UserStatus.Offline; + lastStatus = status; + lastActivity = activity; + lastVisit = status != UserStatus.Offline ? DateTimeOffset.Now : lastVisit; } protected override bool OnHover(HoverEvent e) From 60279476570a20b5a9bf40525c615078a83c5e6a Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 15 Jan 2025 17:01:07 +0900 Subject: [PATCH 0579/1275] Move animation handling to `Carousel` implementation to better handle add/removes With the animation logic being external, it was going to make it very hard to apply the scroll offset when a new panel is added or removed before the current selection. There's no real reason for the animations to be local to beatmap carousel. If there's a usage in the future where the animation is to change, we can add more customisation to `Carousel` itself. --- .../SongSelect/TestSceneBeatmapCarouselV2.cs | 28 ++++++++++++++- .../Screens/SelectV2/BeatmapCarouselPanel.cs | 15 +------- osu.Game/Screens/SelectV2/Carousel.cs | 36 ++++++++++++++++--- osu.Game/Screens/SelectV2/ICarouselPanel.cs | 4 +-- 4 files changed, 62 insertions(+), 21 deletions(-) diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2.cs index 1d7d6041ae..f99e0a418a 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2.cs @@ -168,7 +168,33 @@ namespace osu.Game.Tests.Visual.SongSelect } [Test] - public void TestScrollPositionVelocityMaintained() + public void TestScrollPositionMaintainedOnAddSecondSelected() + { + Quad positionBefore = default; + + AddStep("add 10 beatmaps", () => + { + for (int i = 0; i < 10; i++) + beatmapSets.Add(TestResources.CreateTestBeatmapSetInfo(RNG.Next(1, 4))); + }); + + AddUntilStep("visual item added", () => carousel.ChildrenOfType().Count(), () => Is.GreaterThan(0)); + + AddStep("select middle beatmap", () => carousel.CurrentSelection = beatmapSets.ElementAt(beatmapSets.Count - 2)); + AddStep("scroll to selected item", () => scroll.ScrollTo(scroll.ChildrenOfType().Single(p => p.Item!.Selected.Value))); + + AddUntilStep("wait for scroll finished", () => scroll.Current, () => Is.EqualTo(scroll.Target)); + + AddStep("save selected screen position", () => positionBefore = carousel.ChildrenOfType().FirstOrDefault(p => p.Item!.Selected.Value)!.ScreenSpaceDrawQuad); + + AddStep("remove first beatmap", () => beatmapSets.Remove(beatmapSets.Last())); + AddUntilStep("sorting finished", () => carousel.IsFiltering, () => Is.False); + AddAssert("select screen position unchanged", () => carousel.ChildrenOfType().Single(p => p.Item!.Selected.Value).ScreenSpaceDrawQuad, + () => Is.EqualTo(positionBefore)); + } + + [Test] + public void TestScrollPositionMaintainedOnAddLastSelected() { Quad positionBefore = default; diff --git a/osu.Game/Screens/SelectV2/BeatmapCarouselPanel.cs b/osu.Game/Screens/SelectV2/BeatmapCarouselPanel.cs index 5b8ae211d1..27023b50be 100644 --- a/osu.Game/Screens/SelectV2/BeatmapCarouselPanel.cs +++ b/osu.Game/Screens/SelectV2/BeatmapCarouselPanel.cs @@ -9,7 +9,6 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Pooling; using osu.Framework.Graphics.Shapes; using osu.Framework.Input.Events; -using osu.Framework.Utils; using osu.Game.Beatmaps; using osu.Game.Graphics.Sprites; using osuTK; @@ -98,18 +97,6 @@ namespace osu.Game.Screens.SelectV2 return true; } - protected override void Update() - { - base.Update(); - - Debug.Assert(Item != null); - - if (DrawYPosition != Item.CarouselYPosition) - { - DrawYPosition = Interpolation.DampContinuously(DrawYPosition, Item.CarouselYPosition, 50, Time.Elapsed); - } - } - - public double DrawYPosition { get; private set; } + public double DrawYPosition { get; set; } } } diff --git a/osu.Game/Screens/SelectV2/Carousel.cs b/osu.Game/Screens/SelectV2/Carousel.cs index aeab6a96d0..12a86be7b9 100644 --- a/osu.Game/Screens/SelectV2/Carousel.cs +++ b/osu.Game/Screens/SelectV2/Carousel.cs @@ -14,6 +14,7 @@ using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Pooling; using osu.Framework.Graphics.Shapes; using osu.Framework.Logging; +using osu.Framework.Utils; using osu.Game.Graphics.Containers; using osuTK; using osuTK.Graphics; @@ -107,7 +108,7 @@ namespace osu.Game.Screens.SelectV2 private List? displayedCarouselItems; - private readonly DoublePrecisionScroll scroll; + private readonly CarouselScrollContainer scroll; protected Carousel() { @@ -118,7 +119,7 @@ namespace osu.Game.Screens.SelectV2 Colour = Color4.Black, RelativeSizeAxes = Axes.Both, }, - scroll = new DoublePrecisionScroll + scroll = new CarouselScrollContainer { RelativeSizeAxes = Axes.Both, Masking = false, @@ -389,13 +390,13 @@ namespace osu.Game.Screens.SelectV2 /// Implementation of scroll container which handles very large vertical lists by internally using double precision /// for pre-display Y values. /// - private partial class DoublePrecisionScroll : OsuScrollContainer + private partial class CarouselScrollContainer : OsuScrollContainer { public readonly Container Panels; public void SetLayoutHeight(float height) => Panels.Height = height; - public DoublePrecisionScroll() + public CarouselScrollContainer() { // Managing our own custom layout within ScrollContent causes feedback with public ScrollContainer calculations, // so we must maintain one level of separation from ScrollContent. @@ -406,6 +407,33 @@ namespace osu.Game.Screens.SelectV2 }); } + public override void OffsetScrollPosition(double offset) + { + base.OffsetScrollPosition(offset); + + foreach (var panel in Panels) + { + var c = (ICarouselPanel)panel; + Debug.Assert(c.Item != null); + + c.DrawYPosition += offset; + } + } + + protected override void Update() + { + base.Update(); + + foreach (var panel in Panels) + { + var c = (ICarouselPanel)panel; + Debug.Assert(c.Item != null); + + if (c.DrawYPosition != c.Item.CarouselYPosition) + c.DrawYPosition = Interpolation.DampContinuously(c.DrawYPosition, c.Item.CarouselYPosition, 50, Time.Elapsed); + } + } + public override void Clear(bool disposeChildren) { Panels.Height = 0; diff --git a/osu.Game/Screens/SelectV2/ICarouselPanel.cs b/osu.Game/Screens/SelectV2/ICarouselPanel.cs index d729df7876..117feab621 100644 --- a/osu.Game/Screens/SelectV2/ICarouselPanel.cs +++ b/osu.Game/Screens/SelectV2/ICarouselPanel.cs @@ -11,9 +11,9 @@ namespace osu.Game.Screens.SelectV2 public interface ICarouselPanel { /// - /// The Y position which should be used for displaying this item within the carousel. + /// The Y position which should be used for displaying this item within the carousel. This is managed by and should not be set manually. /// - double DrawYPosition { get; } + double DrawYPosition { get; set; } /// /// The carousel item this drawable is representing. This is managed by and should not be set manually. From 2763cb0b4e9febbfc7f9d185c4acc737214e9a58 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Wed, 15 Jan 2025 17:14:16 +0900 Subject: [PATCH 0580/1275] Fix inspection --- osu.Game/Overlays/Dashboard/CurrentlyOnlineDisplay.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Overlays/Dashboard/CurrentlyOnlineDisplay.cs b/osu.Game/Overlays/Dashboard/CurrentlyOnlineDisplay.cs index ef07f4538c..e6e1850721 100644 --- a/osu.Game/Overlays/Dashboard/CurrentlyOnlineDisplay.cs +++ b/osu.Game/Overlays/Dashboard/CurrentlyOnlineDisplay.cs @@ -196,8 +196,8 @@ namespace osu.Game.Overlays.Dashboard public IEnumerable FilterTerms { get; } - [Resolved(canBeNull: true)] - private IPerformFromScreenRunner performer { get; set; } + [Resolved] + private IPerformFromScreenRunner? performer { get; set; } public bool FilteringActive { set; get; } From 7ca3a6fc26f78c639ddefb725c25f40442c94dc6 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Wed, 15 Jan 2025 17:48:22 +0900 Subject: [PATCH 0581/1275] 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 0a21183e54648953b653e2e56b8150a11a93c69a Mon Sep 17 00:00:00 2001 From: Jay Lawton Date: Wed, 15 Jan 2025 20:34:21 +1000 Subject: [PATCH 0582/1275] reading mono nerf (#31510) --- osu.Game.Rulesets.Taiko/Difficulty/Skills/Reading.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Skills/Reading.cs b/osu.Game.Rulesets.Taiko/Difficulty/Skills/Reading.cs index 9de058f289..885131404a 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Skills/Reading.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Skills/Reading.cs @@ -3,6 +3,7 @@ using osu.Game.Rulesets.Difficulty.Preprocessing; using osu.Game.Rulesets.Difficulty.Skills; +using osu.Game.Rulesets.Difficulty.Utils; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Taiko.Difficulty.Evaluators; using osu.Game.Rulesets.Taiko.Difficulty.Preprocessing; @@ -34,6 +35,9 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Skills } var taikoObject = (TaikoDifficultyHitObject)current; + int index = taikoObject.Colour.MonoStreak?.HitObjects.IndexOf(taikoObject) ?? 0; + + currentStrain *= DifficultyCalculationUtils.Logistic(index, 4, -1 / 25.0, 0.5) + 0.5; currentStrain *= StrainDecayBase; currentStrain += ReadingEvaluator.EvaluateDifficultyOf(taikoObject) * SkillMultiplier; From e22dc09149097555fe81b66e5ff8ef36fca9caaf Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 15 Jan 2025 20:42:46 +0900 Subject: [PATCH 0583/1275] 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 0584/1275] Implement spectator list display - First step for https://github.com/ppy/osu/issues/22087 - Supersedes / closes https://github.com/ppy/osu/pull/22795 Roughly uses design shown in https://github.com/ppy/osu/pull/22795#issuecomment-1579936284 with some modifications to better fit everything else, and some customisation options so it can fit better on other skins. --- .../Visual/Gameplay/TestSceneSpectatorList.cs | 49 ++++ .../Localisation/HUD/SpectatorListStrings.cs | 19 ++ osu.Game/Online/Chat/DrawableLinkCompiler.cs | 16 +- osu.Game/Screens/Play/HUD/SpectatorList.cs | 219 ++++++++++++++++++ 4 files changed, 302 insertions(+), 1 deletion(-) create mode 100644 osu.Game.Tests/Visual/Gameplay/TestSceneSpectatorList.cs create mode 100644 osu.Game/Localisation/HUD/SpectatorListStrings.cs create mode 100644 osu.Game/Screens/Play/HUD/SpectatorList.cs diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSpectatorList.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSpectatorList.cs new file mode 100644 index 0000000000..3cd37baafd --- /dev/null +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSpectatorList.cs @@ -0,0 +1,49 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Threading; +using NUnit.Framework; +using osu.Framework.Bindables; +using osu.Framework.Graphics; +using osu.Framework.Utils; +using osu.Game.Graphics; +using osu.Game.Screens.Play; +using osu.Game.Screens.Play.HUD; + +namespace osu.Game.Tests.Visual.Gameplay +{ + [TestFixture] + public partial class TestSceneSpectatorList : OsuTestScene + { + private readonly BindableList spectators = new BindableList(); + private readonly Bindable localUserPlayingState = new Bindable(); + + private int counter; + + [Test] + public void TestBasics() + { + SpectatorList list = null!; + AddStep("create spectator list", () => Child = list = new SpectatorList + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Spectators = { BindTarget = spectators }, + UserPlayingState = { BindTarget = localUserPlayingState } + }); + + AddStep("start playing", () => localUserPlayingState.Value = LocalUserPlayingState.Playing); + AddStep("add a user", () => + { + int id = Interlocked.Increment(ref counter); + spectators.Add(new SpectatorList.Spectator(id, $"User {id}")); + }); + AddStep("remove random user", () => spectators.RemoveAt(RNG.Next(0, spectators.Count))); + AddStep("enter break", () => localUserPlayingState.Value = LocalUserPlayingState.Break); + AddStep("stop playing", () => localUserPlayingState.Value = LocalUserPlayingState.NotPlaying); + AddStep("change font to venera", () => list.Font.Value = Typeface.Venera); + AddStep("change font to torus", () => list.Font.Value = Typeface.Torus); + AddStep("change header colour", () => list.HeaderColour.Value = new Colour4(RNG.NextSingle(), RNG.NextSingle(), RNG.NextSingle(), 1)); + } + } +} diff --git a/osu.Game/Localisation/HUD/SpectatorListStrings.cs b/osu.Game/Localisation/HUD/SpectatorListStrings.cs new file mode 100644 index 0000000000..8d82250526 --- /dev/null +++ b/osu.Game/Localisation/HUD/SpectatorListStrings.cs @@ -0,0 +1,19 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Localisation; + +namespace osu.Game.Localisation.HUD +{ + public static class SpectatorListStrings + { + private const string prefix = @"osu.Game.Resources.Localisation.SpectatorList"; + + /// + /// "Spectators ({0})" + /// + public static LocalisableString SpectatorCount(int arg0) => new TranslatableString(getKey(@"spectator_count"), @"Spectators ({0})", arg0); + + private static string getKey(string key) => $@"{prefix}:{key}"; + } +} diff --git a/osu.Game/Online/Chat/DrawableLinkCompiler.cs b/osu.Game/Online/Chat/DrawableLinkCompiler.cs index fa107a0e43..f640a3dab5 100644 --- a/osu.Game/Online/Chat/DrawableLinkCompiler.cs +++ b/osu.Game/Online/Chat/DrawableLinkCompiler.cs @@ -14,6 +14,7 @@ using osu.Game.Graphics.Containers; using osu.Game.Graphics.UserInterface; using osu.Game.Overlays; using osuTK; +using osuTK.Graphics; namespace osu.Game.Online.Chat { @@ -27,6 +28,18 @@ namespace osu.Game.Online.Chat /// public readonly SlimReadOnlyListWrapper Parts; + public new Color4 IdleColour + { + get => base.IdleColour; + set => base.IdleColour = value; + } + + public new Color4 HoverColour + { + get => base.HoverColour; + set => base.HoverColour = value; + } + [Resolved] private OverlayColourProvider? overlayColourProvider { get; set; } @@ -56,7 +69,8 @@ namespace osu.Game.Online.Chat [BackgroundDependencyLoader] private void load(OsuColour colours) { - IdleColour = overlayColourProvider?.Light2 ?? colours.Blue; + if (IdleColour == default) + IdleColour = overlayColourProvider?.Light2 ?? colours.Blue; } protected override IEnumerable EffectTargets => Parts; diff --git a/osu.Game/Screens/Play/HUD/SpectatorList.cs b/osu.Game/Screens/Play/HUD/SpectatorList.cs new file mode 100644 index 0000000000..ad94b23cd7 --- /dev/null +++ b/osu.Game/Screens/Play/HUD/SpectatorList.cs @@ -0,0 +1,219 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using System.Collections.Specialized; +using osu.Framework.Allocation; +using osu.Framework.Bindables; +using osu.Framework.Extensions.LocalisationExtensions; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Pooling; +using osu.Game.Configuration; +using osu.Game.Graphics; +using osu.Game.Graphics.Sprites; +using osu.Game.Online.Chat; +using osu.Game.Users; +using osu.Game.Localisation.HUD; +using osu.Game.Localisation.SkinComponents; + +namespace osu.Game.Screens.Play.HUD +{ + public partial class SpectatorList : CompositeDrawable + { + private const int max_spectators_displayed = 10; + + public BindableList Spectators { get; } = new BindableList(); + public Bindable UserPlayingState { get; } = new Bindable(); + + [SettingSource(typeof(SkinnableComponentStrings), nameof(SkinnableComponentStrings.Font), nameof(SkinnableComponentStrings.FontDescription))] + public Bindable Font { get; } = new Bindable(Typeface.Torus); + + [SettingSource(typeof(SkinnableComponentStrings), nameof(SkinnableComponentStrings.TextColour), nameof(SkinnableComponentStrings.TextColourDescription))] + public BindableColour4 HeaderColour { get; } = new BindableColour4(Colour4.White); + + protected OsuSpriteText Header { get; private set; } = null!; + + private FillFlowContainer mainFlow = null!; + private FillFlowContainer spectatorsFlow = null!; + private DrawablePool pool = null!; + + [BackgroundDependencyLoader] + private void load(OsuColour colours) + { + AutoSizeAxes = Axes.Both; + + InternalChildren = new Drawable[] + { + mainFlow = new FillFlowContainer + { + AutoSizeAxes = Axes.Both, + AutoSizeDuration = 250, + AutoSizeEasing = Easing.OutQuint, + Direction = FillDirection.Vertical, + Children = new Drawable[] + { + Header = new OsuSpriteText + { + Colour = colours.Blue0, + Font = OsuFont.GetFont(size: 12, weight: FontWeight.Bold), + }, + spectatorsFlow = new FillFlowContainer + { + AutoSizeAxes = Axes.Both, + Direction = FillDirection.Vertical, + } + } + }, + pool = new DrawablePool(max_spectators_displayed), + }; + + HeaderColour.Value = Header.Colour; + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + Spectators.BindCollectionChanged(onSpectatorsChanged, true); + UserPlayingState.BindValueChanged(_ => updateVisibility()); + + Font.BindValueChanged(_ => updateAppearance()); + HeaderColour.BindValueChanged(_ => updateAppearance(), true); + FinishTransforms(true); + } + + private void onSpectatorsChanged(object? sender, NotifyCollectionChangedEventArgs e) + { + switch (e.Action) + { + case NotifyCollectionChangedAction.Add: + { + for (int i = 0; i < e.NewItems!.Count; i++) + { + var spectator = (Spectator)e.NewItems![i]!; + int index = e.NewStartingIndex + i; + + if (index >= max_spectators_displayed) + break; + + spectatorsFlow.Insert(e.NewStartingIndex + i, pool.Get(entry => + { + entry.Current.Value = spectator; + entry.UserPlayingState = UserPlayingState; + })); + } + + break; + } + + case NotifyCollectionChangedAction.Remove: + { + spectatorsFlow.RemoveAll(entry => e.OldItems!.Contains(entry.Current.Value), false); + + for (int i = 0; i < spectatorsFlow.Count; i++) + spectatorsFlow.SetLayoutPosition(spectatorsFlow[i], i); + + if (Spectators.Count >= max_spectators_displayed && spectatorsFlow.Count < max_spectators_displayed) + { + for (int i = spectatorsFlow.Count; i < max_spectators_displayed; i++) + { + var spectator = Spectators[i]; + spectatorsFlow.Insert(i, pool.Get(entry => + { + entry.Current.Value = spectator; + entry.UserPlayingState = UserPlayingState; + })); + } + } + + break; + } + + case NotifyCollectionChangedAction.Reset: + { + spectatorsFlow.Clear(false); + break; + } + + default: + throw new NotSupportedException(); + } + + Header.Text = SpectatorListStrings.SpectatorCount(Spectators.Count).ToUpper(); + updateVisibility(); + } + + private void updateVisibility() + { + mainFlow.FadeTo(Spectators.Count > 0 && UserPlayingState.Value != LocalUserPlayingState.NotPlaying ? 1 : 0, 250, Easing.OutQuint); + } + + private void updateAppearance() + { + Header.Font = OsuFont.GetFont(Font.Value, 12, FontWeight.Bold); + Header.Colour = HeaderColour.Value; + } + + private partial class SpectatorListEntry : PoolableDrawable + { + public Bindable Current { get; } = new Bindable(); + + private readonly BindableWithCurrent current = new BindableWithCurrent(); + + public Bindable UserPlayingState + { + get => current.Current; + set => current.Current = value; + } + + private OsuSpriteText username = null!; + private DrawableLinkCompiler? linkCompiler; + + [Resolved] + private OsuGame? game { get; set; } + + [BackgroundDependencyLoader] + private void load() + { + AutoSizeAxes = Axes.Both; + + InternalChildren = new Drawable[] + { + username = new OsuSpriteText(), + }; + } + + protected override void LoadComplete() + { + base.LoadComplete(); + UserPlayingState.BindValueChanged(_ => updateEnabledState()); + Current.BindValueChanged(_ => updateState(), true); + } + + private void updateState() + { + username.Text = Current.Value.Username; + linkCompiler?.Expire(); + AddInternal(linkCompiler = new DrawableLinkCompiler([username]) + { + IdleColour = Colour4.White, + Action = () => game?.HandleLink(new LinkDetails(LinkAction.OpenUserProfile, Current.Value)), + }); + updateEnabledState(); + } + + private void updateEnabledState() + { + if (linkCompiler != null) + linkCompiler.Enabled.Value = UserPlayingState.Value != LocalUserPlayingState.Playing; + } + } + + public record Spectator(int OnlineID, string Username) : IUser + { + public CountryCode CountryCode => CountryCode.Unknown; + public bool IsBot => false; + } + } +} From 43fc48a3f300c13433f957ed99c65541e0c4f801 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 14 Jan 2025 14:44:13 +0100 Subject: [PATCH 0585/1275] Add client methods allowing users to be notified of who is watching them --- .../Visual/Gameplay/TestSceneSpectatorList.cs | 9 ++++- osu.Game/Online/Spectator/ISpectatorClient.cs | 12 ++++++ .../Online/Spectator/OnlineSpectatorClient.cs | 2 + osu.Game/Online/Spectator/SpectatorClient.cs | 35 ++++++++++++++++- osu.Game/Online/Spectator/SpectatorUser.cs | 39 +++++++++++++++++++ osu.Game/Screens/Play/HUD/SpectatorList.cs | 14 ++----- 6 files changed, 97 insertions(+), 14 deletions(-) create mode 100644 osu.Game/Online/Spectator/SpectatorUser.cs diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSpectatorList.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSpectatorList.cs index 3cd37baafd..5be1829b85 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneSpectatorList.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSpectatorList.cs @@ -7,6 +7,7 @@ using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Utils; using osu.Game.Graphics; +using osu.Game.Online.Spectator; using osu.Game.Screens.Play; using osu.Game.Screens.Play.HUD; @@ -15,7 +16,7 @@ namespace osu.Game.Tests.Visual.Gameplay [TestFixture] public partial class TestSceneSpectatorList : OsuTestScene { - private readonly BindableList spectators = new BindableList(); + private readonly BindableList spectators = new BindableList(); private readonly Bindable localUserPlayingState = new Bindable(); private int counter; @@ -36,7 +37,11 @@ namespace osu.Game.Tests.Visual.Gameplay AddStep("add a user", () => { int id = Interlocked.Increment(ref counter); - spectators.Add(new SpectatorList.Spectator(id, $"User {id}")); + spectators.Add(new SpectatorUser + { + OnlineID = id, + Username = $"User {id}" + }); }); AddStep("remove random user", () => spectators.RemoveAt(RNG.Next(0, spectators.Count))); AddStep("enter break", () => localUserPlayingState.Value = LocalUserPlayingState.Break); diff --git a/osu.Game/Online/Spectator/ISpectatorClient.cs b/osu.Game/Online/Spectator/ISpectatorClient.cs index 2dc2283c23..2b73037cb8 100644 --- a/osu.Game/Online/Spectator/ISpectatorClient.cs +++ b/osu.Game/Online/Spectator/ISpectatorClient.cs @@ -37,5 +37,17 @@ namespace osu.Game.Online.Spectator /// The ID of the user who achieved the score. /// The ID of the score. Task UserScoreProcessed(int userId, long scoreId); + + /// + /// Signals that another user has started watching this client. + /// + /// The information about the user who started watching. + Task UserStartedWatching(SpectatorUser[] user); + + /// + /// Signals that another user has ended watching this client + /// + /// The ID of the user who ended watching. + Task UserEndedWatching(int userId); } } diff --git a/osu.Game/Online/Spectator/OnlineSpectatorClient.cs b/osu.Game/Online/Spectator/OnlineSpectatorClient.cs index 036cfa1d76..645d7054dc 100644 --- a/osu.Game/Online/Spectator/OnlineSpectatorClient.cs +++ b/osu.Game/Online/Spectator/OnlineSpectatorClient.cs @@ -42,6 +42,8 @@ namespace osu.Game.Online.Spectator connection.On(nameof(ISpectatorClient.UserSentFrames), ((ISpectatorClient)this).UserSentFrames); connection.On(nameof(ISpectatorClient.UserFinishedPlaying), ((ISpectatorClient)this).UserFinishedPlaying); connection.On(nameof(ISpectatorClient.UserScoreProcessed), ((ISpectatorClient)this).UserScoreProcessed); + connection.On(nameof(ISpectatorClient.UserStartedWatching), ((ISpectatorClient)this).UserStartedWatching); + connection.On(nameof(ISpectatorClient.UserEndedWatching), ((ISpectatorClient)this).UserEndedWatching); connection.On(nameof(IStatefulUserHubClient.DisconnectRequested), ((IStatefulUserHubClient)this).DisconnectRequested); }; diff --git a/osu.Game/Online/Spectator/SpectatorClient.cs b/osu.Game/Online/Spectator/SpectatorClient.cs index fb7a3d13ca..ac11dad0f0 100644 --- a/osu.Game/Online/Spectator/SpectatorClient.cs +++ b/osu.Game/Online/Spectator/SpectatorClient.cs @@ -36,9 +36,14 @@ namespace osu.Game.Online.Spectator public abstract IBindable IsConnected { get; } /// - /// The states of all users currently being watched. + /// The states of all users currently being watched by the local user. /// - public virtual IBindableDictionary WatchedUserStates => watchedUserStates; + public IBindableDictionary WatchedUserStates => watchedUserStates; + + /// + /// All users who are currently watching the local user. + /// + public IBindableList WatchingUsers => watchingUsers; /// /// A global list of all players currently playing. @@ -82,6 +87,7 @@ namespace osu.Game.Online.Spectator private readonly BindableDictionary watchedUserStates = new BindableDictionary(); + private readonly BindableList watchingUsers = new BindableList(); private readonly BindableList playingUsers = new BindableList(); private readonly SpectatorState currentState = new SpectatorState(); @@ -127,6 +133,7 @@ namespace osu.Game.Online.Spectator { playingUsers.Clear(); watchedUserStates.Clear(); + watchingUsers.Clear(); } }), true); } @@ -179,6 +186,30 @@ namespace osu.Game.Online.Spectator return Task.CompletedTask; } + Task ISpectatorClient.UserStartedWatching(SpectatorUser[] users) + { + Schedule(() => + { + foreach (var user in users) + { + if (!watchingUsers.Contains(user)) + watchingUsers.Add(user); + } + }); + + return Task.CompletedTask; + } + + Task ISpectatorClient.UserEndedWatching(int userId) + { + Schedule(() => + { + watchingUsers.RemoveAll(u => u.OnlineID == userId); + }); + + return Task.CompletedTask; + } + Task IStatefulUserHubClient.DisconnectRequested() { Schedule(() => DisconnectInternal()); diff --git a/osu.Game/Online/Spectator/SpectatorUser.cs b/osu.Game/Online/Spectator/SpectatorUser.cs new file mode 100644 index 0000000000..9c9563be70 --- /dev/null +++ b/osu.Game/Online/Spectator/SpectatorUser.cs @@ -0,0 +1,39 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using MessagePack; +using osu.Game.Users; + +namespace osu.Game.Online.Spectator +{ + [Serializable] + [MessagePackObject] + public class SpectatorUser : IUser, IEquatable + { + [Key(0)] + public int OnlineID { get; set; } + + [Key(1)] + public string Username { get; set; } = string.Empty; + + [IgnoreMember] + public CountryCode CountryCode => CountryCode.Unknown; + + [IgnoreMember] + public bool IsBot => false; + + public bool Equals(SpectatorUser? other) + { + if (other is null) return false; + if (ReferenceEquals(this, other)) return true; + + return OnlineID == other.OnlineID; + } + + public override bool Equals(object? obj) => Equals(obj as SpectatorUser); + + // ReSharper disable once NonReadonlyMemberInGetHashCode + public override int GetHashCode() => OnlineID; + } +} diff --git a/osu.Game/Screens/Play/HUD/SpectatorList.cs b/osu.Game/Screens/Play/HUD/SpectatorList.cs index ad94b23cd7..90b2ae0a3d 100644 --- a/osu.Game/Screens/Play/HUD/SpectatorList.cs +++ b/osu.Game/Screens/Play/HUD/SpectatorList.cs @@ -13,9 +13,9 @@ using osu.Game.Configuration; using osu.Game.Graphics; using osu.Game.Graphics.Sprites; using osu.Game.Online.Chat; -using osu.Game.Users; using osu.Game.Localisation.HUD; using osu.Game.Localisation.SkinComponents; +using osu.Game.Online.Spectator; namespace osu.Game.Screens.Play.HUD { @@ -23,7 +23,7 @@ namespace osu.Game.Screens.Play.HUD { private const int max_spectators_displayed = 10; - public BindableList Spectators { get; } = new BindableList(); + public BindableList Spectators { get; } = new BindableList(); public Bindable UserPlayingState { get; } = new Bindable(); [SettingSource(typeof(SkinnableComponentStrings), nameof(SkinnableComponentStrings.Font), nameof(SkinnableComponentStrings.FontDescription))] @@ -91,7 +91,7 @@ namespace osu.Game.Screens.Play.HUD { for (int i = 0; i < e.NewItems!.Count; i++) { - var spectator = (Spectator)e.NewItems![i]!; + var spectator = (SpectatorUser)e.NewItems![i]!; int index = e.NewStartingIndex + i; if (index >= max_spectators_displayed) @@ -157,7 +157,7 @@ namespace osu.Game.Screens.Play.HUD private partial class SpectatorListEntry : PoolableDrawable { - public Bindable Current { get; } = new Bindable(); + public Bindable Current { get; } = new Bindable(); private readonly BindableWithCurrent current = new BindableWithCurrent(); @@ -209,11 +209,5 @@ namespace osu.Game.Screens.Play.HUD linkCompiler.Enabled.Value = UserPlayingState.Value != LocalUserPlayingState.Playing; } } - - public record Spectator(int OnlineID, string Username) : IUser - { - public CountryCode CountryCode => CountryCode.Unknown; - public bool IsBot => false; - } } } From 12b2631e5e2b85f621866e87579ef69b218e2ee9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 14 Jan 2025 15:03:37 +0100 Subject: [PATCH 0586/1275] Add a skinnable variant of spectator list & hook it up to online data --- osu.Game/Screens/Play/HUD/SpectatorList.cs | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/osu.Game/Screens/Play/HUD/SpectatorList.cs b/osu.Game/Screens/Play/HUD/SpectatorList.cs index 90b2ae0a3d..733f2d2514 100644 --- a/osu.Game/Screens/Play/HUD/SpectatorList.cs +++ b/osu.Game/Screens/Play/HUD/SpectatorList.cs @@ -16,6 +16,8 @@ using osu.Game.Online.Chat; using osu.Game.Localisation.HUD; using osu.Game.Localisation.SkinComponents; using osu.Game.Online.Spectator; +using osu.Game.Skinning; +using osuTK; namespace osu.Game.Screens.Play.HUD { @@ -43,8 +45,9 @@ namespace osu.Game.Screens.Play.HUD { AutoSizeAxes = Axes.Both; - InternalChildren = new Drawable[] + InternalChildren = new[] { + Empty().With(t => t.Size = new Vector2(100, 50)), mainFlow = new FillFlowContainer { AutoSizeAxes = Axes.Both, @@ -210,4 +213,16 @@ namespace osu.Game.Screens.Play.HUD } } } + + public partial class SkinnableSpectatorList : SpectatorList, ISerialisableDrawable + { + public bool UsesFixedAnchor { get; set; } + + [BackgroundDependencyLoader] + private void load(SpectatorClient client, Player player) + { + ((IBindableList)Spectators).BindTo(client.WatchingUsers); + ((IBindable)UserPlayingState).BindTo(player.PlayingState); + } + } } From 99c7e164dc7465d2bd748b0c20d895e79087e429 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 15 Jan 2025 13:08:32 +0100 Subject: [PATCH 0587/1275] Add skinnable spectator list to default skins --- .../Legacy/CatchLegacySkinTransformer.cs | 10 ++++++ .../Argon/ManiaArgonSkinTransformer.cs | 11 ++++++ .../Legacy/ManiaLegacySkinTransformer.cs | 11 ++++++ .../Legacy/OsuLegacySkinTransformer.cs | 14 ++++++++ osu.Game/Skinning/ArgonSkin.cs | 35 +++++++++++++++---- osu.Game/Skinning/LegacySkin.cs | 15 +++++++- osu.Game/Skinning/TrianglesSkin.cs | 24 +++++++++---- 7 files changed, 106 insertions(+), 14 deletions(-) diff --git a/osu.Game.Rulesets.Catch/Skinning/Legacy/CatchLegacySkinTransformer.cs b/osu.Game.Rulesets.Catch/Skinning/Legacy/CatchLegacySkinTransformer.cs index 69efb7fbca..978a098990 100644 --- a/osu.Game.Rulesets.Catch/Skinning/Legacy/CatchLegacySkinTransformer.cs +++ b/osu.Game.Rulesets.Catch/Skinning/Legacy/CatchLegacySkinTransformer.cs @@ -4,6 +4,7 @@ using System.Linq; using osu.Framework.Bindables; using osu.Framework.Graphics; +using osu.Game.Screens.Play.HUD; using osu.Game.Skinning; using osuTK; using osuTK.Graphics; @@ -47,6 +48,7 @@ namespace osu.Game.Rulesets.Catch.Skinning.Legacy return new DefaultSkinComponentsContainer(container => { var keyCounter = container.OfType().FirstOrDefault(); + var spectatorList = container.OfType().FirstOrDefault(); if (keyCounter != null) { @@ -55,11 +57,19 @@ namespace osu.Game.Rulesets.Catch.Skinning.Legacy keyCounter.Origin = Anchor.TopRight; keyCounter.Position = new Vector2(0, -40) * 1.6f; } + + if (spectatorList != null) + { + spectatorList.Anchor = Anchor.BottomLeft; + spectatorList.Origin = Anchor.BottomLeft; + spectatorList.Position = new Vector2(10, -10); + } }) { Children = new Drawable[] { new LegacyKeyCounterDisplay(), + new SkinnableSpectatorList(), } }; } diff --git a/osu.Game.Rulesets.Mania/Skinning/Argon/ManiaArgonSkinTransformer.cs b/osu.Game.Rulesets.Mania/Skinning/Argon/ManiaArgonSkinTransformer.cs index c37c18081a..48c487e70d 100644 --- a/osu.Game.Rulesets.Mania/Skinning/Argon/ManiaArgonSkinTransformer.cs +++ b/osu.Game.Rulesets.Mania/Skinning/Argon/ManiaArgonSkinTransformer.cs @@ -9,7 +9,9 @@ using osu.Framework.Testing; using osu.Game.Beatmaps; using osu.Game.Rulesets.Mania.Beatmaps; using osu.Game.Rulesets.Scoring; +using osu.Game.Screens.Play.HUD; using osu.Game.Skinning; +using osuTK; using osuTK.Graphics; namespace osu.Game.Rulesets.Mania.Skinning.Argon @@ -39,6 +41,7 @@ namespace osu.Game.Rulesets.Mania.Skinning.Argon return new DefaultSkinComponentsContainer(container => { var combo = container.ChildrenOfType().FirstOrDefault(); + var spectatorList = container.OfType().FirstOrDefault(); if (combo != null) { @@ -47,9 +50,17 @@ namespace osu.Game.Rulesets.Mania.Skinning.Argon combo.Origin = Anchor.Centre; combo.Y = 200; } + + if (spectatorList != null) + spectatorList.Position = new Vector2(36, -66); }) { new ArgonManiaComboCounter(), + new SkinnableSpectatorList + { + Anchor = Anchor.BottomLeft, + Origin = Anchor.BottomLeft, + } }; } diff --git a/osu.Game.Rulesets.Mania/Skinning/Legacy/ManiaLegacySkinTransformer.cs b/osu.Game.Rulesets.Mania/Skinning/Legacy/ManiaLegacySkinTransformer.cs index 8f425edc44..359f21561f 100644 --- a/osu.Game.Rulesets.Mania/Skinning/Legacy/ManiaLegacySkinTransformer.cs +++ b/osu.Game.Rulesets.Mania/Skinning/Legacy/ManiaLegacySkinTransformer.cs @@ -15,7 +15,9 @@ using osu.Game.Beatmaps; using osu.Game.Rulesets.Mania.Beatmaps; using osu.Game.Rulesets.Objects.Legacy; using osu.Game.Rulesets.Scoring; +using osu.Game.Screens.Play.HUD; using osu.Game.Skinning; +using osuTK; namespace osu.Game.Rulesets.Mania.Skinning.Legacy { @@ -95,6 +97,7 @@ namespace osu.Game.Rulesets.Mania.Skinning.Legacy return new DefaultSkinComponentsContainer(container => { var combo = container.ChildrenOfType().FirstOrDefault(); + var spectatorList = container.OfType().FirstOrDefault(); if (combo != null) { @@ -102,9 +105,17 @@ namespace osu.Game.Rulesets.Mania.Skinning.Legacy combo.Origin = Anchor.Centre; combo.Y = this.GetManiaSkinConfig(LegacyManiaSkinConfigurationLookups.ComboPosition)?.Value ?? 0; } + + if (spectatorList != null) + { + spectatorList.Anchor = Anchor.BottomLeft; + spectatorList.Origin = Anchor.BottomLeft; + spectatorList.Position = new Vector2(10, -10); + } }) { new LegacyManiaComboCounter(), + new SkinnableSpectatorList(), }; } diff --git a/osu.Game.Rulesets.Osu/Skinning/Legacy/OsuLegacySkinTransformer.cs b/osu.Game.Rulesets.Osu/Skinning/Legacy/OsuLegacySkinTransformer.cs index 636a9ecb21..03e4bb24f1 100644 --- a/osu.Game.Rulesets.Osu/Skinning/Legacy/OsuLegacySkinTransformer.cs +++ b/osu.Game.Rulesets.Osu/Skinning/Legacy/OsuLegacySkinTransformer.cs @@ -6,6 +6,7 @@ using System.Linq; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Game.Rulesets.Osu.Objects; +using osu.Game.Screens.Play.HUD; using osu.Game.Skinning; using osuTK; @@ -70,12 +71,24 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy } var combo = container.OfType().FirstOrDefault(); + var spectatorList = container.OfType().FirstOrDefault(); + + Vector2 pos = new Vector2(); if (combo != null) { combo.Anchor = Anchor.BottomLeft; combo.Origin = Anchor.BottomLeft; combo.Scale = new Vector2(1.28f); + + pos += new Vector2(10, -(combo.DrawHeight * 1.56f + 20) * combo.Scale.X); + } + + if (spectatorList != null) + { + spectatorList.Anchor = Anchor.BottomLeft; + spectatorList.Origin = Anchor.BottomLeft; + spectatorList.Position = pos; } }) { @@ -83,6 +96,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy { new LegacyDefaultComboCounter(), new LegacyKeyCounterDisplay(), + new SkinnableSpectatorList(), } }; } diff --git a/osu.Game/Skinning/ArgonSkin.cs b/osu.Game/Skinning/ArgonSkin.cs index 771d10d73b..c3319b738d 100644 --- a/osu.Game/Skinning/ArgonSkin.cs +++ b/osu.Game/Skinning/ArgonSkin.cs @@ -7,7 +7,6 @@ using JetBrains.Annotations; using osu.Framework.Audio.Sample; using osu.Framework.Bindables; using osu.Framework.Graphics; -using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Textures; using osu.Game.Audio; using osu.Game.Beatmaps.Formats; @@ -110,15 +109,37 @@ namespace osu.Game.Skinning case GlobalSkinnableContainers.MainHUDComponents: if (containerLookup.Ruleset != null) { - return new Container + return new DefaultSkinComponentsContainer(container => + { + var comboCounter = container.OfType().FirstOrDefault(); + var spectatorList = container.OfType().FirstOrDefault(); + + Vector2 pos = new Vector2(36, -66); + + if (comboCounter != null) + { + comboCounter.Position = pos; + pos -= new Vector2(0, comboCounter.DrawHeight * 1.4f + 20); + } + + if (spectatorList != null) + spectatorList.Position = pos; + }) { RelativeSizeAxes = Axes.Both, - Child = new ArgonComboCounter + Children = new Drawable[] { - Anchor = Anchor.BottomLeft, - Origin = Anchor.BottomLeft, - Position = new Vector2(36, -66), - Scale = new Vector2(1.3f), + new ArgonComboCounter + { + Anchor = Anchor.BottomLeft, + Origin = Anchor.BottomLeft, + Scale = new Vector2(1.3f), + }, + new SkinnableSpectatorList + { + Anchor = Anchor.BottomLeft, + Origin = Anchor.BottomLeft, + } }, }; } diff --git a/osu.Game/Skinning/LegacySkin.cs b/osu.Game/Skinning/LegacySkin.cs index 6faadfba9b..c607c57fcc 100644 --- a/osu.Game/Skinning/LegacySkin.cs +++ b/osu.Game/Skinning/LegacySkin.cs @@ -367,16 +367,29 @@ namespace osu.Game.Skinning return new DefaultSkinComponentsContainer(container => { var combo = container.OfType().FirstOrDefault(); + var spectatorList = container.OfType().FirstOrDefault(); + + Vector2 pos = new Vector2(); if (combo != null) { combo.Anchor = Anchor.BottomLeft; combo.Origin = Anchor.BottomLeft; combo.Scale = new Vector2(1.28f); + + pos += new Vector2(10, -(combo.DrawHeight * 1.56f + 20) * combo.Scale.X); + } + + if (spectatorList != null) + { + spectatorList.Anchor = Anchor.BottomLeft; + spectatorList.Origin = Anchor.BottomLeft; + spectatorList.Position = pos; } }) { - new LegacyDefaultComboCounter() + new LegacyDefaultComboCounter(), + new SkinnableSpectatorList(), }; } diff --git a/osu.Game/Skinning/TrianglesSkin.cs b/osu.Game/Skinning/TrianglesSkin.cs index d562fd3256..8853a5c4ac 100644 --- a/osu.Game/Skinning/TrianglesSkin.cs +++ b/osu.Game/Skinning/TrianglesSkin.cs @@ -11,6 +11,7 @@ using osu.Framework.Graphics.Textures; using osu.Game.Audio; using osu.Game.Beatmaps.Formats; using osu.Game.Extensions; +using osu.Game.Graphics; using osu.Game.IO; using osu.Game.Screens.Play.HUD; using osu.Game.Screens.Play.HUD.HitErrorMeters; @@ -90,6 +91,7 @@ namespace osu.Game.Skinning var ppCounter = container.OfType().FirstOrDefault(); var songProgress = container.OfType().FirstOrDefault(); var keyCounter = container.OfType().FirstOrDefault(); + var spectatorList = container.OfType().FirstOrDefault(); if (score != null) { @@ -142,17 +144,26 @@ namespace osu.Game.Skinning } } + const float padding = 10; + + // Hard to find this at runtime, so taken from the most expanded state during replay. + const float song_progress_offset_height = 73; + if (songProgress != null && keyCounter != null) { - const float padding = 10; - - // Hard to find this at runtime, so taken from the most expanded state during replay. - const float song_progress_offset_height = 73; - keyCounter.Anchor = Anchor.BottomRight; keyCounter.Origin = Anchor.BottomRight; keyCounter.Position = new Vector2(-padding, -(song_progress_offset_height + padding)); } + + if (spectatorList != null) + { + spectatorList.Font.Value = Typeface.Venera; + spectatorList.HeaderColour.Value = new OsuColour().BlueLighter; + spectatorList.Anchor = Anchor.BottomLeft; + spectatorList.Origin = Anchor.BottomLeft; + spectatorList.Position = new Vector2(padding, -(song_progress_offset_height + padding)); + } }) { Children = new Drawable[] @@ -165,7 +176,8 @@ namespace osu.Game.Skinning new DefaultKeyCounterDisplay(), new BarHitErrorMeter(), new BarHitErrorMeter(), - new TrianglesPerformancePointsCounter() + new TrianglesPerformancePointsCounter(), + new SkinnableSpectatorList(), } }; From 2eb63e6fe045f7e2b6087897669add86cc8932cf Mon Sep 17 00:00:00 2001 From: Andrei Zavatski Date: Wed, 15 Jan 2025 20:38:51 +0300 Subject: [PATCH 0588/1275] 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 0589/1275] 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 0590/1275] 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 0591/1275] Add test --- .../TestSceneCursorTrail.cs | 51 ++++++++++++++++++- 1 file changed, 49 insertions(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneCursorTrail.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneCursorTrail.cs index 17f365f820..a8a65f7edb 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneCursorTrail.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneCursorTrail.cs @@ -6,6 +6,7 @@ using System; using System.Collections.Generic; using System.Linq; +using JetBrains.Annotations; using NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Audio.Sample; @@ -17,6 +18,7 @@ using osu.Framework.Graphics.Textures; using osu.Framework.Testing; using osu.Framework.Testing.Input; using osu.Game.Audio; +using osu.Game.Rulesets.Osu.Skinning; using osu.Game.Rulesets.Osu.Skinning.Legacy; using osu.Game.Rulesets.Osu.UI.Cursor; using osu.Game.Skinning; @@ -103,6 +105,23 @@ namespace osu.Game.Rulesets.Osu.Tests AddStep("contract", () => this.ChildrenOfType().Single().NewPartScale = Vector2.One); } + [Test] + public void TestRotation() + { + createTest(() => + { + var skinContainer = new LegacySkinContainer(renderer, provideMiddle: true, enableRotation: true); + var legacyCursorTrail = new LegacyRotatingCursorTrail(skinContainer) + { + NewPartScale = new Vector2(10) + }; + + skinContainer.Child = legacyCursorTrail; + + return skinContainer; + }); + } + private void createTest(Func createContent) => AddStep("create trail", () => { Clear(); @@ -121,12 +140,14 @@ namespace osu.Game.Rulesets.Osu.Tests private readonly IRenderer renderer; private readonly bool provideMiddle; private readonly bool provideCursor; + private readonly bool enableRotation; - public LegacySkinContainer(IRenderer renderer, bool provideMiddle, bool provideCursor = true) + public LegacySkinContainer(IRenderer renderer, bool provideMiddle, bool provideCursor = true, bool enableRotation = false) { this.renderer = renderer; this.provideMiddle = provideMiddle; this.provideCursor = provideCursor; + this.enableRotation = enableRotation; RelativeSizeAxes = Axes.Both; } @@ -152,7 +173,19 @@ namespace osu.Game.Rulesets.Osu.Tests public ISample GetSample(ISampleInfo sampleInfo) => null; - public IBindable GetConfig(TLookup lookup) => null; + public IBindable GetConfig(TLookup lookup) + { + switch (lookup) + { + case OsuSkinConfiguration osuLookup: + if (osuLookup == OsuSkinConfiguration.CursorTrailRotate) + return SkinUtils.As(new BindableBool(enableRotation)); + + break; + } + + return null; + } public ISkin FindProvider(Func lookupFunction) => lookupFunction(this) ? this : null; @@ -185,5 +218,19 @@ namespace osu.Game.Rulesets.Osu.Tests MoveMouseTo(ToScreenSpace(DrawSize / 2 + DrawSize / 3 * rPos)); } } + + private partial class LegacyRotatingCursorTrail : LegacyCursorTrail + { + public LegacyRotatingCursorTrail([NotNull] ISkin skin) + : base(skin) + { + } + + protected override void Update() + { + base.Update(); + PartRotation += (float)(Time.Elapsed * 0.1); + } + } } } From 46e9da7960ef551d4127305d7ce66907bb47e774 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Thu, 16 Jan 2025 15:34:20 +0900 Subject: [PATCH 0592/1275] Fix style display refreshing on all room updates --- .../Screens/OnlinePlay/Match/RoomSubScreen.cs | 26 ++++++++++--------- 1 file changed, 14 insertions(+), 12 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs b/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs index ec2ed90eca..edb44a7666 100644 --- a/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs @@ -523,19 +523,21 @@ namespace osu.Game.Screens.OnlinePlay.Match if (!this.IsCurrentScreen() || SelectedItem.Value is not PlaylistItem) return; - if (UserStyleDisplayContainer != null) - { - PlaylistItem gameplayItem = SelectedItem.Value.With( - ruleset: GetGameplayRuleset().OnlineID, - beatmap: new Optional(GetGameplayBeatmap())); + if (UserStyleDisplayContainer == null) + return; - UserStyleDisplayContainer.Child = new DrawableRoomPlaylistItem(gameplayItem) - { - AllowReordering = false, - AllowEditing = true, - RequestEdit = _ => OpenStyleSelection() - }; - } + PlaylistItem gameplayItem = SelectedItem.Value.With(ruleset: GetGameplayRuleset().OnlineID, beatmap: new Optional(GetGameplayBeatmap())); + PlaylistItem? currentItem = UserStyleDisplayContainer.SingleOrDefault()?.Item; + + if (gameplayItem.Equals(currentItem)) + return; + + UserStyleDisplayContainer.Child = new DrawableRoomPlaylistItem(gameplayItem) + { + AllowReordering = false, + AllowEditing = true, + RequestEdit = _ => OpenStyleSelection() + }; } protected virtual APIMod[] GetGameplayMods() From 409ea53ad96441104494bb73e75f6155bcd0be76 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Thu, 16 Jan 2025 15:51:53 +0900 Subject: [PATCH 0593/1275] Send `beatmap_id` when creating score --- osu.Game/Online/Rooms/CreateRoomScoreRequest.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/osu.Game/Online/Rooms/CreateRoomScoreRequest.cs b/osu.Game/Online/Rooms/CreateRoomScoreRequest.cs index e0f91032fd..eb2879ba6c 100644 --- a/osu.Game/Online/Rooms/CreateRoomScoreRequest.cs +++ b/osu.Game/Online/Rooms/CreateRoomScoreRequest.cs @@ -31,6 +31,7 @@ namespace osu.Game.Online.Rooms var req = base.CreateWebRequest(); req.Method = HttpMethod.Post; req.AddParameter("version_hash", versionHash); + req.AddParameter("beatmap_id", beatmapInfo.OnlineID.ToString(CultureInfo.InvariantCulture)); req.AddParameter("beatmap_hash", beatmapInfo.MD5Hash); req.AddParameter("ruleset_id", rulesetId.ToString(CultureInfo.InvariantCulture)); return req; From b54d95926329c0af71df64458196ec4339b66147 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Thu, 16 Jan 2025 17:05:18 +0900 Subject: [PATCH 0594/1275] 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 0595/1275] Allow setting of DummyAPIAccess status --- osu.Game/Online/API/DummyAPIAccess.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/osu.Game/Online/API/DummyAPIAccess.cs b/osu.Game/Online/API/DummyAPIAccess.cs index 4cd3c02414..3fef2b59cf 100644 --- a/osu.Game/Online/API/DummyAPIAccess.cs +++ b/osu.Game/Online/API/DummyAPIAccess.cs @@ -28,7 +28,7 @@ namespace osu.Game.Online.API public BindableList Friends { get; } = new BindableList(); - public IBindable Status { get; } = new Bindable(UserStatus.Online); + public Bindable Status { get; } = new Bindable(UserStatus.Online); public Bindable Activity { get; } = new Bindable(); @@ -197,6 +197,7 @@ namespace osu.Game.Online.API IBindable IAPIProvider.LocalUser => LocalUser; IBindableList IAPIProvider.Friends => Friends; + IBindable IAPIProvider.Status => Status; IBindable IAPIProvider.Activity => Activity; /// From aa3ae8324e19769df585bb45fe8af3935f830113 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Thu, 16 Jan 2025 17:29:43 +0900 Subject: [PATCH 0596/1275] Add test for local user presence --- .../Visual/Online/TestSceneUserPanel.cs | 31 ++++++++++++++++++- 1 file changed, 30 insertions(+), 1 deletion(-) diff --git a/osu.Game.Tests/Visual/Online/TestSceneUserPanel.cs b/osu.Game.Tests/Visual/Online/TestSceneUserPanel.cs index b4dafd3107..684e8b7b86 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneUserPanel.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneUserPanel.cs @@ -9,6 +9,7 @@ using osu.Framework.Graphics.Containers; using osu.Framework.Utils; using osu.Game.Beatmaps; using osu.Game.Online; +using osu.Game.Online.API; using osu.Game.Online.API.Requests.Responses; using osu.Game.Online.Metadata; using osu.Game.Overlays; @@ -112,7 +113,11 @@ namespace osu.Game.Tests.Visual.Online CountryCode = CountryCode.AU, CoverUrl = @"https://assets.ppy.sh/user-profile-covers/8195163/4a8e2ad5a02a2642b631438cfa6c6bd7e2f9db289be881cb27df18331f64144c.jpeg", Statistics = new UserStatistics { GlobalRank = null, CountryRank = null } - }) { Width = 300 } + }) { Width = 300 }, + new UserGridPanel(API.LocalUser.Value) + { + Width = 300 + } } } } @@ -180,6 +185,23 @@ namespace osu.Game.Tests.Visual.Online AddStep("set statistics to empty", () => statisticsProvider.UpdateStatistics(new UserStatistics(), Ruleset.Value)); } + [Test] + public void TestLocalUserActivity() + { + AddStep("idle", () => setLocalUserPresence(UserStatus.Online, null)); + AddStep("watching replay", () => setLocalUserPresence(UserStatus.Online, new UserActivity.WatchingReplay(createScore(@"nats")))); + AddStep("spectating user", () => setLocalUserPresence(UserStatus.Online, new UserActivity.SpectatingUser(createScore(@"mrekk")))); + AddStep("solo (osu!)", () => setLocalUserPresence(UserStatus.Online, soloGameStatusForRuleset(0))); + AddStep("solo (osu!taiko)", () => setLocalUserPresence(UserStatus.Online, soloGameStatusForRuleset(1))); + AddStep("solo (osu!catch)", () => setLocalUserPresence(UserStatus.Online, soloGameStatusForRuleset(2))); + AddStep("solo (osu!mania)", () => setLocalUserPresence(UserStatus.Online, soloGameStatusForRuleset(3))); + AddStep("choosing", () => setLocalUserPresence(UserStatus.Online, new UserActivity.ChoosingBeatmap())); + AddStep("editing beatmap", () => setLocalUserPresence(UserStatus.Online, new UserActivity.EditingBeatmap(new BeatmapInfo()))); + AddStep("modding beatmap", () => setLocalUserPresence(UserStatus.Online, new UserActivity.ModdingBeatmap(new BeatmapInfo()))); + AddStep("testing beatmap", () => setLocalUserPresence(UserStatus.Online, new UserActivity.TestingBeatmap(new BeatmapInfo()))); + AddStep("set offline status", () => setLocalUserPresence(UserStatus.Offline, null)); + } + private void setPresence(UserStatus status, UserActivity? activity) { if (status == UserStatus.Offline) @@ -188,6 +210,13 @@ namespace osu.Game.Tests.Visual.Online metadataClient.UserPresenceUpdated(panel.User.OnlineID, new UserPresence { Status = status, Activity = activity }); } + private void setLocalUserPresence(UserStatus status, UserActivity? activity) + { + DummyAPIAccess dummyAPI = (DummyAPIAccess)API; + dummyAPI.Status.Value = status; + dummyAPI.Activity.Value = activity; + } + private UserActivity soloGameStatusForRuleset(int rulesetId) => new UserActivity.InSoloGame(new BeatmapInfo(), rulesetStore.GetRuleset(rulesetId)!); private ScoreInfo createScore(string name) => new ScoreInfo(new TestBeatmap(Ruleset.Value).BeatmapInfo) From a4174a36447fddeeb13c83fa6724520486271c62 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 16 Jan 2025 17:39:34 +0900 Subject: [PATCH 0597/1275] 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 0598/1275] 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 974fa76987a445f0d0d18f823e11e0bb4ffec842 Mon Sep 17 00:00:00 2001 From: molneya <62799417+molneya@users.noreply.github.com> Date: Thu, 16 Jan 2025 17:08:47 +0800 Subject: [PATCH 0599/1275] fix spinners not increasing cumulative strain time (#31525) Co-authored-by: StanR --- .../Difficulty/Evaluators/FlashlightEvaluator.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Difficulty/Evaluators/FlashlightEvaluator.cs b/osu.Game.Rulesets.Osu/Difficulty/Evaluators/FlashlightEvaluator.cs index 5cb5a8f934..9d05f0b074 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/Evaluators/FlashlightEvaluator.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/Evaluators/FlashlightEvaluator.cs @@ -52,12 +52,12 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators var currentObj = (OsuDifficultyHitObject)current.Previous(i); var currentHitObject = (OsuHitObject)(currentObj.BaseObject); + cumulativeStrainTime += lastObj.StrainTime; + if (!(currentObj.BaseObject is Spinner)) { double jumpDistance = (osuHitObject.StackedPosition - currentHitObject.StackedEndPosition).Length; - cumulativeStrainTime += lastObj.StrainTime; - // We want to nerf objects that can be easily seen within the Flashlight circle radius. if (i == 0) smallDistNerf = Math.Min(1.0, jumpDistance / 75.0); From cde8e7b82e204010fad79177f9fa3aa3a7f35b84 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 16 Jan 2025 18:54:51 +0900 Subject: [PATCH 0600/1275] 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 0601/1275] 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 0602/1275] 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 0603/1275] 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 0604/1275] 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 9da8dcd8151009a2252c9b3f45d258f92a501895 Mon Sep 17 00:00:00 2001 From: Jay Lawton Date: Thu, 16 Jan 2025 20:30:02 +1000 Subject: [PATCH 0605/1275] osu!taiko stamina balancing (#31337) * stamina considerations * remove consecutive note count * adjust multiplier * add back comment * adjust tests * adjusts tests post merge * use diffcalcutils --------- Co-authored-by: StanR --- .../TaikoDifficultyCalculatorTest.cs | 8 ++++---- .../Difficulty/Evaluators/StaminaEvaluator.cs | 17 ++++++++--------- .../Difficulty/Skills/Stamina.cs | 9 ++++++--- 3 files changed, 18 insertions(+), 16 deletions(-) diff --git a/osu.Game.Rulesets.Taiko.Tests/TaikoDifficultyCalculatorTest.cs b/osu.Game.Rulesets.Taiko.Tests/TaikoDifficultyCalculatorTest.cs index de3bec5fcf..517f62b6f5 100644 --- a/osu.Game.Rulesets.Taiko.Tests/TaikoDifficultyCalculatorTest.cs +++ b/osu.Game.Rulesets.Taiko.Tests/TaikoDifficultyCalculatorTest.cs @@ -14,13 +14,13 @@ namespace osu.Game.Rulesets.Taiko.Tests { protected override string ResourceAssembly => "osu.Game.Rulesets.Taiko"; - [TestCase(2.837609165845338d, 200, "diffcalc-test")] - [TestCase(2.837609165845338d, 200, "diffcalc-test-strong")] + [TestCase(2.912326627861987d, 200, "diffcalc-test")] + [TestCase(2.912326627861987d, 200, "diffcalc-test-strong")] public void Test(double expectedStarRating, int expectedMaxCombo, string name) => base.Test(expectedStarRating, expectedMaxCombo, name); - [TestCase(3.8005218640444949, 200, "diffcalc-test")] - [TestCase(3.8005218640444949, 200, "diffcalc-test-strong")] + [TestCase(3.9339069955362014d, 200, "diffcalc-test")] + [TestCase(3.9339069955362014d, 200, "diffcalc-test-strong")] public void TestClockRateAdjusted(double expectedStarRating, int expectedMaxCombo, string name) => Test(expectedStarRating, expectedMaxCombo, name, new TaikoModDoubleTime()); diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Evaluators/StaminaEvaluator.cs b/osu.Game.Rulesets.Taiko/Difficulty/Evaluators/StaminaEvaluator.cs index 84d5de4c63..a273d91a38 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Evaluators/StaminaEvaluator.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Evaluators/StaminaEvaluator.cs @@ -19,7 +19,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Evaluators // Interval is capped at a very small value to prevent infinite values. interval = Math.Max(interval, 1); - return 30 / interval; + return 20 / interval; } /// @@ -59,16 +59,15 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Evaluators // Find the previous hit object hit by the current finger, which is n notes prior, n being the number of // available fingers. TaikoDifficultyHitObject taikoCurrent = (TaikoDifficultyHitObject)current; - TaikoDifficultyHitObject? keyPrevious = taikoCurrent.PreviousMono(availableFingersFor(taikoCurrent) - 1); - - if (keyPrevious == null) - { - // There is no previous hit object hit by the current finger - return 0.0; - } + TaikoDifficultyHitObject? taikoPrevious = current.Previous(1) as TaikoDifficultyHitObject; + TaikoDifficultyHitObject? previousMono = taikoCurrent.PreviousMono(availableFingersFor(taikoCurrent) - 1); double objectStrain = 0.5; // Add a base strain to all objects - objectStrain += speedBonus(taikoCurrent.StartTime - keyPrevious.StartTime); + if (taikoPrevious == null) return objectStrain; + + if (previousMono != null) + objectStrain += speedBonus(taikoCurrent.StartTime - previousMono.StartTime) + 0.5 * speedBonus(taikoCurrent.StartTime - taikoPrevious.StartTime); + return objectStrain; } } diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Skills/Stamina.cs b/osu.Game.Rulesets.Taiko/Difficulty/Skills/Stamina.cs index f6914039f0..29f9f16033 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Skills/Stamina.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Skills/Stamina.cs @@ -4,6 +4,7 @@ using System; using osu.Game.Rulesets.Difficulty.Preprocessing; using osu.Game.Rulesets.Difficulty.Skills; +using osu.Game.Rulesets.Difficulty.Utils; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Taiko.Difficulty.Evaluators; using osu.Game.Rulesets.Taiko.Difficulty.Preprocessing; @@ -44,10 +45,12 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Skills var currentObject = current as TaikoDifficultyHitObject; int index = currentObject?.Colour.MonoStreak?.HitObjects.IndexOf(currentObject) ?? 0; - if (singleColourStamina) - return currentStrain / (1 + Math.Exp(-(index - 10) / 2.0)); + double monolengthBonus = 1 + Math.Min(Math.Max((index - 5) / 50.0, 0), 0.30); - return currentStrain; + if (singleColourStamina) + return DifficultyCalculationUtils.Logistic(-(index - 10) / 2.0, currentStrain); + + return currentStrain * monolengthBonus; } protected override double CalculateInitialStrain(double time, DifficultyHitObject current) => singleColourStamina ? 0 : currentStrain * strainDecay(time - current.Previous(0).StartTime); From 840072688749f6c24f3aab3926d9eeed22b36861 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Thu, 16 Jan 2025 19:33:38 +0900 Subject: [PATCH 0606/1275] 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 0607/1275] 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 0608/1275] 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 0609/1275] Bump NVika tool to 4.0.0 Code quality CI runs have suddenly started failing out of nowhere: - Passing run: https://github.com/ppy/osu/actions/runs/12806242929/job/35704267944#step:10:1 - Failing run: https://github.com/ppy/osu/actions/runs/12807108792/job/35707131634#step:10:1 In classic github fashion, they began rolling out another runner change wherein `ubuntu-latest` has started meaning `ubuntu-24.04` rather than `ubuntu-22.04`. `ubuntu-24.04` no longer has .NET 6 bundled. Therefore, upgrade NVika to 4.0.0 because that version is compatible with .NET 8. --- .config/dotnet-tools.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.config/dotnet-tools.json b/.config/dotnet-tools.json index c4ba6e5143..6ec071be2f 100644 --- a/.config/dotnet-tools.json +++ b/.config/dotnet-tools.json @@ -9,7 +9,7 @@ ] }, "nvika": { - "version": "3.0.0", + "version": "4.0.0", "commands": [ "nvika" ] From 65b88ab365df223e07a4b7c56794b75b42d4b338 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Thu, 16 Jan 2025 19:38:14 +0900 Subject: [PATCH 0610/1275] Use MetadataClient for local user status --- .../Visual/Online/TestSceneUserPanel.cs | 38 ++++++++----------- osu.Game/Users/ExtendedUserPanel.cs | 8 +--- 2 files changed, 16 insertions(+), 30 deletions(-) diff --git a/osu.Game.Tests/Visual/Online/TestSceneUserPanel.cs b/osu.Game.Tests/Visual/Online/TestSceneUserPanel.cs index 684e8b7b86..f4fc15da20 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneUserPanel.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneUserPanel.cs @@ -9,7 +9,6 @@ using osu.Framework.Graphics.Containers; using osu.Framework.Utils; using osu.Game.Beatmaps; using osu.Game.Online; -using osu.Game.Online.API; using osu.Game.Online.API.Requests.Responses; using osu.Game.Online.Metadata; using osu.Game.Overlays; @@ -188,33 +187,26 @@ namespace osu.Game.Tests.Visual.Online [Test] public void TestLocalUserActivity() { - AddStep("idle", () => setLocalUserPresence(UserStatus.Online, null)); - AddStep("watching replay", () => setLocalUserPresence(UserStatus.Online, new UserActivity.WatchingReplay(createScore(@"nats")))); - AddStep("spectating user", () => setLocalUserPresence(UserStatus.Online, new UserActivity.SpectatingUser(createScore(@"mrekk")))); - AddStep("solo (osu!)", () => setLocalUserPresence(UserStatus.Online, soloGameStatusForRuleset(0))); - AddStep("solo (osu!taiko)", () => setLocalUserPresence(UserStatus.Online, soloGameStatusForRuleset(1))); - AddStep("solo (osu!catch)", () => setLocalUserPresence(UserStatus.Online, soloGameStatusForRuleset(2))); - AddStep("solo (osu!mania)", () => setLocalUserPresence(UserStatus.Online, soloGameStatusForRuleset(3))); - AddStep("choosing", () => setLocalUserPresence(UserStatus.Online, new UserActivity.ChoosingBeatmap())); - AddStep("editing beatmap", () => setLocalUserPresence(UserStatus.Online, new UserActivity.EditingBeatmap(new BeatmapInfo()))); - AddStep("modding beatmap", () => setLocalUserPresence(UserStatus.Online, new UserActivity.ModdingBeatmap(new BeatmapInfo()))); - AddStep("testing beatmap", () => setLocalUserPresence(UserStatus.Online, new UserActivity.TestingBeatmap(new BeatmapInfo()))); - AddStep("set offline status", () => setLocalUserPresence(UserStatus.Offline, null)); + AddStep("idle", () => setPresence(UserStatus.Online, null, API.LocalUser.Value.OnlineID)); + AddStep("watching replay", () => setPresence(UserStatus.Online, new UserActivity.WatchingReplay(createScore(@"nats")), API.LocalUser.Value.OnlineID)); + AddStep("spectating user", () => setPresence(UserStatus.Online, new UserActivity.SpectatingUser(createScore(@"mrekk")), API.LocalUser.Value.OnlineID)); + AddStep("solo (osu!)", () => setPresence(UserStatus.Online, soloGameStatusForRuleset(0), API.LocalUser.Value.OnlineID)); + AddStep("solo (osu!taiko)", () => setPresence(UserStatus.Online, soloGameStatusForRuleset(1), API.LocalUser.Value.OnlineID)); + AddStep("solo (osu!catch)", () => setPresence(UserStatus.Online, soloGameStatusForRuleset(2), API.LocalUser.Value.OnlineID)); + AddStep("solo (osu!mania)", () => setPresence(UserStatus.Online, soloGameStatusForRuleset(3), API.LocalUser.Value.OnlineID)); + AddStep("choosing", () => setPresence(UserStatus.Online, new UserActivity.ChoosingBeatmap(), API.LocalUser.Value.OnlineID)); + AddStep("editing beatmap", () => setPresence(UserStatus.Online, new UserActivity.EditingBeatmap(new BeatmapInfo()), API.LocalUser.Value.OnlineID)); + AddStep("modding beatmap", () => setPresence(UserStatus.Online, new UserActivity.ModdingBeatmap(new BeatmapInfo()), API.LocalUser.Value.OnlineID)); + AddStep("testing beatmap", () => setPresence(UserStatus.Online, new UserActivity.TestingBeatmap(new BeatmapInfo()), API.LocalUser.Value.OnlineID)); + AddStep("set offline status", () => setPresence(UserStatus.Offline, null, API.LocalUser.Value.OnlineID)); } - private void setPresence(UserStatus status, UserActivity? activity) + private void setPresence(UserStatus status, UserActivity? activity, int? userId = null) { if (status == UserStatus.Offline) - metadataClient.UserPresenceUpdated(panel.User.OnlineID, null); + metadataClient.UserPresenceUpdated(userId ?? panel.User.OnlineID, null); else - metadataClient.UserPresenceUpdated(panel.User.OnlineID, new UserPresence { Status = status, Activity = activity }); - } - - private void setLocalUserPresence(UserStatus status, UserActivity? activity) - { - DummyAPIAccess dummyAPI = (DummyAPIAccess)API; - dummyAPI.Status.Value = status; - dummyAPI.Activity.Value = activity; + metadataClient.UserPresenceUpdated(userId ?? panel.User.OnlineID, new UserPresence { Status = status, Activity = activity }); } private UserActivity soloGameStatusForRuleset(int rulesetId) => new UserActivity.InSoloGame(new BeatmapInfo(), rulesetStore.GetRuleset(rulesetId)!); diff --git a/osu.Game/Users/ExtendedUserPanel.cs b/osu.Game/Users/ExtendedUserPanel.cs index eb1115e296..2fc2a97b47 100644 --- a/osu.Game/Users/ExtendedUserPanel.cs +++ b/osu.Game/Users/ExtendedUserPanel.cs @@ -94,13 +94,7 @@ namespace osu.Game.Users private void updatePresence() { - UserPresence? presence; - - if (User.Equals(api?.LocalUser.Value)) - presence = new UserPresence { Status = api.Status.Value, Activity = api.Activity.Value }; - else - presence = metadata?.GetPresence(User.OnlineID); - + UserPresence? presence = metadata?.GetPresence(User.OnlineID); UserStatus status = presence?.Status ?? UserStatus.Offline; UserActivity? activity = presence?.Activity; From 94db39317b626a90c6dd60c2c9dee530bdc58fe2 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Thu, 16 Jan 2025 20:43:22 +0900 Subject: [PATCH 0611/1275] Add xmldoc --- osu.Game/Configuration/SessionStatics.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/osu.Game/Configuration/SessionStatics.cs b/osu.Game/Configuration/SessionStatics.cs index bdfb0217ad..d2069e4027 100644 --- a/osu.Game/Configuration/SessionStatics.cs +++ b/osu.Game/Configuration/SessionStatics.cs @@ -95,6 +95,9 @@ namespace osu.Game.Configuration /// DailyChallengeIntroPlayed, + /// + /// The activity for the current user to broadcast to other players. + /// UserOnlineActivity, } } From a6057a9f54e186557694861f292a132c5c881d0f Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 16 Jan 2025 20:25:16 +0900 Subject: [PATCH 0612/1275] 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 0613/1275] Fix potential index accounting mistake when creating spectator list with spectators already present Noticed by accident, but if the `BindCollectionChanged()` callback fires immediately in `LoadComplete()` when set up and there are spectators present already, then `NewStartingIndex` in the related event is -1: https://github.com/dotnet/runtime/blob/b03f83de362f7168c94daa2f4b192959abefe366/src/libraries/System.ObjectModel/src/System/Collections/Specialized/NotifyCollectionChangedEventArgs.cs#L84-L92 which kinda breaks the math introducing off-by-ones and in result causes 11 items to be displayed together rather than 10. --- osu.Game/Screens/Play/HUD/SpectatorList.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/Play/HUD/SpectatorList.cs b/osu.Game/Screens/Play/HUD/SpectatorList.cs index 04bd03f153..438aa61d9d 100644 --- a/osu.Game/Screens/Play/HUD/SpectatorList.cs +++ b/osu.Game/Screens/Play/HUD/SpectatorList.cs @@ -95,7 +95,7 @@ namespace osu.Game.Screens.Play.HUD for (int i = 0; i < e.NewItems!.Count; i++) { var spectator = (Spectator)e.NewItems![i]!; - int index = e.NewStartingIndex + i; + int index = Math.Max(e.NewStartingIndex, 0) + i; if (index >= max_spectators_displayed) break; From 1f1e940adaa1d943707cd3191d876d054659c66b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 16 Jan 2025 15:13:16 +0100 Subject: [PATCH 0614/1275] Restore virtual modifier to fix tests (and mark for posterity) --- osu.Game/Online/Spectator/SpectatorClient.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/osu.Game/Online/Spectator/SpectatorClient.cs b/osu.Game/Online/Spectator/SpectatorClient.cs index ac11dad0f0..91f009b76f 100644 --- a/osu.Game/Online/Spectator/SpectatorClient.cs +++ b/osu.Game/Online/Spectator/SpectatorClient.cs @@ -6,6 +6,7 @@ using System.Collections.Generic; using System.Diagnostics; using System.Linq; using System.Threading.Tasks; +using JetBrains.Annotations; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Development; @@ -38,7 +39,8 @@ namespace osu.Game.Online.Spectator /// /// The states of all users currently being watched by the local user. /// - public IBindableDictionary WatchedUserStates => watchedUserStates; + [UsedImplicitly] // Marked virtual due to mock use in testing + public virtual IBindableDictionary WatchedUserStates => watchedUserStates; /// /// All users who are currently watching the local user. @@ -58,6 +60,7 @@ namespace osu.Game.Online.Spectator /// /// Called whenever new frames arrive from the server. /// + [UsedImplicitly] // Marked virtual due to mock use in testing public virtual event Action? OnNewFrames; /// From 5c799d733f2543cbb35295cea68333ab4bd4f31d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 16 Jan 2025 15:25:56 +0100 Subject: [PATCH 0615/1275] Bind to playing state via `GameplayState` instead to fix more tests --- osu.Game/Screens/Play/GameplayState.cs | 11 ++++++++++- osu.Game/Screens/Play/HUD/SpectatorList.cs | 4 ++-- osu.Game/Screens/Play/Player.cs | 2 +- 3 files changed, 13 insertions(+), 4 deletions(-) diff --git a/osu.Game/Screens/Play/GameplayState.cs b/osu.Game/Screens/Play/GameplayState.cs index 478acd7229..bfeabcc82e 100644 --- a/osu.Game/Screens/Play/GameplayState.cs +++ b/osu.Game/Screens/Play/GameplayState.cs @@ -69,6 +69,11 @@ namespace osu.Game.Screens.Play private readonly Bindable lastJudgementResult = new Bindable(); + /// + /// The local user's playing state (whether actively playing, paused, or not playing due to watching a replay or similar). + /// + public IBindable Playing { get; } = new Bindable(); + public GameplayState( IBeatmap beatmap, Ruleset ruleset, @@ -76,7 +81,8 @@ namespace osu.Game.Screens.Play Score? score = null, ScoreProcessor? scoreProcessor = null, HealthProcessor? healthProcessor = null, - Storyboard? storyboard = null) + Storyboard? storyboard = null, + IBindable? localUserPlaying = null) { Beatmap = beatmap; Ruleset = ruleset; @@ -92,6 +98,9 @@ namespace osu.Game.Screens.Play ScoreProcessor = scoreProcessor ?? ruleset.CreateScoreProcessor(); HealthProcessor = healthProcessor ?? ruleset.CreateHealthProcessor(beatmap.HitObjects[0].StartTime); Storyboard = storyboard ?? new Storyboard(); + + if (localUserPlaying != null) + Playing.BindTo(localUserPlaying); } /// diff --git a/osu.Game/Screens/Play/HUD/SpectatorList.cs b/osu.Game/Screens/Play/HUD/SpectatorList.cs index ab4958f0c1..35a2d1eefb 100644 --- a/osu.Game/Screens/Play/HUD/SpectatorList.cs +++ b/osu.Game/Screens/Play/HUD/SpectatorList.cs @@ -242,10 +242,10 @@ namespace osu.Game.Screens.Play.HUD public bool UsesFixedAnchor { get; set; } [BackgroundDependencyLoader] - private void load(SpectatorClient client, Player player) + private void load(SpectatorClient client, GameplayState gameplayState) { ((IBindableList)Spectators).BindTo(client.WatchingUsers); - ((IBindable)UserPlayingState).BindTo(player.PlayingState); + ((IBindable)UserPlayingState).BindTo(gameplayState.Playing); } } } diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs index 228b77b780..a797603e17 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -261,7 +261,7 @@ namespace osu.Game.Screens.Play Score.ScoreInfo.Ruleset = ruleset.RulesetInfo; Score.ScoreInfo.Mods = gameplayMods; - dependencies.CacheAs(GameplayState = new GameplayState(playableBeatmap, ruleset, gameplayMods, Score, ScoreProcessor, HealthProcessor, Beatmap.Value.Storyboard)); + dependencies.CacheAs(GameplayState = new GameplayState(playableBeatmap, ruleset, gameplayMods, Score, ScoreProcessor, HealthProcessor, Beatmap.Value.Storyboard, PlayingState)); var rulesetSkinProvider = new RulesetSkinProvidingContainer(ruleset, playableBeatmap, Beatmap.Value.Skin); GameplayClockContainer.Add(new GameplayScrollWheelHandling()); From 1949c01103c4dec239761c4591222044554e5045 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 16 Jan 2025 15:29:07 +0100 Subject: [PATCH 0616/1275] Fix skin deserialisation test --- .../Archives/modified-argon-20250116.osk | Bin 0 -> 1675 bytes osu.Game.Tests/Skins/SkinDeserialisationTest.cs | 2 ++ 2 files changed, 2 insertions(+) create mode 100644 osu.Game.Tests/Resources/Archives/modified-argon-20250116.osk diff --git a/osu.Game.Tests/Resources/Archives/modified-argon-20250116.osk b/osu.Game.Tests/Resources/Archives/modified-argon-20250116.osk new file mode 100644 index 0000000000000000000000000000000000000000..811e91b74916988a54a9ff2b73cafe0e0ff0a490 GIT binary patch literal 1675 zcmZ{kdpHw%7{`Au>w-j9CyA7%NLq|6k|()sWSeVcGHlFcE*qLu?u>?RYA&UyL}IHY zw}iukIc8W+qm!d@B$-}iam4+gEE2n7JF0(J@TWABHC zhspo|t=~NWP(#TSV={#dJI7XPwNZmerCE186rJYOQfZ#Co*{Z0v?59~;u@ms)E3N@ z4|bh^`|pFdCHQ`*XkF|GHdEAY^ohcD8EGoSlvszBP3?iUYP{ZTiEs@bavhzYN)2vRc0<$f-j=rNLdmlSS^{G) z(+6+GE^f?}5xbRp9gY-x-6in25_!xIn?!B>m=Fs9$N>PX`F9gCg%n{NLXDtQj^i+x zS#*z!2M34g-ec_Ho{2>nq4@l4EUpi30ypV*@9X@{wac03>>_8vRB=IErrE?7W#cr^ zv)MMHLt>72mM7L7yGCz^G3YQB1ICijuhGd8Ov=ZRr2Gj;P|)}r;`pt1$mA(7LxcKV zK>2XNj|1*H?-iG>0Dzhf02=@RE(9{g(c`dfM0jKbg-D@MzfU^N!bgbU(DV<|UE5-n zvDS{d7vlvj8wbqSFk_p_D>_qc#|UA8mf=T*OV&I3@)H{vyVuY{>UPqne@D)giggNy z_1el$uQZ02a%|DNQBYAIWJ>SH1ekqg6UVRD3!E6v$jxq!Fo!aVO22xG zV25`R$Dl8uU>&aVV6U~GJdX%+`DJrjFhpZmXH3lb_P4BODY3QYgZuOaA?2;7xrEeJ z6Z$nVZ3p+xBXxL(xee1}qoWw-luT~;rJkX|}<#Bb}GI_!*eBBh?Z zI>P@zd2-L)rZcHLE*f&<>A^o)i7bDVaDb(4UT9izp#c7pUHDm%G;#*aq7f4_<{cbJI-Lc#)cST=F9`=s#G>pDQ0Z#2`^SFH&I!O6J z)>|cItJXk<$n!}zwvfHc>_q0Ej%s_!yZq;{jL7sa?vjeU?1lBiY1YV^-0oa!M+sIs5)b7)i*5og$;VB4BQi_q>xZ$JwMI)1!Qph>(JpwEGItxb7 zu^yV1D<1DH$(Eklm-N-Kzn1n~Y2pa0 zzVcj+w9U2H6`vLEzyGyyBrVQN`9uEkUgWVL8p;W|vTl86ZY^iad^$CA@B(~MGkY?J zTafeiA-=lKC1-k=G%UX7mrE~w!>YPXJo-lq96@lW2obAG*oG&cHR`Xbe^J~>Vx7(5 zM8q}Ad~SE`t3*=(01^P83IM=_5$Gf`F)Z+ZwkV-@8}|0_-8eY-pxg{TYaLJHVl?i6 zxPLY&S!lDjB=}5}L7s-T@@rwVP#$2=a`I5{a|$mj>6`T*lVz)9iMOnzZdv}xmddhd mSxKRCKO>WD5810MD+vaXy%7ctS@A8d2o%8#03a3eE&Ct9*SzTf literal 0 HcmV?d00001 diff --git a/osu.Game.Tests/Skins/SkinDeserialisationTest.cs b/osu.Game.Tests/Skins/SkinDeserialisationTest.cs index 962a9b2a7a..55836302e6 100644 --- a/osu.Game.Tests/Skins/SkinDeserialisationTest.cs +++ b/osu.Game.Tests/Skins/SkinDeserialisationTest.cs @@ -71,6 +71,8 @@ namespace osu.Game.Tests.Skins "Archives/modified-classic-20240724.osk", // Covers skinnable mod display "Archives/modified-default-20241207.osk", + // Covers skinnable spectator list + "Archives/modified-argon-20250116.osk", }; /// From b9894f67ceac3ba42995cd81b0692c414620053f 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 0617/1275] 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 5fc277aa7f88677ab68291ef592a1fdc9cb8d1be Mon Sep 17 00:00:00 2001 From: Rudi Herouard Date: Thu, 16 Jan 2025 21:53:56 +0100 Subject: [PATCH 0618/1275] 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 a83f917d87c51f95d2778afce0048c08f8af125f Mon Sep 17 00:00:00 2001 From: Jay Lawton Date: Fri, 17 Jan 2025 07:14:05 +1000 Subject: [PATCH 0619/1275] osu!taiko star rating and performance points rebalance (#31338) * rebalance * revert pp scaling change * further rebalancing * comment * adjust tests --- .../TaikoDifficultyCalculatorTest.cs | 8 +++--- .../Difficulty/TaikoDifficultyAttributes.cs | 4 +-- .../Difficulty/TaikoDifficultyCalculator.cs | 27 +++++++++++++------ .../Difficulty/TaikoPerformanceCalculator.cs | 8 +++--- 4 files changed, 30 insertions(+), 17 deletions(-) diff --git a/osu.Game.Rulesets.Taiko.Tests/TaikoDifficultyCalculatorTest.cs b/osu.Game.Rulesets.Taiko.Tests/TaikoDifficultyCalculatorTest.cs index 517f62b6f5..b4cbe03511 100644 --- a/osu.Game.Rulesets.Taiko.Tests/TaikoDifficultyCalculatorTest.cs +++ b/osu.Game.Rulesets.Taiko.Tests/TaikoDifficultyCalculatorTest.cs @@ -14,13 +14,13 @@ namespace osu.Game.Rulesets.Taiko.Tests { protected override string ResourceAssembly => "osu.Game.Rulesets.Taiko"; - [TestCase(2.912326627861987d, 200, "diffcalc-test")] - [TestCase(2.912326627861987d, 200, "diffcalc-test-strong")] + [TestCase(3.3172381854905493d, 200, "diffcalc-test")] + [TestCase(3.3172381854905493d, 200, "diffcalc-test-strong")] public void Test(double expectedStarRating, int expectedMaxCombo, string name) => base.Test(expectedStarRating, expectedMaxCombo, name); - [TestCase(3.9339069955362014d, 200, "diffcalc-test")] - [TestCase(3.9339069955362014d, 200, "diffcalc-test-strong")] + [TestCase(4.4640702427013101d, 200, "diffcalc-test")] + [TestCase(4.4640702427013101d, 200, "diffcalc-test-strong")] public void TestClockRateAdjusted(double expectedStarRating, int expectedMaxCombo, string name) => Test(expectedStarRating, expectedMaxCombo, name, new TaikoModDoubleTime()); diff --git a/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyAttributes.cs b/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyAttributes.cs index ef729e1f07..37e6996e5a 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyAttributes.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyAttributes.cs @@ -40,8 +40,8 @@ namespace osu.Game.Rulesets.Taiko.Difficulty [JsonProperty("mono_stamina_factor")] public double MonoStaminaFactor { get; set; } - [JsonProperty("reading_difficult_strains")] - public double ReadingTopStrains { get; set; } + [JsonProperty("rhythm_difficult_strains")] + public double RhythmTopStrains { get; set; } [JsonProperty("colour_difficult_strains")] public double ColourTopStrains { get; set; } diff --git a/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs b/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs index f8ff6f6065..3ad9d17526 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs @@ -24,10 +24,13 @@ namespace osu.Game.Rulesets.Taiko.Difficulty public class TaikoDifficultyCalculator : DifficultyCalculator { private const double difficulty_multiplier = 0.084375; - private const double rhythm_skill_multiplier = 1.24 * difficulty_multiplier; + private const double rhythm_skill_multiplier = 0.65 * difficulty_multiplier; private const double reading_skill_multiplier = 0.100 * difficulty_multiplier; private const double colour_skill_multiplier = 0.375 * difficulty_multiplier; - private const double stamina_skill_multiplier = 0.375 * difficulty_multiplier; + private const double stamina_skill_multiplier = 0.445 * difficulty_multiplier; + + private double strainLengthBonus; + private double patternMultiplier; public override int Version => 20241007; @@ -116,8 +119,16 @@ namespace osu.Game.Rulesets.Taiko.Difficulty double monoStaminaFactor = staminaRating == 0 ? 1 : Math.Pow(monoStaminaRating / staminaRating, 5); double colourDifficultStrains = colour.CountTopWeightedStrains(); - double readingDifficultStrains = reading.CountTopWeightedStrains(); - double staminaDifficultStrains = stamina.CountTopWeightedStrains(); + double rhythmDifficultStrains = rhythm.CountTopWeightedStrains(); + // Due to constraints of strain in cases where difficult strain values don't shift with range changes, we manually apply clockrate. + double staminaDifficultStrains = stamina.CountTopWeightedStrains() * clockRate; + + // As we don't have pattern integration in osu!taiko, we apply the other two skills relative to rhythm. + patternMultiplier = Math.Pow(staminaRating * colourRating, 0.10); + + strainLengthBonus = 1 + + Math.Min(Math.Max((staminaDifficultStrains - 1350) / 5000, 0), 0.15) + + Math.Min(Math.Max((staminaRating - 7.0) / 1.0, 0), 0.05); double combinedRating = combinedDifficultyValue(rhythm, reading, colour, stamina, isRelax); double starRating = rescale(combinedRating * 1.4); @@ -125,7 +136,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty // Converts are penalised outside the scope of difficulty calculation, as our assumptions surrounding standard play-styles becomes out-of-scope. if (beatmap.BeatmapInfo.Ruleset.OnlineID == 0) { - starRating *= 0.825; + starRating *= 0.7; // For maps with relax, multiple inputs are more likely to be abused. if (isRelax) @@ -144,7 +155,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty ColourDifficulty = colourRating, StaminaDifficulty = staminaRating, MonoStaminaFactor = monoStaminaFactor, - ReadingTopStrains = readingDifficultStrains, + RhythmTopStrains = rhythmDifficultStrains, ColourTopStrains = colourDifficultStrains, StaminaTopStrains = staminaDifficultStrains, GreatHitWindow = hitWindows.WindowFor(HitResult.Great) / clockRate, @@ -173,10 +184,10 @@ namespace osu.Game.Rulesets.Taiko.Difficulty for (int i = 0; i < colourPeaks.Count; i++) { - double rhythmPeak = rhythmPeaks[i] * rhythm_skill_multiplier; + double rhythmPeak = rhythmPeaks[i] * rhythm_skill_multiplier * patternMultiplier; double readingPeak = readingPeaks[i] * reading_skill_multiplier; double colourPeak = colourPeaks[i] * colour_skill_multiplier; - double staminaPeak = staminaPeaks[i] * stamina_skill_multiplier; + double staminaPeak = staminaPeaks[i] * stamina_skill_multiplier * strainLengthBonus; if (isRelax) { diff --git a/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceCalculator.cs b/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceCalculator.cs index 4933c9dee6..c29ea3ba73 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceCalculator.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceCalculator.cs @@ -73,8 +73,10 @@ namespace osu.Game.Rulesets.Taiko.Difficulty private double computeDifficultyValue(ScoreInfo score, TaikoDifficultyAttributes attributes) { - double baseDifficulty = 5 * Math.Max(1.0, attributes.StarRating / 0.115) - 4.0; - double difficultyValue = Math.Min(Math.Pow(baseDifficulty, 3) / 69052.51, Math.Pow(baseDifficulty, 2.25) / 1150.0); + double baseDifficulty = 5 * Math.Max(1.0, attributes.StarRating / 0.110) - 4.0; + double difficultyValue = Math.Min(Math.Pow(baseDifficulty, 3) / 69052.51, Math.Pow(baseDifficulty, 2.25) / 1250.0); + + difficultyValue *= 1 + 0.10 * Math.Max(0, attributes.StarRating - 10); double lengthBonus = 1 + 0.1 * Math.Min(1.0, totalHits / 1500.0); difficultyValue *= lengthBonus; @@ -95,7 +97,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty // Scale accuracy more harshly on nearly-completely mono (single coloured) speed maps. double accScalingExponent = 2 + attributes.MonoStaminaFactor; - double accScalingShift = 400 - 100 * attributes.MonoStaminaFactor; + double accScalingShift = 500 - 100 * attributes.MonoStaminaFactor; return difficultyValue * Math.Pow(SpecialFunctions.Erf(accScalingShift / (Math.Sqrt(2) * estimatedUnstableRate.Value)), accScalingExponent); } From daa7921c2d1510db65cd638a29662aef2b0aca91 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 17 Jan 2025 12:55:11 +0900 Subject: [PATCH 0620/1275] Mark `IsTablet` with `new` to avoid inspection Co-authored-by: Susko3 --- osu.Android/OsuGameActivity.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Android/OsuGameActivity.cs b/osu.Android/OsuGameActivity.cs index 0b5deef6fb..66c697801b 100644 --- a/osu.Android/OsuGameActivity.cs +++ b/osu.Android/OsuGameActivity.cs @@ -49,7 +49,7 @@ namespace osu.Android /// Adjusted on startup to match expected UX for the current device type (phone/tablet). public ScreenOrientation DefaultOrientation = ScreenOrientation.Unspecified; - public bool IsTablet { get; private set; } + public new bool IsTablet { get; private set; } private readonly OsuGameAndroid game; From 224f39825f5f452ec6e7341666b2cae6ac700334 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 17 Jan 2025 14:16:38 +0900 Subject: [PATCH 0621/1275] 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 0622/1275] 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 0623/1275] Separate the local user state --- osu.Game/Online/Metadata/MetadataClient.cs | 5 ++++ .../Online/Metadata/OnlineMetadataClient.cs | 27 ++++++++++++------- .../Visual/Metadata/TestMetadataClient.cs | 19 ++++++++++--- 3 files changed, 38 insertions(+), 13 deletions(-) diff --git a/osu.Game/Online/Metadata/MetadataClient.cs b/osu.Game/Online/Metadata/MetadataClient.cs index 6578f70f74..507f43467c 100644 --- a/osu.Game/Online/Metadata/MetadataClient.cs +++ b/osu.Game/Online/Metadata/MetadataClient.cs @@ -37,6 +37,11 @@ namespace osu.Game.Online.Metadata /// public abstract IBindable IsWatchingUserPresence { get; } + /// + /// The information about the current user. + /// + public abstract UserPresence LocalUserState { get; } + /// /// Dictionary keyed by user ID containing all of the information about currently online users received from the server. /// diff --git a/osu.Game/Online/Metadata/OnlineMetadataClient.cs b/osu.Game/Online/Metadata/OnlineMetadataClient.cs index 01d7a564fa..04abca1e9b 100644 --- a/osu.Game/Online/Metadata/OnlineMetadataClient.cs +++ b/osu.Game/Online/Metadata/OnlineMetadataClient.cs @@ -23,6 +23,9 @@ namespace osu.Game.Online.Metadata public override IBindable IsWatchingUserPresence => isWatchingUserPresence; private readonly BindableBool isWatchingUserPresence = new BindableBool(); + public override UserPresence LocalUserState => localUserState; + private UserPresence localUserState; + public override IBindableDictionary UserStates => userStates; private readonly BindableDictionary userStates = new BindableDictionary(); @@ -110,6 +113,7 @@ namespace osu.Game.Online.Metadata userStates.Clear(); friendStates.Clear(); dailyChallengeInfo.Value = null; + localUserState = default; }); return; } @@ -202,9 +206,19 @@ namespace osu.Game.Online.Metadata Schedule(() => { if (presence?.Status != null) - userStates[userId] = presence.Value; + { + if (userId == api.LocalUser.Value.OnlineID) + localUserState = presence.Value; + else + userStates[userId] = presence.Value; + } else - userStates.Remove(userId); + { + if (userId == api.LocalUser.Value.OnlineID) + localUserState = default; + else + userStates.Remove(userId); + } }); return Task.CompletedTask; @@ -242,14 +256,7 @@ namespace osu.Game.Online.Metadata throw new OperationCanceledException(); // must be scheduled before any remote calls to avoid mis-ordering. - Schedule(() => - { - bool hadLocalUserState = userStates.TryGetValue(api.LocalUser.Value.OnlineID, out var presence); - userStates.Clear(); - if (hadLocalUserState) - userStates[api.LocalUser.Value.OnlineID] = presence; - }); - + Schedule(() => userStates.Clear()); Debug.Assert(connection != null); await connection.InvokeAsync(nameof(IMetadataServer.EndWatchingUserPresence)).ConfigureAwait(false); Logger.Log($@"{nameof(OnlineMetadataClient)} stopped watching user presence", LoggingTarget.Network); diff --git a/osu.Game/Tests/Visual/Metadata/TestMetadataClient.cs b/osu.Game/Tests/Visual/Metadata/TestMetadataClient.cs index 36f79a5adc..d32d49b55e 100644 --- a/osu.Game/Tests/Visual/Metadata/TestMetadataClient.cs +++ b/osu.Game/Tests/Visual/Metadata/TestMetadataClient.cs @@ -19,6 +19,9 @@ namespace osu.Game.Tests.Visual.Metadata public override IBindable IsWatchingUserPresence => isWatchingUserPresence; private readonly BindableBool isWatchingUserPresence = new BindableBool(); + public override UserPresence LocalUserState => localUserState; + private UserPresence localUserState; + public override IBindableDictionary UserStates => userStates; private readonly BindableDictionary userStates = new BindableDictionary(); @@ -71,10 +74,20 @@ namespace osu.Game.Tests.Visual.Metadata { if (isWatchingUserPresence.Value) { - if (presence.HasValue) - userStates[userId] = presence.Value; + if (presence?.Status != null) + { + if (userId == api.LocalUser.Value.OnlineID) + localUserState = presence.Value; + else + userStates[userId] = presence.Value; + } else - userStates.Remove(userId); + { + if (userId == api.LocalUser.Value.OnlineID) + localUserState = default; + else + userStates.Remove(userId); + } } return Task.CompletedTask; From 626be9d7806b44434bb773cb9afe002c0639356b Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Fri, 17 Jan 2025 16:01:11 +0900 Subject: [PATCH 0624/1275] Return local user state where appropriate --- osu.Game/Online/Metadata/MetadataClient.cs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/osu.Game/Online/Metadata/MetadataClient.cs b/osu.Game/Online/Metadata/MetadataClient.cs index a72377721a..1b6f96d91b 100644 --- a/osu.Game/Online/Metadata/MetadataClient.cs +++ b/osu.Game/Online/Metadata/MetadataClient.cs @@ -4,8 +4,10 @@ using System; using System.Linq; using System.Threading.Tasks; +using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; +using osu.Game.Online.API; using osu.Game.Users; namespace osu.Game.Online.Metadata @@ -14,6 +16,9 @@ namespace osu.Game.Online.Metadata { public abstract IBindable IsConnected { get; } + [Resolved] + private IAPIProvider api { get; set; } = null!; + #region Beatmap metadata updates public abstract Task GetChangesSince(int queueId); @@ -59,6 +64,9 @@ namespace osu.Game.Online.Metadata /// The user presence, or null if not available or the user's offline. public UserPresence? GetPresence(int userId) { + if (userId == api.LocalUser.Value.OnlineID) + return LocalUserState; + if (FriendStates.TryGetValue(userId, out UserPresence presence)) return presence; From 3bb4b0c2b8a84c5bf3330a84422e6f3c077b346f Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 17 Jan 2025 16:25:48 +0900 Subject: [PATCH 0625/1275] 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 0626/1275] 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 0627/1275] Fix double-retrieval of user presence from dictionary in online display --- .../Overlays/Dashboard/CurrentlyOnlineDisplay.cs | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/osu.Game/Overlays/Dashboard/CurrentlyOnlineDisplay.cs b/osu.Game/Overlays/Dashboard/CurrentlyOnlineDisplay.cs index 39023c16f6..bb4c9d96c8 100644 --- a/osu.Game/Overlays/Dashboard/CurrentlyOnlineDisplay.cs +++ b/osu.Game/Overlays/Dashboard/CurrentlyOnlineDisplay.cs @@ -37,7 +37,7 @@ namespace osu.Game.Overlays.Dashboard private const float padding = 10; private readonly IBindableList playingUsers = new BindableList(); - private readonly IBindableDictionary onlineUsers = new BindableDictionary(); + private readonly IBindableDictionary onlineUserPresences = new BindableDictionary(); private readonly Dictionary userPanels = new Dictionary(); private SearchContainer userFlow; @@ -106,8 +106,8 @@ namespace osu.Game.Overlays.Dashboard { base.LoadComplete(); - onlineUsers.BindTo(metadataClient.UserPresences); - onlineUsers.BindCollectionChanged(onUserUpdated, true); + onlineUserPresences.BindTo(metadataClient.UserPresences); + onlineUserPresences.BindCollectionChanged(onUserPresenceUpdated, true); playingUsers.BindTo(spectatorClient.PlayingUsers); playingUsers.BindCollectionChanged(onPlayingUsersChanged, true); @@ -120,7 +120,7 @@ namespace osu.Game.Overlays.Dashboard searchTextBox.TakeFocus(); } - private void onUserUpdated(object sender, NotifyDictionaryChangedEventArgs e) => Schedule(() => + private void onUserPresenceUpdated(object sender, NotifyDictionaryChangedEventArgs e) => Schedule(() => { switch (e.Action) { @@ -142,8 +142,10 @@ namespace osu.Game.Overlays.Dashboard { userFlow.Add(userPanels[userId] = createUserPanel(user).With(p => { - p.Status.Value = onlineUsers.GetValueOrDefault(userId).Status; - p.Activity.Value = onlineUsers.GetValueOrDefault(userId).Activity; + var presence = onlineUserPresences.GetValueOrDefault(userId); + + p.Status.Value = presence.Status; + p.Activity.Value = presence.Activity; })); }); }); From f59762f0cb4f199e4e00c034807e1084a3237edc Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 17 Jan 2025 17:11:40 +0900 Subject: [PATCH 0628/1275] `Playing` -> `PlayingState` --- osu.Game/Screens/Play/GameplayState.cs | 8 ++++---- osu.Game/Screens/Play/HUD/SpectatorList.cs | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/osu.Game/Screens/Play/GameplayState.cs b/osu.Game/Screens/Play/GameplayState.cs index bfeabcc82e..851e95495f 100644 --- a/osu.Game/Screens/Play/GameplayState.cs +++ b/osu.Game/Screens/Play/GameplayState.cs @@ -72,7 +72,7 @@ namespace osu.Game.Screens.Play /// /// The local user's playing state (whether actively playing, paused, or not playing due to watching a replay or similar). /// - public IBindable Playing { get; } = new Bindable(); + public IBindable PlayingState { get; } = new Bindable(); public GameplayState( IBeatmap beatmap, @@ -82,7 +82,7 @@ namespace osu.Game.Screens.Play ScoreProcessor? scoreProcessor = null, HealthProcessor? healthProcessor = null, Storyboard? storyboard = null, - IBindable? localUserPlaying = null) + IBindable? localUserPlayingState = null) { Beatmap = beatmap; Ruleset = ruleset; @@ -99,8 +99,8 @@ namespace osu.Game.Screens.Play HealthProcessor = healthProcessor ?? ruleset.CreateHealthProcessor(beatmap.HitObjects[0].StartTime); Storyboard = storyboard ?? new Storyboard(); - if (localUserPlaying != null) - Playing.BindTo(localUserPlaying); + if (localUserPlayingState != null) + PlayingState.BindTo(localUserPlayingState); } /// diff --git a/osu.Game/Screens/Play/HUD/SpectatorList.cs b/osu.Game/Screens/Play/HUD/SpectatorList.cs index 35a2d1eefb..ffe6bbf571 100644 --- a/osu.Game/Screens/Play/HUD/SpectatorList.cs +++ b/osu.Game/Screens/Play/HUD/SpectatorList.cs @@ -245,7 +245,7 @@ namespace osu.Game.Screens.Play.HUD private void load(SpectatorClient client, GameplayState gameplayState) { ((IBindableList)Spectators).BindTo(client.WatchingUsers); - ((IBindable)UserPlayingState).BindTo(gameplayState.Playing); + ((IBindable)UserPlayingState).BindTo(gameplayState.PlayingState); } } } From c8b38f05d5990c7a97740f6d6523737297d965b2 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 17 Jan 2025 17:14:06 +0900 Subject: [PATCH 0629/1275] Add note about the visibility logic because it tripped me up --- osu.Game/Screens/Play/HUD/SpectatorList.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/osu.Game/Screens/Play/HUD/SpectatorList.cs b/osu.Game/Screens/Play/HUD/SpectatorList.cs index ffe6bbf571..7158f69a7a 100644 --- a/osu.Game/Screens/Play/HUD/SpectatorList.cs +++ b/osu.Game/Screens/Play/HUD/SpectatorList.cs @@ -159,6 +159,7 @@ namespace osu.Game.Screens.Play.HUD private void updateVisibility() { + // We don't want to show spectators when we are watching a replay. mainFlow.FadeTo(Spectators.Count > 0 && UserPlayingState.Value != LocalUserPlayingState.NotPlaying ? 1 : 0, 250, Easing.OutQuint); } From a1c5fad6d45c24318028c9f00b0750ad2fb77b88 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 15 Jan 2025 20:02:46 +0900 Subject: [PATCH 0630/1275] 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 0631/1275] 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 0632/1275] 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 ad422295c85d257044edd33dba7284b7c8d9b631 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Fri, 10 Jan 2025 21:38:37 +0900 Subject: [PATCH 0633/1275] Add ctor to create Rooms from MultiplayerRooms --- osu.Game/Online/Rooms/Room.cs | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/osu.Game/Online/Rooms/Room.cs b/osu.Game/Online/Rooms/Room.cs index f8660a656e..7647134646 100644 --- a/osu.Game/Online/Rooms/Room.cs +++ b/osu.Game/Online/Rooms/Room.cs @@ -342,6 +342,29 @@ namespace osu.Game.Online.Rooms // Not yet serialised (not implemented). private RoomAvailability availability; + public Room() + { + } + + /// + /// Creates a from a . + /// + public Room(MultiplayerRoom room) + { + RoomID = room.RoomID; + Host = room.Host?.User; + + Name = room.Settings.Name; + Password = room.Settings.Password; + Type = room.Settings.MatchType; + QueueMode = room.Settings.QueueMode; + AutoStartDuration = room.Settings.AutoStartDuration; + AutoSkip = room.Settings.AutoSkip; + + Playlist = room.Playlist.Select(item => new PlaylistItem(item)).ToArray(); + CurrentPlaylistItem = Playlist.FirstOrDefault(item => item.ID == room.Settings.PlaylistItemId); + } + /// /// Copies values from another into this one. /// From 3d2d4ee89f06a88feabcfdda1b73ac1cbeaf1c49 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Fri, 10 Jan 2025 22:07:13 +0900 Subject: [PATCH 0634/1275] Add ctor to create MultiplayerPlaylistItem from PlaylistItem --- osu.Game/Online/Rooms/MultiplayerPlaylistItem.cs | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/osu.Game/Online/Rooms/MultiplayerPlaylistItem.cs b/osu.Game/Online/Rooms/MultiplayerPlaylistItem.cs index 8be703e620..6e467c1d26 100644 --- a/osu.Game/Online/Rooms/MultiplayerPlaylistItem.cs +++ b/osu.Game/Online/Rooms/MultiplayerPlaylistItem.cs @@ -60,5 +60,20 @@ namespace osu.Game.Online.Rooms public MultiplayerPlaylistItem() { } + + public MultiplayerPlaylistItem(PlaylistItem item) + { + ID = item.ID; + OwnerID = item.OwnerID; + BeatmapID = item.Beatmap.OnlineID; + BeatmapChecksum = item.Beatmap.MD5Hash; + RulesetID = item.RulesetID; + RequiredMods = item.RequiredMods.ToArray(); + AllowedMods = item.AllowedMods.ToArray(); + Expired = item.Expired; + PlaylistOrder = item.PlaylistOrder ?? 0; + PlayedAt = item.PlayedAt; + StarRating = item.Beatmap.StarRating; + } } } From 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 0635/1275] 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 0636/1275] Remove unnecessary masking spec It was clipping the daily challenge completion checkmark, and it originates in some veeeeery old code where the profile overlay looked and behaved very differently (0fa02718786a0eefa063cce18e9e5351f509ab59). --- osu.Game/Overlays/Profile/Header/Components/MainDetails.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/osu.Game/Overlays/Profile/Header/Components/MainDetails.cs b/osu.Game/Overlays/Profile/Header/Components/MainDetails.cs index 4bdd5425c0..10bb69f0f5 100644 --- a/osu.Game/Overlays/Profile/Header/Components/MainDetails.cs +++ b/osu.Game/Overlays/Profile/Header/Components/MainDetails.cs @@ -41,7 +41,6 @@ namespace osu.Game.Overlays.Profile.Header.Components AutoSizeAxes = Axes.Y, AutoSizeDuration = 200, AutoSizeEasing = Easing.OutQuint, - Masking = true, Direction = FillDirection.Vertical, Spacing = new Vector2(0, 15), Children = new Drawable[] From 3c4bfc0a01f8a1474de23078e935ce64f58f2ccb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 17 Jan 2025 11:16:35 +0100 Subject: [PATCH 0637/1275] Merge spectator list classes into one skinnable --- .../Legacy/CatchLegacySkinTransformer.cs | 4 +- .../Argon/ManiaArgonSkinTransformer.cs | 4 +- .../Legacy/ManiaLegacySkinTransformer.cs | 4 +- .../Legacy/OsuLegacySkinTransformer.cs | 4 +- .../Archives/modified-argon-20250116.osk | Bin 1675 -> 1670 bytes .../Visual/Gameplay/TestSceneSpectatorList.cs | 55 ++++++++++++------ osu.Game/Screens/Play/HUD/SpectatorList.cs | 17 ++---- osu.Game/Skinning/ArgonSkin.cs | 4 +- osu.Game/Skinning/LegacySkin.cs | 4 +- osu.Game/Skinning/TrianglesSkin.cs | 4 +- 10 files changed, 57 insertions(+), 43 deletions(-) diff --git a/osu.Game.Rulesets.Catch/Skinning/Legacy/CatchLegacySkinTransformer.cs b/osu.Game.Rulesets.Catch/Skinning/Legacy/CatchLegacySkinTransformer.cs index 978a098990..11649da2f1 100644 --- a/osu.Game.Rulesets.Catch/Skinning/Legacy/CatchLegacySkinTransformer.cs +++ b/osu.Game.Rulesets.Catch/Skinning/Legacy/CatchLegacySkinTransformer.cs @@ -48,7 +48,7 @@ namespace osu.Game.Rulesets.Catch.Skinning.Legacy return new DefaultSkinComponentsContainer(container => { var keyCounter = container.OfType().FirstOrDefault(); - var spectatorList = container.OfType().FirstOrDefault(); + var spectatorList = container.OfType().FirstOrDefault(); if (keyCounter != null) { @@ -69,7 +69,7 @@ namespace osu.Game.Rulesets.Catch.Skinning.Legacy Children = new Drawable[] { new LegacyKeyCounterDisplay(), - new SkinnableSpectatorList(), + new SpectatorList(), } }; } diff --git a/osu.Game.Rulesets.Mania/Skinning/Argon/ManiaArgonSkinTransformer.cs b/osu.Game.Rulesets.Mania/Skinning/Argon/ManiaArgonSkinTransformer.cs index 48c487e70d..6f010ffe48 100644 --- a/osu.Game.Rulesets.Mania/Skinning/Argon/ManiaArgonSkinTransformer.cs +++ b/osu.Game.Rulesets.Mania/Skinning/Argon/ManiaArgonSkinTransformer.cs @@ -41,7 +41,7 @@ namespace osu.Game.Rulesets.Mania.Skinning.Argon return new DefaultSkinComponentsContainer(container => { var combo = container.ChildrenOfType().FirstOrDefault(); - var spectatorList = container.OfType().FirstOrDefault(); + var spectatorList = container.OfType().FirstOrDefault(); if (combo != null) { @@ -56,7 +56,7 @@ namespace osu.Game.Rulesets.Mania.Skinning.Argon }) { new ArgonManiaComboCounter(), - new SkinnableSpectatorList + new SpectatorList { Anchor = Anchor.BottomLeft, Origin = Anchor.BottomLeft, diff --git a/osu.Game.Rulesets.Mania/Skinning/Legacy/ManiaLegacySkinTransformer.cs b/osu.Game.Rulesets.Mania/Skinning/Legacy/ManiaLegacySkinTransformer.cs index 359f21561f..76af569b95 100644 --- a/osu.Game.Rulesets.Mania/Skinning/Legacy/ManiaLegacySkinTransformer.cs +++ b/osu.Game.Rulesets.Mania/Skinning/Legacy/ManiaLegacySkinTransformer.cs @@ -97,7 +97,7 @@ namespace osu.Game.Rulesets.Mania.Skinning.Legacy return new DefaultSkinComponentsContainer(container => { var combo = container.ChildrenOfType().FirstOrDefault(); - var spectatorList = container.OfType().FirstOrDefault(); + var spectatorList = container.OfType().FirstOrDefault(); if (combo != null) { @@ -115,7 +115,7 @@ namespace osu.Game.Rulesets.Mania.Skinning.Legacy }) { new LegacyManiaComboCounter(), - new SkinnableSpectatorList(), + new SpectatorList(), }; } diff --git a/osu.Game.Rulesets.Osu/Skinning/Legacy/OsuLegacySkinTransformer.cs b/osu.Game.Rulesets.Osu/Skinning/Legacy/OsuLegacySkinTransformer.cs index 03e4bb24f1..d39e05b262 100644 --- a/osu.Game.Rulesets.Osu/Skinning/Legacy/OsuLegacySkinTransformer.cs +++ b/osu.Game.Rulesets.Osu/Skinning/Legacy/OsuLegacySkinTransformer.cs @@ -71,7 +71,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy } var combo = container.OfType().FirstOrDefault(); - var spectatorList = container.OfType().FirstOrDefault(); + var spectatorList = container.OfType().FirstOrDefault(); Vector2 pos = new Vector2(); @@ -96,7 +96,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy { new LegacyDefaultComboCounter(), new LegacyKeyCounterDisplay(), - new SkinnableSpectatorList(), + new SpectatorList(), } }; } diff --git a/osu.Game.Tests/Resources/Archives/modified-argon-20250116.osk b/osu.Game.Tests/Resources/Archives/modified-argon-20250116.osk index 811e91b74916988a54a9ff2b73cafe0e0ff0a490..23322e7373514b80b5a36897959ac92d5b074187 100644 GIT binary patch delta 1068 zcmV+{1k?MA4TcR8P)h>@6aWAK2mr@fFL*ij< zWIMF1>VKaTXiLD|6fF&OD!nLq?btc-qw^k{yCcJ>{Qxd7qqX;jT~DvO9NnN1flqY8 zlz68!rACB}5K-4x*|o9Ov$pf)98^nz)Un=(S?h;Q%rIme zIxL|QcxB8u+AQPkmzgvG9Wq<`pOJChQkc2H941^XM8M`K#B!O1?s&PNM9gjif<{1p z9!9PLm_8sP<1Q9+C09m_7MOh}8S9xOQ;3*ylFSJ4AbyzN-F{h#fPe#0?_>diz`x*y zfSF=S)zg_BHk&i5lA16-u-h{NGxfbPR&=Pi`$eVc=}B=5xRGs@LW;`w6nMbV7$+i& zI-gZ`-Kdv+DsiLJFibcKmsJ~5!#Tiz{8H4Aa^AClN0L0*sdKW$4aO_;NJppwssZo` z<1<7<7%;4o(qYimvdy;o_$3i$na4X*DD8hI?1cg9V|m6o<6l7mqudPfoU&I>dtKQn zXJ2KT+RsGod($E#AecfRV;oK?h_qjGE4G!f1!=*wzygmjS)sO*t}@hSY@z0V3y|8v zp$K%{Qn%y~%n+X{TNQGdy<$7pCj<7W?Ty);=!-MksKip6Z_ri)?dmXs(P+R~#MN8a zE2a&*JWwsOU6@n5*v8V)=2oX1Of#e+j^^>Rz)3vQVy4@7>dL~(Hyjylik5XHSoLW} z!}V6RpTA3<@4qTrcYPgtIkgyndVgK)?Em$uSGv5lfBA`}!)7B^^?tzBotT%q@UCXz z-E!+WM{A8vDLLmL_X5K?glZ)i|AIwzr&+dZdfu_;YgW{1nXcV(_10WXx@dW3t7%!5 zR|K6GJO`aHDt9WG7|>XiIU5esZg;zd9A=etj?!=UzX4E70|XQR000OC0LNJ|lQIQf z2*+75T13(TCX<*2E(ph2F@6aWAK2mnQVFk0@{+t-m2BmoeSH=PJYd@x#ZA@zKb+)4p7 zlP3XA5=DG4S}T!PcZLH101zGk02Tm~90nMZp8*knAd`C0!5Gsv4UKlasH(_>L*ij< zWIJtH)&D*x(3XI^i7gFuYPl$R?btc-qw^k{+atrMeFrWuqqVn&UC*w&99^Rkfsb^; zlz6K&rN)Gf5K-3``L(gq^S0B)98^nu6kHT|(==AXLJNr_B398*XoN2G2H(h@aDbQn@5>l%_&OTrQ%VhwM z&!qGlB{aNj$aQs*#p#*WgLbzz)Hf#4YN*0}wPAz^=q9jr!#^?lIUdVIwY%-&;C;Yx zB2`1nDd;CON*GsquqJ-xIL35CJQE;#_y)#*54TJZ5wIQrNQr4IHeVTpMs0<@USeo^yz484{r`M}#L+m7&j5%b_cX4^N(f%@sY5JNab*xunR{CKRGmIFA z4oj&7UKulvHp_VXWhM(ihs=ildt_X<6lSh5hY42;5wbaouw3M!J6<6uk+AEKpb?ON z`;n^vrVoh7q>IH|$(0eK1!mt##(E;j>_kjCNoIt2nB33rZoaHkK*)ipcX9=xz(3)c zfSF=S)zg_BHk&i5lA18zv77eS9*ecB5YYSBV>)L{Z97w5ZyU8qNWK=9gU!so*{PcO=QvtvV-P+;F@!k8Ff0pc(+5 zFg`;xf&s%SDIEq~E!zx&pxyRMLd-JTTXQJwe@E<@3F>3{o^!^(fFefO8R7+Hucr30 z(o?~|%67FMsoMBvRYpiKg-|9qnoYw; z)E*ASpzD^|C5Lc^_*D6-kc<2k+l4+ku#au8%x*=$JF|^SJhk}>Z8gv?Pa_zO2b{%R zy@kDE+OW$5)iQ&fIkk7&SvuO>>Xd_Nj#SLiJb4m0iDz2ObQ@1yU0C^!Bd1N#vML3u zKF(;kURL}Ct!?mp|3$gF>uW22LjSMH`{P>Y|1X!lvem8q%TFvFHXE_3_XDo(#Jt>v zcQp&|mRrs_T4!`hNjV3(708dbvLE_}bBNTEF#Z{f>W;OVj_I^ruenW*?b~MC^Q>Tx zd86f-t)^vJ-oY?5VN`C_Gzp-wDtOksCJ4ISoxEn5e~z+mcfSBoO9KQ66aWAK2mnQV zFq16>UI;~eFk0@{+t-tm1uh6hd@x#ZA@zKd$pteFMSL(?E0I@sh64Zq5R(Z8Hy=fO rFj~sD spectators = new BindableList(); - private readonly Bindable localUserPlayingState = new Bindable(); - private int counter; [Test] public void TestBasics() { SpectatorList list = null!; - AddStep("create spectator list", () => Child = list = new SpectatorList + Bindable playingState = new Bindable(); + GameplayState gameplayState = new GameplayState(new Beatmap(), new OsuRuleset(), healthProcessor: new OsuHealthProcessor(0), localUserPlayingState: playingState); + TestSpectatorClient client = new TestSpectatorClient(); + + AddStep("create spectator list", () => { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Spectators = { BindTarget = spectators }, - UserPlayingState = { BindTarget = localUserPlayingState } + Children = new Drawable[] + { + client, + new DependencyProvidingContainer + { + RelativeSizeAxes = Axes.Both, + CachedDependencies = + [ + (typeof(GameplayState), gameplayState), + (typeof(SpectatorClient), client) + ], + Child = list = new SpectatorList + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + } + } + }; }); - AddStep("start playing", () => localUserPlayingState.Value = LocalUserPlayingState.Playing); + AddStep("start playing", () => playingState.Value = LocalUserPlayingState.Playing); AddRepeatStep("add a user", () => { int id = Interlocked.Increment(ref counter); - spectators.Add(new SpectatorUser - { - OnlineID = id, - Username = $"User {id}" - }); + ((ISpectatorClient)client).UserStartedWatching([ + new SpectatorUser + { + OnlineID = id, + Username = $"User {id}" + } + ]); }, 10); - AddRepeatStep("remove random user", () => spectators.RemoveAt(RNG.Next(0, spectators.Count)), 5); + AddRepeatStep("remove random user", () => ((ISpectatorClient)client).UserEndedWatching(client.WatchingUsers[RNG.Next(client.WatchingUsers.Count)].OnlineID), 5); AddStep("change font to venera", () => list.Font.Value = Typeface.Venera); AddStep("change font to torus", () => list.Font.Value = Typeface.Torus); AddStep("change header colour", () => list.HeaderColour.Value = new Colour4(RNG.NextSingle(), RNG.NextSingle(), RNG.NextSingle(), 1)); - AddStep("enter break", () => localUserPlayingState.Value = LocalUserPlayingState.Break); - AddStep("stop playing", () => localUserPlayingState.Value = LocalUserPlayingState.NotPlaying); + AddStep("enter break", () => playingState.Value = LocalUserPlayingState.Break); + AddStep("stop playing", () => playingState.Value = LocalUserPlayingState.NotPlaying); } } } diff --git a/osu.Game/Screens/Play/HUD/SpectatorList.cs b/osu.Game/Screens/Play/HUD/SpectatorList.cs index 7158f69a7a..7b6bf6f55e 100644 --- a/osu.Game/Screens/Play/HUD/SpectatorList.cs +++ b/osu.Game/Screens/Play/HUD/SpectatorList.cs @@ -24,7 +24,7 @@ using osuTK.Graphics; namespace osu.Game.Screens.Play.HUD { - public partial class SpectatorList : CompositeDrawable + public partial class SpectatorList : CompositeDrawable, ISerialisableDrawable { private const int max_spectators_displayed = 10; @@ -44,7 +44,7 @@ namespace osu.Game.Screens.Play.HUD private DrawablePool pool = null!; [BackgroundDependencyLoader] - private void load(OsuColour colours) + private void load(OsuColour colours, SpectatorClient client, GameplayState gameplayState) { AutoSizeAxes = Axes.Y; @@ -73,6 +73,9 @@ namespace osu.Game.Screens.Play.HUD }; HeaderColour.Value = Header.Colour; + + ((IBindableList)Spectators).BindTo(client.WatchingUsers); + ((IBindable)UserPlayingState).BindTo(gameplayState.PlayingState); } protected override void LoadComplete() @@ -236,17 +239,7 @@ namespace osu.Game.Screens.Play.HUD linkCompiler.Enabled.Value = UserPlayingState.Value != LocalUserPlayingState.Playing; } } - } - public partial class SkinnableSpectatorList : SpectatorList, ISerialisableDrawable - { public bool UsesFixedAnchor { get; set; } - - [BackgroundDependencyLoader] - private void load(SpectatorClient client, GameplayState gameplayState) - { - ((IBindableList)Spectators).BindTo(client.WatchingUsers); - ((IBindable)UserPlayingState).BindTo(gameplayState.PlayingState); - } } } diff --git a/osu.Game/Skinning/ArgonSkin.cs b/osu.Game/Skinning/ArgonSkin.cs index c3319b738d..bd31ccd5c9 100644 --- a/osu.Game/Skinning/ArgonSkin.cs +++ b/osu.Game/Skinning/ArgonSkin.cs @@ -112,7 +112,7 @@ namespace osu.Game.Skinning return new DefaultSkinComponentsContainer(container => { var comboCounter = container.OfType().FirstOrDefault(); - var spectatorList = container.OfType().FirstOrDefault(); + var spectatorList = container.OfType().FirstOrDefault(); Vector2 pos = new Vector2(36, -66); @@ -135,7 +135,7 @@ namespace osu.Game.Skinning Origin = Anchor.BottomLeft, Scale = new Vector2(1.3f), }, - new SkinnableSpectatorList + new SpectatorList { Anchor = Anchor.BottomLeft, Origin = Anchor.BottomLeft, diff --git a/osu.Game/Skinning/LegacySkin.cs b/osu.Game/Skinning/LegacySkin.cs index c607c57fcc..08fa068830 100644 --- a/osu.Game/Skinning/LegacySkin.cs +++ b/osu.Game/Skinning/LegacySkin.cs @@ -367,7 +367,7 @@ namespace osu.Game.Skinning return new DefaultSkinComponentsContainer(container => { var combo = container.OfType().FirstOrDefault(); - var spectatorList = container.OfType().FirstOrDefault(); + var spectatorList = container.OfType().FirstOrDefault(); Vector2 pos = new Vector2(); @@ -389,7 +389,7 @@ namespace osu.Game.Skinning }) { new LegacyDefaultComboCounter(), - new SkinnableSpectatorList(), + new SpectatorList(), }; } diff --git a/osu.Game/Skinning/TrianglesSkin.cs b/osu.Game/Skinning/TrianglesSkin.cs index 8853a5c4ac..06fe1c80ee 100644 --- a/osu.Game/Skinning/TrianglesSkin.cs +++ b/osu.Game/Skinning/TrianglesSkin.cs @@ -91,7 +91,7 @@ namespace osu.Game.Skinning var ppCounter = container.OfType().FirstOrDefault(); var songProgress = container.OfType().FirstOrDefault(); var keyCounter = container.OfType().FirstOrDefault(); - var spectatorList = container.OfType().FirstOrDefault(); + var spectatorList = container.OfType().FirstOrDefault(); if (score != null) { @@ -177,7 +177,7 @@ namespace osu.Game.Skinning new BarHitErrorMeter(), new BarHitErrorMeter(), new TrianglesPerformancePointsCounter(), - new SkinnableSpectatorList(), + new SpectatorList(), } }; From a42c03cea457b9e6786983d77d966a461d1a10ed Mon Sep 17 00:00:00 2001 From: Jay Lawton Date: Fri, 17 Jan 2025 21:15:22 +1000 Subject: [PATCH 0638/1275] osu!taiko further considerations for rhythm (#31339) * further considerations for rhythm * new rhythm balancing * fix license header * use isNormal to validate ratio * adjust tests --------- Co-authored-by: StanR --- .../TaikoDifficultyCalculatorTest.cs | 8 ++-- .../Difficulty/Evaluators/RhythmEvaluator.cs | 48 +++++++++++++------ 2 files changed, 38 insertions(+), 18 deletions(-) diff --git a/osu.Game.Rulesets.Taiko.Tests/TaikoDifficultyCalculatorTest.cs b/osu.Game.Rulesets.Taiko.Tests/TaikoDifficultyCalculatorTest.cs index b4cbe03511..d760b9aef6 100644 --- a/osu.Game.Rulesets.Taiko.Tests/TaikoDifficultyCalculatorTest.cs +++ b/osu.Game.Rulesets.Taiko.Tests/TaikoDifficultyCalculatorTest.cs @@ -14,13 +14,13 @@ namespace osu.Game.Rulesets.Taiko.Tests { protected override string ResourceAssembly => "osu.Game.Rulesets.Taiko"; - [TestCase(3.3172381854905493d, 200, "diffcalc-test")] - [TestCase(3.3172381854905493d, 200, "diffcalc-test-strong")] + [TestCase(3.3167800835687551d, 200, "diffcalc-test")] + [TestCase(3.3167800835687551d, 200, "diffcalc-test-strong")] public void Test(double expectedStarRating, int expectedMaxCombo, string name) => base.Test(expectedStarRating, expectedMaxCombo, name); - [TestCase(4.4640702427013101d, 200, "diffcalc-test")] - [TestCase(4.4640702427013101d, 200, "diffcalc-test-strong")] + [TestCase(4.4631326105105122d, 200, "diffcalc-test")] + [TestCase(4.4631326105105122d, 200, "diffcalc-test-strong")] public void TestClockRateAdjusted(double expectedStarRating, int expectedMaxCombo, string name) => Test(expectedStarRating, expectedMaxCombo, name, new TaikoModDoubleTime()); diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Evaluators/RhythmEvaluator.cs b/osu.Game.Rulesets.Taiko/Difficulty/Evaluators/RhythmEvaluator.cs index 3a294f7123..7d58eada5e 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Evaluators/RhythmEvaluator.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Evaluators/RhythmEvaluator.cs @@ -21,27 +21,39 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Evaluators return -multiplier * Math.Pow(Math.Cos(denominator * Math.PI * ratio), power); } + /// + /// Validates the ratio by ensuring it is a normal number in cases where maps breach regular mapping conditions. + /// + private static double validateRatio(double ratio) + { + return double.IsNormal(ratio) ? ratio : 0; + } + /// /// Calculates the difficulty of a given ratio using a combination of periodic penalties and bonuses. /// private static double ratioDifficulty(double ratio, int terms = 8) { double difficulty = 0; + ratio = validateRatio(ratio); for (int i = 1; i <= terms; ++i) { - difficulty += termPenalty(ratio, i, 2, 1); + difficulty += termPenalty(ratio, i, 4, 1); } - difficulty += terms; + difficulty += terms / (1 + ratio); // Give bonus to near-1 ratios - difficulty += DifficultyCalculationUtils.BellCurve(ratio, 1, 0.7); + difficulty += DifficultyCalculationUtils.BellCurve(ratio, 1, 0.5); // Penalize ratios that are VERY near 1 - difficulty -= DifficultyCalculationUtils.BellCurve(ratio, 1, 0.5); + difficulty -= DifficultyCalculationUtils.BellCurve(ratio, 1, 0.3); - return difficulty / Math.Sqrt(8); + difficulty = Math.Max(difficulty, 0); + difficulty /= Math.Sqrt(8); + + return difficulty; } /// @@ -55,10 +67,10 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Evaluators ? sameInterval(sameRhythmHitObjects, 4) : 1.0; // Returns a non-penalty if there are 6 or more notes within an interval. - // Scale penalties dynamically based on hit object duration relative to hitWindow. - double penaltyScaling = Math.Max(1 - sameRhythmHitObjects.Duration / (hitWindow * 2), 0.5); + // The duration penalty is based on hit object duration relative to hitWindow. + double durationPenalty = Math.Max(1 - sameRhythmHitObjects.Duration * 2 / hitWindow, 0.5); - return Math.Min(longIntervalPenalty, shortIntervalPenalty) * penaltyScaling; + return Math.Min(longIntervalPenalty, shortIntervalPenalty) * durationPenalty; double sameInterval(SameRhythmHitObjects startObject, int intervalCount) { @@ -82,7 +94,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Evaluators { double ratio = intervals[i]!.Value / intervals[j]!.Value; if (Math.Abs(1 - ratio) <= threshold) // If any two intervals are similar, apply a penalty. - return 0.3; + return 0.80; } } @@ -95,6 +107,8 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Evaluators double intervalDifficulty = ratioDifficulty(sameRhythmHitObjects.HitObjectIntervalRatio); double? previousInterval = sameRhythmHitObjects.Previous?.HitObjectInterval; + intervalDifficulty *= repeatedIntervalPenalty(sameRhythmHitObjects, hitWindow); + // If a previous interval exists and there are multiple hit objects in the sequence: if (previousInterval != null && sameRhythmHitObjects.Children.Count > 1) { @@ -111,9 +125,6 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Evaluators } } - // Apply consistency penalty. - intervalDifficulty *= repeatedIntervalPenalty(sameRhythmHitObjects, hitWindow); - // Penalise patterns that can be hit within a single hit window. intervalDifficulty *= DifficultyCalculationUtils.Logistic( sameRhythmHitObjects.Duration / hitWindow, @@ -137,11 +148,20 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Evaluators TaikoDifficultyHitObjectRhythm rhythm = ((TaikoDifficultyHitObject)hitObject).Rhythm; double difficulty = 0.0d; + double sameRhythm = 0; + double samePattern = 0; + double intervalPenalty = 0; + if (rhythm.SameRhythmHitObjects?.FirstHitObject == hitObject) // Difficulty for SameRhythmHitObjects - difficulty += evaluateDifficultyOf(rhythm.SameRhythmHitObjects, hitWindow); + { + sameRhythm += 10.0 * evaluateDifficultyOf(rhythm.SameRhythmHitObjects, hitWindow); + intervalPenalty = repeatedIntervalPenalty(rhythm.SameRhythmHitObjects, hitWindow); + } if (rhythm.SamePatterns?.FirstHitObject == hitObject) // Difficulty for SamePatterns - difficulty += 0.5 * evaluateDifficultyOf(rhythm.SamePatterns); + samePattern += 1.15 * evaluateDifficultyOf(rhythm.SamePatterns); + + difficulty += Math.Max(sameRhythm, samePattern) * intervalPenalty; return difficulty; } 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 0639/1275] 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 ad28de8ae3aa1d2817fc8511929d52c2c3ab0b20 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Fri, 17 Jan 2025 21:44:40 +0900 Subject: [PATCH 0640/1275] Create multiplayer rooms via multiplayer server --- .../Multiplayer/IMultiplayerLoungeServer.cs | 2 + .../Online/Multiplayer/MultiplayerClient.cs | 42 ++++++++++++------ .../Online/Multiplayer/MultiplayerRoom.cs | 9 ++++ .../Multiplayer/MultiplayerRoomSettings.cs | 14 ++++++ .../Multiplayer/OnlineMultiplayerClient.cs | 25 +++++++++++ osu.Game/Online/Rooms/Room.cs | 23 ---------- .../Match/MultiplayerMatchSettingsOverlay.cs | 44 +++++++++---------- .../Multiplayer/MultiplayerMatchSubScreen.cs | 5 +-- .../Multiplayer/TestMultiplayerClient.cs | 5 +++ 9 files changed, 105 insertions(+), 64 deletions(-) diff --git a/osu.Game/Online/Multiplayer/IMultiplayerLoungeServer.cs b/osu.Game/Online/Multiplayer/IMultiplayerLoungeServer.cs index f266c38b8b..c5eb6f9b36 100644 --- a/osu.Game/Online/Multiplayer/IMultiplayerLoungeServer.cs +++ b/osu.Game/Online/Multiplayer/IMultiplayerLoungeServer.cs @@ -10,6 +10,8 @@ namespace osu.Game.Online.Multiplayer /// public interface IMultiplayerLoungeServer { + Task CreateRoom(MultiplayerRoom room); + /// /// Request to join a multiplayer room. /// diff --git a/osu.Game/Online/Multiplayer/MultiplayerClient.cs b/osu.Game/Online/Multiplayer/MultiplayerClient.cs index 4a28124583..d0c3a1fa06 100644 --- a/osu.Game/Online/Multiplayer/MultiplayerClient.cs +++ b/osu.Game/Online/Multiplayer/MultiplayerClient.cs @@ -165,6 +165,15 @@ namespace osu.Game.Online.Multiplayer private readonly TaskChain joinOrLeaveTaskChain = new TaskChain(); private CancellationTokenSource? joinCancellationSource; + public async Task CreateRoom(Room room) + { + if (Room != null) + throw new InvalidOperationException("Cannot join a multiplayer room while already in one."); + + var cancellationSource = joinCancellationSource = new CancellationTokenSource(); + await initRoom(room, r => CreateRoom(new MultiplayerRoom(room)), cancellationSource.Token); + } + /// /// Joins the for a given API . /// @@ -175,34 +184,34 @@ namespace osu.Game.Online.Multiplayer if (Room != null) throw new InvalidOperationException("Cannot join a multiplayer room while already in one."); - var cancellationSource = joinCancellationSource = new CancellationTokenSource(); + Debug.Assert(room.RoomID != null); + var cancellationSource = joinCancellationSource = new CancellationTokenSource(); + await initRoom(room, r => JoinRoom(room.RoomID.Value, password ?? room.Password), cancellationSource.Token); + } + + private async Task initRoom(Room room, Func> initFunc, CancellationToken cancellationToken) + { await joinOrLeaveTaskChain.Add(async () => { - Debug.Assert(room.RoomID != null); - - // Join the server-side room. - var joinedRoom = await JoinRoom(room.RoomID.Value, password ?? room.Password).ConfigureAwait(false); - Debug.Assert(joinedRoom != null); + // Initialise the server-side room. + MultiplayerRoom joinedRoom = await initFunc(room).ConfigureAwait(false); // Populate users. - Debug.Assert(joinedRoom.Users != null); await PopulateUsers(joinedRoom.Users).ConfigureAwait(false); // Update the stored room (must be done on update thread for thread-safety). await runOnUpdateThreadAsync(() => { Debug.Assert(Room == null); + Debug.Assert(APIRoom == null); Room = joinedRoom; APIRoom = room; - Debug.Assert(joinedRoom.Playlist.Count > 0); - + APIRoom.RoomID = joinedRoom.RoomID; APIRoom.Playlist = joinedRoom.Playlist.Select(item => new PlaylistItem(item)).ToArray(); APIRoom.CurrentPlaylistItem = APIRoom.Playlist.Single(item => item.ID == joinedRoom.Settings.PlaylistItemId); - - // The server will null out the end date upon the host joining the room, but the null value is never communicated to the client. APIRoom.EndDate = null; Debug.Assert(LocalUser != null); @@ -216,8 +225,8 @@ namespace osu.Game.Online.Multiplayer postServerShuttingDownNotification(); OnRoomJoined(); - }, cancellationSource.Token).ConfigureAwait(false); - }, cancellationSource.Token).ConfigureAwait(false); + }, cancellationToken).ConfigureAwait(false); + }, cancellationToken).ConfigureAwait(false); } /// @@ -227,6 +236,13 @@ namespace osu.Game.Online.Multiplayer { } + /// + /// Creates the with the given settings. + /// + /// The room. + /// The joined + protected abstract Task CreateRoom(MultiplayerRoom room); + /// /// Joins the with a given ID. /// diff --git a/osu.Game/Online/Multiplayer/MultiplayerRoom.cs b/osu.Game/Online/Multiplayer/MultiplayerRoom.cs index 00048fa931..f7bd4490ff 100644 --- a/osu.Game/Online/Multiplayer/MultiplayerRoom.cs +++ b/osu.Game/Online/Multiplayer/MultiplayerRoom.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; +using System.Linq; using MessagePack; using Newtonsoft.Json; using osu.Game.Online.Rooms; @@ -65,6 +66,14 @@ namespace osu.Game.Online.Multiplayer RoomID = roomId; } + public MultiplayerRoom(Room room) + { + RoomID = room.RoomID ?? 0; + Settings = new MultiplayerRoomSettings(room); + Host = room.Host != null ? new MultiplayerRoomUser(room.Host.OnlineID) : null; + Playlist = room.Playlist.Select(p => new MultiplayerPlaylistItem(p)).ToArray(); + } + public override string ToString() => $"RoomID:{RoomID} Host:{Host?.UserID} Users:{Users.Count} State:{State} Settings: [{Settings}]"; } } diff --git a/osu.Game/Online/Multiplayer/MultiplayerRoomSettings.cs b/osu.Game/Online/Multiplayer/MultiplayerRoomSettings.cs index c73b02874e..c264ec1eef 100644 --- a/osu.Game/Online/Multiplayer/MultiplayerRoomSettings.cs +++ b/osu.Game/Online/Multiplayer/MultiplayerRoomSettings.cs @@ -35,6 +35,20 @@ namespace osu.Game.Online.Multiplayer [IgnoreMember] public bool AutoStartEnabled => AutoStartDuration != TimeSpan.Zero; + public MultiplayerRoomSettings() + { + } + + public MultiplayerRoomSettings(Room room) + { + Name = room.Name; + Password = room.Password ?? string.Empty; + MatchType = room.Type; + QueueMode = room.QueueMode; + AutoStartDuration = room.AutoStartDuration; + AutoSkip = room.AutoSkip; + } + public bool Equals(MultiplayerRoomSettings? other) { if (ReferenceEquals(this, other)) return true; diff --git a/osu.Game/Online/Multiplayer/OnlineMultiplayerClient.cs b/osu.Game/Online/Multiplayer/OnlineMultiplayerClient.cs index 40436d730e..524873ef66 100644 --- a/osu.Game/Online/Multiplayer/OnlineMultiplayerClient.cs +++ b/osu.Game/Online/Multiplayer/OnlineMultiplayerClient.cs @@ -266,6 +266,31 @@ namespace osu.Game.Online.Multiplayer return connection.InvokeAsync(nameof(IMultiplayerServer.RemovePlaylistItem), playlistItemId); } + protected override async Task CreateRoom(MultiplayerRoom room) + { + if (!IsConnected.Value) + throw new OperationCanceledException(); + + Debug.Assert(connection != null); + + try + { + return await connection.InvokeAsync(nameof(IMultiplayerServer.CreateRoom), room); + } + catch (HubException exception) + { + if (exception.GetHubExceptionMessage() == HubClientConnector.SERVER_SHUTDOWN_MESSAGE) + { + Debug.Assert(connector != null); + + await connector.Reconnect().ConfigureAwait(false); + return await CreateRoom(room).ConfigureAwait(false); + } + + throw; + } + } + public override Task DisconnectInternal() { if (connector == null) diff --git a/osu.Game/Online/Rooms/Room.cs b/osu.Game/Online/Rooms/Room.cs index 7647134646..f8660a656e 100644 --- a/osu.Game/Online/Rooms/Room.cs +++ b/osu.Game/Online/Rooms/Room.cs @@ -342,29 +342,6 @@ namespace osu.Game.Online.Rooms // Not yet serialised (not implemented). private RoomAvailability availability; - public Room() - { - } - - /// - /// Creates a from a . - /// - public Room(MultiplayerRoom room) - { - RoomID = room.RoomID; - Host = room.Host?.User; - - Name = room.Settings.Name; - Password = room.Settings.Password; - Type = room.Settings.MatchType; - QueueMode = room.Settings.QueueMode; - AutoStartDuration = room.Settings.AutoStartDuration; - AutoSkip = room.Settings.AutoSkip; - - Playlist = room.Playlist.Select(item => new PlaylistItem(item)).ToArray(); - CurrentPlaylistItem = Playlist.FirstOrDefault(item => item.ID == room.Settings.PlaylistItemId); - } - /// /// Copies values from another into this one. /// diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerMatchSettingsOverlay.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerMatchSettingsOverlay.cs index 1372054149..279b140d36 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerMatchSettingsOverlay.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerMatchSettingsOverlay.cs @@ -29,12 +29,6 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match { public partial class MultiplayerMatchSettingsOverlay : RoomSettingsOverlay { - public required Bindable SelectedItem - { - get => selectedItem; - set => selectedItem.Current = value; - } - protected override OsuButton SubmitButton => settings.ApplyButton; protected override bool IsLoading => ongoingOperationTracker.InProgress.Value; @@ -56,7 +50,6 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match RelativeSizeAxes = Axes.Both, RelativePositionAxes = Axes.Y, SettingsApplied = Hide, - SelectedItem = { BindTarget = SelectedItem } }; protected partial class MatchSettings : CompositeDrawable @@ -65,7 +58,6 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match public override bool IsPresent => base.IsPresent || Scheduler.HasPendingTasks; - public readonly Bindable SelectedItem = new Bindable(); public Action? SettingsApplied; public OsuTextBox NameField = null!; @@ -86,9 +78,6 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match [Resolved] private MultiplayerMatchSubScreen matchSubScreen { get; set; } = null!; - [Resolved] - private IRoomManager manager { get; set; } = null!; - [Resolved] private MultiplayerClient client { get; set; } = null!; @@ -279,7 +268,6 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match { RelativeSizeAxes = Axes.X, Height = DrawableRoomPlaylistItem.HEIGHT, - SelectedItem = { BindTarget = SelectedItem } }, selectBeatmapButton = new RoundedButton { @@ -482,19 +470,27 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match } else { - room.Name = NameField.Text; - room.Type = TypePicker.Current.Value; - room.Password = PasswordTextBox.Current.Value; - room.QueueMode = QueueModeDropdown.Current.Value; - room.AutoStartDuration = TimeSpan.FromSeconds((int)startModeDropdown.Current.Value); - room.AutoSkip = AutoSkipCheckbox.Current.Value; + client.CreateRoom(room).ContinueWith(t => Schedule(() => + { + if (t.IsCompleted) + onSuccess(room); + else if (t.IsFaulted) + { + Exception? exception = t.Exception; - if (int.TryParse(MaxParticipantsField.Text, out int max)) - room.MaxParticipants = max; - else - room.MaxParticipants = null; + if (exception is AggregateException ae) + exception = ae.InnerException; - manager.CreateRoom(room, onSuccess, onError); + Debug.Assert(exception != null); + + if (exception.GetHubExceptionMessage() is string message) + onError(message); + else + onError($"Error creating room: {exception}"); + } + else + onError("Error creating room."); + })); } } @@ -520,7 +516,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match if (text.StartsWith(not_found_prefix, StringComparison.Ordinal)) { ErrorText.Text = "The selected beatmap is not available online."; - SelectedItem.Value?.MarkInvalid(); + room.Playlist.SingleOrDefault()?.MarkInvalid(); } else { diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs index edc45dbf7c..06ea5ee033 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs @@ -233,10 +233,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer SelectedItem = SelectedItem }; - protected override RoomSettingsOverlay CreateRoomSettingsOverlay(Room room) => new MultiplayerMatchSettingsOverlay(room) - { - SelectedItem = SelectedItem - }; + protected override RoomSettingsOverlay CreateRoomSettingsOverlay(Room room) => new MultiplayerMatchSettingsOverlay(room); protected override void UpdateMods() { diff --git a/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs b/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs index 4d812abf11..70e298f3e0 100644 --- a/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs +++ b/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs @@ -483,6 +483,11 @@ namespace osu.Game.Tests.Visual.Multiplayer public override Task RemovePlaylistItem(long playlistItemId) => RemoveUserPlaylistItem(api.LocalUser.Value.OnlineID, clone(playlistItemId)); + protected override Task CreateRoom(MultiplayerRoom room) + { + throw new NotImplementedException(); + } + private async Task changeMatchType(MatchType type) { Debug.Assert(ServerRoom != null); From 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 0641/1275] 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 0642/1275] 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 5b4ba9225d7810c21a2456c9824e2a3fe621306a Mon Sep 17 00:00:00 2001 From: Natelytle <92956514+Natelytle@users.noreply.github.com> Date: Fri, 17 Jan 2025 14:37:34 -0500 Subject: [PATCH 0643/1275] Move error function from osu.Game.Utils to osu.Game.Rulesets.Difficulty.Utils (#31520) * Move error function implementation to osu.Game.Rulesets.Difficulty.Utils * Rename ErrorFunction.cs to DifficultyCalculationUtils_ErrorFunction.cs --- .../Difficulty/OsuPerformanceCalculator.cs | 5 ++--- .../Difficulty/TaikoPerformanceCalculator.cs | 6 +++--- .../Difficulty/Utils/DifficultyCalculationUtils.cs | 2 +- .../Utils/DifficultyCalculationUtils_ErrorFunction.cs} | 7 ++----- 4 files changed, 8 insertions(+), 12 deletions(-) rename osu.Game/{Utils/SpecialFunctions.cs => Rulesets/Difficulty/Utils/DifficultyCalculationUtils_ErrorFunction.cs} (99%) diff --git a/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs b/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs index 7013ee55c4..f191180630 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs @@ -10,7 +10,6 @@ using osu.Game.Rulesets.Osu.Difficulty.Skills; using osu.Game.Rulesets.Osu.Mods; using osu.Game.Rulesets.Scoring; using osu.Game.Scoring; -using osu.Game.Utils; namespace osu.Game.Rulesets.Osu.Difficulty { @@ -371,10 +370,10 @@ namespace osu.Game.Rulesets.Osu.Difficulty // Compute the deviation assuming greats and oks are normally distributed, and mehs are uniformly distributed. // Begin with greats and oks first. Ignoring mehs, we can be 99% confident that the deviation is not higher than: - double deviation = hitWindowGreat / (Math.Sqrt(2) * SpecialFunctions.ErfInv(pLowerBound)); + double deviation = hitWindowGreat / (Math.Sqrt(2) * DifficultyCalculationUtils.ErfInv(pLowerBound)); double randomValue = Math.Sqrt(2 / Math.PI) * hitWindowOk * Math.Exp(-0.5 * Math.Pow(hitWindowOk / deviation, 2)) - / (deviation * SpecialFunctions.Erf(hitWindowOk / (Math.Sqrt(2) * deviation))); + / (deviation * DifficultyCalculationUtils.Erf(hitWindowOk / (Math.Sqrt(2) * deviation))); deviation *= Math.Sqrt(1 - randomValue); diff --git a/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceCalculator.cs b/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceCalculator.cs index c29ea3ba73..9e7bf7cb7a 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceCalculator.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceCalculator.cs @@ -5,11 +5,11 @@ using System; using System.Collections.Generic; using System.Linq; using osu.Game.Rulesets.Difficulty; +using osu.Game.Rulesets.Difficulty.Utils; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.Taiko.Objects; using osu.Game.Scoring; -using osu.Game.Utils; namespace osu.Game.Rulesets.Taiko.Difficulty { @@ -99,7 +99,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty double accScalingExponent = 2 + attributes.MonoStaminaFactor; double accScalingShift = 500 - 100 * attributes.MonoStaminaFactor; - return difficultyValue * Math.Pow(SpecialFunctions.Erf(accScalingShift / (Math.Sqrt(2) * estimatedUnstableRate.Value)), accScalingExponent); + return difficultyValue * Math.Pow(DifficultyCalculationUtils.Erf(accScalingShift / (Math.Sqrt(2) * estimatedUnstableRate.Value)), accScalingExponent); } private double computeAccuracyValue(ScoreInfo score, TaikoDifficultyAttributes attributes, bool isConvert) @@ -139,7 +139,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty double pLowerBound = (n * p + z * z / 2) / (n + z * z) - z / (n + z * z) * Math.Sqrt(n * p * (1 - p) + z * z / 4); // We can be 99% confident that the deviation is not higher than: - return attributes.GreatHitWindow / (Math.Sqrt(2) * SpecialFunctions.ErfInv(pLowerBound)); + return attributes.GreatHitWindow / (Math.Sqrt(2) * DifficultyCalculationUtils.ErfInv(pLowerBound)); } private int totalHits => countGreat + countOk + countMeh + countMiss; diff --git a/osu.Game/Rulesets/Difficulty/Utils/DifficultyCalculationUtils.cs b/osu.Game/Rulesets/Difficulty/Utils/DifficultyCalculationUtils.cs index aeccf2fd55..78df8a139b 100644 --- a/osu.Game/Rulesets/Difficulty/Utils/DifficultyCalculationUtils.cs +++ b/osu.Game/Rulesets/Difficulty/Utils/DifficultyCalculationUtils.cs @@ -6,7 +6,7 @@ using System.Linq; namespace osu.Game.Rulesets.Difficulty.Utils { - public static class DifficultyCalculationUtils + public static partial class DifficultyCalculationUtils { /// /// Converts BPM value into milliseconds diff --git a/osu.Game/Utils/SpecialFunctions.cs b/osu.Game/Rulesets/Difficulty/Utils/DifficultyCalculationUtils_ErrorFunction.cs similarity index 99% rename from osu.Game/Utils/SpecialFunctions.cs rename to osu.Game/Rulesets/Difficulty/Utils/DifficultyCalculationUtils_ErrorFunction.cs index 795a84a973..4b89cbe7cc 100644 --- a/osu.Game/Utils/SpecialFunctions.cs +++ b/osu.Game/Rulesets/Difficulty/Utils/DifficultyCalculationUtils_ErrorFunction.cs @@ -3,7 +3,6 @@ // All code is referenced from the following: // https://github.com/mathnet/mathnet-numerics/blob/master/src/Numerics/SpecialFunctions/Erf.cs -// https://github.com/mathnet/mathnet-numerics/blob/master/src/Numerics/Optimization/NelderMeadSimplex.cs /* Copyright (c) 2002-2022 Math.NET @@ -14,12 +13,10 @@ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLI using System; -namespace osu.Game.Utils +namespace osu.Game.Rulesets.Difficulty.Utils { - public class SpecialFunctions + public partial class DifficultyCalculationUtils { - private const double sqrt2_pi = 2.5066282746310005024157652848110452530069867406099d; - /// /// ************************************** /// COEFFICIENTS FOR METHOD ErfImp * From cbbcf54d742f0b74d3c122d8487254862a662df6 Mon Sep 17 00:00:00 2001 From: ILW8 Date: Sat, 18 Jan 2025 02:41:15 +0000 Subject: [PATCH 0644/1275] 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 8354cd5f93a7c1989dbee48fb0c1403b96a7b420 Mon Sep 17 00:00:00 2001 From: Eloise Date: Sat, 18 Jan 2025 13:52:47 +0000 Subject: [PATCH 0645/1275] Penalise the reading difficulty of high velocity notes using "note density" (#31512) * Penalise reading difficulty of high velocity notes at high densities * Use System for math functions * Lawtrohux changes * Clean up density penalty comment * Swap midVelocity and highVelocity back around * code quality pass --------- Co-authored-by: Jay Lawton Co-authored-by: StanR --- .../Difficulty/Evaluators/ReadingEvaluator.cs | 21 +++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Evaluators/ReadingEvaluator.cs b/osu.Game.Rulesets.Taiko/Difficulty/Evaluators/ReadingEvaluator.cs index a6a1513842..2a08f65c7b 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Evaluators/ReadingEvaluator.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Evaluators/ReadingEvaluator.cs @@ -1,6 +1,7 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System; using osu.Game.Rulesets.Difficulty.Utils; using osu.Game.Rulesets.Taiko.Difficulty.Preprocessing; @@ -31,13 +32,25 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Evaluators /// The reading difficulty value for the given hit object. public static double EvaluateDifficultyOf(TaikoDifficultyHitObject noteObject) { - double effectiveBPM = noteObject.EffectiveBPM; - var highVelocity = new VelocityRange(480, 640); var midVelocity = new VelocityRange(360, 480); - return 1.0 * DifficultyCalculationUtils.Logistic(effectiveBPM, highVelocity.Center, 1.0 / (highVelocity.Range / 10)) - + 0.5 * DifficultyCalculationUtils.Logistic(effectiveBPM, midVelocity.Center, 1.0 / (midVelocity.Range / 10)); + // Apply a cap to prevent outlier values on maps that exceed the editor's parameters. + double effectiveBPM = Math.Max(1.0, noteObject.EffectiveBPM); + + double midVelocityDifficulty = 0.5 * DifficultyCalculationUtils.Logistic(effectiveBPM, midVelocity.Center, 1.0 / (midVelocity.Range / 10)); + + // Expected DeltaTime is the DeltaTime this note would need to be spaced equally to a base slider velocity 1/4 note. + double expectedDeltaTime = 21000.0 / effectiveBPM; + double objectDensity = expectedDeltaTime / Math.Max(1.0, noteObject.DeltaTime); + + // High density is penalised at high velocity as it is generally considered easier to read. See https://www.desmos.com/calculator/u63f3ntdsi + double densityPenalty = DifficultyCalculationUtils.Logistic(objectDensity, 0.925, 15); + + double highVelocityDifficulty = (1.0 - 0.33 * densityPenalty) * DifficultyCalculationUtils.Logistic + (effectiveBPM, highVelocity.Center + 8 * densityPenalty, (1.0 + 0.5 * densityPenalty) / (highVelocity.Range / 10)); + + return midVelocityDifficulty + highVelocityDifficulty; } } } From 67723b3e5201f8b10e2aaac8831c4f4960e934ba Mon Sep 17 00:00:00 2001 From: "Bastien D." <37190278+bastoo0@users.noreply.github.com> Date: Sat, 18 Jan 2025 20:26:23 +0100 Subject: [PATCH 0646/1275] Fix osu!catch "buzz slider" SR abuse (#31126) * Implement fix for catch buzz sliders SR abuse * Run formatting --------- Co-authored-by: StanR --- .../Difficulty/Skills/Movement.cs | 25 ++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Catch/Difficulty/Skills/Movement.cs b/osu.Game.Rulesets.Catch/Difficulty/Skills/Movement.cs index 54b85f1745..2d1adbd056 100644 --- a/osu.Game.Rulesets.Catch/Difficulty/Skills/Movement.cs +++ b/osu.Game.Rulesets.Catch/Difficulty/Skills/Movement.cs @@ -26,7 +26,9 @@ namespace osu.Game.Rulesets.Catch.Difficulty.Skills private float? lastPlayerPosition; private float lastDistanceMoved; + private float lastExactDistanceMoved; private double lastStrainTime; + private bool isBuzzSliderTriggered; /// /// The speed multiplier applied to the player's catcher. @@ -59,6 +61,9 @@ namespace osu.Game.Rulesets.Catch.Difficulty.Skills float distanceMoved = playerPosition - lastPlayerPosition.Value; + // For the exact position we consider that the catcher is in the correct position for both objects + float exactDistanceMoved = catchCurrent.NormalizedPosition - lastPlayerPosition.Value; + double weightedStrainTime = catchCurrent.StrainTime + 13 + (3 / catcherSpeedMultiplier); double distanceAddition = (Math.Pow(Math.Abs(distanceMoved), 1.3) / 510); @@ -92,12 +97,30 @@ namespace osu.Game.Rulesets.Catch.Difficulty.Skills playerPosition = catchCurrent.NormalizedPosition; } - distanceAddition *= 1.0 + edgeDashBonus * ((20 - catchCurrent.LastObject.DistanceToHyperDash) / 20) * Math.Pow((Math.Min(catchCurrent.StrainTime * catcherSpeedMultiplier, 265) / 265), 1.5); // Edge Dashes are easier at lower ms values + distanceAddition *= 1.0 + edgeDashBonus * ((20 - catchCurrent.LastObject.DistanceToHyperDash) / 20) + * Math.Pow((Math.Min(catchCurrent.StrainTime * catcherSpeedMultiplier, 265) / 265), 1.5); // Edge Dashes are easier at lower ms values + } + + // There is an edge case where horizontal back and forth sliders create "buzz" patterns which are repeated "movements" with a distance lower than + // the platter's width but high enough to be considered a movement due to the absolute_player_positioning_error and normalized_hitobject_radius offsets + // We are detecting this exact scenario. The first back and forth is counted but all subsequent ones are nullified. + // To achieve that, we need to store the exact distances (distance ignoring absolute_player_positioning_error and normalized_hitobject_radius) + if (Math.Abs(exactDistanceMoved) <= HalfCatcherWidth * 2 && exactDistanceMoved == -lastExactDistanceMoved && catchCurrent.StrainTime == lastStrainTime) + { + if (isBuzzSliderTriggered) + distanceAddition = 0; + else + isBuzzSliderTriggered = true; + } + else + { + isBuzzSliderTriggered = false; } lastPlayerPosition = playerPosition; lastDistanceMoved = distanceMoved; lastStrainTime = catchCurrent.StrainTime; + lastExactDistanceMoved = exactDistanceMoved; return distanceAddition / weightedStrainTime; } From e320f17fafa8d904bb7a436971feabaeb3f64e3b Mon Sep 17 00:00:00 2001 From: James Wilson Date: Sun, 19 Jan 2025 15:47:39 +0000 Subject: [PATCH 0647/1275] Remove redundant angle check (#31566) --- osu.Game.Rulesets.Osu.Tests/OsuDifficultyCalculatorTest.cs | 2 +- osu.Game.Rulesets.Osu/Difficulty/Evaluators/AimEvaluator.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Osu.Tests/OsuDifficultyCalculatorTest.cs b/osu.Game.Rulesets.Osu.Tests/OsuDifficultyCalculatorTest.cs index 7cf5b0529f..defd02b830 100644 --- a/osu.Game.Rulesets.Osu.Tests/OsuDifficultyCalculatorTest.cs +++ b/osu.Game.Rulesets.Osu.Tests/OsuDifficultyCalculatorTest.cs @@ -22,7 +22,7 @@ namespace osu.Game.Rulesets.Osu.Tests public void Test(double expectedStarRating, int expectedMaxCombo, string name) => base.Test(expectedStarRating, expectedMaxCombo, name); - [TestCase(9.6779397290273756d, 239, "diffcalc-test")] + [TestCase(9.6779746353001634d, 239, "diffcalc-test")] [TestCase(1.7691451263718989d, 54, "zero-length-sliders")] [TestCase(0.55785578988249407d, 4, "very-fast-slider")] public void TestClockRateAdjusted(double expectedStarRating, int expectedMaxCombo, string name) diff --git a/osu.Game.Rulesets.Osu/Difficulty/Evaluators/AimEvaluator.cs b/osu.Game.Rulesets.Osu/Difficulty/Evaluators/AimEvaluator.cs index 9a5533e536..d1c92ed6a7 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/Evaluators/AimEvaluator.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/Evaluators/AimEvaluator.cs @@ -71,7 +71,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators if (Math.Max(osuCurrObj.StrainTime, osuLastObj.StrainTime) < 1.25 * Math.Min(osuCurrObj.StrainTime, osuLastObj.StrainTime)) // If rhythms are the same. { - if (osuCurrObj.Angle != null && osuLastObj.Angle != null && osuLastLastObj.Angle != null) + if (osuCurrObj.Angle != null && osuLastObj.Angle != null) { double currAngle = osuCurrObj.Angle.Value; double lastAngle = osuLastObj.Angle.Value; From 72e1b2954c57087d58a9cd5c6fd540c234ca7f66 Mon Sep 17 00:00:00 2001 From: CloneWith Date: Mon, 20 Jan 2025 00:21:10 +0800 Subject: [PATCH 0648/1275] 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 e04727afb13d5478608987e1080270a54bee66ed Mon Sep 17 00:00:00 2001 From: Jay Lawton Date: Mon, 20 Jan 2025 07:55:34 +1000 Subject: [PATCH 0649/1275] Improve convert considerations in osu!taiko (#31546) * return a higher finger count * implement isConvert * diffcalc cleanup * harshen monostaminafactor accuracy curve * readd comment * adjusts tests --- .../TaikoDifficultyCalculatorTest.cs | 8 ++--- .../Difficulty/Evaluators/StaminaEvaluator.cs | 2 +- .../Difficulty/Skills/Stamina.cs | 7 +++-- .../Difficulty/TaikoDifficultyCalculator.cs | 31 ++++++------------- .../Difficulty/TaikoPerformanceCalculator.cs | 2 +- 5 files changed, 21 insertions(+), 29 deletions(-) diff --git a/osu.Game.Rulesets.Taiko.Tests/TaikoDifficultyCalculatorTest.cs b/osu.Game.Rulesets.Taiko.Tests/TaikoDifficultyCalculatorTest.cs index d760b9aef6..6f5c26816f 100644 --- a/osu.Game.Rulesets.Taiko.Tests/TaikoDifficultyCalculatorTest.cs +++ b/osu.Game.Rulesets.Taiko.Tests/TaikoDifficultyCalculatorTest.cs @@ -14,13 +14,13 @@ namespace osu.Game.Rulesets.Taiko.Tests { protected override string ResourceAssembly => "osu.Game.Rulesets.Taiko"; - [TestCase(3.3167800835687551d, 200, "diffcalc-test")] - [TestCase(3.3167800835687551d, 200, "diffcalc-test-strong")] + [TestCase(3.3056113401782845d, 200, "diffcalc-test")] + [TestCase(3.3056113401782845d, 200, "diffcalc-test-strong")] public void Test(double expectedStarRating, int expectedMaxCombo, string name) => base.Test(expectedStarRating, expectedMaxCombo, name); - [TestCase(4.4631326105105122d, 200, "diffcalc-test")] - [TestCase(4.4631326105105122d, 200, "diffcalc-test-strong")] + [TestCase(4.4473902679506896d, 200, "diffcalc-test")] + [TestCase(4.4473902679506896d, 200, "diffcalc-test-strong")] public void TestClockRateAdjusted(double expectedStarRating, int expectedMaxCombo, string name) => Test(expectedStarRating, expectedMaxCombo, name, new TaikoModDoubleTime()); diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Evaluators/StaminaEvaluator.cs b/osu.Game.Rulesets.Taiko/Difficulty/Evaluators/StaminaEvaluator.cs index a273d91a38..b39ad953a4 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Evaluators/StaminaEvaluator.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Evaluators/StaminaEvaluator.cs @@ -42,7 +42,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Evaluators return 2; } - return 4; + return 8; } /// diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Skills/Stamina.cs b/osu.Game.Rulesets.Taiko/Difficulty/Skills/Stamina.cs index 29f9f16033..aea491aca3 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Skills/Stamina.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Skills/Stamina.cs @@ -20,6 +20,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Skills private double strainDecayBase => 0.4; private readonly bool singleColourStamina; + private readonly bool isConvert; private double currentStrain; @@ -28,10 +29,12 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Skills /// /// Mods for use in skill calculations. /// Reads when Stamina is from a single coloured pattern. - public Stamina(Mod[] mods, bool singleColourStamina) + /// Determines if the currently evaluated beatmap is converted. + public Stamina(Mod[] mods, bool singleColourStamina, bool isConvert) : base(mods) { this.singleColourStamina = singleColourStamina; + this.isConvert = isConvert; } private double strainDecay(double ms) => Math.Pow(strainDecayBase, ms / 1000); @@ -45,7 +48,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Skills var currentObject = current as TaikoDifficultyHitObject; int index = currentObject?.Colour.MonoStreak?.HitObjects.IndexOf(currentObject) ?? 0; - double monolengthBonus = 1 + Math.Min(Math.Max((index - 5) / 50.0, 0), 0.30); + double monolengthBonus = isConvert ? 1 : 1 + Math.Min(Math.Max((index - 5) / 50.0, 0), 0.30); if (singleColourStamina) return DifficultyCalculationUtils.Logistic(-(index - 10) / 2.0, currentStrain); diff --git a/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs b/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs index 3ad9d17526..efd3001764 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs @@ -32,6 +32,8 @@ namespace osu.Game.Rulesets.Taiko.Difficulty private double strainLengthBonus; private double patternMultiplier; + private bool isConvert; + public override int Version => 20241007; public TaikoDifficultyCalculator(IRulesetInfo ruleset, IWorkingBeatmap beatmap) @@ -44,13 +46,15 @@ namespace osu.Game.Rulesets.Taiko.Difficulty HitWindows hitWindows = new HitWindows(); hitWindows.SetDifficulty(beatmap.Difficulty.OverallDifficulty); + isConvert = beatmap.BeatmapInfo.Ruleset.OnlineID == 0; + return new Skill[] { new Rhythm(mods, hitWindows.WindowFor(HitResult.Great) / clockRate), new Reading(mods), new Colour(mods), - new Stamina(mods, false), - new Stamina(mods, true) + new Stamina(mods, false, isConvert), + new Stamina(mods, true, isConvert) }; } @@ -130,19 +134,9 @@ namespace osu.Game.Rulesets.Taiko.Difficulty + Math.Min(Math.Max((staminaDifficultStrains - 1350) / 5000, 0), 0.15) + Math.Min(Math.Max((staminaRating - 7.0) / 1.0, 0), 0.05); - double combinedRating = combinedDifficultyValue(rhythm, reading, colour, stamina, isRelax); + double combinedRating = combinedDifficultyValue(rhythm, reading, colour, stamina, isRelax, isConvert); double starRating = rescale(combinedRating * 1.4); - // Converts are penalised outside the scope of difficulty calculation, as our assumptions surrounding standard play-styles becomes out-of-scope. - if (beatmap.BeatmapInfo.Ruleset.OnlineID == 0) - { - starRating *= 0.7; - - // For maps with relax, multiple inputs are more likely to be abused. - if (isRelax) - starRating *= 0.60; - } - HitWindows hitWindows = new TaikoHitWindows(); hitWindows.SetDifficulty(beatmap.Difficulty.OverallDifficulty); @@ -173,7 +167,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty /// For each section, the peak strains of all separate skills are combined into a single peak strain for the section. /// The resulting partial rating of the beatmap is a weighted sum of the combined peaks (higher peaks are weighted more). /// - private double combinedDifficultyValue(Rhythm rhythm, Reading reading, Colour colour, Stamina stamina, bool isRelax) + private double combinedDifficultyValue(Rhythm rhythm, Reading reading, Colour colour, Stamina stamina, bool isRelax, bool isConvert) { List peaks = new List(); @@ -186,14 +180,9 @@ namespace osu.Game.Rulesets.Taiko.Difficulty { double rhythmPeak = rhythmPeaks[i] * rhythm_skill_multiplier * patternMultiplier; double readingPeak = readingPeaks[i] * reading_skill_multiplier; - double colourPeak = colourPeaks[i] * colour_skill_multiplier; + double colourPeak = isRelax ? 0 : colourPeaks[i] * colour_skill_multiplier; // There is no colour difficulty in relax. double staminaPeak = staminaPeaks[i] * stamina_skill_multiplier * strainLengthBonus; - - if (isRelax) - { - colourPeak = 0; // There is no colour difficulty in relax. - staminaPeak /= 1.5; // Stamina difficulty is decreased with an increased available finger count. - } + staminaPeak /= isConvert || isRelax ? 1.5 : 1.0; // Available finger count is increased by 150%, thus we adjust accordingly. double peak = DifficultyCalculationUtils.Norm(2, DifficultyCalculationUtils.Norm(1.5, colourPeak, staminaPeak), rhythmPeak, readingPeak); diff --git a/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceCalculator.cs b/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceCalculator.cs index 9e7bf7cb7a..bcd3693119 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceCalculator.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceCalculator.cs @@ -97,7 +97,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty // Scale accuracy more harshly on nearly-completely mono (single coloured) speed maps. double accScalingExponent = 2 + attributes.MonoStaminaFactor; - double accScalingShift = 500 - 100 * attributes.MonoStaminaFactor; + double accScalingShift = 500 - 100 * (attributes.MonoStaminaFactor * 3); return difficultyValue * Math.Pow(DifficultyCalculationUtils.Erf(accScalingShift / (Math.Sqrt(2) * estimatedUnstableRate.Value)), accScalingExponent); } From b6ce72b6d92d28c6f95cf28255535a16ad6a1ef0 Mon Sep 17 00:00:00 2001 From: Berkan Diler Date: Sun, 19 Jan 2025 23:27:44 +0100 Subject: [PATCH 0650/1275] 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 2d0bc6cb62bd9fe84b7fffb8019ff2e503a6ffc1 Mon Sep 17 00:00:00 2001 From: Jay Lawton Date: Mon, 20 Jan 2025 08:40:09 +1000 Subject: [PATCH 0651/1275] Rebalance stamina length bonus in osu!taiko (#31556) * adjust straincount to assume 1300 * remove comment --------- Co-authored-by: StanR --- .../Difficulty/TaikoDifficultyCalculator.cs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs b/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs index efd3001764..b1dcf2d7a0 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs @@ -124,14 +124,13 @@ namespace osu.Game.Rulesets.Taiko.Difficulty double colourDifficultStrains = colour.CountTopWeightedStrains(); double rhythmDifficultStrains = rhythm.CountTopWeightedStrains(); - // Due to constraints of strain in cases where difficult strain values don't shift with range changes, we manually apply clockrate. - double staminaDifficultStrains = stamina.CountTopWeightedStrains() * clockRate; + double staminaDifficultStrains = stamina.CountTopWeightedStrains(); // As we don't have pattern integration in osu!taiko, we apply the other two skills relative to rhythm. patternMultiplier = Math.Pow(staminaRating * colourRating, 0.10); strainLengthBonus = 1 - + Math.Min(Math.Max((staminaDifficultStrains - 1350) / 5000, 0), 0.15) + + Math.Min(Math.Max((staminaDifficultStrains - 1000) / 3700, 0), 0.15) + Math.Min(Math.Max((staminaRating - 7.0) / 1.0, 0), 0.05); double combinedRating = combinedDifficultyValue(rhythm, reading, colour, stamina, isRelax, isConvert); From a6ca9ba9fb0630562425fe37d0445da5f75e9635 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marvin=20Sch=C3=BCrz?= Date: Mon, 20 Jan 2025 00:51:43 +0100 Subject: [PATCH 0652/1275] Display up to 2 decimal places in `MetronomeDisplay` --- .../Screens/Edit/Timing/MetronomeDisplay.cs | 21 ++++++++++++++----- 1 file changed, 16 insertions(+), 5 deletions(-) diff --git a/osu.Game/Screens/Edit/Timing/MetronomeDisplay.cs b/osu.Game/Screens/Edit/Timing/MetronomeDisplay.cs index 5e5b740b62..5325c8640b 100644 --- a/osu.Game/Screens/Edit/Timing/MetronomeDisplay.cs +++ b/osu.Game/Screens/Edit/Timing/MetronomeDisplay.cs @@ -228,11 +228,13 @@ namespace osu.Game.Screens.Edit.Timing private double effectiveBeatLength; + private double effectiveBpm => 60_000 / effectiveBeatLength; + private TimingControlPoint timingPoint = null!; private bool isSwinging; - private readonly BindableInt interpolatedBpm = new BindableInt(); + private readonly BindableDouble interpolatedBpm = new BindableDouble(); private ScheduledDelegate? latchDelegate; @@ -255,7 +257,17 @@ namespace osu.Game.Screens.Edit.Timing { base.LoadComplete(); - interpolatedBpm.BindValueChanged(_ => bpmText.Text = interpolatedBpm.Value.ToLocalisableString()); + interpolatedBpm.BindValueChanged(_ => updateBpmText()); + } + + private void updateBpmText() + { + double bpm = Math.Round(interpolatedBpm.Value); + + if (Precision.AlmostEquals(bpm, effectiveBpm, 1.0)) + bpm = effectiveBpm; + + bpmText.Text = bpm.ToLocalisableString("0.##"); } protected override void Update() @@ -277,12 +289,11 @@ namespace osu.Game.Screens.Edit.Timing EarlyActivationMilliseconds = timingPoint.BeatLength / 2; - double effectiveBpm = 60000 / effectiveBeatLength; - float bpmRatio = (float)Interpolation.ApplyEasing(Easing.OutQuad, Math.Clamp((effectiveBpm - 30) / 480, 0, 1)); weight.MoveToY((float)Interpolation.Lerp(0.1f, 0.83f, bpmRatio), 600, Easing.OutQuint); - this.TransformBindableTo(interpolatedBpm, (int)Math.Round(effectiveBpm), 600, Easing.OutQuint); + + this.TransformBindableTo(interpolatedBpm, effectiveBpm, 600, Easing.OutQuint); } if (!BeatSyncSource.Clock.IsRunning && isSwinging) From 3532ce1636460d0988fa7d0c3832b25065600cca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marvin=20Sch=C3=BCrz?= Date: Mon, 20 Jan 2025 01:07:13 +0100 Subject: [PATCH 0653/1275] Olibomby insisted on it being like this so i concede --- osu.Game/Screens/Edit/Timing/MetronomeDisplay.cs | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/osu.Game/Screens/Edit/Timing/MetronomeDisplay.cs b/osu.Game/Screens/Edit/Timing/MetronomeDisplay.cs index 5325c8640b..f8236f922a 100644 --- a/osu.Game/Screens/Edit/Timing/MetronomeDisplay.cs +++ b/osu.Game/Screens/Edit/Timing/MetronomeDisplay.cs @@ -262,10 +262,9 @@ namespace osu.Game.Screens.Edit.Timing private void updateBpmText() { - double bpm = Math.Round(interpolatedBpm.Value); - - if (Precision.AlmostEquals(bpm, effectiveBpm, 1.0)) - bpm = effectiveBpm; + double bpm = Precision.AlmostEquals(interpolatedBpm.Value, effectiveBpm, 1.0) + ? effectiveBpm + : Math.Round(interpolatedBpm.Value); bpmText.Text = bpm.ToLocalisableString("0.##"); } From 8f33b4cc6159b4b65fc7df4b757c1d5418eb15ac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marvin=20Sch=C3=BCrz?= Date: Mon, 20 Jan 2025 01:14:21 +0100 Subject: [PATCH 0654/1275] Add comment --- osu.Game/Screens/Edit/Timing/MetronomeDisplay.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/osu.Game/Screens/Edit/Timing/MetronomeDisplay.cs b/osu.Game/Screens/Edit/Timing/MetronomeDisplay.cs index f8236f922a..8a4f1c01b1 100644 --- a/osu.Game/Screens/Edit/Timing/MetronomeDisplay.cs +++ b/osu.Game/Screens/Edit/Timing/MetronomeDisplay.cs @@ -262,6 +262,8 @@ namespace osu.Game.Screens.Edit.Timing private void updateBpmText() { + // While interpolating between two integer values, showing the decimal places would look a bit odd + // so rounding is applied until we're close to the final value. double bpm = Precision.AlmostEquals(interpolatedBpm.Value, effectiveBpm, 1.0) ? effectiveBpm : Math.Round(interpolatedBpm.Value); From e386c9e373618fea4acb371447db8d2bee637701 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marvin=20Sch=C3=BCrz?= Date: Mon, 20 Jan 2025 01:25:22 +0100 Subject: [PATCH 0655/1275] Apply snapping when pasting hitobjects --- osu.Game/Screens/Edit/Compose/ComposeScreen.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/osu.Game/Screens/Edit/Compose/ComposeScreen.cs b/osu.Game/Screens/Edit/Compose/ComposeScreen.cs index f7e523db25..195625dcde 100644 --- a/osu.Game/Screens/Edit/Compose/ComposeScreen.cs +++ b/osu.Game/Screens/Edit/Compose/ComposeScreen.cs @@ -31,6 +31,9 @@ namespace osu.Game.Screens.Edit.Compose [Resolved] private IGameplaySettings globalGameplaySettings { get; set; } + [Resolved] + private IBeatSnapProvider beatSnapProvider { get; set; } + private Bindable clipboard { get; set; } private HitObjectComposer composer; @@ -150,7 +153,7 @@ namespace osu.Game.Screens.Edit.Compose Debug.Assert(objects.Any()); - double timeOffset = clock.CurrentTime - objects.Min(o => o.StartTime); + double timeOffset = beatSnapProvider.SnapTime(clock.CurrentTime) - objects.Min(o => o.StartTime); foreach (var h in objects) h.StartTime += timeOffset; From 45e0d9154e410e0db5aab353c5d67b3e539db015 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marvin=20Sch=C3=BCrz?= Date: Mon, 20 Jan 2025 01:38:18 +0100 Subject: [PATCH 0656/1275] Adjust tests to worked with snapped start time --- osu.Game.Tests/Visual/Editing/TestSceneEditorClipboard.cs | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/osu.Game.Tests/Visual/Editing/TestSceneEditorClipboard.cs b/osu.Game.Tests/Visual/Editing/TestSceneEditorClipboard.cs index a766b253aa..ce9dbd5fb1 100644 --- a/osu.Game.Tests/Visual/Editing/TestSceneEditorClipboard.cs +++ b/osu.Game.Tests/Visual/Editing/TestSceneEditorClipboard.cs @@ -58,7 +58,7 @@ namespace osu.Game.Tests.Visual.Editing AddAssert("is one object", () => EditorBeatmap.HitObjects.Count == 1); - AddAssert("new object selected", () => EditorBeatmap.SelectedHitObjects.Single().StartTime == newTime); + AddAssert("new object selected", () => EditorBeatmap.SelectedHitObjects.Single().StartTime == EditorBeatmap.SnapTime(newTime, null)); } [Test] @@ -122,6 +122,8 @@ namespace osu.Game.Tests.Visual.Editing [TestCase(true)] public void TestCopyPaste(bool deselectAfterCopy) { + const int paste_time = 2000; + var addedObject = new HitCircle { StartTime = 1000 }; AddStep("add hitobject", () => EditorBeatmap.Add(addedObject)); @@ -130,7 +132,7 @@ namespace osu.Game.Tests.Visual.Editing AddStep("copy hitobject", () => Editor.Copy()); - AddStep("move forward in time", () => EditorClock.Seek(2000)); + AddStep("move forward in time", () => EditorClock.Seek(paste_time)); if (deselectAfterCopy) { @@ -144,7 +146,7 @@ namespace osu.Game.Tests.Visual.Editing AddAssert("are two objects", () => EditorBeatmap.HitObjects.Count == 2); - AddAssert("new object selected", () => EditorBeatmap.SelectedHitObjects.Single().StartTime == 2000); + AddAssert("new object selected", () => EditorBeatmap.SelectedHitObjects.Single().StartTime == EditorBeatmap.SnapTime(paste_time, null)); AddUntilStep("timeline selection box is visible", () => Editor.ChildrenOfType().First().ChildrenOfType().First().Alpha > 0); AddUntilStep("composer selection box is visible", () => Editor.ChildrenOfType().First().ChildrenOfType().First().Alpha > 0); From 525e16ad1d8442a01b81ba501b49204ba9705c77 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 20 Jan 2025 15:00:35 +0900 Subject: [PATCH 0657/1275] 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 0658/1275] 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 0659/1275] 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 0660/1275] 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 0661/1275] 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 0662/1275] Always select text when an editor slider-textbox is focused --- osu.Game.Rulesets.Osu/Edit/PreciseMovementPopover.cs | 6 +----- osu.Game.Rulesets.Osu/Edit/PreciseRotationPopover.cs | 6 +----- osu.Game.Rulesets.Osu/Edit/PreciseScalePopover.cs | 6 +----- osu.Game/Graphics/UserInterfaceV2/SliderWithTextBoxInput.cs | 3 +-- 4 files changed, 4 insertions(+), 17 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Edit/PreciseMovementPopover.cs b/osu.Game.Rulesets.Osu/Edit/PreciseMovementPopover.cs index 151ca31ac0..f2cb8794b5 100644 --- a/osu.Game.Rulesets.Osu/Edit/PreciseMovementPopover.cs +++ b/osu.Game.Rulesets.Osu/Edit/PreciseMovementPopover.cs @@ -85,11 +85,7 @@ namespace osu.Game.Rulesets.Osu.Edit { base.LoadComplete(); - ScheduleAfterChildren(() => - { - xInput.TakeFocus(); - xInput.SelectAll(); - }); + ScheduleAfterChildren(() => xInput.TakeFocus()); } protected override void PopIn() diff --git a/osu.Game.Rulesets.Osu/Edit/PreciseRotationPopover.cs b/osu.Game.Rulesets.Osu/Edit/PreciseRotationPopover.cs index 477d3b4e57..ae8ad2c01b 100644 --- a/osu.Game.Rulesets.Osu/Edit/PreciseRotationPopover.cs +++ b/osu.Game.Rulesets.Osu/Edit/PreciseRotationPopover.cs @@ -96,11 +96,7 @@ namespace osu.Game.Rulesets.Osu.Edit { base.LoadComplete(); - ScheduleAfterChildren(() => - { - angleInput.TakeFocus(); - angleInput.SelectAll(); - }); + ScheduleAfterChildren(() => angleInput.TakeFocus()); angleInput.Current.BindValueChanged(angle => rotationInfo.Value = rotationInfo.Value with { Degrees = angle.NewValue }); rotationHandler.CanRotateAroundSelectionOrigin.BindValueChanged(e => diff --git a/osu.Game.Rulesets.Osu/Edit/PreciseScalePopover.cs b/osu.Game.Rulesets.Osu/Edit/PreciseScalePopover.cs index e728290289..ac6d9fbb19 100644 --- a/osu.Game.Rulesets.Osu/Edit/PreciseScalePopover.cs +++ b/osu.Game.Rulesets.Osu/Edit/PreciseScalePopover.cs @@ -139,11 +139,7 @@ namespace osu.Game.Rulesets.Osu.Edit { base.LoadComplete(); - ScheduleAfterChildren(() => - { - scaleInput.TakeFocus(); - scaleInput.SelectAll(); - }); + ScheduleAfterChildren(() => scaleInput.TakeFocus()); scaleInput.Current.BindValueChanged(scale => scaleInfo.Value = scaleInfo.Value with { Scale = scale.NewValue }); xCheckBox.Current.BindValueChanged(_ => diff --git a/osu.Game/Graphics/UserInterfaceV2/SliderWithTextBoxInput.cs b/osu.Game/Graphics/UserInterfaceV2/SliderWithTextBoxInput.cs index c16a6c612d..2fbe3ae89b 100644 --- a/osu.Game/Graphics/UserInterfaceV2/SliderWithTextBoxInput.cs +++ b/osu.Game/Graphics/UserInterfaceV2/SliderWithTextBoxInput.cs @@ -74,6 +74,7 @@ namespace osu.Game.Graphics.UserInterfaceV2 textBox = new LabelledTextBox { Label = labelText, + SelectAllOnFocus = true, }, slider = new SettingsSlider { @@ -92,8 +93,6 @@ namespace osu.Game.Graphics.UserInterfaceV2 public bool TakeFocus() => GetContainingFocusManager()?.ChangeFocus(textBox) == true; - public bool SelectAll() => textBox.SelectAll(); - private bool updatingFromTextBox; private void textChanged(ValueChangedEvent change) From 2b5ea4e6e0e859599affbcb5cf9151060679450b Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Mon, 20 Jan 2025 03:17:01 -0500 Subject: [PATCH 0663/1275] Fix recent editor textbox regressions --- .../UserInterface/TestSceneFormControls.cs | 2 +- .../Graphics/UserInterfaceV2/FormNumberBox.cs | 20 +++++++++---------- .../Graphics/UserInterfaceV2/FormSliderBar.cs | 3 +-- 3 files changed, 11 insertions(+), 14 deletions(-) diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneFormControls.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneFormControls.cs index b9ff78b49f..118fbca97b 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneFormControls.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneFormControls.cs @@ -53,7 +53,7 @@ namespace osu.Game.Tests.Visual.UserInterface Current = { Disabled = true }, TabbableContentContainer = this, }, - new FormNumberBox + new FormNumberBox(allowDecimals: true) { Caption = "Number", HintText = "Insert your favourite number", diff --git a/osu.Game/Graphics/UserInterfaceV2/FormNumberBox.cs b/osu.Game/Graphics/UserInterfaceV2/FormNumberBox.cs index 61d3b3fc31..b739155a36 100644 --- a/osu.Game/Graphics/UserInterfaceV2/FormNumberBox.cs +++ b/osu.Game/Graphics/UserInterfaceV2/FormNumberBox.cs @@ -1,32 +1,30 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using System.Globalization; using osu.Framework.Input; namespace osu.Game.Graphics.UserInterfaceV2 { public partial class FormNumberBox : FormTextBox { - public bool AllowDecimals { get; init; } + private readonly bool allowDecimals; - internal override InnerTextBox CreateTextBox() => new InnerNumberBox + public FormNumberBox(bool allowDecimals = false) + { + this.allowDecimals = allowDecimals; + } + + internal override InnerTextBox CreateTextBox() => new InnerNumberBox(allowDecimals) { - AllowDecimals = AllowDecimals, SelectAllOnFocus = true, }; internal partial class InnerNumberBox : InnerTextBox { - public bool AllowDecimals { get; init; } - - public InnerNumberBox() + public InnerNumberBox(bool allowDecimals) { - InputProperties = new TextInputProperties(TextInputType.Number, false); + InputProperties = new TextInputProperties(allowDecimals ? TextInputType.Decimal : TextInputType.Number, false); } - - protected override bool CanAddCharacter(char character) - => char.IsAsciiDigit(character) || (AllowDecimals && CultureInfo.CurrentCulture.NumberFormat.NumberDecimalSeparator.Contains(character)); } } } diff --git a/osu.Game/Graphics/UserInterfaceV2/FormSliderBar.cs b/osu.Game/Graphics/UserInterfaceV2/FormSliderBar.cs index 532423876e..4e43b133c7 100644 --- a/osu.Game/Graphics/UserInterfaceV2/FormSliderBar.cs +++ b/osu.Game/Graphics/UserInterfaceV2/FormSliderBar.cs @@ -119,7 +119,7 @@ namespace osu.Game.Graphics.UserInterfaceV2 Caption = Caption, TooltipText = HintText, }, - textBox = new FormNumberBox.InnerNumberBox + textBox = new FormNumberBox.InnerNumberBox(allowDecimals: true) { Anchor = Anchor.BottomLeft, Origin = Anchor.BottomLeft, @@ -127,7 +127,6 @@ namespace osu.Game.Graphics.UserInterfaceV2 Width = 0.5f, CommitOnFocusLost = true, SelectAllOnFocus = true, - AllowDecimals = true, OnInputError = () => { flashLayer.Colour = ColourInfo.GradientVertical(colours.Red3.Opacity(0), colours.Red3); From e57565435ed58fc4e549559350886df1fa4d4189 Mon Sep 17 00:00:00 2001 From: Eloise Date: Mon, 20 Jan 2025 08:40:52 +0000 Subject: [PATCH 0664/1275] osu!taiko new rhythm penalty for long intervals using stamina difficulty (#31573) * Replace long interval nerf with a new one that uses stamina difficulty * Turn tabs into spaces * Update unit tests --------- Co-authored-by: StanR --- .../TaikoDifficultyCalculatorTest.cs | 8 ++++---- osu.Game.Rulesets.Taiko/Difficulty/Skills/Rhythm.cs | 3 ++- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/osu.Game.Rulesets.Taiko.Tests/TaikoDifficultyCalculatorTest.cs b/osu.Game.Rulesets.Taiko.Tests/TaikoDifficultyCalculatorTest.cs index 6f5c26816f..76b86eb4d6 100644 --- a/osu.Game.Rulesets.Taiko.Tests/TaikoDifficultyCalculatorTest.cs +++ b/osu.Game.Rulesets.Taiko.Tests/TaikoDifficultyCalculatorTest.cs @@ -14,13 +14,13 @@ namespace osu.Game.Rulesets.Taiko.Tests { protected override string ResourceAssembly => "osu.Game.Rulesets.Taiko"; - [TestCase(3.3056113401782845d, 200, "diffcalc-test")] - [TestCase(3.3056113401782845d, 200, "diffcalc-test-strong")] + [TestCase(3.305554470092722d, 200, "diffcalc-test")] + [TestCase(3.305554470092722d, 200, "diffcalc-test-strong")] public void Test(double expectedStarRating, int expectedMaxCombo, string name) => base.Test(expectedStarRating, expectedMaxCombo, name); - [TestCase(4.4473902679506896d, 200, "diffcalc-test")] - [TestCase(4.4473902679506896d, 200, "diffcalc-test-strong")] + [TestCase(4.4472572672057815d, 200, "diffcalc-test")] + [TestCase(4.4472572672057815d, 200, "diffcalc-test-strong")] public void TestClockRateAdjusted(double expectedStarRating, int expectedMaxCombo, string name) => Test(expectedStarRating, expectedMaxCombo, name, new TaikoModDoubleTime()); diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Skills/Rhythm.cs b/osu.Game.Rulesets.Taiko/Difficulty/Skills/Rhythm.cs index 4fe1ea693e..45d0d0a548 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Skills/Rhythm.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Skills/Rhythm.cs @@ -30,7 +30,8 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Skills double difficulty = RhythmEvaluator.EvaluateDifficultyOf(current, greatHitWindow); // To prevent abuse of exceedingly long intervals between awkward rhythms, we penalise its difficulty. - difficulty *= DifficultyCalculationUtils.Logistic(current.DeltaTime, 350, -1 / 25.0, 0.5) + 0.5; + double staminaDifficulty = StaminaEvaluator.EvaluateDifficultyOf(current) - 0.5; // Remove base strain + difficulty *= DifficultyCalculationUtils.Logistic(staminaDifficulty, 1 / 15.0, 50.0); return difficulty; } From 22e839d62b646f6f42b129df83336694547bef8e Mon Sep 17 00:00:00 2001 From: StanR Date: Mon, 20 Jan 2025 14:39:35 +0500 Subject: [PATCH 0665/1275] Replace indexed skill access with `skills.OfType<...>().Single()` (#30034) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Replace indexed skill access with `skills.First(s is ...)` * Fix comment * Further refactoring to remove casts --------- Co-authored-by: Dan Balasescu Co-authored-by: Bartłomiej Dach --- .../Difficulty/CatchDifficultyCalculator.cs | 3 ++- .../Difficulty/ManiaDifficultyCalculator.cs | 2 +- .../Difficulty/OsuDifficultyCalculator.cs | 24 ++++++++++--------- .../Difficulty/Skills/Aim.cs | 10 ++++---- .../Difficulty/Skills/Stamina.cs | 8 +++---- .../Difficulty/TaikoDifficultyCalculator.cs | 10 ++++---- 6 files changed, 30 insertions(+), 27 deletions(-) diff --git a/osu.Game.Rulesets.Catch/Difficulty/CatchDifficultyCalculator.cs b/osu.Game.Rulesets.Catch/Difficulty/CatchDifficultyCalculator.cs index 7d21409ee8..99df2731ff 100644 --- a/osu.Game.Rulesets.Catch/Difficulty/CatchDifficultyCalculator.cs +++ b/osu.Game.Rulesets.Catch/Difficulty/CatchDifficultyCalculator.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; +using System.Linq; using osu.Game.Beatmaps; using osu.Game.Rulesets.Catch.Beatmaps; using osu.Game.Rulesets.Catch.Difficulty.Preprocessing; @@ -40,7 +41,7 @@ namespace osu.Game.Rulesets.Catch.Difficulty CatchDifficultyAttributes attributes = new CatchDifficultyAttributes { - StarRating = Math.Sqrt(skills[0].DifficultyValue()) * difficulty_multiplier, + StarRating = Math.Sqrt(skills.OfType().Single().DifficultyValue()) * difficulty_multiplier, Mods = mods, ApproachRate = preempt > 1200.0 ? -(preempt - 1800.0) / 120.0 : -(preempt - 1200.0) / 150.0 + 5.0, MaxCombo = beatmap.GetMaxCombo(), diff --git a/osu.Game.Rulesets.Mania/Difficulty/ManiaDifficultyCalculator.cs b/osu.Game.Rulesets.Mania/Difficulty/ManiaDifficultyCalculator.cs index ff9aa4aa7b..1efa7cb42f 100644 --- a/osu.Game.Rulesets.Mania/Difficulty/ManiaDifficultyCalculator.cs +++ b/osu.Game.Rulesets.Mania/Difficulty/ManiaDifficultyCalculator.cs @@ -48,7 +48,7 @@ namespace osu.Game.Rulesets.Mania.Difficulty ManiaDifficultyAttributes attributes = new ManiaDifficultyAttributes { - StarRating = skills[0].DifficultyValue() * difficulty_multiplier, + StarRating = skills.OfType().Single().DifficultyValue() * difficulty_multiplier, Mods = mods, // In osu-stable mania, rate-adjustment mods don't affect the hit window. // This is done the way it is to introduce fractional differences in order to match osu-stable for the time being. diff --git a/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs b/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs index 5a61ea586a..1505c51592 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs @@ -36,20 +36,22 @@ namespace osu.Game.Rulesets.Osu.Difficulty if (beatmap.HitObjects.Count == 0) return new OsuDifficultyAttributes { Mods = mods }; - double aimRating = Math.Sqrt(skills[0].DifficultyValue()) * difficulty_multiplier; - double aimRatingNoSliders = Math.Sqrt(skills[1].DifficultyValue()) * difficulty_multiplier; - double speedRating = Math.Sqrt(skills[2].DifficultyValue()) * difficulty_multiplier; - double speedNotes = ((Speed)skills[2]).RelevantNoteCount(); - double difficultSliders = ((Aim)skills[0]).GetDifficultSliders(); - double flashlightRating = 0.0; - - if (mods.Any(h => h is OsuModFlashlight)) - flashlightRating = Math.Sqrt(skills[3].DifficultyValue()) * difficulty_multiplier; + var aim = skills.OfType().Single(a => a.IncludeSliders); + double aimRating = Math.Sqrt(aim.DifficultyValue()) * difficulty_multiplier; + double aimDifficultyStrainCount = aim.CountTopWeightedStrains(); + double difficultSliders = aim.GetDifficultSliders(); + var aimWithoutSliders = skills.OfType().Single(a => !a.IncludeSliders); + double aimRatingNoSliders = Math.Sqrt(aimWithoutSliders.DifficultyValue()) * difficulty_multiplier; double sliderFactor = aimRating > 0 ? aimRatingNoSliders / aimRating : 1; - double aimDifficultyStrainCount = ((OsuStrainSkill)skills[0]).CountTopWeightedStrains(); - double speedDifficultyStrainCount = ((OsuStrainSkill)skills[2]).CountTopWeightedStrains(); + var speed = skills.OfType().Single(); + double speedRating = Math.Sqrt(speed.DifficultyValue()) * difficulty_multiplier; + double speedNotes = speed.RelevantNoteCount(); + double speedDifficultyStrainCount = speed.CountTopWeightedStrains(); + + var flashlight = skills.OfType().SingleOrDefault(); + double flashlightRating = flashlight == null ? 0.0 : Math.Sqrt(flashlight.DifficultyValue()) * difficulty_multiplier; if (mods.Any(m => m is OsuModTouchDevice)) { diff --git a/osu.Game.Rulesets.Osu/Difficulty/Skills/Aim.cs b/osu.Game.Rulesets.Osu/Difficulty/Skills/Aim.cs index f04b679b73..89adda302c 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/Skills/Aim.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/Skills/Aim.cs @@ -16,14 +16,14 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills /// public class Aim : OsuStrainSkill { - public Aim(Mod[] mods, bool withSliders) + public readonly bool IncludeSliders; + + public Aim(Mod[] mods, bool includeSliders) : base(mods) { - this.withSliders = withSliders; + IncludeSliders = includeSliders; } - private readonly bool withSliders; - private double currentStrain; private double skillMultiplier => 25.6; @@ -38,7 +38,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills protected override double StrainValueAt(DifficultyHitObject current) { currentStrain *= strainDecay(current.DeltaTime); - currentStrain += AimEvaluator.EvaluateDifficultyOf(current, withSliders) * skillMultiplier; + currentStrain += AimEvaluator.EvaluateDifficultyOf(current, IncludeSliders) * skillMultiplier; if (current.BaseObject is Slider) { diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Skills/Stamina.cs b/osu.Game.Rulesets.Taiko/Difficulty/Skills/Stamina.cs index aea491aca3..12e1396dd7 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Skills/Stamina.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Skills/Stamina.cs @@ -19,7 +19,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Skills private double skillMultiplier => 1.1; private double strainDecayBase => 0.4; - private readonly bool singleColourStamina; + public readonly bool SingleColourStamina; private readonly bool isConvert; private double currentStrain; @@ -33,7 +33,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Skills public Stamina(Mod[] mods, bool singleColourStamina, bool isConvert) : base(mods) { - this.singleColourStamina = singleColourStamina; + SingleColourStamina = singleColourStamina; this.isConvert = isConvert; } @@ -50,12 +50,12 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Skills double monolengthBonus = isConvert ? 1 : 1 + Math.Min(Math.Max((index - 5) / 50.0, 0), 0.30); - if (singleColourStamina) + if (SingleColourStamina) return DifficultyCalculationUtils.Logistic(-(index - 10) / 2.0, currentStrain); return currentStrain * monolengthBonus; } - protected override double CalculateInitialStrain(double time, DifficultyHitObject current) => singleColourStamina ? 0 : currentStrain * strainDecay(time - current.Previous(0).StartTime); + protected override double CalculateInitialStrain(double time, DifficultyHitObject current) => SingleColourStamina ? 0 : currentStrain * strainDecay(time - current.Previous(0).StartTime); } } diff --git a/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs b/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs index b1dcf2d7a0..bcd26a06bc 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs @@ -109,11 +109,11 @@ namespace osu.Game.Rulesets.Taiko.Difficulty bool isRelax = mods.Any(h => h is TaikoModRelax); - Rhythm rhythm = (Rhythm)skills.First(x => x is Rhythm); - Reading reading = (Reading)skills.First(x => x is Reading); - Colour colour = (Colour)skills.First(x => x is Colour); - Stamina stamina = (Stamina)skills.First(x => x is Stamina); - Stamina singleColourStamina = (Stamina)skills.Last(x => x is Stamina); + var rhythm = skills.OfType().Single(); + var reading = skills.OfType().Single(); + var colour = skills.OfType().Single(); + var stamina = skills.OfType().Single(s => !s.SingleColourStamina); + var singleColourStamina = skills.OfType().Single(s => s.SingleColourStamina); double rhythmRating = rhythm.DifficultyValue() * rhythm_skill_multiplier; double readingRating = reading.DifficultyValue() * reading_skill_multiplier; From a77dfb106834e8818574e81b1d7880d38c0e929b Mon Sep 17 00:00:00 2001 From: James Wilson Date: Mon, 20 Jan 2025 12:04:31 +0000 Subject: [PATCH 0666/1275] Use correct `HitWindows` class for osu!taiko hit windows in difficulty calculator (#31579) * Use correct `HitWindows` class for osu!taiko hit windows in difficulty calculator * Remove redundant (and incorrect) hit window creation * Balance rhythm against hit window changes --- .../Difficulty/Evaluators/RhythmEvaluator.cs | 2 +- .../Difficulty/TaikoDifficultyCalculator.cs | 5 +---- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Evaluators/RhythmEvaluator.cs b/osu.Game.Rulesets.Taiko/Difficulty/Evaluators/RhythmEvaluator.cs index 7d58eada5e..e7d82453eb 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Evaluators/RhythmEvaluator.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Evaluators/RhythmEvaluator.cs @@ -120,7 +120,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Evaluators intervalDifficulty *= DifficultyCalculationUtils.Logistic( durationDifference / hitWindow, midpointOffset: 0.7, - multiplier: 1.5, + multiplier: 1.0, maxValue: 1); } } diff --git a/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs b/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs index bcd26a06bc..f3b976f970 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs @@ -43,7 +43,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty protected override Skill[] CreateSkills(IBeatmap beatmap, Mod[] mods, double clockRate) { - HitWindows hitWindows = new HitWindows(); + HitWindows hitWindows = new TaikoHitWindows(); hitWindows.SetDifficulty(beatmap.Difficulty.OverallDifficulty); isConvert = beatmap.BeatmapInfo.Ruleset.OnlineID == 0; @@ -68,9 +68,6 @@ namespace osu.Game.Rulesets.Taiko.Difficulty protected override IEnumerable CreateDifficultyHitObjects(IBeatmap beatmap, double clockRate) { - var hitWindows = new HitWindows(); - hitWindows.SetDifficulty(beatmap.Difficulty.OverallDifficulty); - var difficultyHitObjects = new List(); var centreObjects = new List(); var rimObjects = new List(); From 89586d5ab25cb7108ed71d7c516debf9950f60cf Mon Sep 17 00:00:00 2001 From: Rudi Herouard Date: Mon, 20 Jan 2025 13:43:45 +0100 Subject: [PATCH 0667/1275] Fix settings in replay hiding when dragging a slider --- osu.Game/Screens/Play/HUD/PlayerSettingsOverlay.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/osu.Game/Screens/Play/HUD/PlayerSettingsOverlay.cs b/osu.Game/Screens/Play/HUD/PlayerSettingsOverlay.cs index 668c74e0c2..b285b1b799 100644 --- a/osu.Game/Screens/Play/HUD/PlayerSettingsOverlay.cs +++ b/osu.Game/Screens/Play/HUD/PlayerSettingsOverlay.cs @@ -122,7 +122,10 @@ namespace osu.Game.Screens.Play.HUD { float screenMouseX = inputManager.CurrentState.Mouse.Position.X; - Expanded.Value = screenMouseX >= button.ScreenSpaceDrawQuad.TopLeft.X && screenMouseX <= ToScreenSpace(new Vector2(DrawWidth + EXPANDED_WIDTH, 0)).X; + Expanded.Value = + (screenMouseX >= button.ScreenSpaceDrawQuad.TopLeft.X && screenMouseX <= ToScreenSpace(new Vector2(DrawWidth + EXPANDED_WIDTH, 0)).X) + // Stay expanded if the user is dragging a slider. + || inputManager.DraggedDrawable != null; } protected override void OnHoverLost(HoverLostEvent e) From 6b524aba60e2474b9faa281f299fb4ee365fd974 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sat, 4 Jan 2025 14:27:48 +0900 Subject: [PATCH 0668/1275] Enable sentry caching to avoid sentry writing outside of game directory See https://github.com/ppy/osu/discussions/31412. Probably safe enough. --- osu.Game/OsuGame.cs | 8 ++++++-- osu.Game/Utils/SentryLogger.cs | 4 +++- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs index 40d13ae0b7..47e301c4e4 100644 --- a/osu.Game/OsuGame.cs +++ b/osu.Game/OsuGame.cs @@ -233,8 +233,6 @@ namespace osu.Game forwardGeneralLogsToNotifications(); forwardTabletLogsToNotifications(); - - SentryLogger = new SentryLogger(this); } #region IOverlayManager @@ -320,6 +318,12 @@ namespace osu.Game private readonly List dragDropFiles = new List(); private ScheduledDelegate dragDropImportSchedule; + public override void SetupLogging(Storage gameStorage, Storage cacheStorage) + { + base.SetupLogging(gameStorage, cacheStorage); + SentryLogger = new SentryLogger(this, cacheStorage); + } + public override void SetHost(GameHost host) { base.SetHost(host); diff --git a/osu.Game/Utils/SentryLogger.cs b/osu.Game/Utils/SentryLogger.cs index 8d3e5fb834..ed644bf5cb 100644 --- a/osu.Game/Utils/SentryLogger.cs +++ b/osu.Game/Utils/SentryLogger.cs @@ -11,6 +11,7 @@ using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Configuration; using osu.Framework.Logging; +using osu.Framework.Platform; using osu.Framework.Statistics; using osu.Game.Beatmaps; using osu.Game.Configuration; @@ -36,7 +37,7 @@ namespace osu.Game.Utils private readonly OsuGame game; - public SentryLogger(OsuGame game) + public SentryLogger(OsuGame game, Storage? storage = null) { this.game = game; @@ -49,6 +50,7 @@ namespace osu.Game.Utils options.AutoSessionTracking = true; options.IsEnvironmentUser = false; options.IsGlobalModeEnabled = true; + options.CacheDirectoryPath = storage?.GetFullPath(string.Empty); // The reported release needs to match version as reported to Sentry in .github/workflows/sentry-release.yml options.Release = $"osu@{game.Version.Replace($@"-{OsuGameBase.BUILD_SUFFIX}", string.Empty)}"; }); From c8b05ce114a00e9123ba5b3ac8930f1fafde88a9 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 21 Jan 2025 13:40:55 +0900 Subject: [PATCH 0669/1275] Tidy up code quality of `RhythmEvaluator` --- .../Difficulty/Evaluators/RhythmEvaluator.cs | 149 ++++++++---------- 1 file changed, 68 insertions(+), 81 deletions(-) diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Evaluators/RhythmEvaluator.cs b/osu.Game.Rulesets.Taiko/Difficulty/Evaluators/RhythmEvaluator.cs index e7d82453eb..22321a8f6e 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Evaluators/RhythmEvaluator.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Evaluators/RhythmEvaluator.cs @@ -14,48 +14,64 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Evaluators public class RhythmEvaluator { /// - /// Multiplier for a given denominator term. + /// Evaluate the difficulty of a hitobject considering its interval change. /// - private static double termPenalty(double ratio, int denominator, double power, double multiplier) + public static double EvaluateDifficultyOf(DifficultyHitObject hitObject, double hitWindow) { - return -multiplier * Math.Pow(Math.Cos(denominator * Math.PI * ratio), power); - } + TaikoDifficultyHitObjectRhythm rhythm = ((TaikoDifficultyHitObject)hitObject).Rhythm; + double difficulty = 0.0d; - /// - /// Validates the ratio by ensuring it is a normal number in cases where maps breach regular mapping conditions. - /// - private static double validateRatio(double ratio) - { - return double.IsNormal(ratio) ? ratio : 0; - } + double sameRhythm = 0; + double samePattern = 0; + double intervalPenalty = 0; - /// - /// Calculates the difficulty of a given ratio using a combination of periodic penalties and bonuses. - /// - private static double ratioDifficulty(double ratio, int terms = 8) - { - double difficulty = 0; - ratio = validateRatio(ratio); - - for (int i = 1; i <= terms; ++i) + if (rhythm.SameRhythmHitObjects?.FirstHitObject == hitObject) // Difficulty for SameRhythmHitObjects { - difficulty += termPenalty(ratio, i, 4, 1); + sameRhythm += 10.0 * evaluateDifficultyOf(rhythm.SameRhythmHitObjects, hitWindow); + intervalPenalty = repeatedIntervalPenalty(rhythm.SameRhythmHitObjects, hitWindow); } - difficulty += terms / (1 + ratio); + if (rhythm.SamePatterns?.FirstHitObject == hitObject) // Difficulty for SamePatterns + samePattern += 1.15 * ratioDifficulty(rhythm.SamePatterns.IntervalRatio); - // Give bonus to near-1 ratios - difficulty += DifficultyCalculationUtils.BellCurve(ratio, 1, 0.5); - - // Penalize ratios that are VERY near 1 - difficulty -= DifficultyCalculationUtils.BellCurve(ratio, 1, 0.3); - - difficulty = Math.Max(difficulty, 0); - difficulty /= Math.Sqrt(8); + difficulty += Math.Max(sameRhythm, samePattern) * intervalPenalty; return difficulty; } + private static double evaluateDifficultyOf(SameRhythmHitObjects sameRhythmHitObjects, double hitWindow) + { + double intervalDifficulty = ratioDifficulty(sameRhythmHitObjects.HitObjectIntervalRatio); + double? previousInterval = sameRhythmHitObjects.Previous?.HitObjectInterval; + + intervalDifficulty *= repeatedIntervalPenalty(sameRhythmHitObjects, hitWindow); + + // If a previous interval exists and there are multiple hit objects in the sequence: + if (previousInterval != null && sameRhythmHitObjects.Children.Count > 1) + { + double expectedDurationFromPrevious = (double)previousInterval * sameRhythmHitObjects.Children.Count; + double durationDifference = sameRhythmHitObjects.Duration - expectedDurationFromPrevious; + + if (durationDifference > 0) + { + intervalDifficulty *= DifficultyCalculationUtils.Logistic( + durationDifference / hitWindow, + midpointOffset: 0.7, + multiplier: 1.0, + maxValue: 1); + } + } + + // Penalise patterns that can be hit within a single hit window. + intervalDifficulty *= DifficultyCalculationUtils.Logistic( + sameRhythmHitObjects.Duration / hitWindow, + midpointOffset: 0.6, + multiplier: 1, + maxValue: 1); + + return Math.Pow(intervalDifficulty, 0.75); + } + /// /// Determines if the changes in hit object intervals is consistent based on a given threshold. /// @@ -102,68 +118,39 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Evaluators } } - private static double evaluateDifficultyOf(SameRhythmHitObjects sameRhythmHitObjects, double hitWindow) - { - double intervalDifficulty = ratioDifficulty(sameRhythmHitObjects.HitObjectIntervalRatio); - double? previousInterval = sameRhythmHitObjects.Previous?.HitObjectInterval; - - intervalDifficulty *= repeatedIntervalPenalty(sameRhythmHitObjects, hitWindow); - - // If a previous interval exists and there are multiple hit objects in the sequence: - if (previousInterval != null && sameRhythmHitObjects.Children.Count > 1) - { - double expectedDurationFromPrevious = (double)previousInterval * sameRhythmHitObjects.Children.Count; - double durationDifference = sameRhythmHitObjects.Duration - expectedDurationFromPrevious; - - if (durationDifference > 0) - { - intervalDifficulty *= DifficultyCalculationUtils.Logistic( - durationDifference / hitWindow, - midpointOffset: 0.7, - multiplier: 1.0, - maxValue: 1); - } - } - - // Penalise patterns that can be hit within a single hit window. - intervalDifficulty *= DifficultyCalculationUtils.Logistic( - sameRhythmHitObjects.Duration / hitWindow, - midpointOffset: 0.6, - multiplier: 1, - maxValue: 1); - - return Math.Pow(intervalDifficulty, 0.75); - } - - private static double evaluateDifficultyOf(SamePatterns samePatterns) - { - return ratioDifficulty(samePatterns.IntervalRatio); - } - /// - /// Evaluate the difficulty of a hitobject considering its interval change. + /// Calculates the difficulty of a given ratio using a combination of periodic penalties and bonuses. /// - public static double EvaluateDifficultyOf(DifficultyHitObject hitObject, double hitWindow) + private static double ratioDifficulty(double ratio, int terms = 8) { - TaikoDifficultyHitObjectRhythm rhythm = ((TaikoDifficultyHitObject)hitObject).Rhythm; - double difficulty = 0.0d; + double difficulty = 0; - double sameRhythm = 0; - double samePattern = 0; - double intervalPenalty = 0; + // Validate the ratio by ensuring it is a normal number in cases where maps breach regular mapping conditions. + ratio = double.IsNormal(ratio) ? ratio : 0; - if (rhythm.SameRhythmHitObjects?.FirstHitObject == hitObject) // Difficulty for SameRhythmHitObjects + for (int i = 1; i <= terms; ++i) { - sameRhythm += 10.0 * evaluateDifficultyOf(rhythm.SameRhythmHitObjects, hitWindow); - intervalPenalty = repeatedIntervalPenalty(rhythm.SameRhythmHitObjects, hitWindow); + difficulty += termPenalty(ratio, i, 4, 1); } - if (rhythm.SamePatterns?.FirstHitObject == hitObject) // Difficulty for SamePatterns - samePattern += 1.15 * evaluateDifficultyOf(rhythm.SamePatterns); + difficulty += terms / (1 + ratio); - difficulty += Math.Max(sameRhythm, samePattern) * intervalPenalty; + // Give bonus to near-1 ratios + difficulty += DifficultyCalculationUtils.BellCurve(ratio, 1, 0.5); + + // Penalize ratios that are VERY near 1 + difficulty -= DifficultyCalculationUtils.BellCurve(ratio, 1, 0.3); + + difficulty = Math.Max(difficulty, 0); + difficulty /= Math.Sqrt(8); return difficulty; } + + /// + /// Multiplier for a given denominator term. + /// + private static double termPenalty(double ratio, int denominator, double power, double multiplier) => + -multiplier * Math.Pow(Math.Cos(denominator * Math.PI * ratio), power); } } From 46ff9d1aad2d70616114a6b6075b1bdbe6a8f0f2 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 21 Jan 2025 14:13:42 +0900 Subject: [PATCH 0670/1275] Fix beat snap grid being lines not being corectly centered to time This was pointed out as an issue in the osu!taiko editor, but actually affects all rulesets. Has now been fixed everywhere. --- Closes https://github.com/ppy/osu/issues/31548. osu!mania could arguable be consdiered "more correct" with the old display, but I don't think it's a huge deal either way (subjective at best). --- .../Edit/Compose/Components/BeatSnapGrid.cs | 25 ++++++++++++++++--- 1 file changed, 22 insertions(+), 3 deletions(-) diff --git a/osu.Game/Screens/Edit/Compose/Components/BeatSnapGrid.cs b/osu.Game/Screens/Edit/Compose/Components/BeatSnapGrid.cs index 766d5b5601..f1b7951999 100644 --- a/osu.Game/Screens/Edit/Compose/Components/BeatSnapGrid.cs +++ b/osu.Game/Screens/Edit/Compose/Components/BeatSnapGrid.cs @@ -185,9 +185,28 @@ namespace osu.Game.Screens.Edit.Compose.Components private void onDirectionChanged(ValueChangedEvent direction) { - Origin = Anchor = direction.NewValue == ScrollingDirection.Up - ? Anchor.TopLeft - : Anchor.BottomLeft; + switch (direction.NewValue) + { + case ScrollingDirection.Up: + Anchor = Anchor.TopLeft; + Origin = Anchor.CentreLeft; + break; + + case ScrollingDirection.Down: + Anchor = Anchor.BottomLeft; + Origin = Anchor.CentreLeft; + break; + + case ScrollingDirection.Left: + Anchor = Anchor.TopLeft; + Origin = Anchor.TopCentre; + break; + + case ScrollingDirection.Right: + Anchor = Anchor.TopRight; + Origin = Anchor.TopCentre; + break; + } bool isHorizontal = direction.NewValue == ScrollingDirection.Left || direction.NewValue == ScrollingDirection.Right; From f13304293603b49b304d6acf66f2941310943064 Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Tue, 21 Jan 2025 01:14:18 -0500 Subject: [PATCH 0671/1275] Fix silly mistake --- .../Overlays/Settings/Sections/Maintenance/GeneralSettings.cs | 2 +- .../Sections/Maintenance/SystemFileImportComponent.cs | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Game/Overlays/Settings/Sections/Maintenance/GeneralSettings.cs b/osu.Game/Overlays/Settings/Sections/Maintenance/GeneralSettings.cs index ed3e72adbe..99b25808a1 100644 --- a/osu.Game/Overlays/Settings/Sections/Maintenance/GeneralSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/Maintenance/GeneralSettings.cs @@ -20,7 +20,7 @@ namespace osu.Game.Overlays.Settings.Sections.Maintenance private SystemFileImportComponent systemFileImport = null!; [BackgroundDependencyLoader] - private void load(OsuGame game, GameHost host, IPerformFromScreenRunner? performer) + private void load(OsuGameBase game, GameHost host, IPerformFromScreenRunner? performer) { Add(systemFileImport = new SystemFileImportComponent(game, host)); diff --git a/osu.Game/Overlays/Settings/Sections/Maintenance/SystemFileImportComponent.cs b/osu.Game/Overlays/Settings/Sections/Maintenance/SystemFileImportComponent.cs index 9827872702..ded8c81891 100644 --- a/osu.Game/Overlays/Settings/Sections/Maintenance/SystemFileImportComponent.cs +++ b/osu.Game/Overlays/Settings/Sections/Maintenance/SystemFileImportComponent.cs @@ -10,12 +10,12 @@ namespace osu.Game.Overlays.Settings.Sections.Maintenance { public partial class SystemFileImportComponent : Component { - private readonly OsuGame game; + private readonly OsuGameBase game; private readonly GameHost host; private ISystemFileSelector? selector; - public SystemFileImportComponent(OsuGame game, GameHost host) + public SystemFileImportComponent(OsuGameBase game, GameHost host) { this.game = game; this.host = host; From a7c9f84a93fd285c58a914615f40380a454a6884 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 21 Jan 2025 15:14:39 +0900 Subject: [PATCH 0672/1275] Adjust visuals slightly --- .../Screens/Edit/Timing/MetronomeDisplay.cs | 29 ++++++++++++++----- 1 file changed, 21 insertions(+), 8 deletions(-) diff --git a/osu.Game/Screens/Edit/Timing/MetronomeDisplay.cs b/osu.Game/Screens/Edit/Timing/MetronomeDisplay.cs index 8a4f1c01b1..f3bd9ff257 100644 --- a/osu.Game/Screens/Edit/Timing/MetronomeDisplay.cs +++ b/osu.Game/Screens/Edit/Timing/MetronomeDisplay.cs @@ -17,9 +17,10 @@ using osu.Framework.Threading; using osu.Framework.Timing; using osu.Framework.Utils; using osu.Game.Beatmaps.ControlPoints; +using osu.Game.Graphics; using osu.Game.Graphics.Containers; -using osu.Game.Graphics.Sprites; using osu.Game.Overlays; +using osu.Game.Utils; using osuTK; namespace osu.Game.Screens.Edit.Timing @@ -28,7 +29,7 @@ namespace osu.Game.Screens.Edit.Timing { private Container swing = null!; - private OsuSpriteText bpmText = null!; + private OsuTextFlowContainer bpmText = null!; private Drawable weight = null!; private Drawable stick = null!; @@ -213,10 +214,15 @@ namespace osu.Game.Screens.Edit.Timing }, } }, - bpmText = new OsuSpriteText + bpmText = new OsuTextFlowContainer(st => + { + st.Font = OsuFont.Default.With(fixedWidth: true); + st.Spacing = new Vector2(-2.2f, 0); + }) { Name = @"BPM display", Colour = overlayColourProvider.Content1, + AutoSizeAxes = Axes.Both, Anchor = Anchor.BottomCentre, Origin = Anchor.BottomCentre, Y = -3, @@ -262,13 +268,20 @@ namespace osu.Game.Screens.Edit.Timing private void updateBpmText() { + int intPart = (int)interpolatedBpm.Value; + + bpmText.Text = intPart.ToLocalisableString(); + // While interpolating between two integer values, showing the decimal places would look a bit odd // so rounding is applied until we're close to the final value. - double bpm = Precision.AlmostEquals(interpolatedBpm.Value, effectiveBpm, 1.0) - ? effectiveBpm - : Math.Round(interpolatedBpm.Value); + int decimalPlaces = FormatUtils.FindPrecision((decimal)effectiveBpm); - bpmText.Text = bpm.ToLocalisableString("0.##"); + if (decimalPlaces > 0) + { + bool reachedFinalNumber = intPart == (int)effectiveBpm; + + bpmText.AddText((effectiveBpm % 1).ToLocalisableString("." + new string('0', decimalPlaces)), cp => cp.Alpha = reachedFinalNumber ? 0.5f : 0.1f); + } } protected override void Update() @@ -294,7 +307,7 @@ namespace osu.Game.Screens.Edit.Timing weight.MoveToY((float)Interpolation.Lerp(0.1f, 0.83f, bpmRatio), 600, Easing.OutQuint); - this.TransformBindableTo(interpolatedBpm, effectiveBpm, 600, Easing.OutQuint); + this.TransformBindableTo(interpolatedBpm, effectiveBpm, 300, Easing.OutExpo); } if (!BeatSyncSource.Clock.IsRunning && isSwinging) From 3f51626f07cd76c332518e277000f9931cd4ccee Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Tue, 21 Jan 2025 02:20:48 -0500 Subject: [PATCH 0673/1275] Simplify code immensely Co-authored-by: Dean Herbert --- .../Sections/Maintenance/GeneralSettings.cs | 15 +++--- .../Maintenance/SystemFileImportComponent.cs | 51 ------------------- 2 files changed, 9 insertions(+), 57 deletions(-) delete mode 100644 osu.Game/Overlays/Settings/Sections/Maintenance/SystemFileImportComponent.cs diff --git a/osu.Game/Overlays/Settings/Sections/Maintenance/GeneralSettings.cs b/osu.Game/Overlays/Settings/Sections/Maintenance/GeneralSettings.cs index 99b25808a1..47314dcafe 100644 --- a/osu.Game/Overlays/Settings/Sections/Maintenance/GeneralSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/Maintenance/GeneralSettings.cs @@ -1,6 +1,8 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System.Linq; +using System.Threading.Tasks; using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Localisation; @@ -17,12 +19,13 @@ namespace osu.Game.Overlays.Settings.Sections.Maintenance { protected override LocalisableString Header => CommonStrings.General; - private SystemFileImportComponent systemFileImport = null!; + private ISystemFileSelector? selector; [BackgroundDependencyLoader] private void load(OsuGameBase game, GameHost host, IPerformFromScreenRunner? performer) { - Add(systemFileImport = new SystemFileImportComponent(game, host)); + if ((selector = host.CreateSystemFileSelector(game.HandledExtensions.ToArray())) != null) + selector.Selected += f => Task.Run(() => game.Import(f.FullName)); AddRange(new Drawable[] { @@ -31,10 +34,10 @@ namespace osu.Game.Overlays.Settings.Sections.Maintenance Text = DebugSettingsStrings.ImportFiles, Action = () => { - if (systemFileImport.PresentIfAvailable()) - return; - - performer?.PerformFromScreen(menu => menu.Push(new FileImportScreen())); + if (selector != null) + selector.Present(); + else + performer?.PerformFromScreen(menu => menu.Push(new FileImportScreen())); }, }, new SettingsButton diff --git a/osu.Game/Overlays/Settings/Sections/Maintenance/SystemFileImportComponent.cs b/osu.Game/Overlays/Settings/Sections/Maintenance/SystemFileImportComponent.cs deleted file mode 100644 index ded8c81891..0000000000 --- a/osu.Game/Overlays/Settings/Sections/Maintenance/SystemFileImportComponent.cs +++ /dev/null @@ -1,51 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -using System.Linq; -using System.Threading.Tasks; -using osu.Framework.Graphics; -using osu.Framework.Platform; - -namespace osu.Game.Overlays.Settings.Sections.Maintenance -{ - public partial class SystemFileImportComponent : Component - { - private readonly OsuGameBase game; - private readonly GameHost host; - - private ISystemFileSelector? selector; - - public SystemFileImportComponent(OsuGameBase game, GameHost host) - { - this.game = game; - this.host = host; - } - - protected override void LoadComplete() - { - base.LoadComplete(); - - selector = host.CreateSystemFileSelector(game.HandledExtensions.ToArray()); - - if (selector != null) - selector.Selected += f => Schedule(() => startImport(f.FullName)); - } - - public bool PresentIfAvailable() - { - if (selector == null) - return false; - - selector.Present(); - return true; - } - - private void startImport(string path) - { - Task.Factory.StartNew(async () => - { - await game.Import(path).ConfigureAwait(false); - }, TaskCreationOptions.LongRunning); - } - } -} From 3a37817ab20bc1add6534b4a077f56619ead6dcc Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 20 Jan 2025 19:01:33 +0900 Subject: [PATCH 0674/1275] Don't block `Popover` escape handling (just let it work in addition to `GlobalAction.Back`) --- osu.Game/Graphics/UserInterfaceV2/OsuPopover.cs | 9 --------- 1 file changed, 9 deletions(-) diff --git a/osu.Game/Graphics/UserInterfaceV2/OsuPopover.cs b/osu.Game/Graphics/UserInterfaceV2/OsuPopover.cs index 9b4689958c..7abaca4092 100644 --- a/osu.Game/Graphics/UserInterfaceV2/OsuPopover.cs +++ b/osu.Game/Graphics/UserInterfaceV2/OsuPopover.cs @@ -14,7 +14,6 @@ using osu.Framework.Input.Events; using osu.Game.Input.Bindings; using osu.Game.Overlays; using osuTK; -using osuTK.Input; namespace osu.Game.Graphics.UserInterfaceV2 { @@ -75,14 +74,6 @@ namespace osu.Game.Graphics.UserInterfaceV2 samplePopOut?.Play(); } - protected override bool OnKeyDown(KeyDownEvent e) - { - if (e.Key == Key.Escape) - return false; // disable the framework-level handling of escape key for conformity (we use GlobalAction.Back). - - return base.OnKeyDown(e); - } - public virtual bool OnPressed(KeyBindingPressEvent e) { if (e.Repeat) From 9a12f48dcc2ccae6889f35a4add888c4112babd9 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 20 Jan 2025 18:55:42 +0900 Subject: [PATCH 0675/1275] Fix `ComposeBlueprintContainer` handling nudge keys when it can't nudge --- .../Components/ComposeBlueprintContainer.cs | 22 ++++++++++--------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/osu.Game/Screens/Edit/Compose/Components/ComposeBlueprintContainer.cs b/osu.Game/Screens/Edit/Compose/Components/ComposeBlueprintContainer.cs index 5d93c4ea9d..15bbddd97e 100644 --- a/osu.Game/Screens/Edit/Compose/Components/ComposeBlueprintContainer.cs +++ b/osu.Game/Screens/Edit/Compose/Components/ComposeBlueprintContainer.cs @@ -111,25 +111,26 @@ namespace osu.Game.Screens.Edit.Compose.Components protected override bool OnKeyDown(KeyDownEvent e) { + // Until the keys below are global actions, this will prevent conflicts with "seek between sample points" + // which has a default of ctrl+shift+arrows. + if (e.ShiftPressed) + return false; + if (e.ControlPressed) { switch (e.Key) { case Key.Left: - nudgeSelection(new Vector2(-1, 0)); - return true; + return nudgeSelection(new Vector2(-1, 0)); case Key.Right: - nudgeSelection(new Vector2(1, 0)); - return true; + return nudgeSelection(new Vector2(1, 0)); case Key.Up: - nudgeSelection(new Vector2(0, -1)); - return true; + return nudgeSelection(new Vector2(0, -1)); case Key.Down: - nudgeSelection(new Vector2(0, 1)); - return true; + return nudgeSelection(new Vector2(0, 1)); } } @@ -151,7 +152,7 @@ namespace osu.Game.Screens.Edit.Compose.Components /// Move the current selection spatially by the specified delta, in gamefield coordinates (ie. the same coordinates as the blueprints). /// /// - private void nudgeSelection(Vector2 delta) + private bool nudgeSelection(Vector2 delta) { if (!nudgeMovementActive) { @@ -162,12 +163,13 @@ namespace osu.Game.Screens.Edit.Compose.Components var firstBlueprint = SelectionHandler.SelectedBlueprints.FirstOrDefault(); if (firstBlueprint == null) - return; + return false; // convert to game space coordinates delta = firstBlueprint.ToScreenSpace(delta) - firstBlueprint.ToScreenSpace(Vector2.Zero); SelectionHandler.HandleMovement(new MoveSelectionEvent(firstBlueprint, delta)); + return true; } private void updatePlacementNewCombo() From aeca91cde28d29a82a4d159f7f93f9e2251b4b47 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 20 Jan 2025 18:55:28 +0900 Subject: [PATCH 0676/1275] Fix main menu osu logo being activated by function keys and escape --- osu.Game/Screens/Menu/ButtonSystem.cs | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/osu.Game/Screens/Menu/ButtonSystem.cs b/osu.Game/Screens/Menu/ButtonSystem.cs index 41920605b0..25fa689d4c 100644 --- a/osu.Game/Screens/Menu/ButtonSystem.cs +++ b/osu.Game/Screens/Menu/ButtonSystem.cs @@ -245,6 +245,15 @@ namespace osu.Game.Screens.Menu if (e.Repeat || e.ControlPressed || e.ShiftPressed || e.AltPressed || e.SuperPressed) return false; + if (e.Key >= Key.F1 && e.Key <= Key.F35) + return false; + + switch (e.Key) + { + case Key.Escape: + return false; + } + if (triggerInitialOsuLogo()) return true; From b6e7b43b11859046b71c0023e4bfca090cd2f961 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 20 Jan 2025 18:52:18 +0900 Subject: [PATCH 0677/1275] Remove unnecessary input blocking This was already done by `OverlayContainer`. --- osu.Game/Screens/Play/GameplayMenuOverlay.cs | 5 ----- 1 file changed, 5 deletions(-) diff --git a/osu.Game/Screens/Play/GameplayMenuOverlay.cs b/osu.Game/Screens/Play/GameplayMenuOverlay.cs index 2b961278d5..ffd7845356 100644 --- a/osu.Game/Screens/Play/GameplayMenuOverlay.cs +++ b/osu.Game/Screens/Play/GameplayMenuOverlay.cs @@ -166,11 +166,6 @@ namespace osu.Game.Screens.Play protected override void PopOut() => this.FadeOut(TRANSITION_DURATION, Easing.In); - // Don't let mouse down events through the overlay or people can click circles while paused. - protected override bool OnMouseDown(MouseDownEvent e) => true; - - protected override bool OnMouseMove(MouseMoveEvent e) => true; - protected void AddButton(LocalisableString text, Color4 colour, Action? action) { var button = new Button From c8cc36e9af69c551a6149b12ed376fa84f1ac32d Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 21 Jan 2025 17:24:38 +0900 Subject: [PATCH 0678/1275] Add failing test coverage of random rewind button not working --- .../Navigation/TestSceneScreenNavigation.cs | 33 +++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs b/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs index 521d097fb9..88b482ab4c 100644 --- a/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs +++ b/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs @@ -48,6 +48,7 @@ using osu.Game.Screens.Select.Carousel; using osu.Game.Screens.Select.Leaderboards; using osu.Game.Screens.Select.Options; using osu.Game.Tests.Beatmaps.IO; +using osu.Game.Tests.Resources; using osu.Game.Utils; using osuTK; using osuTK.Input; @@ -202,6 +203,38 @@ namespace osu.Game.Tests.Visual.Navigation TextBox filterControlTextBox() => songSelect.ChildrenOfType().Single(); } + [Test] + public void TestSongSelectRandomRewindButton() + { + Guid? originalSelection = null; + TestPlaySongSelect songSelect = null; + + PushAndConfirm(() => songSelect = new TestPlaySongSelect()); + AddUntilStep("wait for song select", () => songSelect.BeatmapSetsLoaded); + + AddStep("Add two beatmaps", () => + { + Game.BeatmapManager.Import(TestResources.CreateTestBeatmapSetInfo(8)); + Game.BeatmapManager.Import(TestResources.CreateTestBeatmapSetInfo(8)); + }); + + AddUntilStep("wait for selected", () => + { + originalSelection = Game.Beatmap.Value.BeatmapInfo.ID; + return !Game.Beatmap.IsDefault; + }); + + AddStep("hit random", () => + { + InputManager.MoveMouseTo(Game.ChildrenOfType().Single()); + InputManager.Click(MouseButton.Left); + }); + AddUntilStep("wait for selection changed", () => Game.Beatmap.Value.BeatmapInfo.ID, () => Is.Not.EqualTo(originalSelection)); + + AddStep("hit random rewind", () => InputManager.Click(MouseButton.Right)); + AddUntilStep("wait for selection reverted", () => Game.Beatmap.Value.BeatmapInfo.ID, () => Is.EqualTo(originalSelection)); + } + [Test] public void TestSongSelectScrollHandling() { From 66be9f2d1b9908001baacf24329bdba585a8ac3e Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 21 Jan 2025 17:05:39 +0900 Subject: [PATCH 0679/1275] Remove right click default for absolute scroll --- osu.Game/Database/RealmAccess.cs | 14 +++++++++++++- osu.Game/Input/Bindings/GlobalActionContainer.cs | 2 +- 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/osu.Game/Database/RealmAccess.cs b/osu.Game/Database/RealmAccess.cs index e1b8de89fa..f0f5864e32 100644 --- a/osu.Game/Database/RealmAccess.cs +++ b/osu.Game/Database/RealmAccess.cs @@ -97,8 +97,9 @@ namespace osu.Game.Database /// 44 2024-11-22 Removed several properties from BeatmapInfo which did not need to be persisted to realm. /// 45 2024-12-23 Change beat snap divisor adjust defaults to be Ctrl+Scroll instead of Ctrl+Shift+Scroll, if not already changed by user. /// 46 2024-12-26 Change beat snap divisor bindings to match stable directionality ¯\_(ツ)_/¯. + /// 47 2025-01-21 Remove right mouse button binding for absolute scroll. Never use mouse buttons (or scroll) for global actions. /// - private const int schema_version = 46; + private const int schema_version = 47; /// /// Lock object which is held during sections, blocking realm retrieval during blocking periods. @@ -1239,6 +1240,17 @@ namespace osu.Game.Database break; } + + case 47: + { + var keyBindings = migration.NewRealm.All(); + + var existingBinding = keyBindings.FirstOrDefault(k => k.ActionInt == (int)GlobalAction.AbsoluteScrollSongList); + if (existingBinding != null && existingBinding.KeyCombination.Keys.SequenceEqual(new[] { InputKey.MouseRight })) + migration.NewRealm.Remove(existingBinding); + + break; + } } Logger.Log($"Migration completed in {stopwatch.ElapsedMilliseconds}ms"); diff --git a/osu.Game/Input/Bindings/GlobalActionContainer.cs b/osu.Game/Input/Bindings/GlobalActionContainer.cs index 6c130ff309..599ca6d6c1 100644 --- a/osu.Game/Input/Bindings/GlobalActionContainer.cs +++ b/osu.Game/Input/Bindings/GlobalActionContainer.cs @@ -205,7 +205,7 @@ namespace osu.Game.Input.Bindings new KeyBinding(InputKey.BackSpace, GlobalAction.DeselectAllMods), new KeyBinding(new[] { InputKey.Control, InputKey.Up }, GlobalAction.IncreaseModSpeed), new KeyBinding(new[] { InputKey.Control, InputKey.Down }, GlobalAction.DecreaseModSpeed), - new KeyBinding(new[] { InputKey.MouseRight }, GlobalAction.AbsoluteScrollSongList), + new KeyBinding(InputKey.None, GlobalAction.AbsoluteScrollSongList), }; private static IEnumerable audioControlKeyBindings => new[] From 6c27e87714ec959d017a2c198b095ea5bfdbb08e Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 21 Jan 2025 17:12:45 +0900 Subject: [PATCH 0680/1275] Add back explicit right click handling of carousel absolute scrolling --- osu.Game/Screens/Select/BeatmapCarousel.cs | 40 ++++++++++++++++----- osu.Game/Screens/SelectV2/Carousel.cs | 41 ++++++++++++++++------ 2 files changed, 62 insertions(+), 19 deletions(-) diff --git a/osu.Game/Screens/Select/BeatmapCarousel.cs b/osu.Game/Screens/Select/BeatmapCarousel.cs index 7e3c26a1ba..a807fc6a34 100644 --- a/osu.Game/Screens/Select/BeatmapCarousel.cs +++ b/osu.Game/Screens/Select/BeatmapCarousel.cs @@ -1181,14 +1181,7 @@ namespace osu.Game.Screens.Select switch (e.Action) { case GlobalAction.AbsoluteScrollSongList: - // The default binding for absolute scroll is right mouse button. - // To avoid conflicts with context menus, disallow absolute scroll completely if it looks like things will fall over. - if (e.CurrentState.Mouse.Buttons.Contains(MouseButton.Right) - && GetContainingInputManager()!.HoveredDrawables.OfType().Any()) - return false; - - ScrollToAbsolutePosition(e.CurrentState.Mouse.Position); - absoluteScrolling = true; + beginAbsoluteScrolling(e); return true; } @@ -1200,11 +1193,32 @@ namespace osu.Game.Screens.Select switch (e.Action) { case GlobalAction.AbsoluteScrollSongList: - absoluteScrolling = false; + endAbsoluteScrolling(); break; } } + protected override bool OnMouseDown(MouseDownEvent e) + { + if (e.Button == MouseButton.Right) + { + // To avoid conflicts with context menus, disallow absolute scroll if it looks like things will fall over. + if (GetContainingInputManager()!.HoveredDrawables.OfType().Any()) + return false; + + beginAbsoluteScrolling(e); + } + + return base.OnMouseDown(e); + } + + protected override void OnMouseUp(MouseUpEvent e) + { + if (e.Button == MouseButton.Right) + endAbsoluteScrolling(); + base.OnMouseUp(e); + } + protected override bool OnMouseMove(MouseMoveEvent e) { if (absoluteScrolling) @@ -1216,6 +1230,14 @@ namespace osu.Game.Screens.Select return base.OnMouseMove(e); } + private void beginAbsoluteScrolling(UIEvent e) + { + ScrollToAbsolutePosition(e.CurrentState.Mouse.Position); + absoluteScrolling = true; + } + + private void endAbsoluteScrolling() => absoluteScrolling = false; + #endregion protected override ScrollbarContainer CreateScrollbar(Direction direction) diff --git a/osu.Game/Screens/SelectV2/Carousel.cs b/osu.Game/Screens/SelectV2/Carousel.cs index a07022b32f..ec1bf6b7c0 100644 --- a/osu.Game/Screens/SelectV2/Carousel.cs +++ b/osu.Game/Screens/SelectV2/Carousel.cs @@ -493,15 +493,7 @@ namespace osu.Game.Screens.SelectV2 switch (e.Action) { case GlobalAction.AbsoluteScrollSongList: - - // The default binding for absolute scroll is right mouse button. - // To avoid conflicts with context menus, disallow absolute scroll completely if it looks like things will fall over. - if (e.CurrentState.Mouse.Buttons.Contains(MouseButton.Right) - && GetContainingInputManager()!.HoveredDrawables.OfType().Any()) - return false; - - ScrollToAbsolutePosition(e.CurrentState.Mouse.Position); - absoluteScrolling = true; + beginAbsoluteScrolling(e); return true; } @@ -513,11 +505,32 @@ namespace osu.Game.Screens.SelectV2 switch (e.Action) { case GlobalAction.AbsoluteScrollSongList: - absoluteScrolling = false; + endAbsoluteScrolling(); break; } } + protected override bool OnMouseDown(MouseDownEvent e) + { + if (e.Button == MouseButton.Right) + { + // To avoid conflicts with context menus, disallow absolute scroll if it looks like things will fall over. + if (GetContainingInputManager()!.HoveredDrawables.OfType().Any()) + return false; + + beginAbsoluteScrolling(e); + } + + return base.OnMouseDown(e); + } + + protected override void OnMouseUp(MouseUpEvent e) + { + if (e.Button == MouseButton.Right) + endAbsoluteScrolling(); + base.OnMouseUp(e); + } + protected override bool OnMouseMove(MouseMoveEvent e) { if (absoluteScrolling) @@ -529,6 +542,14 @@ namespace osu.Game.Screens.SelectV2 return base.OnMouseMove(e); } + private void beginAbsoluteScrolling(UIEvent e) + { + ScrollToAbsolutePosition(e.CurrentState.Mouse.Position); + absoluteScrolling = true; + } + + private void endAbsoluteScrolling() => absoluteScrolling = false; + #endregion } From 0265a2900050d0c11df6d09a38b970ab4f80b923 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 21 Jan 2025 10:02:16 +0100 Subject: [PATCH 0681/1275] Move bindings to `LoadComplete()` --- osu.Game/Screens/Play/HUD/SpectatorList.cs | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/osu.Game/Screens/Play/HUD/SpectatorList.cs b/osu.Game/Screens/Play/HUD/SpectatorList.cs index 7b6bf6f55e..c784fc298a 100644 --- a/osu.Game/Screens/Play/HUD/SpectatorList.cs +++ b/osu.Game/Screens/Play/HUD/SpectatorList.cs @@ -43,8 +43,14 @@ namespace osu.Game.Screens.Play.HUD private FillFlowContainer spectatorsFlow = null!; private DrawablePool pool = null!; + [Resolved] + private SpectatorClient client { get; set; } = null!; + + [Resolved] + private GameplayState gameplayState { get; set; } = null!; + [BackgroundDependencyLoader] - private void load(OsuColour colours, SpectatorClient client, GameplayState gameplayState) + private void load(OsuColour colours) { AutoSizeAxes = Axes.Y; @@ -73,15 +79,15 @@ namespace osu.Game.Screens.Play.HUD }; HeaderColour.Value = Header.Colour; - - ((IBindableList)Spectators).BindTo(client.WatchingUsers); - ((IBindable)UserPlayingState).BindTo(gameplayState.PlayingState); } protected override void LoadComplete() { base.LoadComplete(); + ((IBindableList)Spectators).BindTo(client.WatchingUsers); + ((IBindable)UserPlayingState).BindTo(gameplayState.PlayingState); + Spectators.BindCollectionChanged(onSpectatorsChanged, true); UserPlayingState.BindValueChanged(_ => updateVisibility()); From f88102610d5272fccc32b5d0a73782b5d0c2d127 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 21 Jan 2025 18:35:56 +0900 Subject: [PATCH 0682/1275] Add tooltips explaining multiplayer mod selection buttons --- osu.Game/Localisation/MultiplayerMatchStrings.cs | 15 +++++++++++++++ .../Screens/OnlinePlay/FooterButtonFreeMods.cs | 3 +++ .../Screens/OnlinePlay/FooterButtonFreeStyle.cs | 3 +++ .../Screens/OnlinePlay/OnlinePlaySongSelect.cs | 7 +++++-- .../Screens/OnlinePlay/OnlinePlayStyleSelect.cs | 2 +- osu.Game/Screens/Select/SongSelect.cs | 2 +- 6 files changed, 28 insertions(+), 4 deletions(-) diff --git a/osu.Game/Localisation/MultiplayerMatchStrings.cs b/osu.Game/Localisation/MultiplayerMatchStrings.cs index 95c7168a09..8c9e76d722 100644 --- a/osu.Game/Localisation/MultiplayerMatchStrings.cs +++ b/osu.Game/Localisation/MultiplayerMatchStrings.cs @@ -24,6 +24,21 @@ namespace osu.Game.Localisation /// public static LocalisableString StartMatchWithCountdown(string humanReadableTime) => new TranslatableString(getKey(@"start_match_width_countdown"), @"Start match in {0}", humanReadableTime); + /// + /// "Choose the mods which all players should play with." + /// + public static LocalisableString RequiredModsButtonTooltip => new TranslatableString(getKey(@"required_mods_button_tooltip"), @"Choose the mods which all players should play with."); + + /// + /// "Each player can choose their preferred mods from a selected list." + /// + public static LocalisableString FreeModsButtonTooltip => new TranslatableString(getKey(@"free_mods_button_tooltip"), @"Each player can choose their preferred mods from a selected list."); + + /// + /// "Each player can choose their preferred difficulty, ruleset and mods." + /// + public static LocalisableString FreestyleButtonTooltip => new TranslatableString(getKey(@"freestyle_button_tooltip"), @"Each player can choose their preferred difficulty, ruleset and mods."); + private static string getKey(string key) => $@"{prefix}:{key}"; } } diff --git a/osu.Game/Screens/OnlinePlay/FooterButtonFreeMods.cs b/osu.Game/Screens/OnlinePlay/FooterButtonFreeMods.cs index 952b15a873..402f538716 100644 --- a/osu.Game/Screens/OnlinePlay/FooterButtonFreeMods.cs +++ b/osu.Game/Screens/OnlinePlay/FooterButtonFreeMods.cs @@ -18,6 +18,7 @@ using osu.Game.Graphics.UserInterface; using osu.Game.Rulesets.Mods; using osu.Game.Screens.Select; using osuTK; +using osu.Game.Localisation; namespace osu.Game.Screens.OnlinePlay { @@ -95,6 +96,8 @@ namespace osu.Game.Screens.OnlinePlay SelectedColour = colours.Yellow; DeselectedColour = SelectedColour.Opacity(0.5f); Text = @"freemods"; + + TooltipText = MultiplayerMatchStrings.FreeModsButtonTooltip; } protected override void LoadComplete() diff --git a/osu.Game/Screens/OnlinePlay/FooterButtonFreeStyle.cs b/osu.Game/Screens/OnlinePlay/FooterButtonFreeStyle.cs index cdfb73cee1..0e22b3d3fb 100644 --- a/osu.Game/Screens/OnlinePlay/FooterButtonFreeStyle.cs +++ b/osu.Game/Screens/OnlinePlay/FooterButtonFreeStyle.cs @@ -12,6 +12,7 @@ using osu.Framework.Graphics.UserInterface; using osu.Game.Graphics; using osu.Game.Graphics.Sprites; using osu.Game.Screens.Select; +using osu.Game.Localisation; namespace osu.Game.Screens.OnlinePlay { @@ -72,6 +73,8 @@ namespace osu.Game.Screens.OnlinePlay SelectedColour = colours.Yellow; DeselectedColour = SelectedColour.Opacity(0.5f); Text = @"freestyle"; + + TooltipText = MultiplayerMatchStrings.FreestyleButtonTooltip; } protected override void LoadComplete() diff --git a/osu.Game/Screens/OnlinePlay/OnlinePlaySongSelect.cs b/osu.Game/Screens/OnlinePlay/OnlinePlaySongSelect.cs index 9df01ead42..f6403c010e 100644 --- a/osu.Game/Screens/OnlinePlay/OnlinePlaySongSelect.cs +++ b/osu.Game/Screens/OnlinePlay/OnlinePlaySongSelect.cs @@ -21,6 +21,7 @@ using osu.Game.Rulesets.Mods; using osu.Game.Screens.Select; using osu.Game.Users; using osu.Game.Utils; +using osu.Game.Localisation; namespace osu.Game.Screens.OnlinePlay { @@ -196,14 +197,16 @@ namespace osu.Game.Screens.OnlinePlay IsValidMod = IsValidMod }; - protected override IEnumerable<(FooterButton, OverlayContainer?)> CreateSongSelectFooterButtons() + protected override IEnumerable<(FooterButton button, OverlayContainer? overlay)> CreateSongSelectFooterButtons() { var baseButtons = base.CreateSongSelectFooterButtons().ToList(); + baseButtons.Single(i => i.button is FooterButtonMods).button.TooltipText = MultiplayerMatchStrings.RequiredModsButtonTooltip; + freeModsFooterButton = new FooterButtonFreeMods(freeModSelect) { Current = FreeMods }; var freeStyleButton = new FooterButtonFreeStyle { Current = FreeStyle }; - baseButtons.InsertRange(baseButtons.FindIndex(b => b.Item1 is FooterButtonMods) + 1, new (FooterButton, OverlayContainer?)[] + baseButtons.InsertRange(baseButtons.FindIndex(b => b.button is FooterButtonMods) + 1, new (FooterButton, OverlayContainer?)[] { (freeModsFooterButton, null), (freeStyleButton, null) diff --git a/osu.Game/Screens/OnlinePlay/OnlinePlayStyleSelect.cs b/osu.Game/Screens/OnlinePlay/OnlinePlayStyleSelect.cs index d1fcf94152..22290f8fed 100644 --- a/osu.Game/Screens/OnlinePlay/OnlinePlayStyleSelect.cs +++ b/osu.Game/Screens/OnlinePlay/OnlinePlayStyleSelect.cs @@ -45,7 +45,7 @@ namespace osu.Game.Screens.OnlinePlay protected override FilterControl CreateFilterControl() => new DifficultySelectFilterControl(item); - protected override IEnumerable<(FooterButton, OverlayContainer?)> CreateSongSelectFooterButtons() + protected override IEnumerable<(FooterButton button, OverlayContainer? overlay)> CreateSongSelectFooterButtons() { // Required to create the drawable components. base.CreateSongSelectFooterButtons(); diff --git a/osu.Game/Screens/Select/SongSelect.cs b/osu.Game/Screens/Select/SongSelect.cs index dda7b568d2..c20dcb8593 100644 --- a/osu.Game/Screens/Select/SongSelect.cs +++ b/osu.Game/Screens/Select/SongSelect.cs @@ -415,7 +415,7 @@ namespace osu.Game.Screens.Select /// Creates the buttons to be displayed in the footer. /// /// A set of and an optional which the button opens when pressed. - protected virtual IEnumerable<(FooterButton, OverlayContainer?)> CreateSongSelectFooterButtons() => new (FooterButton, OverlayContainer?)[] + protected virtual IEnumerable<(FooterButton button, OverlayContainer? overlay)> CreateSongSelectFooterButtons() => new (FooterButton, OverlayContainer?)[] { (ModsFooterButton = new FooterButtonMods { Current = Mods }, ModSelect), (new FooterButtonRandom From 6ec718304e4df307d8ae3598de96585ff836a99e Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Tue, 21 Jan 2025 04:58:27 -0500 Subject: [PATCH 0683/1275] Revert "Fix triangles judgement mispositioned on a miss" This reverts commit e5713e52392066a1430ebce460d07d8af01ad29f. --- .../UI/DrawableManiaJudgement.cs | 15 +-------------- 1 file changed, 1 insertion(+), 14 deletions(-) diff --git a/osu.Game.Rulesets.Mania/UI/DrawableManiaJudgement.cs b/osu.Game.Rulesets.Mania/UI/DrawableManiaJudgement.cs index a1dabd66bc..5b87c74bbe 100644 --- a/osu.Game.Rulesets.Mania/UI/DrawableManiaJudgement.cs +++ b/osu.Game.Rulesets.Mania/UI/DrawableManiaJudgement.cs @@ -6,7 +6,6 @@ using osu.Framework.Graphics; using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Scoring; -using osuTK; namespace osu.Game.Rulesets.Mania.UI { @@ -36,20 +35,8 @@ namespace osu.Game.Rulesets.Mania.UI switch (Result) { case HitResult.None: - this.FadeOutFromOne(800); - break; - case HitResult.Miss: - this.ScaleTo(1.6f); - this.ScaleTo(1, 100, Easing.In); - - this.MoveToY(judgement_y_position); - this.MoveToOffset(new Vector2(0, 100), 800, Easing.InQuint); - - this.RotateTo(0); - this.RotateTo(40, 800, Easing.InQuint); - - this.FadeOutFromOne(800); + base.PlayAnimation(); break; default: From cc7c549468591c0414ad8425f8e0118b1b91dd33 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marvin=20Sch=C3=BCrz?= Date: Tue, 21 Jan 2025 11:02:28 +0100 Subject: [PATCH 0684/1275] Add test scene for clipboard snapping --- .../TestSceneEditorClipboardSnapping.cs | 84 +++++++++++++++++++ 1 file changed, 84 insertions(+) create mode 100644 osu.Game.Tests/Visual/Editing/TestSceneEditorClipboardSnapping.cs diff --git a/osu.Game.Tests/Visual/Editing/TestSceneEditorClipboardSnapping.cs b/osu.Game.Tests/Visual/Editing/TestSceneEditorClipboardSnapping.cs new file mode 100644 index 0000000000..e32cad12d2 --- /dev/null +++ b/osu.Game.Tests/Visual/Editing/TestSceneEditorClipboardSnapping.cs @@ -0,0 +1,84 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +#nullable disable + +using System.Linq; +using NUnit.Framework; +using osu.Framework.Utils; +using osu.Game.Beatmaps; +using osu.Game.Beatmaps.ControlPoints; +using osu.Game.Rulesets; +using osu.Game.Rulesets.Objects; +using osu.Game.Rulesets.Osu; +using osu.Game.Rulesets.Osu.Objects; +using osu.Game.Screens.Edit; +using osu.Game.Tests.Beatmaps; + +namespace osu.Game.Tests.Visual.Editing +{ + public partial class TestSceneEditorClipboardSnapping : EditorTestScene + { + protected override Ruleset CreateEditorRuleset() => new OsuRuleset(); + + private const double beat_length = 60_000 / 180.0; // 180 bpm + private const double timing_point_time = 1500; + + protected override IBeatmap CreateBeatmap(RulesetInfo ruleset) + { + var controlPointInfo = new ControlPointInfo(); + controlPointInfo.Add(timing_point_time, new TimingControlPoint { BeatLength = beat_length }); + return new TestBeatmap(ruleset, false) + { + ControlPointInfo = controlPointInfo + }; + } + + [TestCase(1)] + [TestCase(2)] + [TestCase(3)] + [TestCase(4)] + [TestCase(6)] + [TestCase(8)] + [TestCase(12)] + [TestCase(16)] + public void TestPasteSnapping(int divisor) + { + const double paste_time = timing_point_time + 1271; // arbitrary timestamp that doesn't snap to the timing point at any divisor + + var addedObjects = new HitObject[] + { + new HitCircle { StartTime = 1000 }, + new HitCircle { StartTime = 1200 }, + }; + + AddStep("add hitobjects", () => EditorBeatmap.AddRange(addedObjects)); + AddStep("select added objects", () => EditorBeatmap.SelectedHitObjects.AddRange(addedObjects)); + AddStep("copy hitobjects", () => Editor.Copy()); + + AddStep($"set beat divisor to 1/{divisor}", () => + { + var beatDivisor = (BindableBeatDivisor)Editor.Dependencies.Get(typeof(BindableBeatDivisor)); + beatDivisor.SetArbitraryDivisor(divisor); + }); + + AddStep("move forward in time", () => EditorClock.Seek(paste_time)); + AddAssert("not at snapped time", () => EditorClock.CurrentTime != EditorBeatmap.SnapTime(EditorClock.CurrentTime, null)); + + AddStep("paste hitobjects", () => Editor.Paste()); + + AddAssert("first object is snapped", () => Precision.AlmostEquals( + EditorBeatmap.SelectedHitObjects.MinBy(h => h.StartTime).StartTime, + EditorBeatmap.ControlPointInfo.GetClosestSnappedTime(paste_time, divisor) + )); + + AddAssert("duration between pasted objects is same", () => + { + var firstObject = EditorBeatmap.SelectedHitObjects.MinBy(h => h.StartTime); + var secondObject = EditorBeatmap.SelectedHitObjects.MaxBy(h => h.StartTime); + + return Precision.AlmostEquals(secondObject.StartTime - firstObject.StartTime, addedObjects[1].StartTime - addedObjects[0].StartTime); + }); + } + } +} From 001d9cacf21cbe9dee9330b01b9e496e7be1f4f5 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Tue, 21 Jan 2025 19:31:49 +0900 Subject: [PATCH 0685/1275] Configure awaiters --- osu.Game/Online/Multiplayer/MultiplayerClient.cs | 4 ++-- osu.Game/Online/Multiplayer/OnlineMultiplayerClient.cs | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Game/Online/Multiplayer/MultiplayerClient.cs b/osu.Game/Online/Multiplayer/MultiplayerClient.cs index d0c3a1fa06..e5eade8c1d 100644 --- a/osu.Game/Online/Multiplayer/MultiplayerClient.cs +++ b/osu.Game/Online/Multiplayer/MultiplayerClient.cs @@ -171,7 +171,7 @@ namespace osu.Game.Online.Multiplayer throw new InvalidOperationException("Cannot join a multiplayer room while already in one."); var cancellationSource = joinCancellationSource = new CancellationTokenSource(); - await initRoom(room, r => CreateRoom(new MultiplayerRoom(room)), cancellationSource.Token); + await initRoom(room, r => CreateRoom(new MultiplayerRoom(room)), cancellationSource.Token).ConfigureAwait(false); } /// @@ -187,7 +187,7 @@ namespace osu.Game.Online.Multiplayer Debug.Assert(room.RoomID != null); var cancellationSource = joinCancellationSource = new CancellationTokenSource(); - await initRoom(room, r => JoinRoom(room.RoomID.Value, password ?? room.Password), cancellationSource.Token); + await initRoom(room, r => JoinRoom(room.RoomID.Value, password ?? room.Password), cancellationSource.Token).ConfigureAwait(false); } private async Task initRoom(Room room, Func> initFunc, CancellationToken cancellationToken) diff --git a/osu.Game/Online/Multiplayer/OnlineMultiplayerClient.cs b/osu.Game/Online/Multiplayer/OnlineMultiplayerClient.cs index 524873ef66..05f3e44405 100644 --- a/osu.Game/Online/Multiplayer/OnlineMultiplayerClient.cs +++ b/osu.Game/Online/Multiplayer/OnlineMultiplayerClient.cs @@ -275,7 +275,7 @@ namespace osu.Game.Online.Multiplayer try { - return await connection.InvokeAsync(nameof(IMultiplayerServer.CreateRoom), room); + return await connection.InvokeAsync(nameof(IMultiplayerServer.CreateRoom), room).ConfigureAwait(false); } catch (HubException exception) { From b63d94101c1ecc69b68d0e4002b208b5492ab4cf Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Tue, 21 Jan 2025 05:45:37 -0500 Subject: [PATCH 0686/1275] Reapply "Fix triangles judgement mispositioned on a miss" This reverts commit 6ec718304e4df307d8ae3598de96585ff836a99e. --- .../UI/DrawableManiaJudgement.cs | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Mania/UI/DrawableManiaJudgement.cs b/osu.Game.Rulesets.Mania/UI/DrawableManiaJudgement.cs index 5b87c74bbe..a1dabd66bc 100644 --- a/osu.Game.Rulesets.Mania/UI/DrawableManiaJudgement.cs +++ b/osu.Game.Rulesets.Mania/UI/DrawableManiaJudgement.cs @@ -6,6 +6,7 @@ using osu.Framework.Graphics; using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Scoring; +using osuTK; namespace osu.Game.Rulesets.Mania.UI { @@ -35,8 +36,20 @@ namespace osu.Game.Rulesets.Mania.UI switch (Result) { case HitResult.None: + this.FadeOutFromOne(800); + break; + case HitResult.Miss: - base.PlayAnimation(); + this.ScaleTo(1.6f); + this.ScaleTo(1, 100, Easing.In); + + this.MoveToY(judgement_y_position); + this.MoveToOffset(new Vector2(0, 100), 800, Easing.InQuint); + + this.RotateTo(0); + this.RotateTo(40, 800, Easing.InQuint); + + this.FadeOutFromOne(800); break; default: From 459847cb80b3e34ca4d4bf35dabd7d1d081b94d5 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 21 Jan 2025 19:51:13 +0900 Subject: [PATCH 0687/1275] Perform client side validation that the selected beatmap and ruleset have valid online IDs This is local to playlists, since in multiplayer the validation is already provided by `osu-server-spectator`. --- osu.Game/Screens/OnlinePlay/OnlinePlayStyleSelect.cs | 1 + .../OnlinePlay/Playlists/PlaylistsRoomStyleSelect.cs | 7 +++++++ osu.Game/Screens/Select/Carousel/CarouselBeatmap.cs | 3 +++ osu.Game/Screens/Select/FilterCriteria.cs | 2 ++ 4 files changed, 13 insertions(+) diff --git a/osu.Game/Screens/OnlinePlay/OnlinePlayStyleSelect.cs b/osu.Game/Screens/OnlinePlay/OnlinePlayStyleSelect.cs index 22290f8fed..4d34000d3c 100644 --- a/osu.Game/Screens/OnlinePlay/OnlinePlayStyleSelect.cs +++ b/osu.Game/Screens/OnlinePlay/OnlinePlayStyleSelect.cs @@ -89,6 +89,7 @@ namespace osu.Game.Screens.OnlinePlay // Must be from the same set as the playlist item. criteria.BeatmapSetId = beatmapSetId; + criteria.HasOnlineID = true; // Must be within 30s of the playlist item. criteria.Length.Min = itemLength - 30000; diff --git a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomStyleSelect.cs b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomStyleSelect.cs index f3d868b0de..912496ba34 100644 --- a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomStyleSelect.cs +++ b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomStyleSelect.cs @@ -21,6 +21,13 @@ namespace osu.Game.Screens.OnlinePlay.Playlists protected override bool OnStart() { + // Beatmaps without a valid online ID are filtered away; this is just a final safety. + if (base.Beatmap.Value.BeatmapInfo.OnlineID < 0) + return false; + + if (base.Ruleset.Value.OnlineID < 0) + return false; + Beatmap.Value = base.Beatmap.Value.BeatmapInfo; Ruleset.Value = base.Ruleset.Value; this.Exit(); diff --git a/osu.Game/Screens/Select/Carousel/CarouselBeatmap.cs b/osu.Game/Screens/Select/Carousel/CarouselBeatmap.cs index 95186e98d8..dc77b0101e 100644 --- a/osu.Game/Screens/Select/Carousel/CarouselBeatmap.cs +++ b/osu.Game/Screens/Select/Carousel/CarouselBeatmap.cs @@ -90,6 +90,9 @@ namespace osu.Game.Screens.Select.Carousel if (match && criteria.RulesetCriteria != null) match &= criteria.RulesetCriteria.Matches(BeatmapInfo, criteria); + if (match && criteria.HasOnlineID == true) + match &= BeatmapInfo.OnlineID >= 0; + if (match && criteria.BeatmapSetId != null) match &= criteria.BeatmapSetId == BeatmapInfo.BeatmapSet?.OnlineID; diff --git a/osu.Game/Screens/Select/FilterCriteria.cs b/osu.Game/Screens/Select/FilterCriteria.cs index 63dbdfbed3..15cb3c5104 100644 --- a/osu.Game/Screens/Select/FilterCriteria.cs +++ b/osu.Game/Screens/Select/FilterCriteria.cs @@ -58,6 +58,8 @@ namespace osu.Game.Screens.Select public bool AllowConvertedBeatmaps; public int? BeatmapSetId; + public bool? HasOnlineID; + private string searchText = string.Empty; /// From fa20bc6631b084b4fbd3b97c3cd257a005379b0e Mon Sep 17 00:00:00 2001 From: tsunyoku Date: Tue, 21 Jan 2025 14:24:04 +0000 Subject: [PATCH 0688/1275] Remove `EffectiveBPMPreprocessor` --- .../Preprocessing/Reading/EffectiveBPM.cs | 50 ------------------- .../Preprocessing/TaikoDifficultyHitObject.cs | 27 +++++++++- .../Difficulty/TaikoDifficultyCalculator.cs | 13 +++-- 3 files changed, 32 insertions(+), 58 deletions(-) delete mode 100644 osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Reading/EffectiveBPM.cs diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Reading/EffectiveBPM.cs b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Reading/EffectiveBPM.cs deleted file mode 100644 index 17e05d5fbf..0000000000 --- a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Reading/EffectiveBPM.cs +++ /dev/null @@ -1,50 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -using System.Collections.Generic; -using osu.Game.Beatmaps; -using osu.Game.Beatmaps.ControlPoints; - -namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Reading -{ - public class EffectiveBPMPreprocessor - { - private readonly IList noteObjects; - private readonly double globalSliderVelocity; - - public EffectiveBPMPreprocessor(IBeatmap beatmap, List noteObjects) - { - this.noteObjects = noteObjects; - globalSliderVelocity = beatmap.Difficulty.SliderMultiplier; - } - - /// - /// Calculates and sets the effective BPM and slider velocity for each note object, considering clock rate and scroll speed. - /// - public void ProcessEffectiveBPM(ControlPointInfo controlPointInfo, double clockRate) - { - foreach (var currentNoteObject in noteObjects) - { - double startTime = currentNoteObject.StartTime * clockRate; - - // Retrieve the timing point at the note's start time - TimingControlPoint currentControlPoint = controlPointInfo.TimingPointAt(startTime); - - // Calculate the slider velocity at the note's start time. - double currentSliderVelocity = calculateSliderVelocity(controlPointInfo, startTime, clockRate); - currentNoteObject.CurrentSliderVelocity = currentSliderVelocity; - - currentNoteObject.EffectiveBPM = currentControlPoint.BPM * currentSliderVelocity; - } - } - - /// - /// Calculates the slider velocity based on control point info and clock rate. - /// - private double calculateSliderVelocity(ControlPointInfo controlPointInfo, double startTime, double clockRate) - { - var activeEffectControlPoint = controlPointInfo.EffectPointAt(startTime); - return globalSliderVelocity * (activeEffectControlPoint.ScrollSpeed) * clockRate; - } - } -} diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/TaikoDifficultyHitObject.cs b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/TaikoDifficultyHitObject.cs index dfcd08ed94..34c4871a42 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/TaikoDifficultyHitObject.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/TaikoDifficultyHitObject.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Linq; +using osu.Game.Beatmaps.ControlPoints; using osu.Game.Rulesets.Difficulty.Preprocessing; using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Taiko.Objects; @@ -76,11 +77,15 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing /// The list of rim (kat) s in the current beatmap. /// The list of s that is a hit (i.e. not a drumroll or swell) in the current beatmap. /// The position of this in the list. + /// The control point info of the beatmap. + /// The global slider velocity of the beatmap. public TaikoDifficultyHitObject(HitObject hitObject, HitObject lastObject, HitObject lastLastObject, double clockRate, List objects, List centreHitObjects, List rimHitObjects, - List noteObjects, int index) + List noteObjects, int index, + ControlPointInfo controlPointInfo, + double globalSliderVelocity) : base(hitObject, lastObject, clockRate, objects, index) { noteDifficultyHitObjects = noteObjects; @@ -111,6 +116,26 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing NoteIndex = noteObjects.Count; noteObjects.Add(this); } + + double startTime = hitObject.StartTime * clockRate; + + // Retrieve the timing point at the note's start time + TimingControlPoint currentControlPoint = controlPointInfo.TimingPointAt(startTime); + + // Calculate the slider velocity at the note's start time. + double currentSliderVelocity = calculateSliderVelocity(controlPointInfo, globalSliderVelocity, startTime, clockRate); + CurrentSliderVelocity = currentSliderVelocity; + + EffectiveBPM = currentControlPoint.BPM * currentSliderVelocity; + } + + /// + /// Calculates the slider velocity based on control point info and clock rate. + /// + private static double calculateSliderVelocity(ControlPointInfo controlPointInfo, double globalSliderVelocity, double startTime, double clockRate) + { + var activeEffectControlPoint = controlPointInfo.EffectPointAt(startTime); + return globalSliderVelocity * (activeEffectControlPoint.ScrollSpeed) * clockRate; } public TaikoDifficultyHitObject? PreviousMono(int backwardsIndex) => monoDifficultyHitObjects?.ElementAtOrDefault(MonoIndex - (backwardsIndex + 1)); diff --git a/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs b/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs index f3b976f970..1d3075e4ac 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs @@ -13,7 +13,6 @@ using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.Taiko.Difficulty.Preprocessing; using osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Colour; -using osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Reading; using osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Rhythm.Data; using osu.Game.Rulesets.Taiko.Difficulty.Skills; using osu.Game.Rulesets.Taiko.Mods; @@ -72,7 +71,6 @@ namespace osu.Game.Rulesets.Taiko.Difficulty var centreObjects = new List(); var rimObjects = new List(); var noteObjects = new List(); - EffectiveBPMPreprocessor bpmLoader = new EffectiveBPMPreprocessor(beatmap, noteObjects); // Generate TaikoDifficultyHitObjects from the beatmap's hit objects. for (int i = 2; i < beatmap.HitObjects.Count; i++) @@ -86,15 +84,16 @@ namespace osu.Game.Rulesets.Taiko.Difficulty centreObjects, rimObjects, noteObjects, - difficultyHitObjects.Count + difficultyHitObjects.Count, + beatmap.ControlPointInfo, + beatmap.Difficulty.SliderMultiplier )); } - var groupedHitObjects = SameRhythmHitObjects.GroupHitObjects(noteObjects); - TaikoColourDifficultyPreprocessor.ProcessAndAssign(difficultyHitObjects); - SamePatterns.GroupPatterns(groupedHitObjects); - bpmLoader.ProcessEffectiveBPM(beatmap.ControlPointInfo, clockRate); + + var groupedHitObjects = SameRhythmGroupedHitObjects.GroupHitObjects(noteObjects); + SamePatternsGroupedHitObjects.GroupPatterns(groupedHitObjects); return difficultyHitObjects; } From dbe36887f6da2649e9c55e265d6e4eb15429929a Mon Sep 17 00:00:00 2001 From: tsunyoku Date: Tue, 21 Jan 2025 14:24:27 +0000 Subject: [PATCH 0689/1275] Refactor `ColourEvaluator` --- .../Difficulty/Evaluators/ColourEvaluator.cs | 41 ++++++------------- 1 file changed, 13 insertions(+), 28 deletions(-) diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Evaluators/ColourEvaluator.cs b/osu.Game.Rulesets.Taiko/Difficulty/Evaluators/ColourEvaluator.cs index 3ff5b87fb6..c0e90e83c1 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Evaluators/ColourEvaluator.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Evaluators/ColourEvaluator.cs @@ -10,32 +10,8 @@ using osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Colour.Data; namespace osu.Game.Rulesets.Taiko.Difficulty.Evaluators { - public class ColourEvaluator + public static class ColourEvaluator { - /// - /// Evaluate the difficulty of the first note of a . - /// - public static double EvaluateDifficultyOf(MonoStreak monoStreak) - { - return DifficultyCalculationUtils.Logistic(exponent: Math.E * monoStreak.Index - 2 * Math.E) * EvaluateDifficultyOf(monoStreak.Parent) * 0.5; - } - - /// - /// Evaluate the difficulty of the first note of a . - /// - public static double EvaluateDifficultyOf(AlternatingMonoPattern alternatingMonoPattern) - { - return DifficultyCalculationUtils.Logistic(exponent: Math.E * alternatingMonoPattern.Index - 2 * Math.E) * EvaluateDifficultyOf(alternatingMonoPattern.Parent); - } - - /// - /// Evaluate the difficulty of the first note of a . - /// - public static double EvaluateDifficultyOf(RepeatingHitPatterns repeatingHitPattern) - { - return 2 * (1 - DifficultyCalculationUtils.Logistic(exponent: Math.E * repeatingHitPattern.RepetitionInterval - 2 * Math.E)); - } - /// /// Calculates a consistency penalty based on the number of consecutive consistent intervals, /// considering the delta time between each colour sequence. @@ -89,18 +65,27 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Evaluators double difficulty = 0.0d; if (colour.MonoStreak?.FirstHitObject == hitObject) // Difficulty for MonoStreak - difficulty += EvaluateDifficultyOf(colour.MonoStreak); + difficulty += evaluateMonoStreakDifficulty(colour.MonoStreak); if (colour.AlternatingMonoPattern?.FirstHitObject == hitObject) // Difficulty for AlternatingMonoPattern - difficulty += EvaluateDifficultyOf(colour.AlternatingMonoPattern); + difficulty += evaluateAlternatingMonoPatternDifficulty(colour.AlternatingMonoPattern); if (colour.RepeatingHitPattern?.FirstHitObject == hitObject) // Difficulty for RepeatingHitPattern - difficulty += EvaluateDifficultyOf(colour.RepeatingHitPattern); + difficulty += evaluateReadingHitPatternDifficulty(colour.RepeatingHitPattern); double consistencyPenalty = consistentRatioPenalty(taikoObject); difficulty *= consistencyPenalty; return difficulty; } + + private static double evaluateMonoStreakDifficulty(MonoStreak monoStreak) => + DifficultyCalculationUtils.Logistic(exponent: Math.E * monoStreak.Index - 2 * Math.E) * evaluateAlternatingMonoPatternDifficulty(monoStreak.Parent) * 0.5; + + private static double evaluateAlternatingMonoPatternDifficulty(AlternatingMonoPattern alternatingMonoPattern) => + DifficultyCalculationUtils.Logistic(exponent: Math.E * alternatingMonoPattern.Index - 2 * Math.E) * evaluateReadingHitPatternDifficulty(alternatingMonoPattern.Parent); + + private static double evaluateReadingHitPatternDifficulty(RepeatingHitPatterns repeatingHitPattern) => + 2 * (1 - DifficultyCalculationUtils.Logistic(exponent: Math.E * repeatingHitPattern.RepetitionInterval - 2 * Math.E)); } } From 9919179b0b914aba42499467cba38ee2d311034b Mon Sep 17 00:00:00 2001 From: tsunyoku Date: Tue, 21 Jan 2025 14:24:46 +0000 Subject: [PATCH 0690/1275] Format `ReadingEvaluator` --- .../Difficulty/Evaluators/ReadingEvaluator.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Evaluators/ReadingEvaluator.cs b/osu.Game.Rulesets.Taiko/Difficulty/Evaluators/ReadingEvaluator.cs index 2a08f65c7b..5871979613 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Evaluators/ReadingEvaluator.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Evaluators/ReadingEvaluator.cs @@ -47,8 +47,8 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Evaluators // High density is penalised at high velocity as it is generally considered easier to read. See https://www.desmos.com/calculator/u63f3ntdsi double densityPenalty = DifficultyCalculationUtils.Logistic(objectDensity, 0.925, 15); - double highVelocityDifficulty = (1.0 - 0.33 * densityPenalty) * DifficultyCalculationUtils.Logistic - (effectiveBPM, highVelocity.Center + 8 * densityPenalty, (1.0 + 0.5 * densityPenalty) / (highVelocity.Range / 10)); + double highVelocityDifficulty = (1.0 - 0.33 * densityPenalty) + * DifficultyCalculationUtils.Logistic(effectiveBPM, highVelocity.Center + 8 * densityPenalty, (1.0 + 0.5 * densityPenalty) / (highVelocity.Range / 10)); return midVelocityDifficulty + highVelocityDifficulty; } From b8c79d58a731943f46433298db8eb0523ec850b7 Mon Sep 17 00:00:00 2001 From: tsunyoku Date: Tue, 21 Jan 2025 14:25:28 +0000 Subject: [PATCH 0691/1275] Refactor `StaminaEvaluator` --- .../Difficulty/Evaluators/StaminaEvaluator.cs | 54 +++++++++---------- 1 file changed, 27 insertions(+), 27 deletions(-) diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Evaluators/StaminaEvaluator.cs b/osu.Game.Rulesets.Taiko/Difficulty/Evaluators/StaminaEvaluator.cs index b39ad953a4..a9884b2328 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Evaluators/StaminaEvaluator.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Evaluators/StaminaEvaluator.cs @@ -8,8 +8,34 @@ using osu.Game.Rulesets.Taiko.Objects; namespace osu.Game.Rulesets.Taiko.Difficulty.Evaluators { - public class StaminaEvaluator + public static class StaminaEvaluator { + /// + /// Evaluates the minimum mechanical stamina required to play the current object. This is calculated using the + /// maximum possible interval between two hits using the same key, by alternating available fingers for each colour. + /// + public static double EvaluateDifficultyOf(DifficultyHitObject current) + { + if (current.BaseObject is not Hit) + { + return 0.0; + } + + // Find the previous hit object hit by the current finger, which is n notes prior, n being the number of + // available fingers. + TaikoDifficultyHitObject taikoCurrent = (TaikoDifficultyHitObject)current; + TaikoDifficultyHitObject? taikoPrevious = current.Previous(1) as TaikoDifficultyHitObject; + TaikoDifficultyHitObject? previousMono = taikoCurrent.PreviousMono(availableFingersFor(taikoCurrent) - 1); + + double objectStrain = 0.5; // Add a base strain to all objects + if (taikoPrevious == null) return objectStrain; + + if (previousMono != null) + objectStrain += speedBonus(taikoCurrent.StartTime - previousMono.StartTime) + 0.5 * speedBonus(taikoCurrent.StartTime - taikoPrevious.StartTime); + + return objectStrain; + } + /// /// Applies a speed bonus dependent on the time since the last hit performed using this finger. /// @@ -44,31 +70,5 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Evaluators return 8; } - - /// - /// Evaluates the minimum mechanical stamina required to play the current object. This is calculated using the - /// maximum possible interval between two hits using the same key, by alternating available fingers for each colour. - /// - public static double EvaluateDifficultyOf(DifficultyHitObject current) - { - if (current.BaseObject is not Hit) - { - return 0.0; - } - - // Find the previous hit object hit by the current finger, which is n notes prior, n being the number of - // available fingers. - TaikoDifficultyHitObject taikoCurrent = (TaikoDifficultyHitObject)current; - TaikoDifficultyHitObject? taikoPrevious = current.Previous(1) as TaikoDifficultyHitObject; - TaikoDifficultyHitObject? previousMono = taikoCurrent.PreviousMono(availableFingersFor(taikoCurrent) - 1); - - double objectStrain = 0.5; // Add a base strain to all objects - if (taikoPrevious == null) return objectStrain; - - if (previousMono != null) - objectStrain += speedBonus(taikoCurrent.StartTime - previousMono.StartTime) + 0.5 * speedBonus(taikoCurrent.StartTime - taikoPrevious.StartTime); - - return objectStrain; - } } } From ef8867704adaeb813bce65fe1e44844aea86ddce Mon Sep 17 00:00:00 2001 From: tsunyoku Date: Tue, 21 Jan 2025 14:28:15 +0000 Subject: [PATCH 0692/1275] Add xmldoc to explain `IHasInterval.Interval` --- .../Difficulty/Preprocessing/Rhythm/IHasInterval.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/IHasInterval.cs b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/IHasInterval.cs index 8f3917cbde..32b148da2e 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/IHasInterval.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/IHasInterval.cs @@ -8,6 +8,9 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Rhythm /// public interface IHasInterval { + /// + /// The interval between 2 objects start times. + /// double Interval { get; } } } From 20a76d832df7986c623f9e7fecd468fc012782eb Mon Sep 17 00:00:00 2001 From: tsunyoku Date: Tue, 21 Jan 2025 14:29:07 +0000 Subject: [PATCH 0693/1275] Rename rhythm preprocessing objects to be clearer with intent --- .../Difficulty/Evaluators/RhythmEvaluator.cs | 38 +++++++++---------- ...Rhythm.cs => IntervalGroupedHitObjects.cs} | 31 ++++++--------- ...ns.cs => SamePatternsGroupedHitObjects.cs} | 28 +++++++------- ...ects.cs => SameRhythmGroupedHitObjects.cs} | 30 +++++++-------- .../Rhythm/TaikoDifficultyHitObjectRhythm.cs | 4 +- 5 files changed, 60 insertions(+), 71 deletions(-) rename osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/Data/{SameRhythm.cs => IntervalGroupedHitObjects.cs} (62%) rename osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/Data/{SamePatterns.cs => SamePatternsGroupedHitObjects.cs} (50%) rename osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/Data/{SameRhythmHitObjects.cs => SameRhythmGroupedHitObjects.cs} (70%) diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Evaluators/RhythmEvaluator.cs b/osu.Game.Rulesets.Taiko/Difficulty/Evaluators/RhythmEvaluator.cs index 22321a8f6e..8accc6124c 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Evaluators/RhythmEvaluator.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Evaluators/RhythmEvaluator.cs @@ -25,32 +25,32 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Evaluators double samePattern = 0; double intervalPenalty = 0; - if (rhythm.SameRhythmHitObjects?.FirstHitObject == hitObject) // Difficulty for SameRhythmHitObjects + if (rhythm.SameRhythmGroupedHitObjects?.FirstHitObject == hitObject) // Difficulty for SameRhythmGroupedHitObjects { - sameRhythm += 10.0 * evaluateDifficultyOf(rhythm.SameRhythmHitObjects, hitWindow); - intervalPenalty = repeatedIntervalPenalty(rhythm.SameRhythmHitObjects, hitWindow); + sameRhythm += 10.0 * evaluateDifficultyOf(rhythm.SameRhythmGroupedHitObjects, hitWindow); + intervalPenalty = repeatedIntervalPenalty(rhythm.SameRhythmGroupedHitObjects, hitWindow); } - if (rhythm.SamePatterns?.FirstHitObject == hitObject) // Difficulty for SamePatterns - samePattern += 1.15 * ratioDifficulty(rhythm.SamePatterns.IntervalRatio); + if (rhythm.SamePatternsGroupedHitObjects?.FirstHitObject == hitObject) // Difficulty for SamePatternsGroupedHitObjects + samePattern += 1.15 * ratioDifficulty(rhythm.SamePatternsGroupedHitObjects.IntervalRatio); difficulty += Math.Max(sameRhythm, samePattern) * intervalPenalty; return difficulty; } - private static double evaluateDifficultyOf(SameRhythmHitObjects sameRhythmHitObjects, double hitWindow) + private static double evaluateDifficultyOf(SameRhythmGroupedHitObjects sameRhythmGroupedHitObjects, double hitWindow) { - double intervalDifficulty = ratioDifficulty(sameRhythmHitObjects.HitObjectIntervalRatio); - double? previousInterval = sameRhythmHitObjects.Previous?.HitObjectInterval; + double intervalDifficulty = ratioDifficulty(sameRhythmGroupedHitObjects.HitObjectIntervalRatio); + double? previousInterval = sameRhythmGroupedHitObjects.Previous?.HitObjectInterval; - intervalDifficulty *= repeatedIntervalPenalty(sameRhythmHitObjects, hitWindow); + intervalDifficulty *= repeatedIntervalPenalty(sameRhythmGroupedHitObjects, hitWindow); // If a previous interval exists and there are multiple hit objects in the sequence: - if (previousInterval != null && sameRhythmHitObjects.Children.Count > 1) + if (previousInterval != null && sameRhythmGroupedHitObjects.Children.Count > 1) { - double expectedDurationFromPrevious = (double)previousInterval * sameRhythmHitObjects.Children.Count; - double durationDifference = sameRhythmHitObjects.Duration - expectedDurationFromPrevious; + double expectedDurationFromPrevious = (double)previousInterval * sameRhythmGroupedHitObjects.Children.Count; + double durationDifference = sameRhythmGroupedHitObjects.Duration - expectedDurationFromPrevious; if (durationDifference > 0) { @@ -64,7 +64,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Evaluators // Penalise patterns that can be hit within a single hit window. intervalDifficulty *= DifficultyCalculationUtils.Logistic( - sameRhythmHitObjects.Duration / hitWindow, + sameRhythmGroupedHitObjects.Duration / hitWindow, midpointOffset: 0.6, multiplier: 1, maxValue: 1); @@ -75,20 +75,20 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Evaluators /// /// Determines if the changes in hit object intervals is consistent based on a given threshold. /// - private static double repeatedIntervalPenalty(SameRhythmHitObjects sameRhythmHitObjects, double hitWindow, double threshold = 0.1) + private static double repeatedIntervalPenalty(SameRhythmGroupedHitObjects sameRhythmGroupedHitObjects, double hitWindow, double threshold = 0.1) { - double longIntervalPenalty = sameInterval(sameRhythmHitObjects, 3); + double longIntervalPenalty = sameInterval(sameRhythmGroupedHitObjects, 3); - double shortIntervalPenalty = sameRhythmHitObjects.Children.Count < 6 - ? sameInterval(sameRhythmHitObjects, 4) + double shortIntervalPenalty = sameRhythmGroupedHitObjects.Children.Count < 6 + ? sameInterval(sameRhythmGroupedHitObjects, 4) : 1.0; // Returns a non-penalty if there are 6 or more notes within an interval. // The duration penalty is based on hit object duration relative to hitWindow. - double durationPenalty = Math.Max(1 - sameRhythmHitObjects.Duration * 2 / hitWindow, 0.5); + double durationPenalty = Math.Max(1 - sameRhythmGroupedHitObjects.Duration * 2 / hitWindow, 0.5); return Math.Min(longIntervalPenalty, shortIntervalPenalty) * durationPenalty; - double sameInterval(SameRhythmHitObjects startObject, int intervalCount) + double sameInterval(SameRhythmGroupedHitObjects startObject, int intervalCount) { List intervals = new List(); var currentObject = startObject; diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/Data/SameRhythm.cs b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/Data/IntervalGroupedHitObjects.cs similarity index 62% rename from osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/Data/SameRhythm.cs rename to osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/Data/IntervalGroupedHitObjects.cs index b1ca22595b..930b3fc0e4 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/Data/SameRhythm.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/Data/IntervalGroupedHitObjects.cs @@ -1,8 +1,8 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using System; using System.Collections.Generic; +using osu.Framework.Utils; namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Rhythm.Data { @@ -10,35 +10,26 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Rhythm.Data /// A base class for grouping s by their interval. In edges where an interval change /// occurs, the is added to the group with the smaller interval. /// - public abstract class SameRhythm - where ChildType : IHasInterval + public abstract class IntervalGroupedHitObjects + where TChildType : IHasInterval { - public IReadOnlyList Children { get; private set; } + public IReadOnlyList Children { get; private set; } /// - /// Determines if the intervals between two child objects are within a specified margin of error, - /// indicating that the intervals are effectively "flat" or consistent. - /// - private bool isFlat(ChildType current, ChildType previous, double marginOfError) - { - return Math.Abs(current.Interval - previous.Interval) <= marginOfError; - } - - /// - /// Create a new from a list of s, and add + /// Create a new from a list of s, and add /// them to the list until the end of the group. /// /// The list of s. /// /// Index in to start adding children. This will be modified and should be passed into - /// the next 's constructor. + /// the next 's constructor. /// /// /// The margin of error for the interval, within of which no interval change is considered to have occured. /// - protected SameRhythm(List data, ref int i, double marginOfError) + protected IntervalGroupedHitObjects(List data, ref int i, double marginOfError) { - List children = new List(); + List children = new List(); Children = children; children.Add(data[i]); i++; @@ -46,9 +37,9 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Rhythm.Data for (; i < data.Count - 1; i++) { // An interval change occured, add the current data if the next interval is larger. - if (!isFlat(data[i], data[i + 1], marginOfError)) + if (!Precision.AlmostEquals(data[i].Interval, data[i + 1].Interval, marginOfError)) { - if (data[i + 1].Interval > data[i].Interval + marginOfError) + if (Precision.DefinitelyBigger(data[i].Interval, data[i + 1].Interval, marginOfError)) { children.Add(data[i]); i++; @@ -63,7 +54,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Rhythm.Data // Check if the last two objects in the data form a "flat" rhythm pattern within the specified margin of error. // If true, add the current object to the group and increment the index to process the next object. - if (data.Count > 2 && isFlat(data[^1], data[^2], marginOfError)) + if (data.Count > 2 && Precision.AlmostEquals(data[^1].Interval, data[^2].Interval, marginOfError)) { children.Add(data[i]); i++; diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/Data/SamePatterns.cs b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/Data/SamePatternsGroupedHitObjects.cs similarity index 50% rename from osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/Data/SamePatterns.cs rename to osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/Data/SamePatternsGroupedHitObjects.cs index 50839c4561..d4cbc9c1f9 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/Data/SamePatterns.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/Data/SamePatternsGroupedHitObjects.cs @@ -7,21 +7,21 @@ using System.Linq; namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Rhythm.Data { /// - /// Represents grouped by their 's interval. + /// Represents grouped by their 's interval. /// - public class SamePatterns : SameRhythm + public class SamePatternsGroupedHitObjects : IntervalGroupedHitObjects { - public SamePatterns? Previous { get; private set; } + public SamePatternsGroupedHitObjects? Previous { get; private set; } /// - /// The between children within this group. - /// If there is only one child, this will have the value of the first child's . + /// The between children within this group. + /// If there is only one child, this will have the value of the first child's . /// public double ChildrenInterval => Children.Count > 1 ? Children[1].Interval : Children[0].Interval; /// - /// The ratio of between this and the previous . In the - /// case where there is no previous , this will have a value of 1. + /// The ratio of between this and the previous . In the + /// case where there is no previous , this will have a value of 1. /// public double IntervalRatio => ChildrenInterval / Previous?.ChildrenInterval ?? 1.0d; @@ -29,26 +29,26 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Rhythm.Data public IEnumerable AllHitObjects => Children.SelectMany(child => child.Children); - private SamePatterns(SamePatterns? previous, List data, ref int i) + private SamePatternsGroupedHitObjects(SamePatternsGroupedHitObjects? previous, List data, ref int i) : base(data, ref i, 5) { Previous = previous; foreach (TaikoDifficultyHitObject hitObject in AllHitObjects) { - hitObject.Rhythm.SamePatterns = this; + hitObject.Rhythm.SamePatternsGroupedHitObjects = this; } } - public static void GroupPatterns(List data) + public static void GroupPatterns(List data) { - List samePatterns = new List(); + List samePatterns = new List(); - // Index does not need to be incremented, as it is handled within the SameRhythm constructor. + // Index does not need to be incremented, as it is handled within the IntervalGroupedHitObjects constructor. for (int i = 0; i < data.Count;) { - SamePatterns? previous = samePatterns.Count > 0 ? samePatterns[^1] : null; - samePatterns.Add(new SamePatterns(previous, data, ref i)); + SamePatternsGroupedHitObjects? previous = samePatterns.Count > 0 ? samePatterns[^1] : null; + samePatterns.Add(new SamePatternsGroupedHitObjects(previous, data, ref i)); } } } diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/Data/SameRhythmHitObjects.cs b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/Data/SameRhythmGroupedHitObjects.cs similarity index 70% rename from osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/Data/SameRhythmHitObjects.cs rename to osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/Data/SameRhythmGroupedHitObjects.cs index 0ccc6da026..0b59433a2e 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/Data/SameRhythmHitObjects.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/Data/SameRhythmGroupedHitObjects.cs @@ -9,11 +9,11 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Rhythm.Data /// /// Represents a group of s with no rhythm variation. /// - public class SameRhythmHitObjects : SameRhythm, IHasInterval + public class SameRhythmGroupedHitObjects : IntervalGroupedHitObjects, IHasInterval { public TaikoDifficultyHitObject FirstHitObject => Children[0]; - public SameRhythmHitObjects? Previous; + public SameRhythmGroupedHitObjects? Previous; /// /// of the first hit object. @@ -26,30 +26,28 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Rhythm.Data public double Duration => Children[^1].StartTime - Children[0].StartTime; /// - /// The interval in ms of each hit object in this . This is only defined if there is - /// more than two hit objects in this . + /// The interval in ms of each hit object in this . This is only defined if there is + /// more than two hit objects in this . /// public double? HitObjectInterval; /// - /// The ratio of between this and the previous . In the + /// The ratio of between this and the previous . In the /// case where one or both of the is undefined, this will have a value of 1. /// public double HitObjectIntervalRatio = 1; - /// - /// The interval between the of this and the previous . - /// - public double Interval { get; private set; } = double.PositiveInfinity; + /// + public double Interval { get; private set; } - public SameRhythmHitObjects(SameRhythmHitObjects? previous, List data, ref int i) + public SameRhythmGroupedHitObjects(SameRhythmGroupedHitObjects? previous, List data, ref int i) : base(data, ref i, 5) { Previous = previous; foreach (var hitObject in Children) { - hitObject.Rhythm.SameRhythmHitObjects = this; + hitObject.Rhythm.SameRhythmGroupedHitObjects = this; // Pass the HitObjectInterval to each child. hitObject.HitObjectInterval = HitObjectInterval; @@ -58,15 +56,15 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Rhythm.Data calculateIntervals(); } - public static List GroupHitObjects(List data) + public static List GroupHitObjects(List data) { - List flatPatterns = new List(); + List flatPatterns = new List(); - // Index does not need to be incremented, as it is handled within SameRhythm's constructor. + // Index does not need to be incremented, as it is handled within IntervalGroupedHitObjects's constructor. for (int i = 0; i < data.Count;) { - SameRhythmHitObjects? previous = flatPatterns.Count > 0 ? flatPatterns[^1] : null; - flatPatterns.Add(new SameRhythmHitObjects(previous, data, ref i)); + SameRhythmGroupedHitObjects? previous = flatPatterns.Count > 0 ? flatPatterns[^1] : null; + flatPatterns.Add(new SameRhythmGroupedHitObjects(previous, data, ref i)); } return flatPatterns; diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/TaikoDifficultyHitObjectRhythm.cs b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/TaikoDifficultyHitObjectRhythm.cs index beb7bfe5f6..351015ae08 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/TaikoDifficultyHitObjectRhythm.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/TaikoDifficultyHitObjectRhythm.cs @@ -15,12 +15,12 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Rhythm /// /// The group of hit objects with consistent rhythm that this object belongs to. /// - public SameRhythmHitObjects? SameRhythmHitObjects; + public SameRhythmGroupedHitObjects? SameRhythmGroupedHitObjects; /// /// The larger pattern of rhythm groups that this object is part of. /// - public SamePatterns? SamePatterns; + public SamePatternsGroupedHitObjects? SamePatternsGroupedHitObjects; /// /// The ratio of current From e0882d2a53d5452bb539bb9b16a0019b3f4094d2 Mon Sep 17 00:00:00 2001 From: tsunyoku Date: Tue, 21 Jan 2025 14:33:40 +0000 Subject: [PATCH 0694/1275] Make `rescale` a static method --- .../Difficulty/TaikoDifficultyCalculator.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs b/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs index 1d3075e4ac..e07a965ab0 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs @@ -203,9 +203,10 @@ namespace osu.Game.Rulesets.Taiko.Difficulty /// Applies a final re-scaling of the star rating. /// /// The raw star rating value before re-scaling. - private double rescale(double sr) + private static double rescale(double sr) { - if (sr < 0) return sr; + if (sr < 0) + return sr; return 10.43 * Math.Log(sr / 8 + 1); } From 764b0001efc8ec7bc9aff48c525ee78f47b468aa Mon Sep 17 00:00:00 2001 From: tsunyoku Date: Tue, 21 Jan 2025 14:56:51 +0000 Subject: [PATCH 0695/1275] Fix typo in `ColourEvaluator` --- .../Difficulty/Evaluators/ColourEvaluator.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Evaluators/ColourEvaluator.cs b/osu.Game.Rulesets.Taiko/Difficulty/Evaluators/ColourEvaluator.cs index c0e90e83c1..166c01f507 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Evaluators/ColourEvaluator.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Evaluators/ColourEvaluator.cs @@ -71,7 +71,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Evaluators difficulty += evaluateAlternatingMonoPatternDifficulty(colour.AlternatingMonoPattern); if (colour.RepeatingHitPattern?.FirstHitObject == hitObject) // Difficulty for RepeatingHitPattern - difficulty += evaluateReadingHitPatternDifficulty(colour.RepeatingHitPattern); + difficulty += evaluateRepeatingHitPatternsDifficulty(colour.RepeatingHitPattern); double consistencyPenalty = consistentRatioPenalty(taikoObject); difficulty *= consistencyPenalty; @@ -83,9 +83,9 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Evaluators DifficultyCalculationUtils.Logistic(exponent: Math.E * monoStreak.Index - 2 * Math.E) * evaluateAlternatingMonoPatternDifficulty(monoStreak.Parent) * 0.5; private static double evaluateAlternatingMonoPatternDifficulty(AlternatingMonoPattern alternatingMonoPattern) => - DifficultyCalculationUtils.Logistic(exponent: Math.E * alternatingMonoPattern.Index - 2 * Math.E) * evaluateReadingHitPatternDifficulty(alternatingMonoPattern.Parent); + DifficultyCalculationUtils.Logistic(exponent: Math.E * alternatingMonoPattern.Index - 2 * Math.E) * evaluateRepeatingHitPatternsDifficulty(alternatingMonoPattern.Parent); - private static double evaluateReadingHitPatternDifficulty(RepeatingHitPatterns repeatingHitPattern) => + private static double evaluateRepeatingHitPatternsDifficulty(RepeatingHitPatterns repeatingHitPattern) => 2 * (1 - DifficultyCalculationUtils.Logistic(exponent: Math.E * repeatingHitPattern.RepetitionInterval - 2 * Math.E)); } } From 1c4bc6dffd64126ab1b380ab0e6d11ff17c16a32 Mon Sep 17 00:00:00 2001 From: tsunyoku Date: Tue, 21 Jan 2025 15:00:23 +0000 Subject: [PATCH 0696/1275] Revert `Precision.DefinitelyBigger` usage --- .../Preprocessing/Rhythm/Data/IntervalGroupedHitObjects.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/Data/IntervalGroupedHitObjects.cs b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/Data/IntervalGroupedHitObjects.cs index 930b3fc0e4..cc389d4091 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/Data/IntervalGroupedHitObjects.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/Data/IntervalGroupedHitObjects.cs @@ -39,7 +39,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Rhythm.Data // An interval change occured, add the current data if the next interval is larger. if (!Precision.AlmostEquals(data[i].Interval, data[i + 1].Interval, marginOfError)) { - if (Precision.DefinitelyBigger(data[i].Interval, data[i + 1].Interval, marginOfError)) + if (data[i + 1].Interval > data[i].Interval + marginOfError) { children.Add(data[i]); i++; From 14c68bcc583d1e980225da3f022176412ede3cb8 Mon Sep 17 00:00:00 2001 From: tsunyoku Date: Tue, 21 Jan 2025 15:58:33 +0000 Subject: [PATCH 0697/1275] Replace weird `IntervalGroupedHitObjects` inheritance layer --- .../Rhythm/Data/IntervalGroupedHitObjects.cs | 64 ------------------- .../Data/SamePatternsGroupedHitObjects.cs | 27 ++------ .../Data/SameRhythmGroupedHitObjects.cs | 57 ++++------------- .../TaikoRhythmDifficultyPreprocessor.cs | 63 ++++++++++++++++++ .../Preprocessing/TaikoDifficultyHitObject.cs | 1 + .../Difficulty/TaikoDifficultyCalculator.cs | 6 +- .../Rhythm => Utils}/IHasInterval.cs | 4 +- .../Difficulty/Utils/IntervalGroupingUtils.cs | 64 +++++++++++++++++++ 8 files changed, 152 insertions(+), 134 deletions(-) delete mode 100644 osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/Data/IntervalGroupedHitObjects.cs create mode 100644 osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/TaikoRhythmDifficultyPreprocessor.cs rename osu.Game.Rulesets.Taiko/Difficulty/{Preprocessing/Rhythm => Utils}/IHasInterval.cs (73%) create mode 100644 osu.Game.Rulesets.Taiko/Difficulty/Utils/IntervalGroupingUtils.cs diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/Data/IntervalGroupedHitObjects.cs b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/Data/IntervalGroupedHitObjects.cs deleted file mode 100644 index cc389d4091..0000000000 --- a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/Data/IntervalGroupedHitObjects.cs +++ /dev/null @@ -1,64 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -using System.Collections.Generic; -using osu.Framework.Utils; - -namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Rhythm.Data -{ - /// - /// A base class for grouping s by their interval. In edges where an interval change - /// occurs, the is added to the group with the smaller interval. - /// - public abstract class IntervalGroupedHitObjects - where TChildType : IHasInterval - { - public IReadOnlyList Children { get; private set; } - - /// - /// Create a new from a list of s, and add - /// them to the list until the end of the group. - /// - /// The list of s. - /// - /// Index in to start adding children. This will be modified and should be passed into - /// the next 's constructor. - /// - /// - /// The margin of error for the interval, within of which no interval change is considered to have occured. - /// - protected IntervalGroupedHitObjects(List data, ref int i, double marginOfError) - { - List children = new List(); - Children = children; - children.Add(data[i]); - i++; - - for (; i < data.Count - 1; i++) - { - // An interval change occured, add the current data if the next interval is larger. - if (!Precision.AlmostEquals(data[i].Interval, data[i + 1].Interval, marginOfError)) - { - if (data[i + 1].Interval > data[i].Interval + marginOfError) - { - children.Add(data[i]); - i++; - } - - return; - } - - // No interval change occured - children.Add(data[i]); - } - - // Check if the last two objects in the data form a "flat" rhythm pattern within the specified margin of error. - // If true, add the current object to the group and increment the index to process the next object. - if (data.Count > 2 && Precision.AlmostEquals(data[^1].Interval, data[^2].Interval, marginOfError)) - { - children.Add(data[i]); - i++; - } - } - } -} diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/Data/SamePatternsGroupedHitObjects.cs b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/Data/SamePatternsGroupedHitObjects.cs index d4cbc9c1f9..cb22b2ef82 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/Data/SamePatternsGroupedHitObjects.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/Data/SamePatternsGroupedHitObjects.cs @@ -9,9 +9,11 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Rhythm.Data /// /// Represents grouped by their 's interval. /// - public class SamePatternsGroupedHitObjects : IntervalGroupedHitObjects + public class SamePatternsGroupedHitObjects { - public SamePatternsGroupedHitObjects? Previous { get; private set; } + public IReadOnlyList Children { get; } + + public SamePatternsGroupedHitObjects? Previous { get; } /// /// The between children within this group. @@ -29,27 +31,10 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Rhythm.Data public IEnumerable AllHitObjects => Children.SelectMany(child => child.Children); - private SamePatternsGroupedHitObjects(SamePatternsGroupedHitObjects? previous, List data, ref int i) - : base(data, ref i, 5) + public SamePatternsGroupedHitObjects(SamePatternsGroupedHitObjects? previous, List children) { Previous = previous; - - foreach (TaikoDifficultyHitObject hitObject in AllHitObjects) - { - hitObject.Rhythm.SamePatternsGroupedHitObjects = this; - } - } - - public static void GroupPatterns(List data) - { - List samePatterns = new List(); - - // Index does not need to be incremented, as it is handled within the IntervalGroupedHitObjects constructor. - for (int i = 0; i < data.Count;) - { - SamePatternsGroupedHitObjects? previous = samePatterns.Count > 0 ? samePatterns[^1] : null; - samePatterns.Add(new SamePatternsGroupedHitObjects(previous, data, ref i)); - } + Children = children; } } } diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/Data/SameRhythmGroupedHitObjects.cs b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/Data/SameRhythmGroupedHitObjects.cs index 0b59433a2e..dc6cf45d23 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/Data/SameRhythmGroupedHitObjects.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/Data/SameRhythmGroupedHitObjects.cs @@ -3,14 +3,17 @@ using System.Collections.Generic; using osu.Game.Rulesets.Difficulty.Preprocessing; +using osu.Game.Rulesets.Taiko.Difficulty.Utils; namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Rhythm.Data { /// /// Represents a group of s with no rhythm variation. /// - public class SameRhythmGroupedHitObjects : IntervalGroupedHitObjects, IHasInterval + public class SameRhythmGroupedHitObjects : IHasInterval { + public List Children { get; private set; } + public TaikoDifficultyHitObject FirstHitObject => Children[0]; public SameRhythmGroupedHitObjects? Previous; @@ -40,53 +43,21 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Rhythm.Data /// public double Interval { get; private set; } - public SameRhythmGroupedHitObjects(SameRhythmGroupedHitObjects? previous, List data, ref int i) - : base(data, ref i, 5) + public SameRhythmGroupedHitObjects(SameRhythmGroupedHitObjects? previous, List children) { Previous = previous; + Children = children; - foreach (var hitObject in Children) - { - hitObject.Rhythm.SameRhythmGroupedHitObjects = this; + // Calculate the average interval between hitobjects, or null if there are fewer than two + HitObjectInterval = Children.Count < 2 ? null : Duration / (Children.Count - 1); - // Pass the HitObjectInterval to each child. - hitObject.HitObjectInterval = HitObjectInterval; - } + // Calculate the ratio between this group's interval and the previous group's interval + HitObjectIntervalRatio = Previous?.HitObjectInterval != null && HitObjectInterval != null + ? HitObjectInterval.Value / Previous.HitObjectInterval.Value + : 1; - calculateIntervals(); - } - - public static List GroupHitObjects(List data) - { - List flatPatterns = new List(); - - // Index does not need to be incremented, as it is handled within IntervalGroupedHitObjects's constructor. - for (int i = 0; i < data.Count;) - { - SameRhythmGroupedHitObjects? previous = flatPatterns.Count > 0 ? flatPatterns[^1] : null; - flatPatterns.Add(new SameRhythmGroupedHitObjects(previous, data, ref i)); - } - - return flatPatterns; - } - - private void calculateIntervals() - { - // Calculate the average interval between hitobjects, or null if there are fewer than two. - HitObjectInterval = Children.Count < 2 ? null : (Children[^1].StartTime - Children[0].StartTime) / (Children.Count - 1); - - // If both the current and previous intervals are available, calculate the ratio. - if (Previous?.HitObjectInterval != null && HitObjectInterval != null) - { - HitObjectIntervalRatio = HitObjectInterval.Value / Previous.HitObjectInterval.Value; - } - - if (Previous == null) - { - return; - } - - Interval = StartTime - Previous.StartTime; + // Calculate the interval from the previous group's start time + Interval = Previous != null ? StartTime - Previous.StartTime : 0; } } } diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/TaikoRhythmDifficultyPreprocessor.cs b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/TaikoRhythmDifficultyPreprocessor.cs new file mode 100644 index 0000000000..fa2135caf3 --- /dev/null +++ b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/TaikoRhythmDifficultyPreprocessor.cs @@ -0,0 +1,63 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Collections.Generic; +using osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Rhythm.Data; + +namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Rhythm +{ + public static class TaikoRhythmDifficultyPreprocessor + { + public static void ProcessAndAssign(List hitObjects) + { + var rhythmGroups = createSameRhythmGroupedHitObjects(hitObjects); + + foreach (var rhythmGroup in rhythmGroups) + { + foreach (var hitObject in rhythmGroup.Children) + { + hitObject.Rhythm.SameRhythmGroupedHitObjects = rhythmGroup; + hitObject.HitObjectInterval = rhythmGroup.HitObjectInterval; + } + } + + var patternGroups = createSamePatternGroupedHitObjects(rhythmGroups); + + foreach (var patternGroup in patternGroups) + { + foreach (var hitObject in patternGroup.AllHitObjects) + { + hitObject.Rhythm.SamePatternsGroupedHitObjects = patternGroup; + } + } + } + + private static List createSameRhythmGroupedHitObjects(List hitObjects) + { + var rhythmGroups = new List(); + var groups = IntervalGroupingUtils.GroupByInterval(hitObjects); + + foreach (var group in groups) + { + var previous = rhythmGroups.Count > 0 ? rhythmGroups[^1] : null; + rhythmGroups.Add(new SameRhythmGroupedHitObjects(previous, group)); + } + + return rhythmGroups; + } + + private static List createSamePatternGroupedHitObjects(List rhythmGroups) + { + var patternGroups = new List(); + var groups = IntervalGroupingUtils.GroupByInterval(rhythmGroups); + + foreach (var group in groups) + { + var previous = patternGroups.Count > 0 ? patternGroups[^1] : null; + patternGroups.Add(new SamePatternsGroupedHitObjects(previous, group)); + } + + return patternGroups; + } + } +} diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/TaikoDifficultyHitObject.cs b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/TaikoDifficultyHitObject.cs index 34c4871a42..0c668797cd 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/TaikoDifficultyHitObject.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/TaikoDifficultyHitObject.cs @@ -9,6 +9,7 @@ using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Taiko.Objects; using osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Colour; using osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Rhythm; +using osu.Game.Rulesets.Taiko.Difficulty.Utils; namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing { diff --git a/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs b/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs index e07a965ab0..acd654f9b8 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs @@ -13,7 +13,7 @@ using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.Taiko.Difficulty.Preprocessing; using osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Colour; -using osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Rhythm.Data; +using osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Rhythm; using osu.Game.Rulesets.Taiko.Difficulty.Skills; using osu.Game.Rulesets.Taiko.Mods; using osu.Game.Rulesets.Taiko.Scoring; @@ -91,9 +91,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty } TaikoColourDifficultyPreprocessor.ProcessAndAssign(difficultyHitObjects); - - var groupedHitObjects = SameRhythmGroupedHitObjects.GroupHitObjects(noteObjects); - SamePatternsGroupedHitObjects.GroupPatterns(groupedHitObjects); + TaikoRhythmDifficultyPreprocessor.ProcessAndAssign(noteObjects); return difficultyHitObjects; } diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/IHasInterval.cs b/osu.Game.Rulesets.Taiko/Difficulty/Utils/IHasInterval.cs similarity index 73% rename from osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/IHasInterval.cs rename to osu.Game.Rulesets.Taiko/Difficulty/Utils/IHasInterval.cs index 32b148da2e..8f80bb6079 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/IHasInterval.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Utils/IHasInterval.cs @@ -1,10 +1,10 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Rhythm +namespace osu.Game.Rulesets.Taiko.Difficulty.Utils { /// - /// The interface for hitobjects that provide an interval value. + /// The interface for objects that provide an interval value. /// public interface IHasInterval { diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Utils/IntervalGroupingUtils.cs b/osu.Game.Rulesets.Taiko/Difficulty/Utils/IntervalGroupingUtils.cs new file mode 100644 index 0000000000..22ded8a966 --- /dev/null +++ b/osu.Game.Rulesets.Taiko/Difficulty/Utils/IntervalGroupingUtils.cs @@ -0,0 +1,64 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Collections.Generic; +using osu.Framework.Utils; +using osu.Game.Rulesets.Taiko.Difficulty.Utils; + +namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Rhythm.Data +{ + public static class IntervalGroupingUtils + { + public static List> GroupByInterval(IReadOnlyList data, double marginOfError = 5) where T : IHasInterval + { + var groups = new List>(); + if (data.Count == 0) + return groups; + + int i = 0; + + while (i < data.Count) + { + var group = createGroup(data, ref i, marginOfError); + groups.Add(group); + } + + return groups; + } + + private static List createGroup(IReadOnlyList data, ref int i, double marginOfError) where T : IHasInterval + { + var children = new List { data[i] }; + i++; + + for (; i < data.Count - 1; i++) + { + // An interval change occured, add the current data if the next interval is larger. + if (!Precision.AlmostEquals(data[i].Interval, data[i + 1].Interval, marginOfError)) + { + if (data[i + 1].Interval > data[i].Interval + marginOfError) + { + children.Add(data[i]); + i++; + } + + return children; + } + + // No interval change occurred + children.Add(data[i]); + } + + // Check if the last two objects in the data form a "flat" rhythm pattern within the specified margin of error. + // If true, add the current object to the group and increment the index to process the next object. + if (data.Count > 2 && i < data.Count && + Precision.AlmostEquals(data[^1].Interval, data[^2].Interval, marginOfError)) + { + children.Add(data[i]); + i++; + } + + return children; + } + } +} From 2a5a2738e152e4d23835e0c618873792ed57f148 Mon Sep 17 00:00:00 2001 From: Layendan Date: Tue, 21 Jan 2025 12:45:23 -0700 Subject: [PATCH 0698/1275] Add context menu to open in browser to rooms --- .../Lounge/Components/DrawableRoom.cs | 35 ++++++++++++++++++- .../OnlinePlay/Lounge/DrawableLoungeRoom.cs | 16 +++++++++ .../Screens/OnlinePlay/Match/RoomSubScreen.cs | 12 +++++-- 3 files changed, 59 insertions(+), 4 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/Lounge/Components/DrawableRoom.cs b/osu.Game/Screens/OnlinePlay/Lounge/Components/DrawableRoom.cs index c39ca347c7..321a1131de 100644 --- a/osu.Game/Screens/OnlinePlay/Lounge/Components/DrawableRoom.cs +++ b/osu.Game/Screens/OnlinePlay/Lounge/Components/DrawableRoom.cs @@ -12,15 +12,19 @@ using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Colour; using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Cursor; using osu.Framework.Graphics.Effects; using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Sprites; +using osu.Framework.Graphics.UserInterface; using osu.Game.Beatmaps; using osu.Game.Beatmaps.Drawables; using osu.Game.Database; using osu.Game.Graphics; using osu.Game.Graphics.Containers; using osu.Game.Graphics.Sprites; +using osu.Game.Graphics.UserInterface; +using osu.Game.Online.API; using osu.Game.Online.Chat; using osu.Game.Online.Rooms; using osu.Game.Overlays; @@ -31,11 +35,17 @@ using Container = osu.Framework.Graphics.Containers.Container; namespace osu.Game.Screens.OnlinePlay.Lounge.Components { - public abstract partial class DrawableRoom : CompositeDrawable + public abstract partial class DrawableRoom : CompositeDrawable, IHasContextMenu { protected const float CORNER_RADIUS = 10; private const float height = 100; + [Resolved] + private IAPIProvider api { get; set; } = null!; + + [Resolved] + private OsuGame? game { get; set; } = null!; + public readonly Room Room; protected readonly Bindable SelectedItem = new Bindable(); @@ -330,6 +340,29 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components } } + public MenuItem[] ContextMenuItems + { + get + { + var items = new List(); + + if (Room.RoomID.HasValue) + { + items.AddRange([new OsuMenuItem("View in browser", MenuItemType.Standard, () => + { + game?.OpenUrlExternally(formatRoomUrl(Room.RoomID.Value)); + }), new OsuMenuItem("Copy link", MenuItemType.Standard, () => + { + game?.CopyUrlToClipboard(formatRoomUrl(Room.RoomID.Value)); + })]); + } + + return items.ToArray(); + } + } + + private string formatRoomUrl(long id) => $@"{api.WebsiteRootUrl}/multiplayer/rooms/{id}"; + protected virtual UpdateableBeatmapBackgroundSprite CreateBackground() => new UpdateableBeatmapBackgroundSprite(); protected virtual IEnumerable CreateBottomDetails() diff --git a/osu.Game/Screens/OnlinePlay/Lounge/DrawableLoungeRoom.cs b/osu.Game/Screens/OnlinePlay/Lounge/DrawableLoungeRoom.cs index 0a55472c2d..2c15e5107a 100644 --- a/osu.Game/Screens/OnlinePlay/Lounge/DrawableLoungeRoom.cs +++ b/osu.Game/Screens/OnlinePlay/Lounge/DrawableLoungeRoom.cs @@ -59,6 +59,9 @@ namespace osu.Game.Screens.OnlinePlay.Lounge [Resolved] private IAPIProvider api { get; set; } = null!; + [Resolved] + private OsuGame? game { get; set; } = null!; + private readonly BindableWithCurrent selectedRoom = new BindableWithCurrent(); private Sample? sampleSelect; private Sample? sampleJoin; @@ -167,6 +170,17 @@ namespace osu.Game.Screens.OnlinePlay.Lounge }) }; + if (Room.RoomID.HasValue) + { + items.AddRange([new OsuMenuItem("View in browser", MenuItemType.Standard, () => + { + game?.OpenUrlExternally(formatRoomUrl(Room.RoomID.Value)); + }), new OsuMenuItem("Copy link", MenuItemType.Standard, () => + { + game?.CopyUrlToClipboard(formatRoomUrl(Room.RoomID.Value)); + })]); + } + if (Room.Type == MatchType.Playlists && Room.Host?.Id == api.LocalUser.Value.Id && Room.StartDate?.AddMinutes(5) >= DateTimeOffset.Now && !Room.HasEnded) { items.Add(new OsuMenuItem("Close playlist", MenuItemType.Destructive, () => @@ -234,6 +248,8 @@ namespace osu.Game.Screens.OnlinePlay.Lounge Room.PropertyChanged -= onRoomPropertyChanged; } + private string formatRoomUrl(long id) => $@"{api.WebsiteRootUrl}/multiplayer/rooms/{id}"; + public partial class PasswordEntryPopover : OsuPopover { private readonly Room room; diff --git a/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs b/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs index 4ef31c02c3..3ba056b18d 100644 --- a/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs @@ -18,6 +18,7 @@ using osu.Framework.Graphics.Shapes; using osu.Framework.Screens; using osu.Game.Audio; using osu.Game.Beatmaps; +using osu.Game.Graphics.Cursor; using osu.Game.Online.API; using osu.Game.Online.Rooms; using osu.Game.Overlays; @@ -156,10 +157,15 @@ namespace osu.Game.Screens.OnlinePlay.Match { new Drawable[] { - new DrawableMatchRoom(Room, allowEdit) + new OsuContextMenuContainer { - OnEdit = () => settingsOverlay.Show(), - SelectedItem = SelectedItem + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Child = new DrawableMatchRoom(Room, allowEdit) + { + OnEdit = () => settingsOverlay.Show(), + SelectedItem = SelectedItem + } } }, null, From fde2b22bbcd86ed44c83a3c018abd15b57bfddf0 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 22 Jan 2025 16:29:50 +0900 Subject: [PATCH 0699/1275] Add transient flag for notifications which shouldn't linger in history --- .../TestSceneNotificationOverlay.cs | 34 +++++++++++++++++++ osu.Game/Online/FriendPresenceNotifier.cs | 2 ++ .../Overlays/NotificationOverlayToastTray.cs | 11 ++++-- .../Overlays/Notifications/Notification.cs | 8 ++++- 4 files changed, 52 insertions(+), 3 deletions(-) diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneNotificationOverlay.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneNotificationOverlay.cs index c584c7dba0..caee5e634e 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneNotificationOverlay.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneNotificationOverlay.cs @@ -83,6 +83,40 @@ namespace osu.Game.Tests.Visual.UserInterface waitForCompletion(); } + [Test] + public void TestNormalDoesForwardToOverlay() + { + SimpleNotification notification = null!; + + AddStep(@"simple #1", () => notificationOverlay.Post(notification = new SimpleNotification + { + Text = @"This shouldn't annoy you too much", + Transient = false, + })); + + AddAssert("notification in toast tray", () => notification.IsInToastTray, () => Is.True); + AddUntilStep("wait for dismissed", () => notification.IsInToastTray, () => Is.False); + + checkDisplayedCount(1); + } + + [Test] + public void TestTransientDoesNotForwardToOverlay() + { + SimpleNotification notification = null!; + + AddStep(@"simple #1", () => notificationOverlay.Post(notification = new SimpleNotification + { + Text = @"This shouldn't annoy you too much", + Transient = true, + })); + + AddAssert("notification in toast tray", () => notification.IsInToastTray, () => Is.True); + AddUntilStep("wait for dismissed", () => notification.IsInToastTray, () => Is.False); + + checkDisplayedCount(0); + } + [Test] public void TestForwardWithFlingRight() { diff --git a/osu.Game/Online/FriendPresenceNotifier.cs b/osu.Game/Online/FriendPresenceNotifier.cs index dd141b756b..e39e3cf94d 100644 --- a/osu.Game/Online/FriendPresenceNotifier.cs +++ b/osu.Game/Online/FriendPresenceNotifier.cs @@ -169,6 +169,7 @@ namespace osu.Game.Online notifications.Post(new SimpleNotification { + Transient = true, Icon = FontAwesome.Solid.UserPlus, Text = $"Online: {string.Join(@", ", onlineAlertQueue.Select(u => u.Username))}", IconColour = colours.Green, @@ -204,6 +205,7 @@ namespace osu.Game.Online notifications.Post(new SimpleNotification { + Transient = true, Icon = FontAwesome.Solid.UserMinus, Text = $"Offline: {string.Join(@", ", offlineAlertQueue.Select(u => u.Username))}", IconColour = colours.Red diff --git a/osu.Game/Overlays/NotificationOverlayToastTray.cs b/osu.Game/Overlays/NotificationOverlayToastTray.cs index df07b4f138..ddb2e02fb8 100644 --- a/osu.Game/Overlays/NotificationOverlayToastTray.cs +++ b/osu.Game/Overlays/NotificationOverlayToastTray.cs @@ -41,7 +41,7 @@ namespace osu.Game.Overlays [Resolved] private OverlayColourProvider colourProvider { get; set; } = null!; - public Action? ForwardNotificationToPermanentStore { get; set; } + public required Action ForwardNotificationToPermanentStore { get; init; } public int UnreadCount => Notifications.Count(n => !n.WasClosed && !n.Read); @@ -142,8 +142,15 @@ namespace osu.Game.Overlays notification.MoveToOffset(new Vector2(400, 0), NotificationOverlay.TRANSITION_LENGTH, Easing.OutQuint); notification.FadeOut(NotificationOverlay.TRANSITION_LENGTH, Easing.OutQuint).OnComplete(_ => { + if (notification.Transient) + { + notification.IsInToastTray = false; + notification.Close(false); + return; + } + RemoveInternal(notification, false); - ForwardNotificationToPermanentStore?.Invoke(notification); + ForwardNotificationToPermanentStore(notification); notification.FadeIn(300, Easing.OutQuint); }); diff --git a/osu.Game/Overlays/Notifications/Notification.cs b/osu.Game/Overlays/Notifications/Notification.cs index d48524d8b0..e41aa8b625 100644 --- a/osu.Game/Overlays/Notifications/Notification.cs +++ b/osu.Game/Overlays/Notifications/Notification.cs @@ -34,10 +34,16 @@ namespace osu.Game.Overlays.Notifications public abstract LocalisableString Text { get; set; } /// - /// Whether this notification should forcefully display itself. + /// Important notifications display for longer, and announce themselves at an OS level (ie flashing the taskbar). + /// This defaults to true. /// public virtual bool IsImportant => true; + /// + /// Transient notifications only show as a toast, and do not linger in notification history. + /// + public bool Transient { get; init; } + /// /// Run on user activating the notification. Return true to close. /// From 4cf4b8c73de124d99995a1a1ea4d1dab5f0e3e28 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 22 Jan 2025 16:36:12 +0900 Subject: [PATCH 0700/1275] Switch `IsImportant` to `init` property isntead of `virtual` --- osu.Desktop/Security/ElevatedPrivilegesChecker.cs | 2 -- .../UserInterface/TestSceneNotificationOverlay.cs | 10 ++++++++-- osu.Game/Database/ModelDownloader.cs | 7 ++++--- osu.Game/Overlays/Notifications/Notification.cs | 2 +- .../Overlays/Notifications/ProgressNotification.cs | 4 ++-- osu.Game/Screens/Play/PlayerLoader.cs | 4 ---- 6 files changed, 15 insertions(+), 14 deletions(-) diff --git a/osu.Desktop/Security/ElevatedPrivilegesChecker.cs b/osu.Desktop/Security/ElevatedPrivilegesChecker.cs index 0bed9830df..4b6ebc9b56 100644 --- a/osu.Desktop/Security/ElevatedPrivilegesChecker.cs +++ b/osu.Desktop/Security/ElevatedPrivilegesChecker.cs @@ -30,8 +30,6 @@ namespace osu.Desktop.Security private partial class ElevatedPrivilegesNotification : SimpleNotification { - public override bool IsImportant => true; - public ElevatedPrivilegesNotification() { Text = $"Running osu! as {(RuntimeInfo.IsUnix ? "root" : "administrator")} does not improve performance, may break integrations and poses a security risk. Please run the game as a normal user."; diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneNotificationOverlay.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneNotificationOverlay.cs index caee5e634e..65c8b913d3 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneNotificationOverlay.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneNotificationOverlay.cs @@ -668,12 +668,18 @@ namespace osu.Game.Tests.Visual.UserInterface private partial class BackgroundNotification : SimpleNotification { - public override bool IsImportant => false; + public BackgroundNotification() + { + IsImportant = false; + } } private partial class BackgroundProgressNotification : ProgressNotification { - public override bool IsImportant => false; + public BackgroundProgressNotification() + { + IsImportant = false; + } } } } diff --git a/osu.Game/Database/ModelDownloader.cs b/osu.Game/Database/ModelDownloader.cs index dfeec259fe..8e89db4d06 100644 --- a/osu.Game/Database/ModelDownloader.cs +++ b/osu.Game/Database/ModelDownloader.cs @@ -131,8 +131,6 @@ namespace osu.Game.Database private partial class DownloadNotification : ProgressNotification { - public override bool IsImportant => false; - protected override Notification CreateCompletionNotification() => new SilencedProgressCompletionNotification { Activated = CompletionClickAction, @@ -141,7 +139,10 @@ namespace osu.Game.Database private partial class SilencedProgressCompletionNotification : ProgressCompletionNotification { - public override bool IsImportant => false; + public SilencedProgressCompletionNotification() + { + IsImportant = false; + } } } } diff --git a/osu.Game/Overlays/Notifications/Notification.cs b/osu.Game/Overlays/Notifications/Notification.cs index e41aa8b625..ccfd1adb39 100644 --- a/osu.Game/Overlays/Notifications/Notification.cs +++ b/osu.Game/Overlays/Notifications/Notification.cs @@ -37,7 +37,7 @@ namespace osu.Game.Overlays.Notifications /// Important notifications display for longer, and announce themselves at an OS level (ie flashing the taskbar). /// This defaults to true. /// - public virtual bool IsImportant => true; + public bool IsImportant { get; init; } = true; /// /// Transient notifications only show as a toast, and do not linger in notification history. diff --git a/osu.Game/Overlays/Notifications/ProgressNotification.cs b/osu.Game/Overlays/Notifications/ProgressNotification.cs index 2362cb11f6..0b42188252 100644 --- a/osu.Game/Overlays/Notifications/ProgressNotification.cs +++ b/osu.Game/Overlays/Notifications/ProgressNotification.cs @@ -191,8 +191,6 @@ namespace osu.Game.Overlays.Notifications public override bool DisplayOnTop => false; - public override bool IsImportant => false; - private readonly ProgressBar progressBar; private Color4 colourQueued; private Color4 colourActive; @@ -206,6 +204,8 @@ namespace osu.Game.Overlays.Notifications public ProgressNotification() { + IsImportant = false; + Content.Add(textDrawable = new OsuTextFlowContainer(t => t.Font = t.Font.With(size: 14, weight: FontWeight.Medium)) { AutoSizeAxes = Axes.Y, diff --git a/osu.Game/Screens/Play/PlayerLoader.cs b/osu.Game/Screens/Play/PlayerLoader.cs index 06086c1004..fc956e15fd 100644 --- a/osu.Game/Screens/Play/PlayerLoader.cs +++ b/osu.Game/Screens/Play/PlayerLoader.cs @@ -663,8 +663,6 @@ namespace osu.Game.Screens.Play private partial class MutedNotification : SimpleNotification { - public override bool IsImportant => true; - public MutedNotification() { Text = NotificationsStrings.GameVolumeTooLow; @@ -716,8 +714,6 @@ namespace osu.Game.Screens.Play private partial class BatteryWarningNotification : SimpleNotification { - public override bool IsImportant => true; - public BatteryWarningNotification() { Text = NotificationsStrings.BatteryLow; From 9e023340b011ae376d8e90823e0c730e33d2920c Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 22 Jan 2025 16:36:48 +0900 Subject: [PATCH 0701/1275] Mark friend notifications as non-important --- osu.Game/Online/FriendPresenceNotifier.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/osu.Game/Online/FriendPresenceNotifier.cs b/osu.Game/Online/FriendPresenceNotifier.cs index e39e3cf94d..75b487384a 100644 --- a/osu.Game/Online/FriendPresenceNotifier.cs +++ b/osu.Game/Online/FriendPresenceNotifier.cs @@ -170,6 +170,7 @@ namespace osu.Game.Online notifications.Post(new SimpleNotification { Transient = true, + IsImportant = false, Icon = FontAwesome.Solid.UserPlus, Text = $"Online: {string.Join(@", ", onlineAlertQueue.Select(u => u.Username))}", IconColour = colours.Green, @@ -206,6 +207,7 @@ namespace osu.Game.Online notifications.Post(new SimpleNotification { Transient = true, + IsImportant = false, Icon = FontAwesome.Solid.UserMinus, Text = $"Offline: {string.Join(@", ", offlineAlertQueue.Select(u => u.Username))}", IconColour = colours.Red From 910c0022e3638e204ba3a0fc201139fb0a55fd73 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 22 Jan 2025 17:03:01 +0900 Subject: [PATCH 0702/1275] Adjust code style slightly --- .../LocalCachedBeatmapMetadataSource.cs | 21 ++++++++++++++----- 1 file changed, 16 insertions(+), 5 deletions(-) diff --git a/osu.Game/Beatmaps/LocalCachedBeatmapMetadataSource.cs b/osu.Game/Beatmaps/LocalCachedBeatmapMetadataSource.cs index 7495805cff..113b16b0db 100644 --- a/osu.Game/Beatmaps/LocalCachedBeatmapMetadataSource.cs +++ b/osu.Game/Beatmaps/LocalCachedBeatmapMetadataSource.cs @@ -114,14 +114,25 @@ namespace osu.Game.Beatmaps } } } - catch (SqliteException sqliteException) when (sqliteException.SqliteErrorCode == 11 || sqliteException.SqliteErrorCode == 26) // SQLITE_CORRUPT, SQLITE_NOTADB + catch (SqliteException sqliteException) { - // only attempt purge & refetch if there is no other refetch in progress - if (cacheDownloadRequest == null) + // There have been cases where the user's local database is corrupt. + // Let's attempt to identify these cases and re-initialise the local cache. + switch (sqliteException.SqliteErrorCode) { - tryPurgeCache(); - prepareLocalCache(); + case 26: // SQLITE_NOTADB + case 11: // SQLITE_CORRUPT + // only attempt purge & re-download if there is no other refetch in progress + if (cacheDownloadRequest != null) + throw; + + tryPurgeCache(); + prepareLocalCache(); + onlineMetadata = null; + return false; } + + throw; } catch (Exception ex) { From 26ef23c9a9c84e582796b6bbc35d38e1493d42da Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 22 Jan 2025 17:04:24 +0900 Subject: [PATCH 0703/1275] Remove outdated ef related catch-when usage --- osu.Game/Database/RealmAccess.cs | 19 +------------------ 1 file changed, 1 insertion(+), 18 deletions(-) diff --git a/osu.Game/Database/RealmAccess.cs b/osu.Game/Database/RealmAccess.cs index e1b8de89fa..28033883d1 100644 --- a/osu.Game/Database/RealmAccess.cs +++ b/osu.Game/Database/RealmAccess.cs @@ -11,7 +11,6 @@ using System.Linq.Expressions; using System.Reflection; using System.Threading; using System.Threading.Tasks; -using osu.Framework; using osu.Framework.Allocation; using osu.Framework.Development; using osu.Framework.Extensions; @@ -413,18 +412,7 @@ namespace osu.Game.Database /// Compact this realm. /// /// - public bool Compact() - { - try - { - return Realm.Compact(getConfiguration()); - } - // Catch can be removed along with entity framework. Is specifically to allow a failure message to arrive to the user (see similar catches in EFToRealmMigrator). - catch (AggregateException ae) when (RuntimeInfo.OS == RuntimeInfo.Platform.macOS && ae.Flatten().InnerException is TypeInitializationException) - { - return true; - } - } + public bool Compact() => Realm.Compact(getConfiguration()); /// /// Run work on realm with a return value. @@ -720,11 +708,6 @@ namespace osu.Game.Database return Realm.GetInstance(getConfiguration()); } - // Catch can be removed along with entity framework. Is specifically to allow a failure message to arrive to the user (see similar catches in EFToRealmMigrator). - catch (AggregateException ae) when (RuntimeInfo.OS == RuntimeInfo.Platform.macOS && ae.Flatten().InnerException is TypeInitializationException) - { - return Realm.GetInstance(); - } finally { if (tookSemaphoreLock) From 6ceb348cf6109c4b5acc653ff227b35dbaa198ae Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 22 Jan 2025 18:24:01 +0900 Subject: [PATCH 0704/1275] Adjust code again to avoid weird `throw` mishandling --- .../Beatmaps/LocalCachedBeatmapMetadataSource.cs | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/osu.Game/Beatmaps/LocalCachedBeatmapMetadataSource.cs b/osu.Game/Beatmaps/LocalCachedBeatmapMetadataSource.cs index 113b16b0db..a1744f74b3 100644 --- a/osu.Game/Beatmaps/LocalCachedBeatmapMetadataSource.cs +++ b/osu.Game/Beatmaps/LocalCachedBeatmapMetadataSource.cs @@ -113,9 +113,14 @@ namespace osu.Game.Beatmaps return queryCacheVersion2(db, beatmapInfo, out onlineMetadata); } } + + onlineMetadata = null; + return false; } catch (SqliteException sqliteException) { + onlineMetadata = null; + // There have been cases where the user's local database is corrupt. // Let's attempt to identify these cases and re-initialise the local cache. switch (sqliteException.SqliteErrorCode) @@ -124,15 +129,15 @@ namespace osu.Game.Beatmaps case 11: // SQLITE_CORRUPT // only attempt purge & re-download if there is no other refetch in progress if (cacheDownloadRequest != null) - throw; + return false; tryPurgeCache(); prepareLocalCache(); - onlineMetadata = null; return false; } - throw; + logForModel(beatmapInfo.BeatmapSet, $@"Cached local retrieval for {beatmapInfo} failed with unhandled sqlite error {sqliteException}."); + return false; } catch (Exception ex) { @@ -140,9 +145,6 @@ namespace osu.Game.Beatmaps onlineMetadata = null; return false; } - - onlineMetadata = null; - return false; } private void tryPurgeCache() From c94b8bf051871e7e6495a20eacabbb0f26622bc2 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 22 Jan 2025 18:36:13 +0900 Subject: [PATCH 0705/1275] Apply NRT to new class --- .../Visual/Editing/TestSceneEditorClipboardSnapping.cs | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/osu.Game.Tests/Visual/Editing/TestSceneEditorClipboardSnapping.cs b/osu.Game.Tests/Visual/Editing/TestSceneEditorClipboardSnapping.cs index e32cad12d2..edaba67591 100644 --- a/osu.Game.Tests/Visual/Editing/TestSceneEditorClipboardSnapping.cs +++ b/osu.Game.Tests/Visual/Editing/TestSceneEditorClipboardSnapping.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System.Linq; using NUnit.Framework; using osu.Framework.Utils; @@ -68,14 +66,14 @@ namespace osu.Game.Tests.Visual.Editing AddStep("paste hitobjects", () => Editor.Paste()); AddAssert("first object is snapped", () => Precision.AlmostEquals( - EditorBeatmap.SelectedHitObjects.MinBy(h => h.StartTime).StartTime, + EditorBeatmap.SelectedHitObjects.MinBy(h => h.StartTime)!.StartTime, EditorBeatmap.ControlPointInfo.GetClosestSnappedTime(paste_time, divisor) )); AddAssert("duration between pasted objects is same", () => { - var firstObject = EditorBeatmap.SelectedHitObjects.MinBy(h => h.StartTime); - var secondObject = EditorBeatmap.SelectedHitObjects.MaxBy(h => h.StartTime); + var firstObject = EditorBeatmap.SelectedHitObjects.MinBy(h => h.StartTime)!; + var secondObject = EditorBeatmap.SelectedHitObjects.MaxBy(h => h.StartTime)!; return Precision.AlmostEquals(secondObject.StartTime - firstObject.StartTime, addedObjects[1].StartTime - addedObjects[0].StartTime); }); From 3da220b8f68829b691e4230a957c3ed2fcd77595 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 22 Jan 2025 11:39:32 +0100 Subject: [PATCH 0706/1275] Fix crash from new combo colour selector when there are no combo colours present Closes https://github.com/ppy/osu/issues/31615. --- .../Edit/Components/TernaryButtons/NewComboTernaryButton.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/Edit/Components/TernaryButtons/NewComboTernaryButton.cs b/osu.Game/Screens/Edit/Components/TernaryButtons/NewComboTernaryButton.cs index 1f95d5f239..c6ecee5f45 100644 --- a/osu.Game/Screens/Edit/Components/TernaryButtons/NewComboTernaryButton.cs +++ b/osu.Game/Screens/Edit/Components/TernaryButtons/NewComboTernaryButton.cs @@ -149,7 +149,7 @@ namespace osu.Game.Screens.Edit.Components.TernaryButtons { Enabled.Value = SelectedHitObject.Value != null; - if (SelectedHitObject.Value == null || SelectedHitObject.Value.ComboOffset == 0) + if (SelectedHitObject.Value == null || SelectedHitObject.Value.ComboOffset == 0 || ComboColours.Count <= 1) { BackgroundColour = colourProvider.Background3; icon.Colour = BackgroundColour.Darken(0.5f); From 02369baec43f0a68a26a960bef20980289b1f6ab Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Wed, 22 Jan 2025 21:44:45 +0900 Subject: [PATCH 0707/1275] Join/Leave rooms via multiplayer server Relevant functionality has been removed from `RoomManager` in the process. --- .../TestSceneMultiplayerLoungeSubScreen.cs | 26 ------ .../Online/Multiplayer/MultiplayerClient.cs | 3 + osu.Game/Online/Rooms/CreateRoomRequest.cs | 2 +- osu.Game/Online/Rooms/JoinRoomRequest.cs | 1 + .../OnlinePlay/Components/RoomManager.cs | 80 ------------------- .../DailyChallenge/DailyChallenge.cs | 10 +-- osu.Game/Screens/OnlinePlay/IRoomManager.cs | 22 ----- .../OnlinePlay/Lounge/LoungeSubScreen.cs | 9 ++- .../Screens/OnlinePlay/Match/RoomSubScreen.cs | 6 +- .../OnlinePlay/Multiplayer/Multiplayer.cs | 3 - .../Multiplayer/MultiplayerLoungeSubScreen.cs | 34 ++++---- .../Multiplayer/MultiplayerMatchSubScreen.cs | 2 + .../Multiplayer/MultiplayerRoomManager.cs | 72 ----------------- .../Screens/OnlinePlay/OnlinePlayScreen.cs | 12 +-- .../Screens/OnlinePlay/OnlinePlaySubScreen.cs | 4 - .../Playlists/PlaylistsLoungeSubScreen.cs | 15 ++++ .../Playlists/PlaylistsRoomSettingsOverlay.cs | 9 ++- .../Playlists/PlaylistsRoomSubScreen.cs | 2 + .../Multiplayer/MultiplayerTestScene.cs | 2 +- .../Multiplayer/TestMultiplayerRoomManager.cs | 10 +-- .../Visual/OnlinePlay/TestRoomManager.cs | 13 ++- 21 files changed, 74 insertions(+), 263 deletions(-) delete mode 100644 osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerRoomManager.cs diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerLoungeSubScreen.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerLoungeSubScreen.cs index 9951f62c77..d06a91433d 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerLoungeSubScreen.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerLoungeSubScreen.cs @@ -8,7 +8,6 @@ using osu.Framework.Graphics.UserInterface; using osu.Framework.Screens; using osu.Framework.Testing; using osu.Game.Graphics.UserInterface; -using osu.Game.Online.Rooms; using osu.Game.Screens.OnlinePlay.Lounge; using osu.Game.Screens.OnlinePlay.Multiplayer; using osu.Game.Tests.Visual.OnlinePlay; @@ -21,23 +20,13 @@ namespace osu.Game.Tests.Visual.Multiplayer protected new TestRoomManager RoomManager => (TestRoomManager)base.RoomManager; private LoungeSubScreen loungeScreen = null!; - private Room? lastJoinedRoom; - private string? lastJoinedPassword; public override void SetUpSteps() { base.SetUpSteps(); AddStep("push screen", () => LoadScreen(loungeScreen = new MultiplayerLoungeSubScreen())); - AddUntilStep("wait for present", () => loungeScreen.IsCurrentScreen()); - - AddStep("bind to event", () => - { - lastJoinedRoom = null; - lastJoinedPassword = null; - RoomManager.JoinRoomRequested = onRoomJoined; - }); } [Test] @@ -46,9 +35,6 @@ namespace osu.Game.Tests.Visual.Multiplayer AddStep("add room", () => RoomManager.AddRooms(1, withPassword: false)); AddStep("select room", () => InputManager.Key(Key.Down)); AddStep("join room", () => InputManager.Key(Key.Enter)); - - AddAssert("room join requested", () => lastJoinedRoom == RoomManager.Rooms.First()); - AddAssert("room join password correct", () => lastJoinedPassword == null); } [Test] @@ -126,9 +112,6 @@ namespace osu.Game.Tests.Visual.Multiplayer AddUntilStep("password prompt appeared", () => (passwordEntryPopover = InputManager.ChildrenOfType().FirstOrDefault()) != null); AddStep("enter password in text box", () => passwordEntryPopover.ChildrenOfType().First().Text = "password"); AddStep("press join room button", () => passwordEntryPopover.ChildrenOfType().First().TriggerClick()); - - AddAssert("room join requested", () => lastJoinedRoom == RoomManager.Rooms.First()); - AddAssert("room join password correct", () => lastJoinedPassword == "password"); } [Test] @@ -142,15 +125,6 @@ namespace osu.Game.Tests.Visual.Multiplayer AddUntilStep("password prompt appeared", () => (passwordEntryPopover = InputManager.ChildrenOfType().FirstOrDefault()) != null); AddStep("enter password in text box", () => passwordEntryPopover.ChildrenOfType().First().Text = "password"); AddStep("press enter", () => InputManager.Key(Key.Enter)); - - AddAssert("room join requested", () => lastJoinedRoom == RoomManager.Rooms.First()); - AddAssert("room join password correct", () => lastJoinedPassword == "password"); - } - - private void onRoomJoined(Room room, string? password) - { - lastJoinedRoom = room; - lastJoinedPassword = password; } } } diff --git a/osu.Game/Online/Multiplayer/MultiplayerClient.cs b/osu.Game/Online/Multiplayer/MultiplayerClient.cs index e5eade8c1d..7dfe974651 100644 --- a/osu.Game/Online/Multiplayer/MultiplayerClient.cs +++ b/osu.Game/Online/Multiplayer/MultiplayerClient.cs @@ -253,6 +253,9 @@ namespace osu.Game.Online.Multiplayer public Task LeaveRoom() { + if (Room == null) + return Task.CompletedTask; + // The join may have not completed yet, so certain tasks that either update the room or reference the room should be cancelled. // This includes the setting of Room itself along with the initial update of the room settings on join. joinCancellationSource?.Cancel(); diff --git a/osu.Game/Online/Rooms/CreateRoomRequest.cs b/osu.Game/Online/Rooms/CreateRoomRequest.cs index 63a3b7bfa8..9773bb5e7d 100644 --- a/osu.Game/Online/Rooms/CreateRoomRequest.cs +++ b/osu.Game/Online/Rooms/CreateRoomRequest.cs @@ -15,6 +15,7 @@ namespace osu.Game.Online.Rooms public CreateRoomRequest(Room room) { Room = room; + Success += r => Room.CopyFrom(r); } protected override WebRequest CreateWebRequest() @@ -23,7 +24,6 @@ namespace osu.Game.Online.Rooms req.ContentType = "application/json"; req.Method = HttpMethod.Post; - req.AddRaw(JsonConvert.SerializeObject(Room)); return req; diff --git a/osu.Game/Online/Rooms/JoinRoomRequest.cs b/osu.Game/Online/Rooms/JoinRoomRequest.cs index dfc7a53fb2..13e7ac8c84 100644 --- a/osu.Game/Online/Rooms/JoinRoomRequest.cs +++ b/osu.Game/Online/Rooms/JoinRoomRequest.cs @@ -16,6 +16,7 @@ namespace osu.Game.Online.Rooms { Room = room; Password = password; + Success += r => Room.CopyFrom(r); } protected override WebRequest CreateWebRequest() diff --git a/osu.Game/Screens/OnlinePlay/Components/RoomManager.cs b/osu.Game/Screens/OnlinePlay/Components/RoomManager.cs index 73f980f0a3..3abb4098fb 100644 --- a/osu.Game/Screens/OnlinePlay/Components/RoomManager.cs +++ b/osu.Game/Screens/OnlinePlay/Components/RoomManager.cs @@ -5,12 +5,10 @@ using System; using System.Collections.Generic; using System.Diagnostics; using System.Linq; -using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Development; using osu.Framework.Graphics; using osu.Framework.Logging; -using osu.Game.Online.API; using osu.Game.Online.Rooms; namespace osu.Game.Screens.OnlinePlay.Components @@ -23,89 +21,11 @@ namespace osu.Game.Screens.OnlinePlay.Components public IBindableList Rooms => rooms; - protected IBindable JoinedRoom => joinedRoom; - private readonly Bindable joinedRoom = new Bindable(); - - [Resolved] - private IAPIProvider api { get; set; } = null!; - public RoomManager() { RelativeSizeAxes = Axes.Both; } - protected override void Dispose(bool isDisposing) - { - base.Dispose(isDisposing); - PartRoom(); - } - - public virtual void CreateRoom(Room room, Action? onSuccess = null, Action? onError = null) - { - room.Host = api.LocalUser.Value; - - var req = new CreateRoomRequest(room); - - req.Success += result => - { - joinedRoom.Value = room; - - AddOrUpdateRoom(result); - room.CopyFrom(result); // Also copy back to the source model, since this is likely to have been stored elsewhere. - - // The server may not contain all properties (such as password), so invoke success with the given room. - onSuccess?.Invoke(room); - }; - - req.Failure += exception => - { - onError?.Invoke(req.Response?.Error ?? exception.Message); - }; - - api.Queue(req); - } - - private JoinRoomRequest? currentJoinRoomRequest; - - public virtual void JoinRoom(Room room, string? password = null, Action? onSuccess = null, Action? onError = null) - { - currentJoinRoomRequest?.Cancel(); - currentJoinRoomRequest = new JoinRoomRequest(room, password); - - currentJoinRoomRequest.Success += result => - { - joinedRoom.Value = room; - - AddOrUpdateRoom(result); - room.CopyFrom(result); // Also copy back to the source model, since this is likely to have been stored elsewhere. - - onSuccess?.Invoke(room); - }; - - currentJoinRoomRequest.Failure += exception => - { - if (exception is OperationCanceledException) - return; - - onError?.Invoke(exception.Message); - }; - - api.Queue(currentJoinRoomRequest); - } - - public virtual void PartRoom() - { - currentJoinRoomRequest?.Cancel(); - - if (joinedRoom.Value == null) - return; - - if (api.State.Value == APIState.Online) - api.Queue(new PartRoomRequest(joinedRoom.Value)); - - joinedRoom.Value = null; - } - private readonly HashSet ignoredRooms = new HashSet(); public void AddOrUpdateRoom(Room room) diff --git a/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallenge.cs b/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallenge.cs index 13a282dd52..e3d6d42c05 100644 --- a/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallenge.cs +++ b/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallenge.cs @@ -34,7 +34,6 @@ using osu.Game.Overlays; using osu.Game.Overlays.Notifications; using osu.Game.Rulesets; using osu.Game.Rulesets.Mods; -using osu.Game.Screens.OnlinePlay.Components; using osu.Game.Screens.OnlinePlay.DailyChallenge.Events; using osu.Game.Screens.OnlinePlay.Match; using osu.Game.Screens.OnlinePlay.Match.Components; @@ -71,9 +70,6 @@ namespace osu.Game.Screens.OnlinePlay.DailyChallenge [Cached] private readonly OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Plum); - [Cached(Type = typeof(IRoomManager))] - private RoomManager roomManager { get; set; } - [Cached] private readonly OnlinePlayBeatmapAvailabilityTracker beatmapAvailabilityTracker = new OnlinePlayBeatmapAvailabilityTracker(); @@ -115,7 +111,6 @@ namespace osu.Game.Screens.OnlinePlay.DailyChallenge { this.room = room; playlistItem = room.Playlist.Single(); - roomManager = new RoomManager(); Padding = new MarginPadding { Horizontal = -HORIZONTAL_OVERFLOW_PADDING }; } @@ -131,7 +126,6 @@ namespace osu.Game.Screens.OnlinePlay.DailyChallenge RelativeSizeAxes = Axes.Both, Children = new Drawable[] { - roomManager, beatmapAvailabilityTracker, new ScreenStack(new RoomBackgroundScreen(playlistItem)) { @@ -426,7 +420,7 @@ namespace osu.Game.Screens.OnlinePlay.DailyChallenge base.OnEntering(e); waves.Show(); - roomManager.JoinRoom(room); + API.Queue(new JoinRoomRequest(room, null)); startLoopingTrack(this, musicController); metadataClient.BeginWatchingMultiplayerRoom(room.RoomID!.Value).ContinueWith(t => @@ -480,7 +474,7 @@ namespace osu.Game.Screens.OnlinePlay.DailyChallenge previewTrackManager.StopAnyPlaying(this); this.Delay(WaveContainer.DISAPPEAR_DURATION).FadeOut(); - roomManager.PartRoom(); + API.Queue(new PartRoomRequest(room)); metadataClient.EndWatchingMultiplayerRoom(room.RoomID!.Value).FireAndForget(); return base.OnExiting(e); diff --git a/osu.Game/Screens/OnlinePlay/IRoomManager.cs b/osu.Game/Screens/OnlinePlay/IRoomManager.cs index ed4fb7b15e..8ecb1dd7e0 100644 --- a/osu.Game/Screens/OnlinePlay/IRoomManager.cs +++ b/osu.Game/Screens/OnlinePlay/IRoomManager.cs @@ -38,27 +38,5 @@ namespace osu.Game.Screens.OnlinePlay /// Removes all s from this . /// void ClearRooms(); - - /// - /// Creates a new . - /// - /// The to create. - /// An action to be invoked if the creation succeeds. - /// An action to be invoked if an error occurred. - void CreateRoom(Room room, Action? onSuccess = null, Action? onError = null); - - /// - /// Joins a . - /// - /// The to join. must be populated. - /// An optional password to use for the join operation. - /// - /// - void JoinRoom(Room room, string? password = null, Action? onSuccess = null, Action? onError = null); - - /// - /// Parts the currently-joined . - /// - void PartRoom(); } } diff --git a/osu.Game/Screens/OnlinePlay/Lounge/LoungeSubScreen.cs b/osu.Game/Screens/OnlinePlay/Lounge/LoungeSubScreen.cs index f00cf7427c..f3f4df166a 100644 --- a/osu.Game/Screens/OnlinePlay/Lounge/LoungeSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Lounge/LoungeSubScreen.cs @@ -263,6 +263,9 @@ namespace osu.Game.Screens.OnlinePlay.Lounge music.EnsurePlayingSomething(); onReturning(); + + // Poll for any newly-created rooms (including potentially the user's own). + ListingPollingComponent.PollImmediately(); } public override bool OnExiting(ScreenExitEvent e) @@ -297,14 +300,14 @@ namespace osu.Game.Screens.OnlinePlay.Lounge popoverContainer.HidePopover(); } - public virtual void Join(Room room, string? password, Action? onSuccess = null, Action? onFailure = null) => Schedule(() => + public void Join(Room room, string? password, Action? onSuccess = null, Action? onFailure = null) => Schedule(() => { if (joiningRoomOperation != null) return; joiningRoomOperation = ongoingOperationTracker?.BeginOperation(); - RoomManager?.JoinRoom(room, password, _ => + TryJoin(room, password, r => { Open(room); joiningRoomOperation?.Dispose(); @@ -318,6 +321,8 @@ namespace osu.Game.Screens.OnlinePlay.Lounge }); }); + protected abstract void TryJoin(Room room, string? password, Action onSuccess, Action onFailure); + /// /// Copies a room and opens it as a fresh (not-yet-created) one. /// diff --git a/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs b/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs index 4ef31c02c3..d37f3b877c 100644 --- a/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs @@ -343,7 +343,9 @@ namespace osu.Game.Screens.OnlinePlay.Match if (!ensureExitConfirmed()) return true; - RoomManager?.PartRoom(); + if (Room.RoomID != null) + PartRoom(); + Mods.Value = Array.Empty(); onLeaving(); @@ -351,6 +353,8 @@ namespace osu.Game.Screens.OnlinePlay.Match return base.OnExiting(e); } + protected abstract void PartRoom(); + private bool ensureExitConfirmed() { if (ExitConfirmed) diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Multiplayer.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Multiplayer.cs index bf316bb3da..dfed32aebc 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Multiplayer.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Multiplayer.cs @@ -8,7 +8,6 @@ using osu.Framework.Logging; using osu.Framework.Screens; using osu.Game.Online.Multiplayer; using osu.Game.Online.Rooms; -using osu.Game.Screens.OnlinePlay.Components; using osu.Game.Screens.OnlinePlay.Lounge; namespace osu.Game.Screens.OnlinePlay.Multiplayer @@ -97,8 +96,6 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer protected override string ScreenTitle => "Multiplayer"; - protected override RoomManager CreateRoomManager() => new MultiplayerRoomManager(); - protected override LoungeSubScreen CreateLounge() => new MultiplayerLoungeSubScreen(); public void Join(Room room, string? password) => Schedule(() => Lounge.Join(room, password)); diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerLoungeSubScreen.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerLoungeSubScreen.cs index dd61caa3db..e901ecbdce 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerLoungeSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerLoungeSubScreen.cs @@ -1,12 +1,13 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System; using System.Collections.Generic; using System.Threading.Tasks; using osu.Framework.Allocation; using osu.Framework.Bindables; +using osu.Framework.Extensions.ExceptionExtensions; using osu.Framework.Logging; -using osu.Framework.Screens; using osu.Framework.Graphics; using osu.Framework.Graphics.UserInterface; using osu.Game.Configuration; @@ -32,19 +33,6 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer private Dropdown roomAccessTypeDropdown = null!; private OsuCheckbox showInProgress = null!; - public override void OnResuming(ScreenTransitionEvent e) - { - base.OnResuming(e); - - // Upon having left a room, we don't know whether we were the only participant, and whether the room is now closed as a result of leaving it. - // To work around this, temporarily remove the room and trigger an immediate listing poll. - if (e.Last is MultiplayerMatchSubScreen match) - { - RoomManager?.RemoveRoom(match.Room); - ListingPollingComponent.PollImmediately(); - } - } - protected override IEnumerable CreateFilterControls() { foreach (var control in base.CreateFilterControls()) @@ -93,6 +81,24 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer protected override ListingPollingComponent CreatePollingComponent() => new MultiplayerListingPollingComponent(); + protected override void TryJoin(Room room, string? password, Action onSuccess, Action onFailure) + { + client.JoinRoom(room, password).ContinueWith(result => + { + if (result.IsCompletedSuccessfully) + onSuccess(room); + else + { + const string message = "Failed to join multiplayer room."; + + if (result.Exception != null) + Logger.Error(result.Exception, message); + + onFailure.Invoke(result.Exception?.AsSingular().Message ?? message); + } + }); + } + protected override void OpenNewRoom(Room room) { if (!client.IsConnected.Value) diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs index 06ea5ee033..553c0c9182 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs @@ -278,6 +278,8 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer return base.OnExiting(e); } + protected override void PartRoom() => client.LeaveRoom(); + private ModSettingChangeTracker? modSettingChangeTracker; private ScheduledDelegate? debouncedModSettingsUpdate; diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerRoomManager.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerRoomManager.cs deleted file mode 100644 index 7f09c9cbe9..0000000000 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerRoomManager.cs +++ /dev/null @@ -1,72 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -using System; -using System.Diagnostics; -using osu.Framework.Allocation; -using osu.Framework.Extensions.ExceptionExtensions; -using osu.Framework.Logging; -using osu.Game.Online.Multiplayer; -using osu.Game.Online.Rooms; -using osu.Game.Screens.OnlinePlay.Components; - -namespace osu.Game.Screens.OnlinePlay.Multiplayer -{ - public partial class MultiplayerRoomManager : RoomManager - { - [Resolved] - private MultiplayerClient multiplayerClient { get; set; } = null!; - - public override void CreateRoom(Room room, Action? onSuccess = null, Action? onError = null) - => base.CreateRoom(room, r => joinMultiplayerRoom(r, r.Password, onSuccess, onError), onError); - - public override void JoinRoom(Room room, string? password = null, Action? onSuccess = null, Action? onError = null) - { - if (!multiplayerClient.IsConnected.Value) - { - onError?.Invoke("Not currently connected to the multiplayer server."); - return; - } - - // this is done here as a pre-check to avoid clicking on already closed rooms in the lounge from triggering a server join. - // should probably be done at a higher level, but due to the current structure of things this is the easiest place for now. - if (room.HasEnded) - { - onError?.Invoke("Cannot join an ended room."); - return; - } - - base.JoinRoom(room, password, r => joinMultiplayerRoom(r, password, onSuccess, onError), onError); - } - - public override void PartRoom() - { - if (JoinedRoom.Value == null) - return; - - base.PartRoom(); - multiplayerClient.LeaveRoom(); - } - - private void joinMultiplayerRoom(Room room, string? password, Action? onSuccess = null, Action? onError = null) - { - Debug.Assert(room.RoomID != null); - - multiplayerClient.JoinRoom(room, password).ContinueWith(t => - { - if (t.IsCompletedSuccessfully) - Schedule(() => onSuccess?.Invoke(room)); - else if (t.IsFaulted) - { - const string message = "Failed to join multiplayer room."; - - if (t.Exception != null) - Logger.Error(t.Exception, message); - - PartRoom(); - Schedule(() => onError?.Invoke(t.Exception?.AsSingular().Message ?? message)); - } - }); - } - } -} diff --git a/osu.Game/Screens/OnlinePlay/OnlinePlayScreen.cs b/osu.Game/Screens/OnlinePlay/OnlinePlayScreen.cs index 17fb667e14..16462b90c1 100644 --- a/osu.Game/Screens/OnlinePlay/OnlinePlayScreen.cs +++ b/osu.Game/Screens/OnlinePlay/OnlinePlayScreen.cs @@ -36,12 +36,12 @@ namespace osu.Game.Screens.OnlinePlay private readonly ScreenStack screenStack = new OnlinePlaySubScreenStack { RelativeSizeAxes = Axes.Both }; private OnlinePlayScreenWaveContainer waves = null!; - [Cached(Type = typeof(IRoomManager))] - protected RoomManager RoomManager { get; private set; } - [Cached] private readonly OngoingOperationTracker ongoingOperationTracker = new OngoingOperationTracker(); + [Cached(Type = typeof(IRoomManager))] + private readonly RoomManager roomManager = new RoomManager(); + [Resolved] protected IAPIProvider API { get; private set; } = null!; @@ -51,8 +51,6 @@ namespace osu.Game.Screens.OnlinePlay Origin = Anchor.Centre; RelativeSizeAxes = Axes.Both; Padding = new MarginPadding { Horizontal = -HORIZONTAL_OVERFLOW_PADDING }; - - RoomManager = CreateRoomManager(); } private readonly IBindable apiState = new Bindable(); @@ -67,7 +65,7 @@ namespace osu.Game.Screens.OnlinePlay { screenStack, new Header(ScreenTitle, screenStack), - RoomManager, + roomManager, ongoingOperationTracker, } }; @@ -165,8 +163,6 @@ namespace osu.Game.Screens.OnlinePlay subScreen.Exit(); } - RoomManager.PartRoom(); - waves.Hide(); this.Delay(WaveContainer.DISAPPEAR_DURATION).FadeOut(); diff --git a/osu.Game/Screens/OnlinePlay/OnlinePlaySubScreen.cs b/osu.Game/Screens/OnlinePlay/OnlinePlaySubScreen.cs index fa1ee004c9..9b35a794a3 100644 --- a/osu.Game/Screens/OnlinePlay/OnlinePlaySubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/OnlinePlaySubScreen.cs @@ -1,7 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Screens; @@ -15,9 +14,6 @@ namespace osu.Game.Screens.OnlinePlay protected sealed override bool PlayExitSound => false; - [Resolved] - protected IRoomManager? RoomManager { get; private set; } - protected OnlinePlaySubScreen() { Anchor = Anchor.Centre; diff --git a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsLoungeSubScreen.cs b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsLoungeSubScreen.cs index d66b4f844c..92415e0eb1 100644 --- a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsLoungeSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsLoungeSubScreen.cs @@ -1,6 +1,7 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System; using System.Collections.Generic; using System.ComponentModel; using System.Linq; @@ -59,6 +60,20 @@ namespace osu.Game.Screens.OnlinePlay.Playlists return criteria; } + protected override void TryJoin(Room room, string? password, Action onSuccess, Action onFailure) + { + var joinRoomRequest = new JoinRoomRequest(room, password); + + joinRoomRequest.Success += r => onSuccess(r); + joinRoomRequest.Failure += exception => + { + if (exception is not OperationCanceledException) + onFailure(exception.Message); + }; + + api.Queue(joinRoomRequest); + } + protected override OsuButton CreateNewRoomButton() => new CreatePlaylistsRoomButton(); protected override Room CreateNewRoom() diff --git a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSettingsOverlay.cs b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSettingsOverlay.cs index 88af161cc8..b3d1d577ed 100644 --- a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSettingsOverlay.cs +++ b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSettingsOverlay.cs @@ -75,9 +75,6 @@ namespace osu.Game.Screens.OnlinePlay.Playlists private PurpleRoundedButton editPlaylistButton = null!; - [Resolved] - private IRoomManager? manager { get; set; } - [Resolved] private IAPIProvider api { get; set; } = null!; @@ -449,7 +446,11 @@ namespace osu.Game.Screens.OnlinePlay.Playlists room.Duration = DurationField.Current.Value; loadingLayer.Show(); - manager?.CreateRoom(room, onSuccess, onError); + + var req = new CreateRoomRequest(room); + req.Success += onSuccess; + req.Failure += e => onError(req.Response?.Error ?? e.Message); + api.Queue(req); } private void hideError() => ErrorText.FadeOut(50); diff --git a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs index 9b4630ac0b..064c355a69 100644 --- a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs @@ -290,6 +290,8 @@ namespace osu.Game.Screens.OnlinePlay.Playlists })); } + protected override void PartRoom() => api.Queue(new PartRoomRequest(Room)); + protected override Screen CreateGameplayScreen(PlaylistItem selectedItem) { return new PlayerLoader(() => new PlaylistsPlayer(Room, selectedItem) diff --git a/osu.Game/Tests/Visual/Multiplayer/MultiplayerTestScene.cs b/osu.Game/Tests/Visual/Multiplayer/MultiplayerTestScene.cs index 42cf317829..dca1fc8f3c 100644 --- a/osu.Game/Tests/Visual/Multiplayer/MultiplayerTestScene.cs +++ b/osu.Game/Tests/Visual/Multiplayer/MultiplayerTestScene.cs @@ -56,7 +56,7 @@ namespace osu.Game.Tests.Visual.Multiplayer AddStep("join room", () => { SelectedRoom.Value = CreateRoom(); - RoomManager.CreateRoom(SelectedRoom.Value); + API.Queue(new CreateRoomRequest(SelectedRoom.Value)); }); AddUntilStep("wait for room join", () => RoomJoined); diff --git a/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerRoomManager.cs b/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerRoomManager.cs index b998a638e5..59ac9a9749 100644 --- a/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerRoomManager.cs +++ b/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerRoomManager.cs @@ -1,12 +1,10 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using System; using System.Collections.Generic; using osu.Game.Online.API.Requests.Responses; using osu.Game.Online.Rooms; using osu.Game.Screens.OnlinePlay.Components; -using osu.Game.Screens.OnlinePlay.Multiplayer; using osu.Game.Tests.Visual.OnlinePlay; namespace osu.Game.Tests.Visual.Multiplayer @@ -15,7 +13,7 @@ namespace osu.Game.Tests.Visual.Multiplayer /// A for use in multiplayer test scenes. /// Should generally not be used by itself outside of a . /// - public partial class TestMultiplayerRoomManager : MultiplayerRoomManager + public partial class TestMultiplayerRoomManager : RoomManager { private readonly TestRoomRequestsHandler requestsHandler; @@ -26,12 +24,6 @@ namespace osu.Game.Tests.Visual.Multiplayer public IReadOnlyList ServerSideRooms => requestsHandler.ServerSideRooms; - public override void CreateRoom(Room room, Action? onSuccess = null, Action? onError = null) - => base.CreateRoom(room, r => onSuccess?.Invoke(r), onError); - - public override void JoinRoom(Room room, string? password = null, Action? onSuccess = null, Action? onError = null) - => base.JoinRoom(room, password, r => onSuccess?.Invoke(r), onError); - /// /// Adds a room to a local "server-side" list that's returned when a is fired. /// diff --git a/osu.Game/Tests/Visual/OnlinePlay/TestRoomManager.cs b/osu.Game/Tests/Visual/OnlinePlay/TestRoomManager.cs index b1e3eafacc..60d169a46f 100644 --- a/osu.Game/Tests/Visual/OnlinePlay/TestRoomManager.cs +++ b/osu.Game/Tests/Visual/OnlinePlay/TestRoomManager.cs @@ -2,7 +2,9 @@ // See the LICENCE file in the repository root for full licence text. using System; +using osu.Framework.Allocation; using osu.Game.Beatmaps; +using osu.Game.Online.API; using osu.Game.Online.API.Requests.Responses; using osu.Game.Online.Rooms; using osu.Game.Rulesets; @@ -15,15 +17,10 @@ namespace osu.Game.Tests.Visual.OnlinePlay /// public partial class TestRoomManager : RoomManager { - public Action? JoinRoomRequested; - private int currentRoomId; - public override void JoinRoom(Room room, string? password = null, Action? onSuccess = null, Action? onError = null) - { - JoinRoomRequested?.Invoke(room, password); - base.JoinRoom(room, password, onSuccess, onError); - } + [Resolved] + private IAPIProvider api { get; set; } = null!; public void AddRooms(int count, RulesetInfo? ruleset = null, bool withPassword = false, bool withSpotlightRooms = false) { @@ -49,7 +46,7 @@ namespace osu.Game.Tests.Visual.OnlinePlay public void AddRoom(Room room) { room.RoomID = -currentRoomId; - CreateRoom(room); + api.Queue(new CreateRoomRequest(room)); currentRoomId++; } } From 2c0d6b14c82969a850b292f785a678016e06ed26 Mon Sep 17 00:00:00 2001 From: tsunyoku Date: Wed, 22 Jan 2025 13:24:30 +0000 Subject: [PATCH 0708/1275] Fix incorrect namespace --- .../Preprocessing/Rhythm/TaikoRhythmDifficultyPreprocessor.cs | 1 + .../Difficulty/Utils/IntervalGroupingUtils.cs | 3 +-- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/TaikoRhythmDifficultyPreprocessor.cs b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/TaikoRhythmDifficultyPreprocessor.cs index fa2135caf3..cd56d835dc 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/TaikoRhythmDifficultyPreprocessor.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/TaikoRhythmDifficultyPreprocessor.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Rhythm.Data; +using osu.Game.Rulesets.Taiko.Difficulty.Utils; namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Rhythm { diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Utils/IntervalGroupingUtils.cs b/osu.Game.Rulesets.Taiko/Difficulty/Utils/IntervalGroupingUtils.cs index 22ded8a966..3b6f5406b4 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Utils/IntervalGroupingUtils.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Utils/IntervalGroupingUtils.cs @@ -3,9 +3,8 @@ using System.Collections.Generic; using osu.Framework.Utils; -using osu.Game.Rulesets.Taiko.Difficulty.Utils; -namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Rhythm.Data +namespace osu.Game.Rulesets.Taiko.Difficulty.Utils { public static class IntervalGroupingUtils { From 753e9ef7c79f85d027557295c0c60fb4fa09210c Mon Sep 17 00:00:00 2001 From: tsunyoku Date: Wed, 22 Jan 2025 13:26:12 +0000 Subject: [PATCH 0709/1275] Keep old behaviour of `double.PositiveInfinity` being the default for `Interval` --- .../Preprocessing/Rhythm/Data/SameRhythmGroupedHitObjects.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/Data/SameRhythmGroupedHitObjects.cs b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/Data/SameRhythmGroupedHitObjects.cs index dc6cf45d23..4f7023059f 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/Data/SameRhythmGroupedHitObjects.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/Data/SameRhythmGroupedHitObjects.cs @@ -57,7 +57,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Rhythm.Data : 1; // Calculate the interval from the previous group's start time - Interval = Previous != null ? StartTime - Previous.StartTime : 0; + Interval = Previous != null ? StartTime - Previous.StartTime : double.PositiveInfinity; } } } From f673d16a1f97d153f74cfbd6e8549886552910cb Mon Sep 17 00:00:00 2001 From: Layendan Date: Wed, 22 Jan 2025 11:42:11 -0700 Subject: [PATCH 0710/1275] Fix formatting --- .../Lounge/Components/DrawableRoom.cs | 19 ++++++++++------- .../OnlinePlay/Lounge/DrawableLoungeRoom.cs | 21 +++++++++++-------- 2 files changed, 23 insertions(+), 17 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/Lounge/Components/DrawableRoom.cs b/osu.Game/Screens/OnlinePlay/Lounge/Components/DrawableRoom.cs index 321a1131de..7fefa0a1a8 100644 --- a/osu.Game/Screens/OnlinePlay/Lounge/Components/DrawableRoom.cs +++ b/osu.Game/Screens/OnlinePlay/Lounge/Components/DrawableRoom.cs @@ -44,7 +44,7 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components private IAPIProvider api { get; set; } = null!; [Resolved] - private OsuGame? game { get; set; } = null!; + private OsuGame? game { get; set; } public readonly Room Room; @@ -348,13 +348,16 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components if (Room.RoomID.HasValue) { - items.AddRange([new OsuMenuItem("View in browser", MenuItemType.Standard, () => - { - game?.OpenUrlExternally(formatRoomUrl(Room.RoomID.Value)); - }), new OsuMenuItem("Copy link", MenuItemType.Standard, () => - { - game?.CopyUrlToClipboard(formatRoomUrl(Room.RoomID.Value)); - })]); + items.AddRange([ + new OsuMenuItem("View in browser", MenuItemType.Standard, () => + { + game?.OpenUrlExternally(formatRoomUrl(Room.RoomID.Value)); + }), + new OsuMenuItem("Copy link", MenuItemType.Standard, () => + { + game?.CopyUrlToClipboard(formatRoomUrl(Room.RoomID.Value)); + }) + ]); } return items.ToArray(); diff --git a/osu.Game/Screens/OnlinePlay/Lounge/DrawableLoungeRoom.cs b/osu.Game/Screens/OnlinePlay/Lounge/DrawableLoungeRoom.cs index 2c15e5107a..da04152bd3 100644 --- a/osu.Game/Screens/OnlinePlay/Lounge/DrawableLoungeRoom.cs +++ b/osu.Game/Screens/OnlinePlay/Lounge/DrawableLoungeRoom.cs @@ -60,7 +60,7 @@ namespace osu.Game.Screens.OnlinePlay.Lounge private IAPIProvider api { get; set; } = null!; [Resolved] - private OsuGame? game { get; set; } = null!; + private OsuGame? game { get; set; } private readonly BindableWithCurrent selectedRoom = new BindableWithCurrent(); private Sample? sampleSelect; @@ -158,7 +158,7 @@ namespace osu.Game.Screens.OnlinePlay.Lounge public Popover GetPopover() => new PasswordEntryPopover(Room); - public MenuItem[] ContextMenuItems + public new MenuItem[] ContextMenuItems { get { @@ -172,13 +172,16 @@ namespace osu.Game.Screens.OnlinePlay.Lounge if (Room.RoomID.HasValue) { - items.AddRange([new OsuMenuItem("View in browser", MenuItemType.Standard, () => - { - game?.OpenUrlExternally(formatRoomUrl(Room.RoomID.Value)); - }), new OsuMenuItem("Copy link", MenuItemType.Standard, () => - { - game?.CopyUrlToClipboard(formatRoomUrl(Room.RoomID.Value)); - })]); + items.AddRange([ + new OsuMenuItem("View in browser", MenuItemType.Standard, () => + { + game?.OpenUrlExternally(formatRoomUrl(Room.RoomID.Value)); + }), + new OsuMenuItem("Copy link", MenuItemType.Standard, () => + { + game?.CopyUrlToClipboard(formatRoomUrl(Room.RoomID.Value)); + }) + ]); } if (Room.Type == MatchType.Playlists && Room.Host?.Id == api.LocalUser.Value.Id && Room.StartDate?.AddMinutes(5) >= DateTimeOffset.Now && !Room.HasEnded) From 865757621082cb3e2cba36a7f6a5dbd5d71d74a4 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 17 Jan 2025 18:25:31 +0900 Subject: [PATCH 0711/1275] Show selection defaults in test scene (and make prettier) --- .../SongSelect/TestSceneBeatmapCarouselV2.cs | 39 ++++++++++++------- 1 file changed, 26 insertions(+), 13 deletions(-) diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2.cs index b13d450c32..984352b2f5 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2.cs @@ -16,6 +16,7 @@ using osu.Framework.Testing; using osu.Framework.Utils; using osu.Game.Beatmaps; using osu.Game.Database; +using osu.Game.Graphics; using osu.Game.Graphics.Containers; using osu.Game.Screens.Select; using osu.Game.Screens.Select.Filter; @@ -117,12 +118,11 @@ namespace osu.Game.Tests.Visual.SongSelect } } }, - stats = new OsuTextFlowContainer(cp => cp.Font = FrameworkFont.Regular.With()) + stats = new OsuTextFlowContainer { + AutoSizeAxes = Axes.Both, Padding = new MarginPadding(10), TextAnchor = Anchor.CentreLeft, - Anchor = Anchor.CentreLeft, - Origin = Anchor.CentreLeft, }, }; }); @@ -258,16 +258,29 @@ namespace osu.Game.Tests.Visual.SongSelect if (carousel.IsNull()) return; - stats.Text = $""" - store - sets: {beatmapSets.Count} - beatmaps: {beatmapCount} - carousel: - sorting: {carousel.IsFiltering} - tracked: {carousel.ItemsTracked} - displayable: {carousel.DisplayableItems} - displayed: {carousel.VisibleItems} - """; + stats.Clear(); + createHeader("beatmap store"); + stats.AddParagraph($""" + sets: {beatmapSets.Count} + beatmaps: {beatmapCount} + """); + createHeader("carousel"); + stats.AddParagraph($""" + sorting: {carousel.IsFiltering} + tracked: {carousel.ItemsTracked} + displayable: {carousel.DisplayableItems} + displayed: {carousel.VisibleItems} + selected: {carousel.CurrentSelection} + """); + + void createHeader(string text) + { + stats.AddParagraph(string.Empty); + stats.AddParagraph(text, cp => + { + cp.Font = cp.Font.With(size: 18, weight: FontWeight.Bold); + }); + } } } } From 6ac2dbc818ff5d5de1249280095e0804284ce327 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 17 Jan 2025 18:49:12 +0900 Subject: [PATCH 0712/1275] Reorder carousel methods into logical regions --- osu.Game/Screens/SelectV2/Carousel.cs | 68 +++++++++++++++++---------- 1 file changed, 44 insertions(+), 24 deletions(-) diff --git a/osu.Game/Screens/SelectV2/Carousel.cs b/osu.Game/Screens/SelectV2/Carousel.cs index ec1bf6b7c0..190792b19e 100644 --- a/osu.Game/Screens/SelectV2/Carousel.cs +++ b/osu.Game/Screens/SelectV2/Carousel.cs @@ -30,10 +30,7 @@ namespace osu.Game.Screens.SelectV2 /// public abstract partial class Carousel : CompositeDrawable { - /// - /// A collection of filters which should be run each time a is executed. - /// - protected IEnumerable Filters { get; init; } = Enumerable.Empty(); + #region Properties and methods for external usage /// /// Height of the area above the carousel that should be treated as visible due to transparency of elements in front of it. @@ -82,15 +79,6 @@ namespace osu.Game.Screens.SelectV2 /// public int VisibleItems => scroll.Panels.Count; - /// - /// All items which are to be considered for display in this carousel. - /// Mutating this list will automatically queue a . - /// - /// - /// Note that an may add new items which are displayed but not tracked in this list. - /// - protected readonly BindableList Items = new BindableList(); - /// /// The currently selected model. /// @@ -114,20 +102,31 @@ namespace osu.Game.Screens.SelectV2 } } - private List? displayedCarouselItems; + #endregion - private readonly CarouselScrollContainer scroll; + #region Properties and methods concerning implementations - protected Carousel() - { - InternalChild = scroll = new CarouselScrollContainer - { - RelativeSizeAxes = Axes.Both, - Masking = false, - }; + /// + /// A collection of filters which should be run each time a is executed. + /// + /// + /// Implementations should add all required filters as part of their initialisation. + /// + /// Importantly, each filter is sequentially run in the order provided. + /// Each filter receives the output of the previous filter. + /// + /// A filter may add, mutate or remove items. + /// + protected IEnumerable Filters { get; init; } = Enumerable.Empty(); - Items.BindCollectionChanged((_, _) => FilterAsync()); - } + /// + /// All items which are to be considered for display in this carousel. + /// Mutating this list will automatically queue a . + /// + /// + /// Note that an may add new items which are displayed but not tracked in this list. + /// + protected readonly BindableList Items = new BindableList(); /// /// Queue an asynchronous filter operation. @@ -151,8 +150,29 @@ namespace osu.Game.Screens.SelectV2 /// A representing the model. protected abstract CarouselItem CreateCarouselItemForModel(T model); + #endregion + + #region Initialisation + + private readonly CarouselScrollContainer scroll; + + protected Carousel() + { + InternalChild = scroll = new CarouselScrollContainer + { + RelativeSizeAxes = Axes.Both, + Masking = false, + }; + + Items.BindCollectionChanged((_, _) => FilterAsync()); + } + + #endregion + #region Filtering and display preparation + private List? displayedCarouselItems; + private Task filterTask = Task.CompletedTask; private CancellationTokenSource cancellationSource = new CancellationTokenSource(); From d5268356277030b4ef36b6fe2623d58193da256c Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 23 Jan 2025 04:03:43 +0900 Subject: [PATCH 0713/1275] Only show loading when doing a user triggered filter --- osu.Game/Screens/SelectV2/BeatmapCarousel.cs | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/osu.Game/Screens/SelectV2/BeatmapCarousel.cs b/osu.Game/Screens/SelectV2/BeatmapCarousel.cs index 93d4c90be0..d9c049bbae 100644 --- a/osu.Game/Screens/SelectV2/BeatmapCarousel.cs +++ b/osu.Game/Screens/SelectV2/BeatmapCarousel.cs @@ -6,7 +6,6 @@ using System.Collections.Generic; using System.Collections.Specialized; using System.Linq; using System.Threading; -using System.Threading.Tasks; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; @@ -14,7 +13,6 @@ using osu.Framework.Graphics.Pooling; using osu.Game.Beatmaps; using osu.Game.Database; using osu.Game.Graphics.UserInterface; -using osu.Game.Online.Multiplayer; using osu.Game.Screens.Select; namespace osu.Game.Screens.SelectV2 @@ -93,14 +91,8 @@ namespace osu.Game.Screens.SelectV2 public void Filter(FilterCriteria criteria) { Criteria = criteria; - FilterAsync().FireAndForget(); - } - - protected override async Task FilterAsync() - { loading.Show(); - await base.FilterAsync().ConfigureAwait(true); - loading.Hide(); + FilterAsync().ContinueWith(_ => Schedule(() => loading.Hide())); } } } From ded1d9f01994e5e54e52f4ee02fd9f02ecad4847 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 23 Jan 2025 15:58:35 +0900 Subject: [PATCH 0714/1275] `displayedCarouselItems` -> `carouselItems` --- osu.Game/Screens/SelectV2/Carousel.cs | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/osu.Game/Screens/SelectV2/Carousel.cs b/osu.Game/Screens/SelectV2/Carousel.cs index 190792b19e..c042da167e 100644 --- a/osu.Game/Screens/SelectV2/Carousel.cs +++ b/osu.Game/Screens/SelectV2/Carousel.cs @@ -72,7 +72,7 @@ namespace osu.Game.Screens.SelectV2 /// /// The number of carousel items currently in rotation for display. /// - public int DisplayableItems => displayedCarouselItems?.Count ?? 0; + public int DisplayableItems => carouselItems?.Count ?? 0; /// /// The number of items currently actualised into drawables. @@ -171,7 +171,7 @@ namespace osu.Game.Screens.SelectV2 #region Filtering and display preparation - private List? displayedCarouselItems; + private List? carouselItems; private Task filterTask = Task.CompletedTask; private CancellationTokenSource cancellationSource = new CancellationTokenSource(); @@ -222,7 +222,7 @@ namespace osu.Game.Screens.SelectV2 return; log("Items ready for display"); - displayedCarouselItems = items.ToList(); + carouselItems = items.ToList(); displayedRange = null; updateSelection(); @@ -253,9 +253,9 @@ namespace osu.Game.Screens.SelectV2 { currentSelectionCarouselItem = null; - if (displayedCarouselItems == null) return; + if (carouselItems == null) return; - foreach (var item in displayedCarouselItems) + foreach (var item in carouselItems) { bool isSelected = item.Model == currentSelection; @@ -306,7 +306,7 @@ namespace osu.Game.Screens.SelectV2 { base.Update(); - if (displayedCarouselItems == null) + if (carouselItems == null) return; var range = getDisplayRange(); @@ -356,15 +356,15 @@ namespace osu.Game.Screens.SelectV2 private DisplayRange getDisplayRange() { - Debug.Assert(displayedCarouselItems != null); + Debug.Assert(carouselItems != null); // Find index range of all items that should be on-screen carouselBoundsItem.CarouselYPosition = visibleUpperBound - DistanceOffscreenToPreload; - int firstIndex = displayedCarouselItems.BinarySearch(carouselBoundsItem); + int firstIndex = carouselItems.BinarySearch(carouselBoundsItem); if (firstIndex < 0) firstIndex = ~firstIndex; carouselBoundsItem.CarouselYPosition = visibleBottomBound + DistanceOffscreenToPreload; - int lastIndex = displayedCarouselItems.BinarySearch(carouselBoundsItem); + int lastIndex = carouselItems.BinarySearch(carouselBoundsItem); if (lastIndex < 0) lastIndex = ~lastIndex; firstIndex = Math.Max(0, firstIndex - 1); @@ -375,11 +375,11 @@ namespace osu.Game.Screens.SelectV2 private void updateDisplayedRange(DisplayRange range) { - Debug.Assert(displayedCarouselItems != null); + Debug.Assert(carouselItems != null); List toDisplay = range.Last - range.First == 0 ? new List() - : displayedCarouselItems.GetRange(range.First, range.Last - range.First + 1); + : carouselItems.GetRange(range.First, range.Last - range.First + 1); // Iterate over all panels which are already displayed and figure which need to be displayed / removed. foreach (var panel in scroll.Panels) @@ -415,9 +415,9 @@ namespace osu.Game.Screens.SelectV2 // Update the total height of all items (to make the scroll container scrollable through the full height even though // most items are not displayed / loaded). - if (displayedCarouselItems.Count > 0) + if (carouselItems.Count > 0) { - var lastItem = displayedCarouselItems[^1]; + var lastItem = carouselItems[^1]; scroll.SetLayoutHeight((float)(lastItem.CarouselYPosition + lastItem.DrawHeight + visibleHalfHeight)); } else From 9a623257f5bd8cfed7f2d691fbb1c2959483c111 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Thu, 23 Jan 2025 16:19:09 +0900 Subject: [PATCH 0715/1275] Adjust + fix tests --- .../StatefulMultiplayerClientTest.cs | 7 +- .../TestSceneDrawableLoungeRoom.cs | 2 +- .../Multiplayer/TestSceneMultiplayer.cs | 15 ++-- .../TestSceneMultiplayerLoungeSubScreen.cs | 58 +++++++++++--- .../TestSceneMultiplayerPlaylist.cs | 10 +-- .../TestScenePlaylistsLoungeSubScreen.cs | 30 ++++++- .../TestScenePlaylistsMatchSettingsOverlay.cs | 78 +++++++------------ .../Visual/TestMultiplayerComponents.cs | 24 ++---- osu.Game/Online/Rooms/Room.cs | 17 ++++ .../OnlinePlay/Lounge/DrawableLoungeRoom.cs | 14 +--- .../OnlinePlay/Lounge/IOnlinePlayLounge.cs | 32 ++++++++ .../OnlinePlay/Lounge/LoungeSubScreen.cs | 19 +++-- .../Screens/OnlinePlay/OnlinePlayScreen.cs | 2 - .../IMultiplayerTestSceneDependencies.cs | 6 -- .../Multiplayer/MultiplayerTestScene.cs | 3 +- .../MultiplayerTestSceneDependencies.cs | 6 +- .../Multiplayer/TestMultiplayerClient.cs | 35 +++++++-- .../Multiplayer/TestMultiplayerRoomManager.cs | 34 -------- .../OnlinePlayTestSceneDependencies.cs | 4 +- .../Visual/OnlinePlay/TestRoomManager.cs | 20 +++-- 20 files changed, 232 insertions(+), 184 deletions(-) create mode 100644 osu.Game/Screens/OnlinePlay/Lounge/IOnlinePlayLounge.cs delete mode 100644 osu.Game/Tests/Visual/Multiplayer/TestMultiplayerRoomManager.cs diff --git a/osu.Game.Tests/NonVisual/Multiplayer/StatefulMultiplayerClientTest.cs b/osu.Game.Tests/NonVisual/Multiplayer/StatefulMultiplayerClientTest.cs index 559db16751..be30e06ed4 100644 --- a/osu.Game.Tests/NonVisual/Multiplayer/StatefulMultiplayerClientTest.cs +++ b/osu.Game.Tests/NonVisual/Multiplayer/StatefulMultiplayerClientTest.cs @@ -8,7 +8,6 @@ using osu.Framework.Extensions.ObjectExtensions; using osu.Framework.Testing; using osu.Game.Online.API.Requests.Responses; using osu.Game.Online.Multiplayer; -using osu.Game.Online.Rooms; using osu.Game.Tests.Visual.Multiplayer; namespace osu.Game.Tests.NonVisual.Multiplayer @@ -72,10 +71,6 @@ namespace osu.Game.Tests.NonVisual.Multiplayer AddStep("create room initially in gameplay", () => { - var newRoom = new Room(); - newRoom.CopyFrom(SelectedRoom.Value!); - - newRoom.RoomID = null; MultiplayerClient.RoomSetupAction = room => { room.State = MultiplayerRoomState.Playing; @@ -86,7 +81,7 @@ namespace osu.Game.Tests.NonVisual.Multiplayer }); }; - RoomManager.CreateRoom(newRoom); + MultiplayerClient.JoinRoom(MultiplayerClient.ServerSideRooms.Single()).ConfigureAwait(false); }); AddUntilStep("wait for room join", () => RoomJoined); diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneDrawableLoungeRoom.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneDrawableLoungeRoom.cs index c5fb52461a..459a90d096 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneDrawableLoungeRoom.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneDrawableLoungeRoom.cs @@ -38,7 +38,7 @@ namespace osu.Game.Tests.Visual.Multiplayer [BackgroundDependencyLoader] private void load() { - var mockLounge = new Mock(); + var mockLounge = new Mock(); mockLounge .Setup(l => l.Join(It.IsAny(), It.IsAny(), It.IsAny>(), It.IsAny>())) .Callback, Action>((_, _, _, d) => diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs index fb653cea8b..0966c61a3a 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs @@ -58,7 +58,6 @@ namespace osu.Game.Tests.Visual.Multiplayer private TestMultiplayerComponents multiplayerComponents = null!; private TestMultiplayerClient multiplayerClient => multiplayerComponents.MultiplayerClient; - private TestMultiplayerRoomManager roomManager => multiplayerComponents.RoomManager; [Resolved] private OsuConfigManager config { get; set; } = null!; @@ -257,7 +256,7 @@ namespace osu.Game.Tests.Visual.Multiplayer { AddStep("create room", () => { - roomManager.AddServerSideRoom(new Room + multiplayerClient.AddServerSideRoom(new Room { Name = "Test Room", Playlist = @@ -286,7 +285,7 @@ namespace osu.Game.Tests.Visual.Multiplayer { AddStep("create room", () => { - roomManager.AddServerSideRoom(new Room + multiplayerClient.AddServerSideRoom(new Room { Name = "Test Room", Playlist = @@ -336,7 +335,7 @@ namespace osu.Game.Tests.Visual.Multiplayer { AddStep("create room", () => { - roomManager.AddServerSideRoom(new Room + multiplayerClient.AddServerSideRoom(new Room { Name = "Test Room", Password = "password", @@ -789,7 +788,7 @@ namespace osu.Game.Tests.Visual.Multiplayer { AddStep("create room", () => { - roomManager.AddServerSideRoom(new Room + multiplayerClient.AddServerSideRoom(new Room { Name = "Test Room", QueueMode = QueueMode.AllPlayers, @@ -810,8 +809,8 @@ namespace osu.Game.Tests.Visual.Multiplayer AddStep("disable polling", () => this.ChildrenOfType().Single().TimeBetweenPolls.Value = 0); AddStep("change server-side settings", () => { - roomManager.ServerSideRooms[0].Name = "New name"; - roomManager.ServerSideRooms[0].Playlist = + multiplayerClient.ServerSideRooms[0].Name = "New name"; + multiplayerClient.ServerSideRooms[0].Playlist = [ new PlaylistItem(beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First(b => b.Ruleset.OnlineID == 0)).BeatmapInfo) { @@ -828,7 +827,7 @@ namespace osu.Game.Tests.Visual.Multiplayer AddAssert("local room has correct settings", () => { var localRoom = this.ChildrenOfType().Single().Room; - return localRoom.Name == roomManager.ServerSideRooms[0].Name && localRoom.Playlist.Single().ID == 2; + return localRoom.Name == multiplayerClient.ServerSideRooms[0].Name && localRoom.Playlist.Single().ID == 2; }); } diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerLoungeSubScreen.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerLoungeSubScreen.cs index d06a91433d..4a259149e2 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerLoungeSubScreen.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerLoungeSubScreen.cs @@ -9,18 +9,26 @@ using osu.Framework.Screens; using osu.Framework.Testing; using osu.Game.Graphics.UserInterface; using osu.Game.Screens.OnlinePlay.Lounge; +using osu.Game.Screens.OnlinePlay.Lounge.Components; using osu.Game.Screens.OnlinePlay.Multiplayer; using osu.Game.Tests.Visual.OnlinePlay; using osuTK.Input; namespace osu.Game.Tests.Visual.Multiplayer { - public partial class TestSceneMultiplayerLoungeSubScreen : OnlinePlayTestScene + public partial class TestSceneMultiplayerLoungeSubScreen : MultiplayerTestScene { protected new TestRoomManager RoomManager => (TestRoomManager)base.RoomManager; private LoungeSubScreen loungeScreen = null!; + private RoomsContainer roomsContainer => loungeScreen.ChildrenOfType().First(); + + public TestSceneMultiplayerLoungeSubScreen() + : base(false) + { + } + public override void SetUpSteps() { base.SetUpSteps(); @@ -32,15 +40,17 @@ namespace osu.Game.Tests.Visual.Multiplayer [Test] public void TestJoinRoomWithoutPassword() { - AddStep("add room", () => RoomManager.AddRooms(1, withPassword: false)); + addRoom(false); AddStep("select room", () => InputManager.Key(Key.Down)); AddStep("join room", () => InputManager.Key(Key.Enter)); + + AddAssert("room joined", () => MultiplayerClient.RoomJoined); } [Test] public void TestPopoverHidesOnBackButton() { - AddStep("add room", () => RoomManager.AddRooms(1, withPassword: true)); + addRoom(true); AddStep("select room", () => InputManager.Key(Key.Down)); AddStep("attempt join room", () => InputManager.Key(Key.Enter)); @@ -53,18 +63,22 @@ namespace osu.Game.Tests.Visual.Multiplayer AddStep("hit escape", () => InputManager.Key(Key.Escape)); AddUntilStep("password prompt hidden", () => !InputManager.ChildrenOfType().Any()); + + AddAssert("room not joined", () => !MultiplayerClient.RoomJoined); } [Test] public void TestPopoverHidesOnLeavingScreen() { - AddStep("add room", () => RoomManager.AddRooms(1, withPassword: true)); + addRoom(true); AddStep("select room", () => InputManager.Key(Key.Down)); AddStep("attempt join room", () => InputManager.Key(Key.Enter)); AddUntilStep("password prompt appeared", () => InputManager.ChildrenOfType().Any()); AddStep("exit screen", () => Stack.Exit()); AddUntilStep("password prompt hidden", () => !InputManager.ChildrenOfType().Any()); + + AddAssert("room not joined", () => !MultiplayerClient.RoomJoined); } [Test] @@ -72,16 +86,18 @@ namespace osu.Game.Tests.Visual.Multiplayer { DrawableLoungeRoom.PasswordEntryPopover? passwordEntryPopover = null; - AddStep("add room", () => RoomManager.AddRooms(1, withPassword: true)); + addRoom(true); AddStep("select room", () => InputManager.Key(Key.Down)); AddStep("attempt join room", () => InputManager.Key(Key.Enter)); AddUntilStep("password prompt appeared", () => (passwordEntryPopover = InputManager.ChildrenOfType().FirstOrDefault()) != null); AddStep("enter password in text box", () => passwordEntryPopover.ChildrenOfType().First().Text = "wrong"); AddStep("press join room button", () => passwordEntryPopover.ChildrenOfType().First().TriggerClick()); - AddAssert("room not joined", () => loungeScreen.IsCurrentScreen()); + AddAssert("still at lounge", () => loungeScreen.IsCurrentScreen()); AddUntilStep("password prompt still visible", () => passwordEntryPopover!.State.Value == Visibility.Visible); AddAssert("textbox still focused", () => InputManager.FocusedDrawable is OsuPasswordTextBox); + + AddAssert("room not joined", () => !MultiplayerClient.RoomJoined); } [Test] @@ -89,16 +105,18 @@ namespace osu.Game.Tests.Visual.Multiplayer { DrawableLoungeRoom.PasswordEntryPopover? passwordEntryPopover = null; - AddStep("add room", () => RoomManager.AddRooms(1, withPassword: true)); + addRoom(true); AddStep("select room", () => InputManager.Key(Key.Down)); AddStep("attempt join room", () => InputManager.Key(Key.Enter)); AddUntilStep("password prompt appeared", () => (passwordEntryPopover = InputManager.ChildrenOfType().FirstOrDefault()) != null); AddStep("enter password in text box", () => passwordEntryPopover.ChildrenOfType().First().Text = "wrong"); AddStep("press enter", () => InputManager.Key(Key.Enter)); - AddAssert("room not joined", () => loungeScreen.IsCurrentScreen()); + AddAssert("still at lounge", () => loungeScreen.IsCurrentScreen()); AddUntilStep("password prompt still visible", () => passwordEntryPopover!.State.Value == Visibility.Visible); AddAssert("textbox still focused", () => InputManager.FocusedDrawable is OsuPasswordTextBox); + + AddAssert("room not joined", () => !MultiplayerClient.RoomJoined); } [Test] @@ -106,12 +124,14 @@ namespace osu.Game.Tests.Visual.Multiplayer { DrawableLoungeRoom.PasswordEntryPopover? passwordEntryPopover = null; - AddStep("add room", () => RoomManager.AddRooms(1, withPassword: true)); + addRoom(true); AddStep("select room", () => InputManager.Key(Key.Down)); AddStep("attempt join room", () => InputManager.Key(Key.Enter)); AddUntilStep("password prompt appeared", () => (passwordEntryPopover = InputManager.ChildrenOfType().FirstOrDefault()) != null); AddStep("enter password in text box", () => passwordEntryPopover.ChildrenOfType().First().Text = "password"); AddStep("press join room button", () => passwordEntryPopover.ChildrenOfType().First().TriggerClick()); + + AddUntilStep("room joined", () => MultiplayerClient.RoomJoined); } [Test] @@ -119,12 +139,30 @@ namespace osu.Game.Tests.Visual.Multiplayer { DrawableLoungeRoom.PasswordEntryPopover? passwordEntryPopover = null; - AddStep("add room", () => RoomManager.AddRooms(1, withPassword: true)); + addRoom(true); AddStep("select room", () => InputManager.Key(Key.Down)); AddStep("attempt join room", () => InputManager.Key(Key.Enter)); AddUntilStep("password prompt appeared", () => (passwordEntryPopover = InputManager.ChildrenOfType().FirstOrDefault()) != null); AddStep("enter password in text box", () => passwordEntryPopover.ChildrenOfType().First().Text = "password"); AddStep("press enter", () => InputManager.Key(Key.Enter)); + + AddAssert("room joined", () => MultiplayerClient.RoomJoined); } + + private void addRoom(bool withPassword) + { + int initialRoomCount = 0; + + AddStep("add room", () => + { + initialRoomCount = roomsContainer.Rooms.Count; + RoomManager.AddRooms(1, withPassword: withPassword); + loungeScreen.RefreshRooms(); + }); + + AddUntilStep("wait for room to appear", () => roomsContainer.Rooms.Count == initialRoomCount + 1); + } + + protected override OnlinePlayTestSceneDependencies CreateOnlinePlayDependencies() => new MultiplayerTestSceneDependencies(); } } diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerPlaylist.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerPlaylist.cs index 36f5bba384..77b75f407b 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerPlaylist.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerPlaylist.cs @@ -127,7 +127,7 @@ namespace osu.Game.Tests.Visual.Multiplayer addItemStep(); AddStep("finish current item", () => MultiplayerClient.FinishCurrentItem().WaitSafely()); - AddStep("leave room", () => RoomManager.PartRoom()); + AddStep("leave room", () => MultiplayerClient.LeaveRoom()); AddUntilStep("wait for room part", () => !RoomJoined); AddUntilStep("item 0 not in lists", () => !inHistoryList(0) && !inQueueList(0)); @@ -148,7 +148,7 @@ namespace osu.Game.Tests.Visual.Multiplayer AddStep("finish current item", () => MultiplayerClient.FinishCurrentItem().WaitSafely()); assertQueueTabCount(2); - AddStep("leave room", () => RoomManager.PartRoom()); + AddStep("leave room", () => MultiplayerClient.LeaveRoom()); AddUntilStep("wait for room part", () => !RoomJoined); assertQueueTabCount(0); } @@ -157,12 +157,12 @@ namespace osu.Game.Tests.Visual.Multiplayer [Test] public void TestJoinRoomWithMixedItemsAddedInCorrectLists() { - AddStep("leave room", () => RoomManager.PartRoom()); + AddStep("leave room", () => MultiplayerClient.LeaveRoom()); AddUntilStep("wait for room part", () => !RoomJoined); AddStep("join room with items", () => { - RoomManager.CreateRoom(new Room + API.Queue(new CreateRoomRequest(new Room { Name = "test name", Playlist = @@ -177,7 +177,7 @@ namespace osu.Game.Tests.Visual.Multiplayer Expired = true } ] - }); + })); }); AddUntilStep("wait for room join", () => RoomJoined); diff --git a/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsLoungeSubScreen.cs b/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsLoungeSubScreen.cs index 8c8dc8d69a..0897a3b2f5 100644 --- a/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsLoungeSubScreen.cs +++ b/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsLoungeSubScreen.cs @@ -35,7 +35,13 @@ namespace osu.Game.Tests.Visual.Playlists [Test] public void TestManyRooms() { - AddStep("add rooms", () => RoomManager.AddRooms(500)); + AddStep("add rooms", () => + { + RoomManager.AddRooms(500); + loungeScreen.RefreshRooms(); + }); + + AddUntilStep("wait for rooms", () => roomsContainer.Rooms.Count == 500); } [Test] @@ -43,7 +49,12 @@ namespace osu.Game.Tests.Visual.Playlists { AddStep("reset mouse", () => InputManager.ReleaseButton(MouseButton.Left)); - AddStep("add rooms", () => RoomManager.AddRooms(30)); + AddStep("add rooms", () => + { + RoomManager.AddRooms(30); + loungeScreen.RefreshRooms(); + }); + AddUntilStep("wait for rooms", () => roomsContainer.Rooms.Count == 30); AddUntilStep("first room is not masked", () => checkRoomVisible(roomsContainer.Rooms[0])); @@ -60,7 +71,12 @@ namespace osu.Game.Tests.Visual.Playlists [Test] public void TestScrollSelectedIntoView() { - AddStep("add rooms", () => RoomManager.AddRooms(30)); + AddStep("add rooms", () => + { + RoomManager.AddRooms(30); + loungeScreen.RefreshRooms(); + }); + AddUntilStep("wait for rooms", () => roomsContainer.Rooms.Count == 30); AddUntilStep("first room is not masked", () => checkRoomVisible(roomsContainer.Rooms[0])); @@ -74,7 +90,13 @@ namespace osu.Game.Tests.Visual.Playlists [Test] public void TestEnteringRoomTakesLeaseOnSelection() { - AddStep("add rooms", () => RoomManager.AddRooms(1)); + AddStep("add rooms", () => + { + RoomManager.AddRooms(1); + loungeScreen.RefreshRooms(); + }); + + AddUntilStep("wait for rooms", () => roomsContainer.Rooms.Count == 1); AddAssert("selected room is not disabled", () => !loungeScreen.SelectedRoom.Disabled); diff --git a/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsMatchSettingsOverlay.cs b/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsMatchSettingsOverlay.cs index 5868331451..51e39e1b7f 100644 --- a/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsMatchSettingsOverlay.cs +++ b/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsMatchSettingsOverlay.cs @@ -3,14 +3,13 @@ using System; using NUnit.Framework; -using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Game.Graphics.Sprites; using osu.Game.Graphics.UserInterface; using osu.Game.Graphics.UserInterfaceV2; +using osu.Game.Online.API; using osu.Game.Online.Rooms; -using osu.Game.Screens.OnlinePlay; using osu.Game.Screens.OnlinePlay.Playlists; using osu.Game.Tests.Visual.OnlinePlay; @@ -21,13 +20,33 @@ namespace osu.Game.Tests.Visual.Playlists protected new TestRoomManager RoomManager => (TestRoomManager)base.RoomManager; private TestRoomSettings settings = null!; - - protected override OnlinePlayTestSceneDependencies CreateOnlinePlayDependencies() => new TestDependencies(); + private Func? handleRequest; public override void SetUpSteps() { base.SetUpSteps(); + AddStep("setup api", () => + { + handleRequest = null; + ((DummyAPIAccess)API).HandleRequest = req => + { + if (req is not CreateRoomRequest createReq || handleRequest == null) + return false; + + if (handleRequest(createReq.Room) is string errorText) + createReq.TriggerFailure(new APIException(errorText, null)); + else + { + var createdRoom = new APICreatedRoom(); + createdRoom.CopyFrom(createReq.Room); + createReq.TriggerSuccess(createdRoom); + } + + return true; + }; + }); + AddStep("create overlay", () => { SelectedRoom.Value = new Room(); @@ -75,10 +94,10 @@ namespace osu.Game.Tests.Visual.Playlists settings.DurationField.Current.Value = expectedDuration; SelectedRoom.Value!.Playlist = [new PlaylistItem(CreateBeatmap(Ruleset.Value).BeatmapInfo)]; - RoomManager.CreateRequested = r => + handleRequest = r => { createdRoom = r; - return string.Empty; + return null; }; }); @@ -103,7 +122,7 @@ namespace osu.Game.Tests.Visual.Playlists errorMessage = $"{not_found_prefix} {beatmap.OnlineID}"; - RoomManager.CreateRequested = _ => errorMessage; + handleRequest = _ => errorMessage; }); AddAssert("error not displayed", () => !settings.ErrorText.IsPresent); @@ -128,7 +147,7 @@ namespace osu.Game.Tests.Visual.Playlists SelectedRoom.Value!.Name = "Test Room"; SelectedRoom.Value!.Playlist = [new PlaylistItem(CreateBeatmap(Ruleset.Value).BeatmapInfo)]; - RoomManager.CreateRequested = _ => failText; + handleRequest = _ => failText; }); AddAssert("error not displayed", () => !settings.ErrorText.IsPresent); @@ -159,48 +178,5 @@ namespace osu.Game.Tests.Visual.Playlists { } } - - private class TestDependencies : OnlinePlayTestSceneDependencies - { - protected override IRoomManager CreateRoomManager() => new TestRoomManager(); - } - - protected class TestRoomManager : IRoomManager - { - public Func? CreateRequested; - - public event Action RoomsUpdated - { - add { } - remove { } - } - - public IBindable InitialRoomsReceived { get; } = new Bindable(true); - - public IBindableList Rooms => null!; - - public void AddOrUpdateRoom(Room room) => throw new NotImplementedException(); - - public void RemoveRoom(Room room) => throw new NotImplementedException(); - - public void ClearRooms() => throw new NotImplementedException(); - - public void CreateRoom(Room room, Action? onSuccess = null, Action? onError = null) - { - if (CreateRequested == null) - return; - - string error = CreateRequested.Invoke(room); - - if (!string.IsNullOrEmpty(error)) - onError?.Invoke(error); - else - onSuccess?.Invoke(room); - } - - public void JoinRoom(Room room, string? password, Action? onSuccess = null, Action? onError = null) => throw new NotImplementedException(); - - public void PartRoom() => throw new NotImplementedException(); - } } } diff --git a/osu.Game.Tests/Visual/TestMultiplayerComponents.cs b/osu.Game.Tests/Visual/TestMultiplayerComponents.cs index 1814fb70c8..e385ff3a03 100644 --- a/osu.Game.Tests/Visual/TestMultiplayerComponents.cs +++ b/osu.Game.Tests/Visual/TestMultiplayerComponents.cs @@ -11,7 +11,6 @@ using osu.Game.Beatmaps; using osu.Game.Online.API; using osu.Game.Online.Multiplayer; using osu.Game.Screens; -using osu.Game.Screens.OnlinePlay.Components; using osu.Game.Tests.Visual.Multiplayer; using osu.Game.Tests.Visual.OnlinePlay; @@ -26,15 +25,12 @@ namespace osu.Game.Tests.Visual /// Provides a to be resolved as a dependency in the screen, /// which is typically a part of . /// Rebinds the to handle requests via a . - /// Provides a for the screen. /// ///

///
public partial class TestMultiplayerComponents : OsuScreen { - public Screens.OnlinePlay.Multiplayer.Multiplayer MultiplayerScreen => multiplayerScreen; - - public TestMultiplayerRoomManager RoomManager => multiplayerScreen.RoomManager; + public Screens.OnlinePlay.Multiplayer.Multiplayer MultiplayerScreen { get; } public IScreen CurrentScreen => screenStack.CurrentScreen; @@ -53,17 +49,17 @@ namespace osu.Game.Tests.Visual private BeatmapManager beatmapManager { get; set; } private readonly OsuScreenStack screenStack; - private readonly TestMultiplayer multiplayerScreen; + private readonly TestRoomRequestsHandler requestsHandler = new TestRoomRequestsHandler(); public TestMultiplayerComponents() { - multiplayerScreen = new TestMultiplayer(); + MultiplayerScreen = new Screens.OnlinePlay.Multiplayer.Multiplayer(); InternalChildren = new Drawable[] { userLookupCache, beatmapLookupCache, - MultiplayerClient = new TestMultiplayerClient(RoomManager), + MultiplayerClient = new TestMultiplayerClient(requestsHandler), screenStack = new OsuScreenStack { Name = nameof(TestMultiplayerComponents), @@ -71,13 +67,13 @@ namespace osu.Game.Tests.Visual } }; - screenStack.Push(multiplayerScreen); + screenStack.Push(MultiplayerScreen); } [BackgroundDependencyLoader] private void load(IAPIProvider api) { - ((DummyAPIAccess)api).HandleRequest = request => multiplayerScreen.RequestsHandler.HandleRequest(request, api.LocalUser.Value, beatmapManager); + ((DummyAPIAccess)api).HandleRequest = request => requestsHandler.HandleRequest(request, api.LocalUser.Value, beatmapManager); } public override bool OnBackButton() => (screenStack.CurrentScreen as OsuScreen)?.OnBackButton() ?? base.OnBackButton(); @@ -90,13 +86,5 @@ namespace osu.Game.Tests.Visual screenStack.Exit(); return true; } - - private partial class TestMultiplayer : Screens.OnlinePlay.Multiplayer.Multiplayer - { - public new TestMultiplayerRoomManager RoomManager { get; private set; } - public TestRoomRequestsHandler RequestsHandler { get; private set; } - - protected override RoomManager CreateRoomManager() => RoomManager = new TestMultiplayerRoomManager(RequestsHandler = new TestRoomRequestsHandler()); - } } } diff --git a/osu.Game/Online/Rooms/Room.cs b/osu.Game/Online/Rooms/Room.cs index f8660a656e..c5e292a19d 100644 --- a/osu.Game/Online/Rooms/Room.cs +++ b/osu.Game/Online/Rooms/Room.cs @@ -342,6 +342,23 @@ namespace osu.Game.Online.Rooms // Not yet serialised (not implemented). private RoomAvailability availability; + public Room() + { + } + + public Room(MultiplayerRoom room) + { + RoomID = room.RoomID; + Name = room.Settings.Name; + Password = room.Settings.Password; + Type = room.Settings.MatchType; + QueueMode = room.Settings.QueueMode; + AutoStartDuration = room.Settings.AutoStartDuration; + AutoSkip = room.Settings.AutoSkip; + Host = room.Host != null ? new APIUser { Id = room.Host.UserID } : null; + Playlist = room.Playlist.Select(p => new PlaylistItem(p)).ToArray(); + } + /// /// Copies values from another into this one. /// diff --git a/osu.Game/Screens/OnlinePlay/Lounge/DrawableLoungeRoom.cs b/osu.Game/Screens/OnlinePlay/Lounge/DrawableLoungeRoom.cs index 0a55472c2d..032a231ad3 100644 --- a/osu.Game/Screens/OnlinePlay/Lounge/DrawableLoungeRoom.cs +++ b/osu.Game/Screens/OnlinePlay/Lounge/DrawableLoungeRoom.cs @@ -24,7 +24,6 @@ using osu.Game.Graphics.UserInterface; using osu.Game.Graphics.UserInterfaceV2; using osu.Game.Input.Bindings; using osu.Game.Online.API; -using osu.Game.Online.API.Requests; using osu.Game.Online.Rooms; using osu.Game.Overlays; using osu.Game.Screens.OnlinePlay.Components; @@ -51,7 +50,7 @@ namespace osu.Game.Screens.OnlinePlay.Lounge } [Resolved(canBeNull: true)] - private LoungeSubScreen? lounge { get; set; } + private IOnlinePlayLounge? lounge { get; set; } [Resolved] private IDialogOverlay? dialogOverlay { get; set; } @@ -163,7 +162,7 @@ namespace osu.Game.Screens.OnlinePlay.Lounge { new OsuMenuItem("Create copy", MenuItemType.Standard, () => { - lounge?.OpenCopy(Room); + lounge?.Clone(Room); }) }; @@ -171,12 +170,7 @@ namespace osu.Game.Screens.OnlinePlay.Lounge { items.Add(new OsuMenuItem("Close playlist", MenuItemType.Destructive, () => { - dialogOverlay?.Push(new ClosePlaylistDialog(Room, () => - { - var request = new ClosePlaylistRequest(Room.RoomID!.Value); - request.Success += () => lounge?.RefreshRooms(); - api.Queue(request); - })); + dialogOverlay?.Push(new ClosePlaylistDialog(Room, () => lounge?.Close(Room))); })); } @@ -239,7 +233,7 @@ namespace osu.Game.Screens.OnlinePlay.Lounge private readonly Room room; [Resolved(canBeNull: true)] - private LoungeSubScreen? lounge { get; set; } + private IOnlinePlayLounge? lounge { get; set; } public override bool HandleNonPositionalInput => true; diff --git a/osu.Game/Screens/OnlinePlay/Lounge/IOnlinePlayLounge.cs b/osu.Game/Screens/OnlinePlay/Lounge/IOnlinePlayLounge.cs new file mode 100644 index 0000000000..8fa7d0751f --- /dev/null +++ b/osu.Game/Screens/OnlinePlay/Lounge/IOnlinePlayLounge.cs @@ -0,0 +1,32 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using osu.Game.Online.Rooms; + +namespace osu.Game.Screens.OnlinePlay.Lounge +{ + public interface IOnlinePlayLounge + { + /// + /// Attempts to join the given room. + /// + /// The room to join. + /// The password. + /// A delegate to invoke if the user joined the room. + /// A delegate to invoke if the user is not able join the room. + void Join(Room room, string? password, Action? onSuccess = null, Action? onFailure = null); + + /// + /// Clones the given room and opens it as a fresh (not-yet-created) one. + /// + /// The room to clone. + void Clone(Room room); + + /// + /// Closes the given room. + /// + /// The room to close. + void Close(Room room); + } +} diff --git a/osu.Game/Screens/OnlinePlay/Lounge/LoungeSubScreen.cs b/osu.Game/Screens/OnlinePlay/Lounge/LoungeSubScreen.cs index f3f4df166a..df17063fdf 100644 --- a/osu.Game/Screens/OnlinePlay/Lounge/LoungeSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Lounge/LoungeSubScreen.cs @@ -21,6 +21,7 @@ using osu.Game.Graphics.Containers; using osu.Game.Graphics.UserInterface; using osu.Game.Input; using osu.Game.Online.API; +using osu.Game.Online.API.Requests; using osu.Game.Online.Rooms; using osu.Game.Overlays; using osu.Game.Rulesets; @@ -33,7 +34,8 @@ using osuTK; namespace osu.Game.Screens.OnlinePlay.Lounge { [Cached] - public abstract partial class LoungeSubScreen : OnlinePlaySubScreen + [Cached(typeof(IOnlinePlayLounge))] + public abstract partial class LoungeSubScreen : OnlinePlaySubScreen, IOnlinePlayLounge { public override string Title => "Lounge"; @@ -323,11 +325,7 @@ namespace osu.Game.Screens.OnlinePlay.Lounge protected abstract void TryJoin(Room room, string? password, Action onSuccess, Action onFailure); - /// - /// Copies a room and opens it as a fresh (not-yet-created) one. - /// - /// The room to copy. - public void OpenCopy(Room room) + public void Clone(Room room) { Debug.Assert(room.RoomID != null); @@ -363,6 +361,15 @@ namespace osu.Game.Screens.OnlinePlay.Lounge api.Queue(req); } + public void Close(Room room) + { + Debug.Assert(room.RoomID != null); + + var request = new ClosePlaylistRequest(room.RoomID.Value); + request.Success += RefreshRooms; + api.Queue(request); + } + /// /// Push a room as a new subscreen. /// diff --git a/osu.Game/Screens/OnlinePlay/OnlinePlayScreen.cs b/osu.Game/Screens/OnlinePlay/OnlinePlayScreen.cs index 16462b90c1..8988c82dee 100644 --- a/osu.Game/Screens/OnlinePlay/OnlinePlayScreen.cs +++ b/osu.Game/Screens/OnlinePlay/OnlinePlayScreen.cs @@ -220,8 +220,6 @@ namespace osu.Game.Screens.OnlinePlay protected abstract string ScreenTitle { get; } - protected virtual RoomManager CreateRoomManager() => new RoomManager(); - protected abstract LoungeSubScreen CreateLounge(); ScreenStack IHasSubScreenStack.SubScreenStack => screenStack; diff --git a/osu.Game/Tests/Visual/Multiplayer/IMultiplayerTestSceneDependencies.cs b/osu.Game/Tests/Visual/Multiplayer/IMultiplayerTestSceneDependencies.cs index efd0b80ebf..262816ae89 100644 --- a/osu.Game/Tests/Visual/Multiplayer/IMultiplayerTestSceneDependencies.cs +++ b/osu.Game/Tests/Visual/Multiplayer/IMultiplayerTestSceneDependencies.cs @@ -1,7 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using osu.Game.Screens.OnlinePlay; using osu.Game.Tests.Visual.OnlinePlay; using osu.Game.Tests.Visual.Spectator; @@ -17,11 +16,6 @@ namespace osu.Game.Tests.Visual.Multiplayer ///
TestMultiplayerClient MultiplayerClient { get; } - /// - /// The cached . - /// - new TestMultiplayerRoomManager RoomManager { get; } - /// /// The cached . /// diff --git a/osu.Game/Tests/Visual/Multiplayer/MultiplayerTestScene.cs b/osu.Game/Tests/Visual/Multiplayer/MultiplayerTestScene.cs index dca1fc8f3c..d1497d5142 100644 --- a/osu.Game/Tests/Visual/Multiplayer/MultiplayerTestScene.cs +++ b/osu.Game/Tests/Visual/Multiplayer/MultiplayerTestScene.cs @@ -17,7 +17,6 @@ namespace osu.Game.Tests.Visual.Multiplayer public const int PLAYER_2_ID = 56; public TestMultiplayerClient MultiplayerClient => OnlinePlayDependencies.MultiplayerClient; - public new TestMultiplayerRoomManager RoomManager => OnlinePlayDependencies.RoomManager; public TestSpectatorClient SpectatorClient => OnlinePlayDependencies.SpectatorClient; protected new MultiplayerTestSceneDependencies OnlinePlayDependencies => (MultiplayerTestSceneDependencies)base.OnlinePlayDependencies; @@ -56,7 +55,7 @@ namespace osu.Game.Tests.Visual.Multiplayer AddStep("join room", () => { SelectedRoom.Value = CreateRoom(); - API.Queue(new CreateRoomRequest(SelectedRoom.Value)); + MultiplayerClient.CreateRoom(SelectedRoom.Value).ConfigureAwait(false); }); AddUntilStep("wait for room join", () => RoomJoined); diff --git a/osu.Game/Tests/Visual/Multiplayer/MultiplayerTestSceneDependencies.cs b/osu.Game/Tests/Visual/Multiplayer/MultiplayerTestSceneDependencies.cs index 88202d4327..24c33f2f49 100644 --- a/osu.Game/Tests/Visual/Multiplayer/MultiplayerTestSceneDependencies.cs +++ b/osu.Game/Tests/Visual/Multiplayer/MultiplayerTestSceneDependencies.cs @@ -3,7 +3,6 @@ using osu.Game.Online.Multiplayer; using osu.Game.Online.Spectator; -using osu.Game.Screens.OnlinePlay; using osu.Game.Tests.Visual.OnlinePlay; using osu.Game.Tests.Visual.Spectator; @@ -16,19 +15,16 @@ namespace osu.Game.Tests.Visual.Multiplayer { public TestMultiplayerClient MultiplayerClient { get; } public TestSpectatorClient SpectatorClient { get; } - public new TestMultiplayerRoomManager RoomManager => (TestMultiplayerRoomManager)base.RoomManager; public MultiplayerTestSceneDependencies() { - MultiplayerClient = new TestMultiplayerClient(RoomManager); + MultiplayerClient = new TestMultiplayerClient(RequestsHandler); SpectatorClient = CreateSpectatorClient(); CacheAs(MultiplayerClient); CacheAs(SpectatorClient); } - protected override IRoomManager CreateRoomManager() => new TestMultiplayerRoomManager(RequestsHandler); - protected virtual TestSpectatorClient CreateSpectatorClient() => new TestSpectatorClient(); } } diff --git a/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs b/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs index 70e298f3e0..d514fc0d7e 100644 --- a/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs +++ b/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs @@ -10,6 +10,7 @@ using MessagePack; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Extensions; +using osu.Game.Beatmaps; using osu.Game.Online; using osu.Game.Online.API; using osu.Game.Online.API.Requests.Responses; @@ -17,6 +18,7 @@ using osu.Game.Online.Multiplayer; using osu.Game.Online.Multiplayer.MatchTypes.TeamVersus; using osu.Game.Online.Rooms; using osu.Game.Rulesets.Mods; +using osu.Game.Tests.Visual.OnlinePlay; namespace osu.Game.Tests.Visual.Multiplayer { @@ -65,15 +67,15 @@ namespace osu.Game.Tests.Visual.Multiplayer [Resolved] private IAPIProvider api { get; set; } = null!; - private readonly TestMultiplayerRoomManager roomManager; - private MultiplayerPlaylistItem? currentItem => ServerRoom?.Playlist[currentIndex]; private int currentIndex; private long lastPlaylistItemId; - public TestMultiplayerClient(TestMultiplayerRoomManager roomManager) + private readonly TestRoomRequestsHandler apiRequestHandler; + + public TestMultiplayerClient(TestRoomRequestsHandler? apiRequestHandler = null) { - this.roomManager = roomManager; + this.apiRequestHandler = apiRequestHandler ?? new TestRoomRequestsHandler(); } public void Connect() => isConnected.Value = true; @@ -214,7 +216,7 @@ namespace osu.Game.Tests.Visual.Multiplayer roomId = clone(roomId); password = clone(password); - ServerAPIRoom = roomManager.ServerSideRooms.Single(r => r.RoomID == roomId); + ServerAPIRoom = ServerSideRooms.Single(r => r.RoomID == roomId); if (password != ServerAPIRoom.Password) throw new InvalidOperationException("Invalid password."); @@ -485,7 +487,15 @@ namespace osu.Game.Tests.Visual.Multiplayer protected override Task CreateRoom(MultiplayerRoom room) { - throw new NotImplementedException(); + Room apiRoom = new Room(room) + { + Type = room.Settings.MatchType == MatchType.Playlists + ? MatchType.HeadToHead + : room.Settings.MatchType + }; + + AddServerSideRoom(apiRoom, api.LocalUser.Value); + return JoinRoom(apiRoom.RoomID!.Value, room.Settings.Password); } private async Task changeMatchType(MatchType type) @@ -680,5 +690,18 @@ namespace osu.Game.Tests.Visual.Multiplayer isConnected.Value = false; return Task.CompletedTask; } + + #region API Room Handling + + public IReadOnlyList ServerSideRooms + => apiRequestHandler.ServerSideRooms; + + public void AddServerSideRoom(Room room, APIUser host) + => apiRequestHandler.AddServerSideRoom(room, host); + + public bool HandleRequest(APIRequest request, APIUser localUser, BeatmapManager beatmapManager) + => apiRequestHandler.HandleRequest(request, localUser, beatmapManager); + + #endregion } } diff --git a/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerRoomManager.cs b/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerRoomManager.cs deleted file mode 100644 index 59ac9a9749..0000000000 --- a/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerRoomManager.cs +++ /dev/null @@ -1,34 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -using System.Collections.Generic; -using osu.Game.Online.API.Requests.Responses; -using osu.Game.Online.Rooms; -using osu.Game.Screens.OnlinePlay.Components; -using osu.Game.Tests.Visual.OnlinePlay; - -namespace osu.Game.Tests.Visual.Multiplayer -{ - /// - /// A for use in multiplayer test scenes. - /// Should generally not be used by itself outside of a . - /// - public partial class TestMultiplayerRoomManager : RoomManager - { - private readonly TestRoomRequestsHandler requestsHandler; - - public TestMultiplayerRoomManager(TestRoomRequestsHandler requestsHandler) - { - this.requestsHandler = requestsHandler; - } - - public IReadOnlyList ServerSideRooms => requestsHandler.ServerSideRooms; - - /// - /// Adds a room to a local "server-side" list that's returned when a is fired. - /// - /// The room. - /// The host. - public void AddServerSideRoom(Room room, APIUser host) => requestsHandler.AddServerSideRoom(room, host); - } -} diff --git a/osu.Game/Tests/Visual/OnlinePlay/OnlinePlayTestSceneDependencies.cs b/osu.Game/Tests/Visual/OnlinePlay/OnlinePlayTestSceneDependencies.cs index e2670c9ad8..203922c057 100644 --- a/osu.Game/Tests/Visual/OnlinePlay/OnlinePlayTestSceneDependencies.cs +++ b/osu.Game/Tests/Visual/OnlinePlay/OnlinePlayTestSceneDependencies.cs @@ -40,7 +40,7 @@ namespace osu.Game.Tests.Visual.OnlinePlay RequestsHandler = new TestRoomRequestsHandler(); OngoingOperationTracker = new OngoingOperationTracker(); AvailabilityTracker = new OnlinePlayBeatmapAvailabilityTracker(); - RoomManager = CreateRoomManager(); + RoomManager = new TestRoomManager(); UserLookupCache = new TestUserLookupCache(); BeatmapLookupCache = new BeatmapLookupCache(); @@ -80,7 +80,5 @@ namespace osu.Game.Tests.Visual.OnlinePlay if (instance is Drawable drawable) drawableComponents.Add(drawable); } - - protected virtual IRoomManager CreateRoomManager() => new TestRoomManager(); } } diff --git a/osu.Game/Tests/Visual/OnlinePlay/TestRoomManager.cs b/osu.Game/Tests/Visual/OnlinePlay/TestRoomManager.cs index 60d169a46f..bff2753929 100644 --- a/osu.Game/Tests/Visual/OnlinePlay/TestRoomManager.cs +++ b/osu.Game/Tests/Visual/OnlinePlay/TestRoomManager.cs @@ -22,8 +22,14 @@ namespace osu.Game.Tests.Visual.OnlinePlay [Resolved] private IAPIProvider api { get; set; } = null!; + [Resolved] + private RulesetStore rulesets { get; set; } = null!; + public void AddRooms(int count, RulesetInfo? ruleset = null, bool withPassword = false, bool withSpotlightRooms = false) { + // Can't reference Osu ruleset project here. + ruleset ??= rulesets.GetRuleset(0)!; + for (int i = 0; i < count; i++) { AddRoom(new Room @@ -33,12 +39,8 @@ namespace osu.Game.Tests.Visual.OnlinePlay Duration = TimeSpan.FromSeconds(10), Category = withSpotlightRooms && i % 2 == 0 ? RoomCategory.Spotlight : RoomCategory.Normal, Password = withPassword ? @"password" : null, - PlaylistItemStats = ruleset == null - ? null - : new Room.RoomPlaylistItemStats { RulesetIDs = [ruleset.OnlineID] }, - Playlist = ruleset == null - ? Array.Empty() - : [new PlaylistItem(new BeatmapInfo { Metadata = new BeatmapMetadata() }) { RulesetID = ruleset.OnlineID }] + PlaylistItemStats = new Room.RoomPlaylistItemStats { RulesetIDs = [ruleset.OnlineID] }, + Playlist = [new PlaylistItem(new BeatmapInfo { Metadata = new BeatmapMetadata() }) { RulesetID = ruleset.OnlineID }] }); } } @@ -46,7 +48,11 @@ namespace osu.Game.Tests.Visual.OnlinePlay public void AddRoom(Room room) { room.RoomID = -currentRoomId; - api.Queue(new CreateRoomRequest(room)); + + var req = new CreateRoomRequest(room); + req.Success += AddOrUpdateRoom; + api.Queue(req); + currentRoomId++; } } From 7c38089c7559350de5080cdad9b55d0e5165d41b Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Thu, 23 Jan 2025 16:22:52 +0900 Subject: [PATCH 0716/1275] Rename methods --- .../Online/Multiplayer/MultiplayerClient.cs | 37 +++++++------ .../Multiplayer/OnlineMultiplayerClient.cs | 54 +++++++++---------- .../Multiplayer/TestMultiplayerClient.cs | 6 +-- 3 files changed, 50 insertions(+), 47 deletions(-) diff --git a/osu.Game/Online/Multiplayer/MultiplayerClient.cs b/osu.Game/Online/Multiplayer/MultiplayerClient.cs index 7dfe974651..a8f314d372 100644 --- a/osu.Game/Online/Multiplayer/MultiplayerClient.cs +++ b/osu.Game/Online/Multiplayer/MultiplayerClient.cs @@ -171,7 +171,7 @@ namespace osu.Game.Online.Multiplayer throw new InvalidOperationException("Cannot join a multiplayer room while already in one."); var cancellationSource = joinCancellationSource = new CancellationTokenSource(); - await initRoom(room, r => CreateRoom(new MultiplayerRoom(room)), cancellationSource.Token).ConfigureAwait(false); + await initRoom(room, r => CreateRoomInternal(new MultiplayerRoom(room)), cancellationSource.Token).ConfigureAwait(false); } /// @@ -187,7 +187,7 @@ namespace osu.Game.Online.Multiplayer Debug.Assert(room.RoomID != null); var cancellationSource = joinCancellationSource = new CancellationTokenSource(); - await initRoom(room, r => JoinRoom(room.RoomID.Value, password ?? room.Password), cancellationSource.Token).ConfigureAwait(false); + await initRoom(room, r => JoinRoomInternal(room.RoomID.Value, password ?? room.Password), cancellationSource.Token).ConfigureAwait(false); } private async Task initRoom(Room room, Func> initFunc, CancellationToken cancellationToken) @@ -236,21 +236,6 @@ namespace osu.Game.Online.Multiplayer { } - /// - /// Creates the with the given settings. - /// - /// The room. - /// The joined - protected abstract Task CreateRoom(MultiplayerRoom room); - - /// - /// Joins the with a given ID. - /// - /// The room ID. - /// An optional password to use when joining the room. - /// The joined . - protected abstract Task JoinRoom(long roomId, string? password = null); - public Task LeaveRoom() { if (Room == null) @@ -279,6 +264,24 @@ namespace osu.Game.Online.Multiplayer }); } + /// + /// Creates the with the given settings. + /// + /// The room. + /// The joined + protected abstract Task CreateRoomInternal(MultiplayerRoom room); + + /// + /// Joins the with a given ID. + /// + /// The room ID. + /// An optional password to use when joining the room. + /// The joined . + protected abstract Task JoinRoomInternal(long roomId, string? password = null); + + /// + /// Leaves the currently-joined . + /// protected abstract Task LeaveRoomInternal(); public abstract Task InvitePlayer(int userId); diff --git a/osu.Game/Online/Multiplayer/OnlineMultiplayerClient.cs b/osu.Game/Online/Multiplayer/OnlineMultiplayerClient.cs index 05f3e44405..068ba27789 100644 --- a/osu.Game/Online/Multiplayer/OnlineMultiplayerClient.cs +++ b/osu.Game/Online/Multiplayer/OnlineMultiplayerClient.cs @@ -75,7 +75,32 @@ namespace osu.Game.Online.Multiplayer } } - protected override async Task JoinRoom(long roomId, string? password = null) + protected override async Task CreateRoomInternal(MultiplayerRoom room) + { + if (!IsConnected.Value) + throw new OperationCanceledException(); + + Debug.Assert(connection != null); + + try + { + return await connection.InvokeAsync(nameof(IMultiplayerServer.CreateRoom), room).ConfigureAwait(false); + } + catch (HubException exception) + { + if (exception.GetHubExceptionMessage() == HubClientConnector.SERVER_SHUTDOWN_MESSAGE) + { + Debug.Assert(connector != null); + + await connector.Reconnect().ConfigureAwait(false); + return await CreateRoomInternal(room).ConfigureAwait(false); + } + + throw; + } + } + + protected override async Task JoinRoomInternal(long roomId, string? password = null) { if (!IsConnected.Value) throw new OperationCanceledException(); @@ -93,7 +118,7 @@ namespace osu.Game.Online.Multiplayer Debug.Assert(connector != null); await connector.Reconnect().ConfigureAwait(false); - return await JoinRoom(roomId, password).ConfigureAwait(false); + return await JoinRoomInternal(roomId, password).ConfigureAwait(false); } throw; @@ -266,31 +291,6 @@ namespace osu.Game.Online.Multiplayer return connection.InvokeAsync(nameof(IMultiplayerServer.RemovePlaylistItem), playlistItemId); } - protected override async Task CreateRoom(MultiplayerRoom room) - { - if (!IsConnected.Value) - throw new OperationCanceledException(); - - Debug.Assert(connection != null); - - try - { - return await connection.InvokeAsync(nameof(IMultiplayerServer.CreateRoom), room).ConfigureAwait(false); - } - catch (HubException exception) - { - if (exception.GetHubExceptionMessage() == HubClientConnector.SERVER_SHUTDOWN_MESSAGE) - { - Debug.Assert(connector != null); - - await connector.Reconnect().ConfigureAwait(false); - return await CreateRoom(room).ConfigureAwait(false); - } - - throw; - } - } - public override Task DisconnectInternal() { if (connector == null) diff --git a/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs b/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs index d514fc0d7e..359b223ad2 100644 --- a/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs +++ b/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs @@ -208,7 +208,7 @@ namespace osu.Game.Tests.Visual.Multiplayer ((IMultiplayerClient)this).UserBeatmapAvailabilityChanged(clone(userId), clone(user.BeatmapAvailability)); } - protected override async Task JoinRoom(long roomId, string? password = null) + protected override async Task JoinRoomInternal(long roomId, string? password = null) { if (RoomJoined || ServerAPIRoom != null) throw new InvalidOperationException("Already joined a room"); @@ -485,7 +485,7 @@ namespace osu.Game.Tests.Visual.Multiplayer public override Task RemovePlaylistItem(long playlistItemId) => RemoveUserPlaylistItem(api.LocalUser.Value.OnlineID, clone(playlistItemId)); - protected override Task CreateRoom(MultiplayerRoom room) + protected override Task CreateRoomInternal(MultiplayerRoom room) { Room apiRoom = new Room(room) { @@ -495,7 +495,7 @@ namespace osu.Game.Tests.Visual.Multiplayer }; AddServerSideRoom(apiRoom, api.LocalUser.Value); - return JoinRoom(apiRoom.RoomID!.Value, room.Settings.Password); + return JoinRoomInternal(apiRoom.RoomID!.Value, room.Settings.Password); } private async Task changeMatchType(MatchType type) From a198b0830affdab861037c0a90525946fa446b5d Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Thu, 23 Jan 2025 17:18:01 +0900 Subject: [PATCH 0717/1275] Add comment indicating RoomManager shouldn't exist --- osu.Game/Screens/OnlinePlay/Components/RoomManager.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/osu.Game/Screens/OnlinePlay/Components/RoomManager.cs b/osu.Game/Screens/OnlinePlay/Components/RoomManager.cs index 3abb4098fb..a1b61ea7a3 100644 --- a/osu.Game/Screens/OnlinePlay/Components/RoomManager.cs +++ b/osu.Game/Screens/OnlinePlay/Components/RoomManager.cs @@ -13,6 +13,7 @@ using osu.Game.Online.Rooms; namespace osu.Game.Screens.OnlinePlay.Components { + // Todo: This class should be inlined into the lounge. public partial class RoomManager : Component, IRoomManager { public event Action? RoomsUpdated; From b4e8a17f0386523e1fb15faf7e13ffd8aa0011c8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 23 Jan 2025 09:57:23 +0100 Subject: [PATCH 0718/1275] Roll back windows build image to 2019 on android build job Per workaround suggested in https://github.com/actions/runner-images/issues/11402#issuecomment-2596473501. Applying this now as my hopes for a swift resolution without changes on our side are slim to none (read thread linked above in full to learn why). --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d8645d728e..a88f1320cd 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -96,7 +96,7 @@ jobs: build-only-android: name: Build only (Android) - runs-on: windows-latest + runs-on: windows-2019 timeout-minutes: 60 steps: - name: Checkout From f2d8ea299777ad6168eb90d04a574d10bf083837 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Thu, 23 Jan 2025 18:25:55 +0900 Subject: [PATCH 0719/1275] Fix incorrect continuation --- .../Multiplayer/Match/MultiplayerMatchSettingsOverlay.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerMatchSettingsOverlay.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerMatchSettingsOverlay.cs index 279b140d36..72b581eac1 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerMatchSettingsOverlay.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerMatchSettingsOverlay.cs @@ -472,7 +472,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match { client.CreateRoom(room).ContinueWith(t => Schedule(() => { - if (t.IsCompleted) + if (t.IsCompletedSuccessfully) onSuccess(room); else if (t.IsFaulted) { From 6dbf466009f6ab12f2613eebb970a2a1d1e101b3 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Thu, 23 Jan 2025 18:30:11 +0900 Subject: [PATCH 0720/1275] Fix incorrect exception handling In particular, when the exception is: `AggregateException { AggregateException { HubException } }`, then the existing code will only unwrap the first aggregate exception. The overlay's code was copied from the extension so both have been adjusted here. --- .../Online/Multiplayer/MultiplayerClientExtensions.cs | 9 +++------ .../Multiplayer/Match/MultiplayerMatchSettingsOverlay.cs | 8 ++------ 2 files changed, 5 insertions(+), 12 deletions(-) diff --git a/osu.Game/Online/Multiplayer/MultiplayerClientExtensions.cs b/osu.Game/Online/Multiplayer/MultiplayerClientExtensions.cs index d846e7f566..1cc5a8e70a 100644 --- a/osu.Game/Online/Multiplayer/MultiplayerClientExtensions.cs +++ b/osu.Game/Online/Multiplayer/MultiplayerClientExtensions.cs @@ -5,6 +5,7 @@ using System; using System.Diagnostics; using System.Threading.Tasks; using Microsoft.AspNetCore.SignalR; +using osu.Framework.Extensions.ExceptionExtensions; using osu.Framework.Logging; namespace osu.Game.Online.Multiplayer @@ -16,12 +17,8 @@ namespace osu.Game.Online.Multiplayer { if (t.IsFaulted) { - Exception? exception = t.Exception; - - if (exception is AggregateException ae) - exception = ae.InnerException; - - Debug.Assert(exception != null); + Debug.Assert(t.Exception != null); + Exception exception = t.Exception.AsSingular(); if (exception.GetHubExceptionMessage() is string message) // Hub exceptions generally contain something we can show the user directly. diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerMatchSettingsOverlay.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerMatchSettingsOverlay.cs index 72b581eac1..2a5a83fadf 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerMatchSettingsOverlay.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerMatchSettingsOverlay.cs @@ -476,12 +476,8 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match onSuccess(room); else if (t.IsFaulted) { - Exception? exception = t.Exception; - - if (exception is AggregateException ae) - exception = ae.InnerException; - - Debug.Assert(exception != null); + Debug.Assert(t.Exception != null); + Exception exception = t.Exception.AsSingular(); if (exception.GetHubExceptionMessage() is string message) onError(message); From c67c0a7fc02d29a093821d00b95a249e73f01a4a Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 23 Jan 2025 16:07:18 +0900 Subject: [PATCH 0721/1275] Move `Selected` status to drawables Basically, I don't want bindables in `CarouselItem`. It means there needs to be a bind-unbind process on pooling. By moving these to the drawable and just updating every frame, we can simplify things a lot. --- osu.Game/Screens/SelectV2/CarouselItem.cs | 24 ++++++++++++++++------- 1 file changed, 17 insertions(+), 7 deletions(-) diff --git a/osu.Game/Screens/SelectV2/CarouselItem.cs b/osu.Game/Screens/SelectV2/CarouselItem.cs index 4636e8a32f..2cb96a3d7f 100644 --- a/osu.Game/Screens/SelectV2/CarouselItem.cs +++ b/osu.Game/Screens/SelectV2/CarouselItem.cs @@ -2,7 +2,6 @@ // See the LICENCE file in the repository root for full licence text. using System; -using osu.Framework.Bindables; namespace osu.Game.Screens.SelectV2 { @@ -10,9 +9,9 @@ namespace osu.Game.Screens.SelectV2 /// Represents a single display item for display in a . /// This is used to house information related to the attached model that helps with display and tracking. /// - public abstract class CarouselItem : IComparable + public sealed class CarouselItem : IComparable { - public readonly BindableBool Selected = new BindableBool(); + public const float DEFAULT_HEIGHT = 40; /// /// The model this item is representing. @@ -20,16 +19,27 @@ namespace osu.Game.Screens.SelectV2 public readonly object Model; /// - /// The current Y position in the carousel. This is managed by and should not be set manually. + /// The current Y position in the carousel. + /// This is managed by and should not be set manually. /// public double CarouselYPosition { get; set; } /// - /// The height this item will take when displayed. + /// The height this item will take when displayed. Defaults to . /// - public abstract float DrawHeight { get; } + public float DrawHeight { get; set; } = DEFAULT_HEIGHT; - protected CarouselItem(object model) + /// + /// Whether this item should be a valid target for user group selection hotkeys. + /// + public bool IsGroupSelectionTarget { get; set; } + + /// + /// Whether this item is visible or collapsed (hidden). + /// + public bool IsVisible { get; set; } = true; + + public CarouselItem(object model) { Model = model; } From 980f6cf18e0d2177b9b8a63c5afcf803eea48e1d Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 23 Jan 2025 16:09:18 +0900 Subject: [PATCH 0722/1275] Make `CarouselItem` `sealed` and remove `BeatmapCarouselItem` concept Less abstraction is better. As far as I can tell, we don't need a custom model for this. If there's any tracking to be done, it should be done within `BeatmapCarousel`'s implementation (or a filter). --- .../SongSelect/TestSceneBeatmapCarouselV2.cs | 10 +- .../SelectV2/BeatmapCarouselFilterSorting.cs | 43 ++++----- .../Screens/SelectV2/BeatmapCarouselItem.cs | 48 ---------- .../Screens/SelectV2/BeatmapCarouselPanel.cs | 95 ++++++++++++------- osu.Game/Screens/SelectV2/ICarouselPanel.cs | 13 +++ 5 files changed, 99 insertions(+), 110 deletions(-) delete mode 100644 osu.Game/Screens/SelectV2/BeatmapCarouselItem.cs diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2.cs index 984352b2f5..dee61bbcde 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2.cs @@ -181,15 +181,15 @@ namespace osu.Game.Tests.Visual.SongSelect AddUntilStep("visual item added", () => carousel.ChildrenOfType().Count(), () => Is.GreaterThan(0)); AddStep("select middle beatmap", () => carousel.CurrentSelection = beatmapSets.ElementAt(beatmapSets.Count - 2)); - AddStep("scroll to selected item", () => scroll.ScrollTo(scroll.ChildrenOfType().Single(p => p.Item!.Selected.Value))); + AddStep("scroll to selected item", () => scroll.ScrollTo(scroll.ChildrenOfType().Single(p => p.Selected.Value))); AddUntilStep("wait for scroll finished", () => scroll.Current, () => Is.EqualTo(scroll.Target)); - AddStep("save selected screen position", () => positionBefore = carousel.ChildrenOfType().FirstOrDefault(p => p.Item!.Selected.Value)!.ScreenSpaceDrawQuad); + AddStep("save selected screen position", () => positionBefore = carousel.ChildrenOfType().FirstOrDefault(p => p.Selected.Value)!.ScreenSpaceDrawQuad); AddStep("remove first beatmap", () => beatmapSets.Remove(beatmapSets.Last())); AddUntilStep("sorting finished", () => carousel.IsFiltering, () => Is.False); - AddAssert("select screen position unchanged", () => carousel.ChildrenOfType().Single(p => p.Item!.Selected.Value).ScreenSpaceDrawQuad, + AddAssert("select screen position unchanged", () => carousel.ChildrenOfType().Single(p => p.Selected.Value).ScreenSpaceDrawQuad, () => Is.EqualTo(positionBefore)); } @@ -212,11 +212,11 @@ namespace osu.Game.Tests.Visual.SongSelect AddUntilStep("wait for scroll finished", () => scroll.Current, () => Is.EqualTo(scroll.Target)); - AddStep("save selected screen position", () => positionBefore = carousel.ChildrenOfType().FirstOrDefault(p => p.Item!.Selected.Value)!.ScreenSpaceDrawQuad); + AddStep("save selected screen position", () => positionBefore = carousel.ChildrenOfType().FirstOrDefault(p => p.Selected.Value)!.ScreenSpaceDrawQuad); AddStep("remove first beatmap", () => beatmapSets.Remove(beatmapSets.Last())); AddUntilStep("sorting finished", () => carousel.IsFiltering, () => Is.False); - AddAssert("select screen position unchanged", () => carousel.ChildrenOfType().Single(p => p.Item!.Selected.Value).ScreenSpaceDrawQuad, + AddAssert("select screen position unchanged", () => carousel.ChildrenOfType().Single(p => p.Selected.Value).ScreenSpaceDrawQuad, () => Is.EqualTo(positionBefore)); } diff --git a/osu.Game/Screens/SelectV2/BeatmapCarouselFilterSorting.cs b/osu.Game/Screens/SelectV2/BeatmapCarouselFilterSorting.cs index df41aa3e86..dd82bf3495 100644 --- a/osu.Game/Screens/SelectV2/BeatmapCarouselFilterSorting.cs +++ b/osu.Game/Screens/SelectV2/BeatmapCarouselFilterSorting.cs @@ -28,37 +28,32 @@ namespace osu.Game.Screens.SelectV2 return items.OrderDescending(Comparer.Create((a, b) => { - int comparison = 0; + int comparison; - if (a.Model is BeatmapInfo ab && b.Model is BeatmapInfo bb) + var ab = (BeatmapInfo)a.Model; + var bb = (BeatmapInfo)b.Model; + + switch (criteria.Sort) { - switch (criteria.Sort) - { - case SortMode.Artist: - comparison = OrdinalSortByCaseStringComparer.DEFAULT.Compare(ab.BeatmapSet!.Metadata.Artist, bb.BeatmapSet!.Metadata.Artist); - if (comparison == 0) - goto case SortMode.Title; - break; + case SortMode.Artist: + comparison = OrdinalSortByCaseStringComparer.DEFAULT.Compare(ab.BeatmapSet!.Metadata.Artist, bb.BeatmapSet!.Metadata.Artist); + if (comparison == 0) + goto case SortMode.Title; + break; - case SortMode.Difficulty: - comparison = ab.StarRating.CompareTo(bb.StarRating); - break; + case SortMode.Difficulty: + comparison = ab.StarRating.CompareTo(bb.StarRating); + break; - case SortMode.Title: - comparison = OrdinalSortByCaseStringComparer.DEFAULT.Compare(ab.BeatmapSet!.Metadata.Title, bb.BeatmapSet!.Metadata.Title); - break; + case SortMode.Title: + comparison = OrdinalSortByCaseStringComparer.DEFAULT.Compare(ab.BeatmapSet!.Metadata.Title, bb.BeatmapSet!.Metadata.Title); + break; - default: - throw new ArgumentOutOfRangeException(); - } + default: + throw new ArgumentOutOfRangeException(); } - if (comparison != 0) return comparison; - - if (a is BeatmapCarouselItem aItem && b is BeatmapCarouselItem bItem) - return aItem.ID.CompareTo(bItem.ID); - - return 0; + return comparison; })); }, cancellationToken).ConfigureAwait(false); } diff --git a/osu.Game/Screens/SelectV2/BeatmapCarouselItem.cs b/osu.Game/Screens/SelectV2/BeatmapCarouselItem.cs deleted file mode 100644 index dd7aae3db9..0000000000 --- a/osu.Game/Screens/SelectV2/BeatmapCarouselItem.cs +++ /dev/null @@ -1,48 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -using System; -using osu.Game.Beatmaps; -using osu.Game.Database; - -namespace osu.Game.Screens.SelectV2 -{ - public class BeatmapCarouselItem : CarouselItem - { - public readonly Guid ID; - - /// - /// Whether this item has a header providing extra information for it. - /// When displaying items which don't have header, we should make sure enough information is included inline. - /// - public bool HasGroupHeader { get; set; } - - /// - /// Whether this item is a group header. - /// Group headers are generally larger in display. Setting this will account for the size difference. - /// - public bool IsGroupHeader { get; set; } - - public override float DrawHeight => IsGroupHeader ? 80 : 40; - - public BeatmapCarouselItem(object model) - : base(model) - { - ID = (Model as IHasGuidPrimaryKey)?.ID ?? Guid.NewGuid(); - } - - public override string? ToString() - { - switch (Model) - { - case BeatmapInfo bi: - return $"Difficulty: {bi.DifficultyName} ({bi.StarRating:N1}*)"; - - case BeatmapSetInfo si: - return $"{si.Metadata}"; - } - - return Model.ToString(); - } - } -} diff --git a/osu.Game/Screens/SelectV2/BeatmapCarouselPanel.cs b/osu.Game/Screens/SelectV2/BeatmapCarouselPanel.cs index 27023b50be..da3e1b0964 100644 --- a/osu.Game/Screens/SelectV2/BeatmapCarouselPanel.cs +++ b/osu.Game/Screens/SelectV2/BeatmapCarouselPanel.cs @@ -21,27 +21,41 @@ namespace osu.Game.Screens.SelectV2 [Resolved] private BeatmapCarousel carousel { get; set; } = null!; - public CarouselItem? Item - { - get => item; - set - { - item = value; - - selected.UnbindBindings(); - - if (item != null) - selected.BindTo(item.Selected); - } - } - - private readonly BindableBool selected = new BindableBool(); - private CarouselItem? item; + private Box activationFlash = null!; + private Box background = null!; + private OsuSpriteText text = null!; [BackgroundDependencyLoader] private void load() { - selected.BindValueChanged(value => + InternalChildren = new Drawable[] + { + background = new Box + { + Alpha = 0.8f, + RelativeSizeAxes = Axes.Both, + }, + activationFlash = new Box + { + Colour = Color4.White, + Blending = BlendingParameters.Additive, + Alpha = 0, + RelativeSizeAxes = Axes.Both, + }, + text = new OsuSpriteText + { + Padding = new MarginPadding(5), + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + } + }; + + Selected.BindValueChanged(value => + { + activationFlash.FadeTo(value.NewValue ? 0.2f : 0, 500, Easing.OutQuint); + }); + + KeyboardSelected.BindValueChanged(value => { if (value.NewValue) { @@ -59,6 +73,8 @@ namespace osu.Game.Screens.SelectV2 { base.FreeAfterUse(); Item = null; + Selected.Value = false; + KeyboardSelected.Value = false; } protected override void PrepareForUse() @@ -72,31 +88,44 @@ namespace osu.Game.Screens.SelectV2 Size = new Vector2(500, Item.DrawHeight); Masking = true; - InternalChildren = new Drawable[] - { - new Box - { - Colour = (Item.Model is BeatmapInfo ? Color4.Aqua : Color4.Yellow).Darken(5), - RelativeSizeAxes = Axes.Both, - }, - new OsuSpriteText - { - Text = Item.ToString() ?? string.Empty, - Padding = new MarginPadding(5), - Anchor = Anchor.CentreLeft, - Origin = Anchor.CentreLeft, - } - }; + background.Colour = (Item.Model is BeatmapInfo ? Color4.Aqua : Color4.Yellow).Darken(5); + text.Text = getTextFor(Item.Model); this.FadeInFromZero(500, Easing.OutQuint); } + private string getTextFor(object item) + { + switch (item) + { + case BeatmapInfo bi: + return $"Difficulty: {bi.DifficultyName} ({bi.StarRating:N1}*)"; + + case BeatmapSetInfo si: + return $"{si.Metadata}"; + } + + return "unknown"; + } + protected override bool OnClick(ClickEvent e) { - carousel.CurrentSelection = Item!.Model; + if (carousel.CurrentSelection == Item!.Model) + carousel.ActivateSelection(); + else + carousel.CurrentSelection = Item!.Model; return true; } + public CarouselItem? Item { get; set; } + public BindableBool Selected { get; } = new BindableBool(); + public BindableBool KeyboardSelected { get; } = new BindableBool(); + public double DrawYPosition { get; set; } + + public void FlashFromActivation() + { + activationFlash.FadeOutFromOne(500, Easing.OutQuint); + } } } diff --git a/osu.Game/Screens/SelectV2/ICarouselPanel.cs b/osu.Game/Screens/SelectV2/ICarouselPanel.cs index 117feab621..c592734d8d 100644 --- a/osu.Game/Screens/SelectV2/ICarouselPanel.cs +++ b/osu.Game/Screens/SelectV2/ICarouselPanel.cs @@ -1,6 +1,7 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using osu.Framework.Bindables; using osu.Framework.Graphics; namespace osu.Game.Screens.SelectV2 @@ -10,6 +11,18 @@ namespace osu.Game.Screens.SelectV2 /// public interface ICarouselPanel { + /// + /// Whether this item has selection. + /// This is managed by and should not be set manually. + /// + BindableBool Selected { get; } + + /// + /// Whether this item has keyboard selection. + /// This is managed by and should not be set manually. + /// + BindableBool KeyboardSelected { get; } + /// /// The Y position which should be used for displaying this item within the carousel. This is managed by and should not be set manually. /// From ecef5e5d715b5f783ad630040131eb9791fd16fd Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 23 Jan 2025 16:10:42 +0900 Subject: [PATCH 0723/1275] Add set-difficulty tracking in `BeatmapCarouselFilterGrouping` Rather than tracking inside individual items, let's just maintain a single dictionary which is refreshed every time we regenerate filters. --- .../SelectV2/BeatmapCarouselFilterGrouping.cs | 29 ++++++++++++++++--- 1 file changed, 25 insertions(+), 4 deletions(-) diff --git a/osu.Game/Screens/SelectV2/BeatmapCarouselFilterGrouping.cs b/osu.Game/Screens/SelectV2/BeatmapCarouselFilterGrouping.cs index 6cdd15d301..4f0767048a 100644 --- a/osu.Game/Screens/SelectV2/BeatmapCarouselFilterGrouping.cs +++ b/osu.Game/Screens/SelectV2/BeatmapCarouselFilterGrouping.cs @@ -13,6 +13,13 @@ namespace osu.Game.Screens.SelectV2 { public class BeatmapCarouselFilterGrouping : ICarouselFilter { + /// + /// Beatmap sets contain difficulties as related panels. This dictionary holds the relationships between set-difficulties to allow expanding them on selection. + /// + public IDictionary> SetItems => setItems; + + private readonly Dictionary> setItems = new Dictionary>(); + private readonly Func getCriteria; public BeatmapCarouselFilterGrouping(Func getCriteria) @@ -27,7 +34,10 @@ namespace osu.Game.Screens.SelectV2 if (criteria.SplitOutDifficulties) { foreach (var item in items) - ((BeatmapCarouselItem)item).HasGroupHeader = false; + { + item.IsVisible = true; + item.IsGroupSelectionTarget = true; + } return items; } @@ -44,14 +54,25 @@ namespace osu.Game.Screens.SelectV2 { // Add set header if (lastItem == null || (lastItem.Model is BeatmapInfo b2 && b2.BeatmapSet!.OnlineID != b.BeatmapSet!.OnlineID)) - newItems.Add(new BeatmapCarouselItem(b.BeatmapSet!) { IsGroupHeader = true }); + { + newItems.Add(new CarouselItem(b.BeatmapSet!) + { + DrawHeight = 80, + IsGroupSelectionTarget = true + }); + } + + if (!setItems.TryGetValue(b.BeatmapSet!, out var related)) + setItems[b.BeatmapSet!] = related = new HashSet(); + + related.Add(item); } newItems.Add(item); lastItem = item; - var beatmapCarouselItem = (BeatmapCarouselItem)item; - beatmapCarouselItem.HasGroupHeader = true; + item.IsGroupSelectionTarget = false; + item.IsVisible = false; } return newItems; From 2f94456a06dbdc50fcc4d87b4823e1baac27179b Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 23 Jan 2025 16:11:02 +0900 Subject: [PATCH 0724/1275] Add selection and activation flow --- osu.Game/Screens/SelectV2/BeatmapCarousel.cs | 49 ++- osu.Game/Screens/SelectV2/Carousel.cs | 347 +++++++++++++++---- 2 files changed, 329 insertions(+), 67 deletions(-) diff --git a/osu.Game/Screens/SelectV2/BeatmapCarousel.cs b/osu.Game/Screens/SelectV2/BeatmapCarousel.cs index d9c049bbae..e3bc487154 100644 --- a/osu.Game/Screens/SelectV2/BeatmapCarousel.cs +++ b/osu.Game/Screens/SelectV2/BeatmapCarousel.cs @@ -26,6 +26,8 @@ namespace osu.Game.Screens.SelectV2 private readonly LoadingLayer loading; + private readonly BeatmapCarouselFilterGrouping grouping; + public BeatmapCarousel() { DebounceDelay = 100; @@ -34,7 +36,7 @@ namespace osu.Game.Screens.SelectV2 Filters = new ICarouselFilter[] { new BeatmapCarouselFilterSorting(() => Criteria), - new BeatmapCarouselFilterGrouping(() => Criteria), + grouping = new BeatmapCarouselFilterGrouping(() => Criteria), }; AddInternal(carouselPanelPool); @@ -51,7 +53,50 @@ namespace osu.Game.Screens.SelectV2 protected override Drawable GetDrawableForDisplay(CarouselItem item) => carouselPanelPool.Get(); - protected override CarouselItem CreateCarouselItemForModel(BeatmapInfo model) => new BeatmapCarouselItem(model); + protected override void HandleItemDeselected(object? model) + { + base.HandleItemDeselected(model); + + var deselectedSet = model as BeatmapSetInfo ?? (model as BeatmapInfo)?.BeatmapSet; + + if (grouping.SetItems.TryGetValue(deselectedSet!, out var group)) + { + foreach (var i in group) + i.IsVisible = false; + } + } + + protected override void HandleItemSelected(object? model) + { + base.HandleItemSelected(model); + + // Selecting a set isn't valid – let's re-select the first difficulty. + if (model is BeatmapSetInfo setInfo) + { + CurrentSelection = setInfo.Beatmaps.First(); + return; + } + + var currentSelectionSet = (model as BeatmapInfo)?.BeatmapSet; + + if (currentSelectionSet == null) + return; + + if (grouping.SetItems.TryGetValue(currentSelectionSet, out var group)) + { + foreach (var i in group) + i.IsVisible = true; + } + } + + protected override void HandleItemActivated(CarouselItem item) + { + base.HandleItemActivated(item); + + // TODO: maybe this should be handled by the panel itself? + if (GetMaterialisedDrawableForItem(item) is BeatmapCarouselPanel drawable) + drawable.FlashFromActivation(); + } private void beatmapSetsChanged(object? beatmaps, NotifyCollectionChangedEventArgs changed) { diff --git a/osu.Game/Screens/SelectV2/Carousel.cs b/osu.Game/Screens/SelectV2/Carousel.cs index c042da167e..598a898686 100644 --- a/osu.Game/Screens/SelectV2/Carousel.cs +++ b/osu.Game/Screens/SelectV2/Carousel.cs @@ -28,7 +28,8 @@ namespace osu.Game.Screens.SelectV2 /// A highly efficient vertical list display that is used primarily for the song select screen, /// but flexible enough to be used for other use cases. ///
- public abstract partial class Carousel : CompositeDrawable + public abstract partial class Carousel : CompositeDrawable, IKeyBindingHandler + where T : notnull { #region Properties and methods for external usage @@ -80,26 +81,34 @@ namespace osu.Game.Screens.SelectV2 public int VisibleItems => scroll.Panels.Count; /// - /// The currently selected model. + /// The currently selected model. Generally of type T. /// /// - /// Setting this will ensure is set to true only on the matching . - /// Of note, if no matching item exists all items will be deselected while waiting for potential new item which matches. + /// A carousel may create panels for non-T types. + /// To keep things simple, we therefore avoid generic constraints on the current selection. + /// + /// The selection is never reset due to not existing. It can be set to anything. + /// If no matching carousel item exists, there will be no visually selected item while waiting for potential new item which matches. /// - public virtual object? CurrentSelection + public object? CurrentSelection { - get => currentSelection; - set + get => currentSelection.Model; + set => setSelection(value); + } + + /// + /// Activate the current selection, if a selection exists. + /// + public void ActivateSelection() + { + if (currentSelection.CarouselItem != currentKeyboardSelection.CarouselItem) { - if (currentSelectionCarouselItem != null) - currentSelectionCarouselItem.Selected.Value = false; - - currentSelection = value; - - currentSelectionCarouselItem = null; - currentSelectionYPosition = null; - updateSelection(); + CurrentSelection = currentKeyboardSelection.Model; + return; } + + if (currentSelection.CarouselItem != null) + HandleItemActivated(currentSelection.CarouselItem); } #endregion @@ -144,11 +153,42 @@ namespace osu.Game.Screens.SelectV2 protected abstract Drawable GetDrawableForDisplay(CarouselItem item); /// - /// Create an internal carousel representation for the provided model object. + /// Given a , find a drawable representation if it is currently displayed in the carousel. /// - /// The model. - /// A representing the model. - protected abstract CarouselItem CreateCarouselItemForModel(T model); + /// + /// This will only return a drawable if it is "on-screen". + /// + /// The item to find a related drawable representation. + /// The drawable representation if it exists. + protected Drawable? GetMaterialisedDrawableForItem(CarouselItem item) => + scroll.Panels.SingleOrDefault(p => ((ICarouselPanel)p).Item == item); + + /// + /// Called when an item is "selected". + /// + protected virtual void HandleItemSelected(object? model) + { + } + + /// + /// Called when an item is "deselected". + /// + protected virtual void HandleItemDeselected(object? model) + { + } + + /// + /// Called when an item is "activated". + /// + /// + /// An activated item should for instance: + /// - Open or close a folder + /// - Start gameplay on a beatmap difficulty. + /// + /// The carousel item which was activated. + protected virtual void HandleItemActivated(CarouselItem item) + { + } #endregion @@ -197,7 +237,7 @@ namespace osu.Game.Screens.SelectV2 // Copy must be performed on update thread for now (see ConfigureAwait above). // Could potentially be optimised in the future if it becomes an issue. - IEnumerable items = new List(Items.Select(CreateCarouselItemForModel)); + IEnumerable items = new List(Items.Select(m => new CarouselItem(m))); await Task.Run(async () => { @@ -210,7 +250,7 @@ namespace osu.Game.Screens.SelectV2 } log("Updating Y positions"); - await updateYPositions(items, cts.Token).ConfigureAwait(false); + updateYPositions(items, visibleHalfHeight, SpacingBetweenPanels); } catch (OperationCanceledException) { @@ -225,58 +265,231 @@ namespace osu.Game.Screens.SelectV2 carouselItems = items.ToList(); displayedRange = null; - updateSelection(); + // Need to call this to ensure correct post-selection logic is handled on the new items list. + HandleItemSelected(currentSelection.Model); + + refreshAfterSelection(); void log(string text) => Logger.Log($"Carousel[op {cts.GetHashCode().ToString()}] {stopwatch.ElapsedMilliseconds} ms: {text}"); } - private async Task updateYPositions(IEnumerable carouselItems, CancellationToken cancellationToken) => await Task.Run(() => + private static void updateYPositions(IEnumerable carouselItems, float offset, float spacing) { - float yPos = visibleHalfHeight; - foreach (var item in carouselItems) + updateItemYPosition(item, ref offset, spacing); + } + + private static void updateItemYPosition(CarouselItem item, ref float offset, float spacing) + { + item.CarouselYPosition = offset; + if (item.IsVisible) + offset += item.DrawHeight + spacing; + } + + #endregion + + #region Input handling + + public bool OnPressed(KeyBindingPressEvent e) + { + switch (e.Action) { - item.CarouselYPosition = yPos; - yPos += item.DrawHeight + SpacingBetweenPanels; + case GlobalAction.Select: + ActivateSelection(); + return true; + + case GlobalAction.SelectNext: + selectNext(1, isGroupSelection: false); + return true; + + case GlobalAction.SelectNextGroup: + selectNext(1, isGroupSelection: true); + return true; + + case GlobalAction.SelectPrevious: + selectNext(-1, isGroupSelection: false); + return true; + + case GlobalAction.SelectPreviousGroup: + selectNext(-1, isGroupSelection: true); + return true; } - }, cancellationToken).ConfigureAwait(false); + + return false; + } + + public void OnReleased(KeyBindingReleaseEvent e) + { + } + + /// + /// Select the next valid selection relative to a current selection. + /// This is generally for keyboard based traversal. + /// + /// Positive for downwards, negative for upwards. + /// Whether the selection should traverse groups. Group selection updates the actual selection immediately, while non-group selection will only prepare a future keyboard selection. + /// Whether selection was possible. + private bool selectNext(int direction, bool isGroupSelection) + { + // Ensure sanity + Debug.Assert(direction != 0); + direction = direction > 0 ? 1 : -1; + + if (carouselItems == null || carouselItems.Count == 0) + return false; + + // If the user has a different keyboard selection and requests + // group selection, first transfer the keyboard selection to actual selection. + if (isGroupSelection && currentSelection.CarouselItem != currentKeyboardSelection.CarouselItem) + { + ActivateSelection(); + return true; + } + + CarouselItem? selectionItem = currentKeyboardSelection.CarouselItem; + int selectionIndex = currentKeyboardSelection.Index ?? -1; + + // To keep things simple, let's first handle the cases where there's no selection yet. + if (selectionItem == null || selectionIndex < 0) + { + // Start by selecting the first item. + selectionItem = carouselItems.First(); + selectionIndex = 0; + + // In the forwards case, immediately attempt selection of this panel. + // If selection fails, continue with standard logic to find the next valid selection. + if (direction > 0 && attemptSelection(selectionItem)) + return true; + + // In the backwards direction we can just allow the selection logic to go ahead and loop around to the last valid. + } + + Debug.Assert(selectionItem != null); + + // As a second special case, if we're group selecting backwards and the current selection isn't + // a group, base this selection operation from the closest previous group. + if (isGroupSelection && direction < 0) + { + while (!carouselItems[selectionIndex].IsGroupSelectionTarget) + selectionIndex--; + } + + CarouselItem? newItem; + + // Iterate over every item back to the current selection, finding the first valid item. + // The fail condition is when we reach the selection after a cyclic loop over every item. + do + { + selectionIndex += direction; + newItem = carouselItems[(selectionIndex + carouselItems.Count) % carouselItems.Count]; + + if (attemptSelection(newItem)) + return true; + } while (newItem != selectionItem); + + return false; + + bool attemptSelection(CarouselItem item) + { + if (!item.IsVisible || (isGroupSelection && !item.IsGroupSelectionTarget)) + return false; + + if (isGroupSelection) + setSelection(item.Model); + else + setKeyboardSelection(item.Model); + + return true; + } + } #endregion #region Selection handling - private object? currentSelection; - private CarouselItem? currentSelectionCarouselItem; - private double? currentSelectionYPosition; + private Selection currentKeyboardSelection = new Selection(); + private Selection currentSelection = new Selection(); - private void updateSelection() + private void setSelection(object? model) { - currentSelectionCarouselItem = null; + if (currentSelection.Model == model) + return; - if (carouselItems == null) return; + var previousSelection = currentSelection; - foreach (var item in carouselItems) + if (previousSelection.Model != null) + HandleItemDeselected(previousSelection.Model); + + currentSelection = currentKeyboardSelection = new Selection(model); + HandleItemSelected(currentSelection.Model); + + // ensure the selection hasn't changed in the handling of selection. + // if it's changed, avoid a second update of selection/scroll. + if (currentSelection.Model != model) + return; + + refreshAfterSelection(); + scrollToSelection(); + } + + private void setKeyboardSelection(object? model) + { + currentKeyboardSelection = new Selection(model); + + refreshAfterSelection(); + scrollToSelection(); + } + + /// + /// Call after a selection of items change to re-attach s to current s. + /// + private void refreshAfterSelection() + { + float yPos = visibleHalfHeight; + + // Invalidate display range as panel positions and visible status may have changed. + // Position transfer won't happen unless we invalidate this. + displayedRange = null; + + // The case where no items are available for display yet. + if (carouselItems == null) { - bool isSelected = item.Model == currentSelection; - - if (isSelected) - { - currentSelectionCarouselItem = item; - - if (currentSelectionYPosition != item.CarouselYPosition) - { - if (currentSelectionYPosition != null) - { - float adjustment = (float)(item.CarouselYPosition - currentSelectionYPosition.Value); - scroll.OffsetScrollPosition(adjustment); - } - - currentSelectionYPosition = item.CarouselYPosition; - } - } - - item.Selected.Value = isSelected; + currentKeyboardSelection = new Selection(); + currentSelection = new Selection(); + return; } + + float spacing = SpacingBetweenPanels; + int count = carouselItems.Count; + + Selection prevKeyboard = currentKeyboardSelection; + + // We are performing two important operations here: + // - Update all Y positions. After a selection occurs, panels may have changed visibility state and therefore Y positions. + // - Link selected models to CarouselItems. If a selection changed, this is where we find the relevant CarouselItems for further use. + for (int i = 0; i < count; i++) + { + var item = carouselItems[i]; + + updateItemYPosition(item, ref yPos, spacing); + + if (ReferenceEquals(item.Model, currentKeyboardSelection.Model)) + currentKeyboardSelection = new Selection(item.Model, item, item.CarouselYPosition, i); + + if (ReferenceEquals(item.Model, currentSelection.Model)) + currentSelection = new Selection(item.Model, item, item.CarouselYPosition, i); + } + + // If a keyboard selection is currently made, we want to keep the view stable around the selection. + // That means that we should offset the immediate scroll position by any change in Y position for the selection. + if (prevKeyboard.YPosition != null && currentKeyboardSelection.YPosition != prevKeyboard.YPosition) + scroll.OffsetScrollPosition((float)(currentKeyboardSelection.YPosition!.Value - prevKeyboard.YPosition.Value)); + } + + private void scrollToSelection() + { + if (currentKeyboardSelection.CarouselItem != null) + scroll.ScrollTo(currentKeyboardSelection.CarouselItem.CarouselYPosition - visibleHalfHeight); } #endregion @@ -285,7 +498,7 @@ namespace osu.Game.Screens.SelectV2 private DisplayRange? displayedRange; - private readonly CarouselItem carouselBoundsItem = new BoundsCarouselItem(); + private readonly CarouselItem carouselBoundsItem = new CarouselItem(new object()); /// /// The position of the lower visible bound with respect to the current scroll position. @@ -335,6 +548,9 @@ namespace osu.Game.Screens.SelectV2 float dist = Math.Abs(1f - posInScroll.Y / visibleHalfHeight); panel.X = offsetX(dist, visibleHalfHeight); + + c.Selected.Value = c.Item == currentSelection?.CarouselItem; + c.KeyboardSelected.Value = c.Item == currentKeyboardSelection?.CarouselItem; } } @@ -381,6 +597,8 @@ namespace osu.Game.Screens.SelectV2 ? new List() : carouselItems.GetRange(range.First, range.Last - range.First + 1); + toDisplay.RemoveAll(i => !i.IsVisible); + // Iterate over all panels which are already displayed and figure which need to be displayed / removed. foreach (var panel in scroll.Panels) { @@ -434,6 +652,15 @@ namespace osu.Game.Screens.SelectV2 #region Internal helper classes + /// + /// Bookkeeping for a current selection. + /// + /// The selected model. If null, there's no selection. + /// A related carousel item representation for the model. May be null if selection is not present as an item, or if has not been run yet. + /// The Y position of the selection as of the last run of . May be null if selection is not present as an item, or if has not been run yet. + /// The index of the selection as of the last run of . May be null if selection is not present as an item, or if has not been run yet. + private record Selection(object? Model = null, CarouselItem? CarouselItem = null, double? YPosition = null, int? Index = null); + private record DisplayRange(int First, int Last); /// @@ -573,16 +800,6 @@ namespace osu.Game.Screens.SelectV2 #endregion } - private class BoundsCarouselItem : CarouselItem - { - public override float DrawHeight => 0; - - public BoundsCarouselItem() - : base(new object()) - { - } - } - #endregion } } From 9ab045495d4dcb0d2d5afb52e4f4d69a5ed3074d Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 23 Jan 2025 16:24:04 +0900 Subject: [PATCH 0725/1275] Tidy up tests in preparation for adding more --- .../SongSelect/BeatmapCarouselV2TestScene.cs | 187 ++++++++++++ .../SongSelect/TestSceneBeatmapCarouselV2.cs | 286 ------------------ .../TestSceneBeatmapCarouselV2Basics.cs | 119 ++++++++ 3 files changed, 306 insertions(+), 286 deletions(-) create mode 100644 osu.Game.Tests/Visual/SongSelect/BeatmapCarouselV2TestScene.cs delete mode 100644 osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2.cs create mode 100644 osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2Basics.cs diff --git a/osu.Game.Tests/Visual/SongSelect/BeatmapCarouselV2TestScene.cs b/osu.Game.Tests/Visual/SongSelect/BeatmapCarouselV2TestScene.cs new file mode 100644 index 0000000000..eaa29abf01 --- /dev/null +++ b/osu.Game.Tests/Visual/SongSelect/BeatmapCarouselV2TestScene.cs @@ -0,0 +1,187 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Linq; +using NUnit.Framework; +using osu.Framework.Allocation; +using osu.Framework.Bindables; +using osu.Framework.Extensions.ObjectExtensions; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; +using osu.Framework.Testing; +using osu.Framework.Utils; +using osu.Game.Beatmaps; +using osu.Game.Database; +using osu.Game.Graphics; +using osu.Game.Graphics.Containers; +using osu.Game.Screens.Select; +using osu.Game.Screens.Select.Filter; +using osu.Game.Screens.SelectV2; +using osu.Game.Tests.Beatmaps; +using osu.Game.Tests.Resources; +using osuTK.Graphics; +using BeatmapCarousel = osu.Game.Screens.SelectV2.BeatmapCarousel; + +namespace osu.Game.Tests.Visual.SongSelect +{ + public abstract partial class BeatmapCarouselV2TestScene : OsuManualInputManagerTestScene + { + protected readonly BindableList BeatmapSets = new BindableList(); + + protected BeatmapCarousel Carousel = null!; + + protected OsuScrollContainer Scroll => Carousel.ChildrenOfType>().Single(); + + [Cached(typeof(BeatmapStore))] + private BeatmapStore store; + + private OsuTextFlowContainer stats = null!; + + private int beatmapCount; + + protected BeatmapCarouselV2TestScene() + { + store = new TestBeatmapStore + { + BeatmapSets = { BindTarget = BeatmapSets } + }; + + BeatmapSets.BindCollectionChanged((_, _) => beatmapCount = BeatmapSets.Sum(s => s.Beatmaps.Count)); + + Scheduler.AddDelayed(updateStats, 100, true); + } + + [SetUpSteps] + public void SetUpSteps() + { + AddStep("reset beatmaps", () => BeatmapSets.Clear()); + + CreateCarousel(); + + SortBy(new FilterCriteria { Sort = SortMode.Title }); + } + + protected void CreateCarousel() + { + AddStep("create components", () => + { + Box topBox; + Children = new Drawable[] + { + new GridContainer + { + RelativeSizeAxes = Axes.Both, + ColumnDimensions = new[] + { + new Dimension(GridSizeMode.Relative, 1), + }, + RowDimensions = new[] + { + new Dimension(GridSizeMode.Absolute, 200), + new Dimension(), + new Dimension(GridSizeMode.Absolute, 200), + }, + Content = new[] + { + new Drawable[] + { + topBox = new Box + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Colour = Color4.Cyan, + RelativeSizeAxes = Axes.Both, + Alpha = 0.4f, + }, + }, + new Drawable[] + { + Carousel = new BeatmapCarousel + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Width = 500, + RelativeSizeAxes = Axes.Y, + }, + }, + new[] + { + new Box + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Colour = Color4.Cyan, + RelativeSizeAxes = Axes.Both, + Alpha = 0.4f, + }, + topBox.CreateProxy(), + } + } + }, + stats = new OsuTextFlowContainer + { + AutoSizeAxes = Axes.Both, + Padding = new MarginPadding(10), + TextAnchor = Anchor.CentreLeft, + }, + }; + }); + } + + protected void SortBy(FilterCriteria criteria) => AddStep($"sort by {criteria.Sort}", () => Carousel.Filter(criteria)); + + protected void WaitForDrawablePanels() => AddUntilStep("drawable panels loaded", () => Carousel.ChildrenOfType().Count(), () => Is.GreaterThan(0)); + protected void WaitForSorting() => AddUntilStep("sorting finished", () => Carousel.IsFiltering, () => Is.False); + protected void WaitForScrolling() => AddUntilStep("scroll finished", () => Scroll.Current, () => Is.EqualTo(Scroll.Target)); + + /// + /// Add requested beatmap sets count to list. + /// + /// The count of beatmap sets to add. + /// If not null, the number of difficulties per set. If null, randomised difficulty count will be used. + protected void AddBeatmaps(int count, int? fixedDifficultiesPerSet = null) => AddStep($"add {count} beatmaps", () => + { + for (int i = 0; i < count; i++) + BeatmapSets.Add(TestResources.CreateTestBeatmapSetInfo(fixedDifficultiesPerSet ?? RNG.Next(1, 4))); + }); + + protected void RemoveLastBeatmap() => + AddStep("remove last beatmap", () => + { + if (BeatmapSets.Count == 0) return; + + BeatmapSets.Remove(BeatmapSets.Last()); + }); + + private void updateStats() + { + if (Carousel.IsNull()) + return; + + stats.Clear(); + createHeader("beatmap store"); + stats.AddParagraph($""" + sets: {BeatmapSets.Count} + beatmaps: {beatmapCount} + """); + createHeader("carousel"); + stats.AddParagraph($""" + sorting: {Carousel.IsFiltering} + tracked: {Carousel.ItemsTracked} + displayable: {Carousel.DisplayableItems} + displayed: {Carousel.VisibleItems} + selected: {Carousel.CurrentSelection} + """); + + void createHeader(string text) + { + stats.AddParagraph(string.Empty); + stats.AddParagraph(text, cp => + { + cp.Font = cp.Font.With(size: 18, weight: FontWeight.Bold); + }); + } + } + } +} diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2.cs deleted file mode 100644 index dee61bbcde..0000000000 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2.cs +++ /dev/null @@ -1,286 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using NUnit.Framework; -using osu.Framework.Allocation; -using osu.Framework.Bindables; -using osu.Framework.Extensions.ObjectExtensions; -using osu.Framework.Graphics; -using osu.Framework.Graphics.Containers; -using osu.Framework.Graphics.Primitives; -using osu.Framework.Graphics.Shapes; -using osu.Framework.Testing; -using osu.Framework.Utils; -using osu.Game.Beatmaps; -using osu.Game.Database; -using osu.Game.Graphics; -using osu.Game.Graphics.Containers; -using osu.Game.Screens.Select; -using osu.Game.Screens.Select.Filter; -using osu.Game.Screens.SelectV2; -using osu.Game.Tests.Beatmaps; -using osu.Game.Tests.Resources; -using osuTK.Graphics; -using BeatmapCarousel = osu.Game.Screens.SelectV2.BeatmapCarousel; - -namespace osu.Game.Tests.Visual.SongSelect -{ - [TestFixture] - public partial class TestSceneBeatmapCarouselV2 : OsuManualInputManagerTestScene - { - private readonly BindableList beatmapSets = new BindableList(); - - [Cached(typeof(BeatmapStore))] - private BeatmapStore store; - - private OsuTextFlowContainer stats = null!; - private BeatmapCarousel carousel = null!; - - private OsuScrollContainer scroll => carousel.ChildrenOfType>().Single(); - - private int beatmapCount; - - public TestSceneBeatmapCarouselV2() - { - store = new TestBeatmapStore - { - BeatmapSets = { BindTarget = beatmapSets } - }; - - beatmapSets.BindCollectionChanged((_, _) => - { - beatmapCount = beatmapSets.Sum(s => s.Beatmaps.Count); - }); - - Scheduler.AddDelayed(updateStats, 100, true); - } - - [SetUpSteps] - public void SetUpSteps() - { - AddStep("create components", () => - { - beatmapSets.Clear(); - - Box topBox; - Children = new Drawable[] - { - new GridContainer - { - RelativeSizeAxes = Axes.Both, - ColumnDimensions = new[] - { - new Dimension(GridSizeMode.Relative, 1), - }, - RowDimensions = new[] - { - new Dimension(GridSizeMode.Absolute, 200), - new Dimension(), - new Dimension(GridSizeMode.Absolute, 200), - }, - Content = new[] - { - new Drawable[] - { - topBox = new Box - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Colour = Color4.Cyan, - RelativeSizeAxes = Axes.Both, - Alpha = 0.4f, - }, - }, - new Drawable[] - { - carousel = new BeatmapCarousel - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Width = 500, - RelativeSizeAxes = Axes.Y, - }, - }, - new[] - { - new Box - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Colour = Color4.Cyan, - RelativeSizeAxes = Axes.Both, - Alpha = 0.4f, - }, - topBox.CreateProxy(), - } - } - }, - stats = new OsuTextFlowContainer - { - AutoSizeAxes = Axes.Both, - Padding = new MarginPadding(10), - TextAnchor = Anchor.CentreLeft, - }, - }; - }); - - AddStep("sort by title", () => - { - carousel.Filter(new FilterCriteria { Sort = SortMode.Title }); - }); - } - - [Test] - public void TestBasic() - { - AddStep("add 10 beatmaps", () => - { - for (int i = 0; i < 10; i++) - beatmapSets.Add(TestResources.CreateTestBeatmapSetInfo(RNG.Next(1, 4))); - }); - - AddStep("add 1 beatmap", () => beatmapSets.Add(TestResources.CreateTestBeatmapSetInfo(RNG.Next(1, 4)))); - - AddStep("remove all beatmaps", () => beatmapSets.Clear()); - } - - [Test] - public void TestSorting() - { - AddStep("add 10 beatmaps", () => - { - for (int i = 0; i < 10; i++) - beatmapSets.Add(TestResources.CreateTestBeatmapSetInfo(RNG.Next(1, 4))); - }); - - AddStep("sort by difficulty", () => - { - carousel.Filter(new FilterCriteria { Sort = SortMode.Difficulty }); - }); - - AddStep("sort by artist", () => - { - carousel.Filter(new FilterCriteria { Sort = SortMode.Artist }); - }); - } - - [Test] - public void TestScrollPositionMaintainedOnAddSecondSelected() - { - Quad positionBefore = default; - - AddStep("add 10 beatmaps", () => - { - for (int i = 0; i < 10; i++) - beatmapSets.Add(TestResources.CreateTestBeatmapSetInfo(RNG.Next(1, 4))); - }); - - AddUntilStep("visual item added", () => carousel.ChildrenOfType().Count(), () => Is.GreaterThan(0)); - - AddStep("select middle beatmap", () => carousel.CurrentSelection = beatmapSets.ElementAt(beatmapSets.Count - 2)); - AddStep("scroll to selected item", () => scroll.ScrollTo(scroll.ChildrenOfType().Single(p => p.Selected.Value))); - - AddUntilStep("wait for scroll finished", () => scroll.Current, () => Is.EqualTo(scroll.Target)); - - AddStep("save selected screen position", () => positionBefore = carousel.ChildrenOfType().FirstOrDefault(p => p.Selected.Value)!.ScreenSpaceDrawQuad); - - AddStep("remove first beatmap", () => beatmapSets.Remove(beatmapSets.Last())); - AddUntilStep("sorting finished", () => carousel.IsFiltering, () => Is.False); - AddAssert("select screen position unchanged", () => carousel.ChildrenOfType().Single(p => p.Selected.Value).ScreenSpaceDrawQuad, - () => Is.EqualTo(positionBefore)); - } - - [Test] - public void TestScrollPositionMaintainedOnAddLastSelected() - { - Quad positionBefore = default; - - AddStep("add 10 beatmaps", () => - { - for (int i = 0; i < 10; i++) - beatmapSets.Add(TestResources.CreateTestBeatmapSetInfo(RNG.Next(1, 4))); - }); - - AddUntilStep("visual item added", () => carousel.ChildrenOfType().Count(), () => Is.GreaterThan(0)); - - AddStep("scroll to last item", () => scroll.ScrollToEnd(false)); - - AddStep("select last beatmap", () => carousel.CurrentSelection = beatmapSets.First()); - - AddUntilStep("wait for scroll finished", () => scroll.Current, () => Is.EqualTo(scroll.Target)); - - AddStep("save selected screen position", () => positionBefore = carousel.ChildrenOfType().FirstOrDefault(p => p.Selected.Value)!.ScreenSpaceDrawQuad); - - AddStep("remove first beatmap", () => beatmapSets.Remove(beatmapSets.Last())); - AddUntilStep("sorting finished", () => carousel.IsFiltering, () => Is.False); - AddAssert("select screen position unchanged", () => carousel.ChildrenOfType().Single(p => p.Selected.Value).ScreenSpaceDrawQuad, - () => Is.EqualTo(positionBefore)); - } - - [Test] - public void TestAddRemoveOneByOne() - { - AddRepeatStep("add beatmaps", () => beatmapSets.Add(TestResources.CreateTestBeatmapSetInfo(RNG.Next(1, 4))), 20); - - AddRepeatStep("remove beatmaps", () => beatmapSets.RemoveAt(RNG.Next(0, beatmapSets.Count)), 20); - } - - [Test] - [Explicit] - public void TestInsane() - { - const int count = 200000; - - List generated = new List(); - - AddStep($"populate {count} test beatmaps", () => - { - generated.Clear(); - Task.Run(() => - { - for (int j = 0; j < count; j++) - generated.Add(TestResources.CreateTestBeatmapSetInfo(RNG.Next(1, 4))); - }).ConfigureAwait(true); - }); - - AddUntilStep("wait for beatmaps populated", () => generated.Count, () => Is.GreaterThan(count / 3)); - AddUntilStep("this takes a while", () => generated.Count, () => Is.GreaterThan(count / 3 * 2)); - AddUntilStep("maybe they are done now", () => generated.Count, () => Is.EqualTo(count)); - - AddStep("add all beatmaps", () => beatmapSets.AddRange(generated)); - } - - private void updateStats() - { - if (carousel.IsNull()) - return; - - stats.Clear(); - createHeader("beatmap store"); - stats.AddParagraph($""" - sets: {beatmapSets.Count} - beatmaps: {beatmapCount} - """); - createHeader("carousel"); - stats.AddParagraph($""" - sorting: {carousel.IsFiltering} - tracked: {carousel.ItemsTracked} - displayable: {carousel.DisplayableItems} - displayed: {carousel.VisibleItems} - selected: {carousel.CurrentSelection} - """); - - void createHeader(string text) - { - stats.AddParagraph(string.Empty); - stats.AddParagraph(text, cp => - { - cp.Font = cp.Font.With(size: 18, weight: FontWeight.Bold); - }); - } - } - } -} diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2Basics.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2Basics.cs new file mode 100644 index 0000000000..8d801930fc --- /dev/null +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2Basics.cs @@ -0,0 +1,119 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using NUnit.Framework; +using osu.Framework.Graphics.Primitives; +using osu.Framework.Testing; +using osu.Framework.Utils; +using osu.Game.Beatmaps; +using osu.Game.Screens.Select; +using osu.Game.Screens.Select.Filter; +using osu.Game.Screens.SelectV2; +using osu.Game.Tests.Resources; + +namespace osu.Game.Tests.Visual.SongSelect +{ + /// + /// Currently covers adding and removing of items and scrolling. + /// If we add more tests here, these two categories can likely be split out into separate scenes. + /// + [TestFixture] + public partial class TestSceneBeatmapCarouselV2Basics : BeatmapCarouselV2TestScene + { + [Test] + public void TestBasics() + { + AddBeatmaps(1); + AddBeatmaps(10); + RemoveLastBeatmap(); + AddStep("remove all beatmaps", () => BeatmapSets.Clear()); + } + + [Test] + public void TestAddRemoveOneByOne() + { + AddRepeatStep("add beatmaps", () => BeatmapSets.Add(TestResources.CreateTestBeatmapSetInfo(RNG.Next(1, 4))), 20); + AddRepeatStep("remove beatmaps", () => BeatmapSets.RemoveAt(RNG.Next(0, BeatmapSets.Count)), 20); + } + + [Test] + public void TestSorting() + { + AddBeatmaps(10); + SortBy(new FilterCriteria { Sort = SortMode.Difficulty }); + SortBy(new FilterCriteria { Sort = SortMode.Artist }); + } + + [Test] + public void TestScrollPositionMaintainedOnAddSecondSelected() + { + Quad positionBefore = default; + + AddBeatmaps(10); + WaitForDrawablePanels(); + + AddStep("select middle beatmap", () => Carousel.CurrentSelection = BeatmapSets.ElementAt(BeatmapSets.Count - 2)); + AddStep("scroll to selected item", () => Scroll.ScrollTo(Scroll.ChildrenOfType().Single(p => p.Selected.Value))); + + WaitForScrolling(); + + AddStep("save selected screen position", () => positionBefore = Carousel.ChildrenOfType().FirstOrDefault(p => p.Selected.Value)!.ScreenSpaceDrawQuad); + + RemoveLastBeatmap(); + WaitForSorting(); + + AddAssert("select screen position unchanged", () => Carousel.ChildrenOfType().Single(p => p.Selected.Value).ScreenSpaceDrawQuad, + () => Is.EqualTo(positionBefore)); + } + + [Test] + public void TestScrollPositionMaintainedOnAddLastSelected() + { + Quad positionBefore = default; + + AddBeatmaps(10); + WaitForDrawablePanels(); + + AddStep("scroll to last item", () => Scroll.ScrollToEnd(false)); + + AddStep("select last beatmap", () => Carousel.CurrentSelection = BeatmapSets.First()); + + WaitForScrolling(); + + AddStep("save selected screen position", () => positionBefore = Carousel.ChildrenOfType().FirstOrDefault(p => p.Selected.Value)!.ScreenSpaceDrawQuad); + + RemoveLastBeatmap(); + WaitForSorting(); + AddAssert("select screen position unchanged", () => Carousel.ChildrenOfType().Single(p => p.Selected.Value).ScreenSpaceDrawQuad, + () => Is.EqualTo(positionBefore)); + } + + [Test] + [Explicit] + public void TestPerformanceWithManyBeatmaps() + { + const int count = 200000; + + List generated = new List(); + + AddStep($"populate {count} test beatmaps", () => + { + generated.Clear(); + Task.Run(() => + { + for (int j = 0; j < count; j++) + generated.Add(TestResources.CreateTestBeatmapSetInfo(RNG.Next(1, 4))); + }).ConfigureAwait(true); + }); + + AddUntilStep("wait for beatmaps populated", () => generated.Count, () => Is.GreaterThan(count / 3)); + AddUntilStep("this takes a while", () => generated.Count, () => Is.GreaterThan(count / 3 * 2)); + AddUntilStep("maybe they are done now", () => generated.Count, () => Is.EqualTo(count)); + + AddStep("add all beatmaps", () => BeatmapSets.AddRange(generated)); + } + } +} From ffca90779fcc9a781fb6a5c6063c0f1baa927f81 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 23 Jan 2025 16:48:03 +0900 Subject: [PATCH 0726/1275] Fix sort direction being flipped --- .../Visual/SongSelect/BeatmapCarouselV2TestScene.cs | 10 ++++++---- .../SongSelect/TestSceneBeatmapCarouselV2Basics.cs | 10 +++++----- .../Screens/SelectV2/BeatmapCarouselFilterSorting.cs | 2 +- 3 files changed, 12 insertions(+), 10 deletions(-) diff --git a/osu.Game.Tests/Visual/SongSelect/BeatmapCarouselV2TestScene.cs b/osu.Game.Tests/Visual/SongSelect/BeatmapCarouselV2TestScene.cs index eaa29abf01..3aa9f60181 100644 --- a/osu.Game.Tests/Visual/SongSelect/BeatmapCarouselV2TestScene.cs +++ b/osu.Game.Tests/Visual/SongSelect/BeatmapCarouselV2TestScene.cs @@ -55,7 +55,7 @@ namespace osu.Game.Tests.Visual.SongSelect [SetUpSteps] public void SetUpSteps() { - AddStep("reset beatmaps", () => BeatmapSets.Clear()); + RemoveAllBeatmaps(); CreateCarousel(); @@ -146,12 +146,14 @@ namespace osu.Game.Tests.Visual.SongSelect BeatmapSets.Add(TestResources.CreateTestBeatmapSetInfo(fixedDifficultiesPerSet ?? RNG.Next(1, 4))); }); - protected void RemoveLastBeatmap() => - AddStep("remove last beatmap", () => + protected void RemoveAllBeatmaps() => AddStep("clear all beatmaps", () => BeatmapSets.Clear()); + + protected void RemoveFirstBeatmap() => + AddStep("remove first beatmap", () => { if (BeatmapSets.Count == 0) return; - BeatmapSets.Remove(BeatmapSets.Last()); + BeatmapSets.Remove(BeatmapSets.First()); }); private void updateStats() diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2Basics.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2Basics.cs index 8d801930fc..748831bf7b 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2Basics.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2Basics.cs @@ -28,8 +28,8 @@ namespace osu.Game.Tests.Visual.SongSelect { AddBeatmaps(1); AddBeatmaps(10); - RemoveLastBeatmap(); - AddStep("remove all beatmaps", () => BeatmapSets.Clear()); + RemoveFirstBeatmap(); + RemoveAllBeatmaps(); } [Test] @@ -62,7 +62,7 @@ namespace osu.Game.Tests.Visual.SongSelect AddStep("save selected screen position", () => positionBefore = Carousel.ChildrenOfType().FirstOrDefault(p => p.Selected.Value)!.ScreenSpaceDrawQuad); - RemoveLastBeatmap(); + RemoveFirstBeatmap(); WaitForSorting(); AddAssert("select screen position unchanged", () => Carousel.ChildrenOfType().Single(p => p.Selected.Value).ScreenSpaceDrawQuad, @@ -79,13 +79,13 @@ namespace osu.Game.Tests.Visual.SongSelect AddStep("scroll to last item", () => Scroll.ScrollToEnd(false)); - AddStep("select last beatmap", () => Carousel.CurrentSelection = BeatmapSets.First()); + AddStep("select last beatmap", () => Carousel.CurrentSelection = BeatmapSets.Last()); WaitForScrolling(); AddStep("save selected screen position", () => positionBefore = Carousel.ChildrenOfType().FirstOrDefault(p => p.Selected.Value)!.ScreenSpaceDrawQuad); - RemoveLastBeatmap(); + RemoveFirstBeatmap(); WaitForSorting(); AddAssert("select screen position unchanged", () => Carousel.ChildrenOfType().Single(p => p.Selected.Value).ScreenSpaceDrawQuad, () => Is.EqualTo(positionBefore)); diff --git a/osu.Game/Screens/SelectV2/BeatmapCarouselFilterSorting.cs b/osu.Game/Screens/SelectV2/BeatmapCarouselFilterSorting.cs index dd82bf3495..0298616aa8 100644 --- a/osu.Game/Screens/SelectV2/BeatmapCarouselFilterSorting.cs +++ b/osu.Game/Screens/SelectV2/BeatmapCarouselFilterSorting.cs @@ -26,7 +26,7 @@ namespace osu.Game.Screens.SelectV2 { var criteria = getCriteria(); - return items.OrderDescending(Comparer.Create((a, b) => + return items.Order(Comparer.Create((a, b) => { int comparison; From eaea053c7d8824d26ba43821bc4e46bb9ba227a7 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 23 Jan 2025 17:19:09 +0900 Subject: [PATCH 0727/1275] Add test coverage of various selection examples Where possible I've tried to match the test and method names of `TestSceneBeatmapCarousel` for easy coverage comparison. --- .../TestSceneBeatmapCarouselV2Selection.cs | 216 ++++++++++++++++++ 1 file changed, 216 insertions(+) create mode 100644 osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2Selection.cs diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2Selection.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2Selection.cs new file mode 100644 index 0000000000..305774b7d3 --- /dev/null +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2Selection.cs @@ -0,0 +1,216 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Linq; +using NUnit.Framework; +using osu.Framework.Testing; +using osu.Game.Beatmaps; +using osu.Game.Screens.SelectV2; +using osuTK.Input; + +namespace osu.Game.Tests.Visual.SongSelect +{ + [TestFixture] + public partial class TestSceneBeatmapCarouselV2Selection : BeatmapCarouselV2TestScene + { + /// + /// Keyboard selection via up and down arrows doesn't actually change the selection until + /// the select key is pressed. + /// + [Test] + public void TestKeyboardSelectionKeyRepeat() + { + AddBeatmaps(10); + WaitForDrawablePanels(); + checkNoSelection(); + + select(); + checkNoSelection(); + + AddStep("press down arrow", () => InputManager.PressKey(Key.Down)); + checkSelectionIterating(false); + + AddStep("press up arrow", () => InputManager.PressKey(Key.Up)); + checkSelectionIterating(false); + + AddStep("release down arrow", () => InputManager.ReleaseKey(Key.Down)); + checkSelectionIterating(false); + + AddStep("release up arrow", () => InputManager.ReleaseKey(Key.Up)); + checkSelectionIterating(false); + + select(); + checkHasSelection(); + } + + /// + /// Keyboard selection via left and right arrows moves between groups, updating the selection + /// immediately. + /// + [Test] + public void TestGroupSelectionKeyRepeat() + { + AddBeatmaps(10); + WaitForDrawablePanels(); + checkNoSelection(); + + AddStep("press right arrow", () => InputManager.PressKey(Key.Right)); + checkSelectionIterating(true); + + AddStep("press left arrow", () => InputManager.PressKey(Key.Left)); + checkSelectionIterating(true); + + AddStep("release right arrow", () => InputManager.ReleaseKey(Key.Right)); + checkSelectionIterating(true); + + AddStep("release left arrow", () => InputManager.ReleaseKey(Key.Left)); + checkSelectionIterating(false); + } + + [Test] + public void TestCarouselRemembersSelection() + { + AddBeatmaps(10); + WaitForDrawablePanels(); + + selectNextGroup(); + + object? selection = null; + + AddStep("store drawable selection", () => selection = getSelectedPanel()?.Item?.Model); + + checkHasSelection(); + AddAssert("drawable selection non-null", () => selection, () => Is.Not.Null); + AddAssert("drawable selection matches carousel selection", () => selection, () => Is.EqualTo(Carousel.CurrentSelection)); + + RemoveAllBeatmaps(); + AddUntilStep("no drawable selection", getSelectedPanel, () => Is.Null); + + AddBeatmaps(10); + WaitForDrawablePanels(); + + checkHasSelection(); + AddAssert("no drawable selection", getSelectedPanel, () => Is.Null); + + AddStep("add previous selection", () => BeatmapSets.Add(((BeatmapInfo)selection!).BeatmapSet!)); + + AddUntilStep("drawable selection restored", () => getSelectedPanel()?.Item?.Model, () => Is.EqualTo(selection)); + AddAssert("drawable selection matches carousel selection", () => selection, () => Is.EqualTo(Carousel.CurrentSelection)); + + BeatmapCarouselPanel? getSelectedPanel() => Carousel.ChildrenOfType().SingleOrDefault(p => p.Selected.Value); + } + + [Test] + public void TestTraversalBeyondStart() + { + const int total_set_count = 200; + + AddBeatmaps(total_set_count); + WaitForDrawablePanels(); + + selectNextGroup(); + waitForSelection(0, 0); + selectPrevGroup(); + waitForSelection(total_set_count - 1, 0); + } + + [Test] + public void TestTraversalBeyondEnd() + { + const int total_set_count = 200; + + AddBeatmaps(total_set_count); + WaitForDrawablePanels(); + + selectPrevGroup(); + waitForSelection(total_set_count - 1, 0); + selectNextGroup(); + waitForSelection(0, 0); + } + + [Test] + public void TestKeyboardSelection() + { + AddBeatmaps(10, 3); + WaitForDrawablePanels(); + + selectNextPanel(); + selectNextPanel(); + selectNextPanel(); + selectNextPanel(); + checkNoSelection(); + + select(); + waitForSelection(3, 0); + + selectNextPanel(); + waitForSelection(3, 0); + + select(); + waitForSelection(3, 1); + + selectNextPanel(); + waitForSelection(3, 1); + + select(); + waitForSelection(3, 2); + + selectNextPanel(); + waitForSelection(3, 2); + + select(); + waitForSelection(4, 0); + } + + [Test] + public void TestEmptyTraversal() + { + selectNextPanel(); + checkNoSelection(); + + selectNextGroup(); + checkNoSelection(); + + selectPrevPanel(); + checkNoSelection(); + + selectPrevGroup(); + checkNoSelection(); + } + + private void waitForSelection(int set, int? diff = null) + { + AddUntilStep($"selected is set{set}{(diff.HasValue ? $" diff{diff.Value}" : "")}", () => + { + if (diff != null) + return ReferenceEquals(Carousel.CurrentSelection, BeatmapSets[set].Beatmaps[diff.Value]); + + return BeatmapSets[set].Beatmaps.Contains(Carousel.CurrentSelection); + }); + } + + private void selectNextPanel() => AddStep("select next panel", () => InputManager.Key(Key.Down)); + private void selectPrevPanel() => AddStep("select prev panel", () => InputManager.Key(Key.Up)); + private void selectNextGroup() => AddStep("select next group", () => InputManager.Key(Key.Right)); + private void selectPrevGroup() => AddStep("select prev group", () => InputManager.Key(Key.Left)); + + private void select() => AddStep("select", () => InputManager.Key(Key.Enter)); + + private void checkNoSelection() => AddAssert("has no selection", () => Carousel.CurrentSelection, () => Is.Null); + private void checkHasSelection() => AddAssert("has selection", () => Carousel.CurrentSelection, () => Is.Not.Null); + + private void checkSelectionIterating(bool isIterating) + { + object? selection = null; + + for (int i = 0; i < 3; i++) + { + AddStep("store selection", () => selection = Carousel.CurrentSelection); + if (isIterating) + AddUntilStep("selection changed", () => Carousel.CurrentSelection != selection); + else + AddUntilStep("selection not changed", () => Carousel.CurrentSelection == selection); + } + } + } +} From e9d6411e615ba85a2989511a9f374682b20d25cf Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Thu, 23 Jan 2025 19:10:11 +0900 Subject: [PATCH 0728/1275] Clean up error handling --- .../Match/MultiplayerMatchSettingsOverlay.cs | 58 +++++++++---------- 1 file changed, 27 insertions(+), 31 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerMatchSettingsOverlay.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerMatchSettingsOverlay.cs index 2a5a83fadf..eda3bace40 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerMatchSettingsOverlay.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerMatchSettingsOverlay.cs @@ -463,9 +463,9 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match .ContinueWith(t => Schedule(() => { if (t.IsCompletedSuccessfully) - onSuccess(room); + onSuccess(); else - onError(t.Exception?.AsSingular().Message ?? "Error changing settings."); + onError(t.Exception, "Error changing settings"); })); } else @@ -473,26 +473,16 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match client.CreateRoom(room).ContinueWith(t => Schedule(() => { if (t.IsCompletedSuccessfully) - onSuccess(room); - else if (t.IsFaulted) - { - Debug.Assert(t.Exception != null); - Exception exception = t.Exception.AsSingular(); - - if (exception.GetHubExceptionMessage() is string message) - onError(message); - else - onError($"Error creating room: {exception}"); - } + onSuccess(); else - onError("Error creating room."); + onError(t.Exception, "Error creating room"); })); } } private void hideError() => ErrorText.FadeOut(50); - private void onSuccess(Room room) => Schedule(() => + private void onSuccess() => Schedule(() => { Debug.Assert(applyingSettingsOperation != null); @@ -502,28 +492,34 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match applyingSettingsOperation = null; }); - private void onError(string text) => Schedule(() => + private void onError(Exception? exception, string description) { - Debug.Assert(applyingSettingsOperation != null); + if (exception is AggregateException aggregateException) + exception = aggregateException.AsSingular(); - // see https://github.com/ppy/osu-web/blob/2c97aaeb64fb4ed97c747d8383a35b30f57428c7/app/Models/Multiplayer/PlaylistItem.php#L48. - const string not_found_prefix = "beatmaps not found:"; + string message = exception?.GetHubExceptionMessage() ?? $"{description} ({exception?.Message})"; - if (text.StartsWith(not_found_prefix, StringComparison.Ordinal)) + Schedule(() => { - ErrorText.Text = "The selected beatmap is not available online."; - room.Playlist.SingleOrDefault()?.MarkInvalid(); - } - else - { - ErrorText.Text = text; - } + Debug.Assert(applyingSettingsOperation != null); - ErrorText.FadeIn(50); + // see https://github.com/ppy/osu-web/blob/2c97aaeb64fb4ed97c747d8383a35b30f57428c7/app/Models/Multiplayer/PlaylistItem.php#L48. + const string not_found_prefix = "beatmaps not found:"; - applyingSettingsOperation.Dispose(); - applyingSettingsOperation = null; - }); + if (message.StartsWith(not_found_prefix, StringComparison.Ordinal)) + { + ErrorText.Text = "The selected beatmap is not available online."; + room.Playlist.SingleOrDefault()?.MarkInvalid(); + } + else + ErrorText.Text = message; + + ErrorText.FadeIn(50); + + applyingSettingsOperation.Dispose(); + applyingSettingsOperation = null; + }); + } protected override void Dispose(bool isDisposing) { From 8f17a44976439ba30c8ee13f1200d72821847c5a Mon Sep 17 00:00:00 2001 From: tsunyoku Date: Thu, 23 Jan 2025 10:29:04 +0000 Subject: [PATCH 0729/1275] Remove unused default value --- .../Preprocessing/Rhythm/Data/SameRhythmGroupedHitObjects.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/Data/SameRhythmGroupedHitObjects.cs b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/Data/SameRhythmGroupedHitObjects.cs index 4f7023059f..b77176b49d 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/Data/SameRhythmGroupedHitObjects.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/Data/SameRhythmGroupedHitObjects.cs @@ -38,7 +38,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Rhythm.Data /// The ratio of between this and the previous . In the /// case where one or both of the is undefined, this will have a value of 1. /// - public double HitObjectIntervalRatio = 1; + public double HitObjectIntervalRatio; /// public double Interval { get; private set; } From 2feab314267ae017cce1334ed8e83ba4fdfc0ec7 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 23 Jan 2025 22:41:20 +0900 Subject: [PATCH 0730/1275] Adjust inline commentary based on review feedback --- osu.Game/Screens/SelectV2/Carousel.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/osu.Game/Screens/SelectV2/Carousel.cs b/osu.Game/Screens/SelectV2/Carousel.cs index 598a898686..8194ddaaed 100644 --- a/osu.Game/Screens/SelectV2/Carousel.cs +++ b/osu.Game/Screens/SelectV2/Carousel.cs @@ -366,8 +366,8 @@ namespace osu.Game.Screens.SelectV2 Debug.Assert(selectionItem != null); - // As a second special case, if we're group selecting backwards and the current selection isn't - // a group, base this selection operation from the closest previous group. + // As a second special case, if we're group selecting backwards and the current selection isn't a group, + // make sure to go back to the group header this item belongs to, so that the block below doesn't find it and stop too early. if (isGroupSelection && direction < 0) { while (!carouselItems[selectionIndex].IsGroupSelectionTarget) @@ -423,8 +423,8 @@ namespace osu.Game.Screens.SelectV2 currentSelection = currentKeyboardSelection = new Selection(model); HandleItemSelected(currentSelection.Model); - // ensure the selection hasn't changed in the handling of selection. - // if it's changed, avoid a second update of selection/scroll. + // `HandleItemSelected` can alter `CurrentSelection`, which will recursively call `setSelection()` again. + // if that happens, the rest of this method should be a no-op. if (currentSelection.Model != model) return; From 0716b73d2aa43f6343c700a3b9bb0eb451542f26 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 23 Jan 2025 22:44:39 +0900 Subject: [PATCH 0731/1275] `ActivateSelection` -> `TryActivateSelection` --- osu.Game/Screens/SelectV2/BeatmapCarouselPanel.cs | 2 +- osu.Game/Screens/SelectV2/Carousel.cs | 9 +++++---- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/osu.Game/Screens/SelectV2/BeatmapCarouselPanel.cs b/osu.Game/Screens/SelectV2/BeatmapCarouselPanel.cs index da3e1b0964..9219656365 100644 --- a/osu.Game/Screens/SelectV2/BeatmapCarouselPanel.cs +++ b/osu.Game/Screens/SelectV2/BeatmapCarouselPanel.cs @@ -111,7 +111,7 @@ namespace osu.Game.Screens.SelectV2 protected override bool OnClick(ClickEvent e) { if (carousel.CurrentSelection == Item!.Model) - carousel.ActivateSelection(); + carousel.TryActivateSelection(); else carousel.CurrentSelection = Item!.Model; return true; diff --git a/osu.Game/Screens/SelectV2/Carousel.cs b/osu.Game/Screens/SelectV2/Carousel.cs index 8194ddaaed..6899c10451 100644 --- a/osu.Game/Screens/SelectV2/Carousel.cs +++ b/osu.Game/Screens/SelectV2/Carousel.cs @@ -97,9 +97,10 @@ namespace osu.Game.Screens.SelectV2 } /// - /// Activate the current selection, if a selection exists. + /// Activate the current selection, if a selection exists and matches keyboard selection. + /// If keyboard selection does not match selection, this will transfer the selection on first invocation. /// - public void ActivateSelection() + public void TryActivateSelection() { if (currentSelection.CarouselItem != currentKeyboardSelection.CarouselItem) { @@ -295,7 +296,7 @@ namespace osu.Game.Screens.SelectV2 switch (e.Action) { case GlobalAction.Select: - ActivateSelection(); + TryActivateSelection(); return true; case GlobalAction.SelectNext: @@ -342,7 +343,7 @@ namespace osu.Game.Screens.SelectV2 // group selection, first transfer the keyboard selection to actual selection. if (isGroupSelection && currentSelection.CarouselItem != currentKeyboardSelection.CarouselItem) { - ActivateSelection(); + TryActivateSelection(); return true; } From d5369d3508c4ae9227a5f5858536153f947ee600 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 23 Jan 2025 23:53:09 +0900 Subject: [PATCH 0732/1275] Add regions to `BeatmapCarousel` --- osu.Game/Screens/SelectV2/BeatmapCarousel.cs | 97 ++++++++++++-------- 1 file changed, 61 insertions(+), 36 deletions(-) diff --git a/osu.Game/Screens/SelectV2/BeatmapCarousel.cs b/osu.Game/Screens/SelectV2/BeatmapCarousel.cs index e3bc487154..540eedbd92 100644 --- a/osu.Game/Screens/SelectV2/BeatmapCarousel.cs +++ b/osu.Game/Screens/SelectV2/BeatmapCarousel.cs @@ -22,8 +22,6 @@ namespace osu.Game.Screens.SelectV2 { private IBindableList detachedBeatmaps = null!; - private readonly DrawablePool carouselPanelPool = new DrawablePool(100); - private readonly LoadingLayer loading; private readonly BeatmapCarouselFilterGrouping grouping; @@ -39,19 +37,60 @@ namespace osu.Game.Screens.SelectV2 grouping = new BeatmapCarouselFilterGrouping(() => Criteria), }; - AddInternal(carouselPanelPool); - AddInternal(loading = new LoadingLayer(dimBackground: true)); } [BackgroundDependencyLoader] private void load(BeatmapStore beatmapStore, CancellationToken? cancellationToken) + { + setupPools(); + setupBeatmaps(beatmapStore, cancellationToken); + } + + #region Beatmap source hookup + + private void setupBeatmaps(BeatmapStore beatmapStore, CancellationToken? cancellationToken) { detachedBeatmaps = beatmapStore.GetBeatmapSets(cancellationToken); detachedBeatmaps.BindCollectionChanged(beatmapSetsChanged, true); } - protected override Drawable GetDrawableForDisplay(CarouselItem item) => carouselPanelPool.Get(); + private void beatmapSetsChanged(object? beatmaps, NotifyCollectionChangedEventArgs changed) + { + // TODO: moving management of BeatmapInfo tracking to BeatmapStore might be something we want to consider. + // right now we are managing this locally which is a bit of added overhead. + IEnumerable? newBeatmapSets = changed.NewItems?.Cast(); + IEnumerable? beatmapSetInfos = changed.OldItems?.Cast(); + + switch (changed.Action) + { + case NotifyCollectionChangedAction.Add: + Items.AddRange(newBeatmapSets!.SelectMany(s => s.Beatmaps)); + break; + + case NotifyCollectionChangedAction.Remove: + + foreach (var set in beatmapSetInfos!) + { + foreach (var beatmap in set.Beatmaps) + Items.RemoveAll(i => i is BeatmapInfo bi && beatmap.Equals(bi)); + } + + break; + + case NotifyCollectionChangedAction.Move: + case NotifyCollectionChangedAction.Replace: + throw new NotImplementedException(); + + case NotifyCollectionChangedAction.Reset: + Items.Clear(); + break; + } + } + + #endregion + + #region Selection handling protected override void HandleItemDeselected(object? model) { @@ -98,38 +137,9 @@ namespace osu.Game.Screens.SelectV2 drawable.FlashFromActivation(); } - private void beatmapSetsChanged(object? beatmaps, NotifyCollectionChangedEventArgs changed) - { - // TODO: moving management of BeatmapInfo tracking to BeatmapStore might be something we want to consider. - // right now we are managing this locally which is a bit of added overhead. - IEnumerable? newBeatmapSets = changed.NewItems?.Cast(); - IEnumerable? beatmapSetInfos = changed.OldItems?.Cast(); + #endregion - switch (changed.Action) - { - case NotifyCollectionChangedAction.Add: - Items.AddRange(newBeatmapSets!.SelectMany(s => s.Beatmaps)); - break; - - case NotifyCollectionChangedAction.Remove: - - foreach (var set in beatmapSetInfos!) - { - foreach (var beatmap in set.Beatmaps) - Items.RemoveAll(i => i is BeatmapInfo bi && beatmap.Equals(bi)); - } - - break; - - case NotifyCollectionChangedAction.Move: - case NotifyCollectionChangedAction.Replace: - throw new NotImplementedException(); - - case NotifyCollectionChangedAction.Reset: - Items.Clear(); - break; - } - } + #region Filtering public FilterCriteria Criteria { get; private set; } = new FilterCriteria(); @@ -139,5 +149,20 @@ namespace osu.Game.Screens.SelectV2 loading.Show(); FilterAsync().ContinueWith(_ => Schedule(() => loading.Hide())); } + + #endregion + + #region Drawable pooling + + private readonly DrawablePool carouselPanelPool = new DrawablePool(100); + + private void setupPools() + { + AddInternal(carouselPanelPool); + } + + protected override Drawable GetDrawableForDisplay(CarouselItem item) => carouselPanelPool.Get(); + + #endregion } } From f4270ab3b994dad45acfc9c735da1a52c323e0ee Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 23 Jan 2025 23:58:51 +0900 Subject: [PATCH 0733/1275] Simplify selection handling logic --- osu.Game/Screens/SelectV2/BeatmapCarousel.cs | 32 +++++++++----------- 1 file changed, 14 insertions(+), 18 deletions(-) diff --git a/osu.Game/Screens/SelectV2/BeatmapCarousel.cs b/osu.Game/Screens/SelectV2/BeatmapCarousel.cs index 540eedbd92..aca71efe93 100644 --- a/osu.Game/Screens/SelectV2/BeatmapCarousel.cs +++ b/osu.Game/Screens/SelectV2/BeatmapCarousel.cs @@ -92,19 +92,6 @@ namespace osu.Game.Screens.SelectV2 #region Selection handling - protected override void HandleItemDeselected(object? model) - { - base.HandleItemDeselected(model); - - var deselectedSet = model as BeatmapSetInfo ?? (model as BeatmapInfo)?.BeatmapSet; - - if (grouping.SetItems.TryGetValue(deselectedSet!, out var group)) - { - foreach (var i in group) - i.IsVisible = false; - } - } - protected override void HandleItemSelected(object? model) { base.HandleItemSelected(model); @@ -116,15 +103,24 @@ namespace osu.Game.Screens.SelectV2 return; } - var currentSelectionSet = (model as BeatmapInfo)?.BeatmapSet; + if (model is BeatmapInfo beatmapInfo) + setVisibilityOfSetItems(beatmapInfo.BeatmapSet!, true); + } - if (currentSelectionSet == null) - return; + protected override void HandleItemDeselected(object? model) + { + base.HandleItemDeselected(model); - if (grouping.SetItems.TryGetValue(currentSelectionSet, out var group)) + if (model is BeatmapInfo beatmapInfo) + setVisibilityOfSetItems(beatmapInfo.BeatmapSet!, false); + } + + private void setVisibilityOfSetItems(BeatmapSetInfo set, bool visible) + { + if (grouping.SetItems.TryGetValue(set, out var group)) { foreach (var i in group) - i.IsVisible = true; + i.IsVisible = visible; } } From 13c64b59af7a6e809ffdb2632973f10ef14ff722 Mon Sep 17 00:00:00 2001 From: Layendan Date: Thu, 23 Jan 2025 15:36:20 -0700 Subject: [PATCH 0734/1275] Inherit menu items from parent class --- .../OnlinePlay/Lounge/Components/DrawableRoom.cs | 2 +- .../OnlinePlay/Lounge/DrawableLoungeRoom.cs | 16 ++-------------- 2 files changed, 3 insertions(+), 15 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/Lounge/Components/DrawableRoom.cs b/osu.Game/Screens/OnlinePlay/Lounge/Components/DrawableRoom.cs index 7fefa0a1a8..7463e05c96 100644 --- a/osu.Game/Screens/OnlinePlay/Lounge/Components/DrawableRoom.cs +++ b/osu.Game/Screens/OnlinePlay/Lounge/Components/DrawableRoom.cs @@ -340,7 +340,7 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components } } - public MenuItem[] ContextMenuItems + public virtual MenuItem[] ContextMenuItems { get { diff --git a/osu.Game/Screens/OnlinePlay/Lounge/DrawableLoungeRoom.cs b/osu.Game/Screens/OnlinePlay/Lounge/DrawableLoungeRoom.cs index da04152bd3..700cc09eb6 100644 --- a/osu.Game/Screens/OnlinePlay/Lounge/DrawableLoungeRoom.cs +++ b/osu.Game/Screens/OnlinePlay/Lounge/DrawableLoungeRoom.cs @@ -158,7 +158,7 @@ namespace osu.Game.Screens.OnlinePlay.Lounge public Popover GetPopover() => new PasswordEntryPopover(Room); - public new MenuItem[] ContextMenuItems + public override MenuItem[] ContextMenuItems { get { @@ -170,19 +170,7 @@ namespace osu.Game.Screens.OnlinePlay.Lounge }) }; - if (Room.RoomID.HasValue) - { - items.AddRange([ - new OsuMenuItem("View in browser", MenuItemType.Standard, () => - { - game?.OpenUrlExternally(formatRoomUrl(Room.RoomID.Value)); - }), - new OsuMenuItem("Copy link", MenuItemType.Standard, () => - { - game?.CopyUrlToClipboard(formatRoomUrl(Room.RoomID.Value)); - }) - ]); - } + items.AddRange(base.ContextMenuItems); if (Room.Type == MatchType.Playlists && Room.Host?.Id == api.LocalUser.Value.Id && Room.StartDate?.AddMinutes(5) >= DateTimeOffset.Now && !Room.HasEnded) { From b0a7237fd6c397f2412a4e209af40094788bcc30 Mon Sep 17 00:00:00 2001 From: Layendan Date: Thu, 23 Jan 2025 15:37:30 -0700 Subject: [PATCH 0735/1275] Fix formatting --- osu.Game/Screens/OnlinePlay/Lounge/DrawableLoungeRoom.cs | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/Lounge/DrawableLoungeRoom.cs b/osu.Game/Screens/OnlinePlay/Lounge/DrawableLoungeRoom.cs index 700cc09eb6..47630ce1ff 100644 --- a/osu.Game/Screens/OnlinePlay/Lounge/DrawableLoungeRoom.cs +++ b/osu.Game/Screens/OnlinePlay/Lounge/DrawableLoungeRoom.cs @@ -39,7 +39,7 @@ namespace osu.Game.Screens.OnlinePlay.Lounge /// /// A with lounge-specific interactions such as selection and hover sounds. /// - public partial class DrawableLoungeRoom : DrawableRoom, IFilterable, IHasContextMenu, IHasPopover, IKeyBindingHandler + public partial class DrawableLoungeRoom : DrawableRoom, IFilterable, IHasPopover, IKeyBindingHandler { private const float transition_duration = 60; private const float selection_border_width = 4; @@ -59,9 +59,6 @@ namespace osu.Game.Screens.OnlinePlay.Lounge [Resolved] private IAPIProvider api { get; set; } = null!; - [Resolved] - private OsuGame? game { get; set; } - private readonly BindableWithCurrent selectedRoom = new BindableWithCurrent(); private Sample? sampleSelect; private Sample? sampleJoin; From d326f23576176fb02700cb9a3cfc989374f44664 Mon Sep 17 00:00:00 2001 From: Layendan Date: Thu, 23 Jan 2025 15:39:18 -0700 Subject: [PATCH 0736/1275] Remove unused method --- osu.Game/Screens/OnlinePlay/Lounge/DrawableLoungeRoom.cs | 2 -- 1 file changed, 2 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/Lounge/DrawableLoungeRoom.cs b/osu.Game/Screens/OnlinePlay/Lounge/DrawableLoungeRoom.cs index 47630ce1ff..f2afbcef71 100644 --- a/osu.Game/Screens/OnlinePlay/Lounge/DrawableLoungeRoom.cs +++ b/osu.Game/Screens/OnlinePlay/Lounge/DrawableLoungeRoom.cs @@ -236,8 +236,6 @@ namespace osu.Game.Screens.OnlinePlay.Lounge Room.PropertyChanged -= onRoomPropertyChanged; } - private string formatRoomUrl(long id) => $@"{api.WebsiteRootUrl}/multiplayer/rooms/{id}"; - public partial class PasswordEntryPopover : OsuPopover { private readonly Room room; From 61a818e4eddc8805a6584095656cf70511e945e5 Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Thu, 23 Jan 2025 21:22:35 -0500 Subject: [PATCH 0737/1275] Hide Discord RPC error messages away from user attention --- osu.Desktop/DiscordRichPresence.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Desktop/DiscordRichPresence.cs b/osu.Desktop/DiscordRichPresence.cs index 7dd9250ab6..6afb3e319d 100644 --- a/osu.Desktop/DiscordRichPresence.cs +++ b/osu.Desktop/DiscordRichPresence.cs @@ -82,7 +82,7 @@ namespace osu.Desktop }; client.OnReady += onReady; - client.OnError += (_, e) => Logger.Log($"An error occurred with Discord RPC Client: {e.Message} ({e.Code})", LoggingTarget.Network, LogLevel.Error); + client.OnError += (_, e) => Logger.Log($"An error occurred with Discord RPC Client: {e.Message} ({e.Code})", LoggingTarget.Network); try { From 5cc8181bad679ab8f1171531493f47c856c0633b Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 24 Jan 2025 15:29:49 +0900 Subject: [PATCH 0738/1275] Expose `GameplayStartTime` in `IGameplayClock` --- .../TestSceneClicksPerSecondCalculator.cs | 1 + osu.Game/Rulesets/UI/FrameStabilityContainer.cs | 8 ++++---- osu.Game/Screens/Play/GameplayClockContainer.cs | 2 ++ osu.Game/Screens/Play/IGameplayClock.cs | 5 +++++ .../Play/MasterGameplayClockContainer.cs | 17 ++++++++--------- 5 files changed, 20 insertions(+), 13 deletions(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneClicksPerSecondCalculator.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneClicksPerSecondCalculator.cs index db06329d74..55d57d7a65 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneClicksPerSecondCalculator.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneClicksPerSecondCalculator.cs @@ -120,6 +120,7 @@ namespace osu.Game.Tests.Visual.Gameplay public double FramesPerSecond => throw new NotImplementedException(); public FrameTimeInfo TimeInfo => throw new NotImplementedException(); public double StartTime => throw new NotImplementedException(); + public double GameplayStartTime => throw new NotImplementedException(); public IAdjustableAudioComponent AdjustmentsFromMods => adjustableAudioComponent; diff --git a/osu.Game/Rulesets/UI/FrameStabilityContainer.cs b/osu.Game/Rulesets/UI/FrameStabilityContainer.cs index 92258f3fc9..50111e64a8 100644 --- a/osu.Game/Rulesets/UI/FrameStabilityContainer.cs +++ b/osu.Game/Rulesets/UI/FrameStabilityContainer.cs @@ -42,7 +42,7 @@ namespace osu.Game.Rulesets.UI private readonly Bindable waitingOnFrames = new Bindable(); - private readonly double gameplayStartTime; + public double GameplayStartTime { get; } private IGameplayClock? parentGameplayClock; @@ -85,7 +85,7 @@ namespace osu.Game.Rulesets.UI framedClock = new FramedClock(manualClock = new ManualClock()); - this.gameplayStartTime = gameplayStartTime; + GameplayStartTime = gameplayStartTime; } [BackgroundDependencyLoader(true)] @@ -257,8 +257,8 @@ namespace osu.Game.Rulesets.UI return; } - if (manualClock.CurrentTime < gameplayStartTime) - manualClock.CurrentTime = proposedTime = Math.Min(gameplayStartTime, proposedTime); + if (manualClock.CurrentTime < GameplayStartTime) + manualClock.CurrentTime = proposedTime = Math.Min(GameplayStartTime, proposedTime); else if (Math.Abs(manualClock.CurrentTime - proposedTime) > sixty_frame_time * 1.2f) { proposedTime = proposedTime > manualClock.CurrentTime diff --git a/osu.Game/Screens/Play/GameplayClockContainer.cs b/osu.Game/Screens/Play/GameplayClockContainer.cs index 255877e0aa..2afdcfaebb 100644 --- a/osu.Game/Screens/Play/GameplayClockContainer.cs +++ b/osu.Game/Screens/Play/GameplayClockContainer.cs @@ -39,6 +39,8 @@ namespace osu.Game.Screens.Play /// public double StartTime { get; protected set; } + public double GameplayStartTime { get; protected set; } + public IAdjustableAudioComponent AdjustmentsFromMods { get; } = new AudioAdjustments(); private readonly BindableBool isPaused = new BindableBool(true); diff --git a/osu.Game/Screens/Play/IGameplayClock.cs b/osu.Game/Screens/Play/IGameplayClock.cs index ad28e343ff..bef7362aa9 100644 --- a/osu.Game/Screens/Play/IGameplayClock.cs +++ b/osu.Game/Screens/Play/IGameplayClock.cs @@ -18,6 +18,11 @@ namespace osu.Game.Screens.Play /// double StartTime { get; } + /// + /// The time from which actual gameplay should start. When intro time is skipped, this will be the seeked location. + /// + double GameplayStartTime { get; } + /// /// All adjustments applied to this clock which come from mods. /// diff --git a/osu.Game/Screens/Play/MasterGameplayClockContainer.cs b/osu.Game/Screens/Play/MasterGameplayClockContainer.cs index 3851806788..0b47d8ed85 100644 --- a/osu.Game/Screens/Play/MasterGameplayClockContainer.cs +++ b/osu.Game/Screens/Play/MasterGameplayClockContainer.cs @@ -57,8 +57,6 @@ namespace osu.Game.Screens.Play private Track track; - private readonly double skipTargetTime; - [Resolved] private MusicController musicController { get; set; } = null!; @@ -66,16 +64,16 @@ namespace osu.Game.Screens.Play /// Create a new master gameplay clock container. /// /// The beatmap to be used for time and metadata references. - /// The latest time which should be used when introducing gameplay. Will be used when skipping forward. - public MasterGameplayClockContainer(WorkingBeatmap beatmap, double skipTargetTime) + /// The latest time which should be used when introducing gameplay. Will be used when skipping forward. + public MasterGameplayClockContainer(WorkingBeatmap beatmap, double gameplayStartTime) : base(beatmap.Track, applyOffsets: true, requireDecoupling: true) { this.beatmap = beatmap; - this.skipTargetTime = skipTargetTime; track = beatmap.Track; StartTime = findEarliestStartTime(); + GameplayStartTime = gameplayStartTime; } private double findEarliestStartTime() @@ -84,7 +82,7 @@ namespace osu.Game.Screens.Play // generally this is either zero, or some point earlier than zero in the case of storyboards, lead-ins etc. // start with the originally provided latest time (if before zero). - double time = Math.Min(0, skipTargetTime); + double time = Math.Min(0, GameplayStartTime); // if a storyboard is present, it may dictate the appropriate start time by having events in negative time space. // this is commonly used to display an intro before the audio track start. @@ -119,10 +117,10 @@ namespace osu.Game.Screens.Play ///
public void Skip() { - if (GameplayClock.CurrentTime > skipTargetTime - MINIMUM_SKIP_TIME) + if (GameplayClock.CurrentTime > GameplayStartTime - MINIMUM_SKIP_TIME) return; - double skipTarget = skipTargetTime - MINIMUM_SKIP_TIME; + double skipTarget = GameplayStartTime - MINIMUM_SKIP_TIME; if (StartTime < -10000 && GameplayClock.CurrentTime < 0 && skipTarget > 6000) // double skip exception for storyboards with very long intros @@ -187,7 +185,8 @@ namespace osu.Game.Screens.Play } else { - Logger.Log($"Playback discrepancy detected ({playbackDiscrepancyCount} of allowed {allowed_playback_discrepancies}): {elapsedGameplayClockTime:N1} vs {elapsedValidationTime:N1}"); + Logger.Log( + $"Playback discrepancy detected ({playbackDiscrepancyCount} of allowed {allowed_playback_discrepancies}): {elapsedGameplayClockTime:N1} vs {elapsedValidationTime:N1}"); } elapsedValidationTime = null; From fb10996951a1821d06f6ffe5a092763cb1e44bca Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 24 Jan 2025 15:30:02 +0900 Subject: [PATCH 0739/1275] Consume `GameplayStartTime` for more lenient offset adjustments --- osu.Game/Screens/Play/PlayerSettings/BeatmapOffsetControl.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/Play/PlayerSettings/BeatmapOffsetControl.cs b/osu.Game/Screens/Play/PlayerSettings/BeatmapOffsetControl.cs index e988760834..503e9ad15e 100644 --- a/osu.Game/Screens/Play/PlayerSettings/BeatmapOffsetControl.cs +++ b/osu.Game/Screens/Play/PlayerSettings/BeatmapOffsetControl.cs @@ -291,7 +291,7 @@ namespace osu.Game.Screens.Play.PlayerSettings Debug.Assert(gameplayClock != null); // TODO: the blocking conditions should probably display a message. - if (!player.IsBreakTime.Value && gameplayClock.CurrentTime - gameplayClock.StartTime > 10000) + if (!player.IsBreakTime.Value && gameplayClock.CurrentTime - gameplayClock.GameplayStartTime > 10000) return false; if (gameplayClock.IsPaused.Value) From ee78e1b2234bd7c1a94a7be58d48c9b82ce88923 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 24 Jan 2025 15:33:39 +0900 Subject: [PATCH 0740/1275] Add safeties against attempting to apply previous play while offset adjust is not allowed This should theoretically not be possible, but while we are sharing this control's implementation between gameplay and non-gameplay usages, let's ensure nothing weird can occur. --- .../Screens/Play/PlayerSettings/BeatmapOffsetControl.cs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/osu.Game/Screens/Play/PlayerSettings/BeatmapOffsetControl.cs b/osu.Game/Screens/Play/PlayerSettings/BeatmapOffsetControl.cs index e988760834..9465624b02 100644 --- a/osu.Game/Screens/Play/PlayerSettings/BeatmapOffsetControl.cs +++ b/osu.Game/Screens/Play/PlayerSettings/BeatmapOffsetControl.cs @@ -245,6 +245,9 @@ namespace osu.Game.Screens.Play.PlayerSettings Text = BeatmapOffsetControlStrings.CalibrateUsingLastPlay, Action = () => { + if (Current.Disabled) + return; + Current.Value = lastPlayBeatmapOffset - lastPlayAverage; lastAppliedScore.Value = ReferenceScore.Value; }, @@ -277,6 +280,9 @@ namespace osu.Game.Screens.Play.PlayerSettings protected override void Update() { base.Update(); + + if (useAverageButton != null) + useAverageButton.Enabled.Value = allowOffsetAdjust; Current.Disabled = !allowOffsetAdjust; } From 8f8a6455b4dfde594d78234c1cd3ca346337570f Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 24 Jan 2025 15:34:03 +0900 Subject: [PATCH 0741/1275] Bypass offset disallowed status when handling realm callbacks Hopefully don't need to overthink this one. --- osu.Game/Screens/Play/PlayerSettings/BeatmapOffsetControl.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/osu.Game/Screens/Play/PlayerSettings/BeatmapOffsetControl.cs b/osu.Game/Screens/Play/PlayerSettings/BeatmapOffsetControl.cs index 9465624b02..c7367ea8c6 100644 --- a/osu.Game/Screens/Play/PlayerSettings/BeatmapOffsetControl.cs +++ b/osu.Game/Screens/Play/PlayerSettings/BeatmapOffsetControl.cs @@ -121,7 +121,11 @@ namespace osu.Game.Screens.Play.PlayerSettings // At the point we reach here, it's not guaranteed that all realm writes have taken place (there may be some in-flight). // We are only aware of writes that originated from our own flow, so if we do see one that's active we can avoid handling the feedback value arriving. if (realmWriteTask == null) + { + Current.Disabled = false; + Current.Disabled = allowOffsetAdjust; Current.Value = val; + } if (realmWriteTask?.IsCompleted == true) { From 05b1002e9d4f7de5d5db4ef28784dd3b8bf57c99 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 24 Jan 2025 15:57:13 +0900 Subject: [PATCH 0742/1275] Adjust layout and code quality slightly --- .../OnlinePlay/Lounge/Components/DrawableRoom.cs | 14 ++++---------- .../OnlinePlay/Lounge/DrawableLoungeRoom.cs | 11 ++++------- 2 files changed, 8 insertions(+), 17 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/Lounge/Components/DrawableRoom.cs b/osu.Game/Screens/OnlinePlay/Lounge/Components/DrawableRoom.cs index 7463e05c96..4402d1cf5c 100644 --- a/osu.Game/Screens/OnlinePlay/Lounge/Components/DrawableRoom.cs +++ b/osu.Game/Screens/OnlinePlay/Lounge/Components/DrawableRoom.cs @@ -349,23 +349,17 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components if (Room.RoomID.HasValue) { items.AddRange([ - new OsuMenuItem("View in browser", MenuItemType.Standard, () => - { - game?.OpenUrlExternally(formatRoomUrl(Room.RoomID.Value)); - }), - new OsuMenuItem("Copy link", MenuItemType.Standard, () => - { - game?.CopyUrlToClipboard(formatRoomUrl(Room.RoomID.Value)); - }) + new OsuMenuItem("View in browser", MenuItemType.Standard, () => game?.OpenUrlExternally(formatRoomUrl(Room.RoomID.Value))), + new OsuMenuItem("Copy link", MenuItemType.Standard, () => game?.CopyUrlToClipboard(formatRoomUrl(Room.RoomID.Value))) ]); } return items.ToArray(); + + string formatRoomUrl(long id) => $@"{api.WebsiteRootUrl}/multiplayer/rooms/{id}"; } } - private string formatRoomUrl(long id) => $@"{api.WebsiteRootUrl}/multiplayer/rooms/{id}"; - protected virtual UpdateableBeatmapBackgroundSprite CreateBackground() => new UpdateableBeatmapBackgroundSprite(); protected virtual IEnumerable CreateBottomDetails() diff --git a/osu.Game/Screens/OnlinePlay/Lounge/DrawableLoungeRoom.cs b/osu.Game/Screens/OnlinePlay/Lounge/DrawableLoungeRoom.cs index f2afbcef71..1cabb22e30 100644 --- a/osu.Game/Screens/OnlinePlay/Lounge/DrawableLoungeRoom.cs +++ b/osu.Game/Screens/OnlinePlay/Lounge/DrawableLoungeRoom.cs @@ -159,16 +159,13 @@ namespace osu.Game.Screens.OnlinePlay.Lounge { get { - var items = new List - { - new OsuMenuItem("Create copy", MenuItemType.Standard, () => - { - lounge?.OpenCopy(Room); - }) - }; + var items = new List(); items.AddRange(base.ContextMenuItems); + items.Add(new OsuMenuItemSpacer()); + items.Add(new OsuMenuItem("Create copy", MenuItemType.Standard, () => lounge?.OpenCopy(Room))); + if (Room.Type == MatchType.Playlists && Room.Host?.Id == api.LocalUser.Value.Id && Room.StartDate?.AddMinutes(5) >= DateTimeOffset.Now && !Room.HasEnded) { items.Add(new OsuMenuItem("Close playlist", MenuItemType.Destructive, () => From 28a59f4e29bce5a14c8672de6a7ed8b5bb417fcc Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 24 Jan 2025 16:45:14 +0900 Subject: [PATCH 0743/1275] Move line to correct location --- osu.Game/Screens/Play/PlayerSettings/BeatmapOffsetControl.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/Play/PlayerSettings/BeatmapOffsetControl.cs b/osu.Game/Screens/Play/PlayerSettings/BeatmapOffsetControl.cs index c7367ea8c6..ace001f635 100644 --- a/osu.Game/Screens/Play/PlayerSettings/BeatmapOffsetControl.cs +++ b/osu.Game/Screens/Play/PlayerSettings/BeatmapOffsetControl.cs @@ -123,8 +123,8 @@ namespace osu.Game.Screens.Play.PlayerSettings if (realmWriteTask == null) { Current.Disabled = false; - Current.Disabled = allowOffsetAdjust; Current.Value = val; + Current.Disabled = allowOffsetAdjust; } if (realmWriteTask?.IsCompleted == true) From 721b2dfbbaed488fbc65cd44b91506bc073703eb Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 24 Jan 2025 17:16:51 +0900 Subject: [PATCH 0744/1275] Fix average button not correctly becoming disabled where it previously would --- .../PlayerSettings/BeatmapOffsetControl.cs | 19 ++++++++----------- 1 file changed, 8 insertions(+), 11 deletions(-) diff --git a/osu.Game/Screens/Play/PlayerSettings/BeatmapOffsetControl.cs b/osu.Game/Screens/Play/PlayerSettings/BeatmapOffsetControl.cs index ace001f635..e0b0a1b0ab 100644 --- a/osu.Game/Screens/Play/PlayerSettings/BeatmapOffsetControl.cs +++ b/osu.Game/Screens/Play/PlayerSettings/BeatmapOffsetControl.cs @@ -138,15 +138,15 @@ namespace osu.Game.Screens.Play.PlayerSettings ReferenceScore.BindValueChanged(scoreChanged, true); } + // the last play graph is relative to the offset at the point of the last play, so we need to factor that out for some usages. + private double adjustmentSinceLastPlay => lastPlayBeatmapOffset - Current.Value; + private void currentChanged(ValueChangedEvent offset) { Scheduler.AddOnce(updateOffset); void updateOffset() { - // the last play graph is relative to the offset at the point of the last play, so we need to factor that out. - double adjustmentSinceLastPlay = lastPlayBeatmapOffset - Current.Value; - // Negative is applied here because the play graph is considering a hit offset, not track (as we currently use for clocks). lastPlayGraph?.UpdateOffset(-adjustmentSinceLastPlay); @@ -157,11 +157,6 @@ namespace osu.Game.Screens.Play.PlayerSettings return; } - if (useAverageButton != null) - { - useAverageButton.Enabled.Value = !Precision.AlmostEquals(lastPlayAverage, adjustmentSinceLastPlay, Current.Precision / 2); - } - realmWriteTask = realm.WriteAsync(r => { var setInfo = r.Find(beatmap.Value.BeatmapSetInfo.ID); @@ -255,7 +250,6 @@ namespace osu.Game.Screens.Play.PlayerSettings Current.Value = lastPlayBeatmapOffset - lastPlayAverage; lastAppliedScore.Value = ReferenceScore.Value; }, - Enabled = { Value = !Precision.AlmostEquals(lastPlayAverage, 0, Current.Precision / 2) } }, globalOffsetText = new LinkFlowContainer { @@ -285,9 +279,12 @@ namespace osu.Game.Screens.Play.PlayerSettings { base.Update(); + bool allow = allowOffsetAdjust; + if (useAverageButton != null) - useAverageButton.Enabled.Value = allowOffsetAdjust; - Current.Disabled = !allowOffsetAdjust; + useAverageButton.Enabled.Value = allow && !Precision.AlmostEquals(lastPlayAverage, adjustmentSinceLastPlay, Current.Precision / 2); + + Current.Disabled = !allow; } private bool allowOffsetAdjust From 17b1739ae49b549692c61eaddae35682b9e9053b Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 24 Jan 2025 18:00:05 +0900 Subject: [PATCH 0745/1275] Combine countless update methods all called together into a single method --- .../Screens/OnlinePlay/Match/RoomSubScreen.cs | 52 +++++++------------ 1 file changed, 19 insertions(+), 33 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs b/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs index edb44a7666..9915560a95 100644 --- a/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs @@ -355,11 +355,11 @@ namespace osu.Game.Screens.OnlinePlay.Match public override void OnResuming(ScreenTransitionEvent e) { base.OnResuming(e); + updateBeatmap(); + updateSpecifics(); + beginHandlingTrack(); - Scheduler.AddOnce(updateMods); - Scheduler.AddOnce(updateRuleset); - Scheduler.AddOnce(updateUserStyle); } protected bool ExitConfirmed { get; private set; } @@ -448,9 +448,7 @@ namespace osu.Game.Screens.OnlinePlay.Match updateUserMods(); updateBeatmap(); - updateMods(); - updateRuleset(); - updateUserStyle(); + updateSpecifics(); if (!item.AllowedMods.Any()) { @@ -501,43 +499,31 @@ namespace osu.Game.Screens.OnlinePlay.Match UserModsSelectOverlay.Beatmap.Value = Beatmap.Value; } - private void updateMods() + private void updateSpecifics() { if (!this.IsCurrentScreen() || SelectedItem.Value is not PlaylistItem) return; var rulesetInstance = GetGameplayRuleset().CreateInstance(); Mods.Value = GetGameplayMods().Select(m => m.ToMod(rulesetInstance)).ToArray(); - } - - private void updateRuleset() - { - if (!this.IsCurrentScreen() || SelectedItem.Value is not PlaylistItem) - return; Ruleset.Value = GetGameplayRuleset(); - } - private void updateUserStyle() - { - if (!this.IsCurrentScreen() || SelectedItem.Value is not PlaylistItem) - return; - - if (UserStyleDisplayContainer == null) - return; - - PlaylistItem gameplayItem = SelectedItem.Value.With(ruleset: GetGameplayRuleset().OnlineID, beatmap: new Optional(GetGameplayBeatmap())); - PlaylistItem? currentItem = UserStyleDisplayContainer.SingleOrDefault()?.Item; - - if (gameplayItem.Equals(currentItem)) - return; - - UserStyleDisplayContainer.Child = new DrawableRoomPlaylistItem(gameplayItem) + if (UserStyleDisplayContainer != null) { - AllowReordering = false, - AllowEditing = true, - RequestEdit = _ => OpenStyleSelection() - }; + PlaylistItem gameplayItem = SelectedItem.Value.With(ruleset: GetGameplayRuleset().OnlineID, beatmap: new Optional(GetGameplayBeatmap())); + PlaylistItem? currentItem = UserStyleDisplayContainer.SingleOrDefault()?.Item; + + if (gameplayItem.Equals(currentItem)) + return; + + UserStyleDisplayContainer.Child = new DrawableRoomPlaylistItem(gameplayItem) + { + AllowReordering = false, + AllowEditing = true, + RequestEdit = _ => OpenStyleSelection() + }; + } } protected virtual APIMod[] GetGameplayMods() From 92429b2ed9e8f7a658196659656aeb9ec7dcd14d Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 24 Jan 2025 18:34:04 +0900 Subject: [PATCH 0746/1275] Adjust comments on `ICarouselPanel` to imply external use --- osu.Game/Screens/SelectV2/ICarouselPanel.cs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/osu.Game/Screens/SelectV2/ICarouselPanel.cs b/osu.Game/Screens/SelectV2/ICarouselPanel.cs index c592734d8d..2776fdec6c 100644 --- a/osu.Game/Screens/SelectV2/ICarouselPanel.cs +++ b/osu.Game/Screens/SelectV2/ICarouselPanel.cs @@ -3,33 +3,33 @@ using osu.Framework.Bindables; using osu.Framework.Graphics; +using osu.Framework.Graphics.Pooling; namespace osu.Game.Screens.SelectV2 { /// /// An interface to be attached to any s which are used for display inside a . + /// Importantly, all properties in this interface are managed by and should not be written to elsewhere. /// public interface ICarouselPanel { /// - /// Whether this item has selection. - /// This is managed by and should not be set manually. + /// Whether this item has selection. Should be read from to update the visual state. /// BindableBool Selected { get; } /// - /// Whether this item has keyboard selection. - /// This is managed by and should not be set manually. + /// Whether this item has keyboard selection. Should be read from to update the visual state. /// BindableBool KeyboardSelected { get; } /// - /// The Y position which should be used for displaying this item within the carousel. This is managed by and should not be set manually. + /// The Y position used internally for positioning in the carousel. /// double DrawYPosition { get; set; } /// - /// The carousel item this drawable is representing. This is managed by and should not be set manually. + /// The carousel item this drawable is representing. Will be set before is called. /// CarouselItem? Item { get; set; } } From 9366bfbf0d317e18884086f5532c5cf12443f904 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 24 Jan 2025 18:40:48 +0900 Subject: [PATCH 0747/1275] Move activation drawable flow portion to `ICarouselPanel` --- osu.Game/Screens/SelectV2/BeatmapCarousel.cs | 9 --------- osu.Game/Screens/SelectV2/BeatmapCarouselPanel.cs | 2 +- osu.Game/Screens/SelectV2/Carousel.cs | 3 +++ osu.Game/Screens/SelectV2/ICarouselPanel.cs | 5 +++++ 4 files changed, 9 insertions(+), 10 deletions(-) diff --git a/osu.Game/Screens/SelectV2/BeatmapCarousel.cs b/osu.Game/Screens/SelectV2/BeatmapCarousel.cs index aca71efe93..630f7b6583 100644 --- a/osu.Game/Screens/SelectV2/BeatmapCarousel.cs +++ b/osu.Game/Screens/SelectV2/BeatmapCarousel.cs @@ -124,15 +124,6 @@ namespace osu.Game.Screens.SelectV2 } } - protected override void HandleItemActivated(CarouselItem item) - { - base.HandleItemActivated(item); - - // TODO: maybe this should be handled by the panel itself? - if (GetMaterialisedDrawableForItem(item) is BeatmapCarouselPanel drawable) - drawable.FlashFromActivation(); - } - #endregion #region Filtering diff --git a/osu.Game/Screens/SelectV2/BeatmapCarouselPanel.cs b/osu.Game/Screens/SelectV2/BeatmapCarouselPanel.cs index 9219656365..398ec7bf4c 100644 --- a/osu.Game/Screens/SelectV2/BeatmapCarouselPanel.cs +++ b/osu.Game/Screens/SelectV2/BeatmapCarouselPanel.cs @@ -123,7 +123,7 @@ namespace osu.Game.Screens.SelectV2 public double DrawYPosition { get; set; } - public void FlashFromActivation() + public void Activated() { activationFlash.FadeOutFromOne(500, Easing.OutQuint); } diff --git a/osu.Game/Screens/SelectV2/Carousel.cs b/osu.Game/Screens/SelectV2/Carousel.cs index 6899c10451..6ff27c6198 100644 --- a/osu.Game/Screens/SelectV2/Carousel.cs +++ b/osu.Game/Screens/SelectV2/Carousel.cs @@ -109,7 +109,10 @@ namespace osu.Game.Screens.SelectV2 } if (currentSelection.CarouselItem != null) + { + (GetMaterialisedDrawableForItem(currentSelection.CarouselItem) as ICarouselPanel)?.Activated(); HandleItemActivated(currentSelection.CarouselItem); + } } #endregion diff --git a/osu.Game/Screens/SelectV2/ICarouselPanel.cs b/osu.Game/Screens/SelectV2/ICarouselPanel.cs index 2776fdec6c..a956bb22a3 100644 --- a/osu.Game/Screens/SelectV2/ICarouselPanel.cs +++ b/osu.Game/Screens/SelectV2/ICarouselPanel.cs @@ -23,6 +23,11 @@ namespace osu.Game.Screens.SelectV2 /// BindableBool KeyboardSelected { get; } + /// + /// Called when the panel is activated. Should be used to update the panel's visual state. + /// + void Activated(); + /// /// The Y position used internally for positioning in the carousel. /// From 15b6e28ebe888b1a87574891be1a0db3b04093b7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 21 Jan 2025 12:16:36 +0100 Subject: [PATCH 0748/1275] Remove dependence of blueprint containers on `IPositionSnapProvider` --- .../Edit/CatchBlueprintContainer.cs | 29 +++++++ .../Edit/CatchHitObjectComposer.cs | 20 ++--- .../Editor/TestSceneManiaBeatSnapGrid.cs | 6 -- .../Blueprints/HoldNoteSelectionBlueprint.cs | 3 +- .../Edit/ManiaBlueprintContainer.cs | 25 +++++- .../Components/PathControlPointVisualiser.cs | 8 +- .../Edit/OsuBlueprintContainer.cs | 67 ++++++++++++++- .../Edit/OsuHitObjectComposer.cs | 80 ++++++++--------- .../Edit/TaikoBlueprintContainer.cs | 25 +++++- .../SkinEditor/SkinBlueprintContainer.cs | 5 ++ osu.Game/Rulesets/Edit/HitObjectComposer.cs | 32 +------ .../Edit/ScrollingHitObjectComposer.cs | 17 ++++ .../Compose/Components/BlueprintContainer.cs | 86 +++---------------- .../Components/ComposeBlueprintContainer.cs | 6 +- .../Components/EditorBlueprintContainer.cs | 29 +++---- .../Compose/Components/Timeline/Timeline.cs | 4 +- .../Timeline/TimelineBlueprintContainer.cs | 17 ++++ 17 files changed, 263 insertions(+), 196 deletions(-) diff --git a/osu.Game.Rulesets.Catch/Edit/CatchBlueprintContainer.cs b/osu.Game.Rulesets.Catch/Edit/CatchBlueprintContainer.cs index 3979d30616..47035b0227 100644 --- a/osu.Game.Rulesets.Catch/Edit/CatchBlueprintContainer.cs +++ b/osu.Game.Rulesets.Catch/Edit/CatchBlueprintContainer.cs @@ -1,16 +1,22 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System.Collections.Generic; +using System.Linq; +using osu.Framework.Input.Events; using osu.Game.Rulesets.Catch.Edit.Blueprints; using osu.Game.Rulesets.Catch.Objects; using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Objects; using osu.Game.Screens.Edit.Compose.Components; +using osuTK; namespace osu.Game.Rulesets.Catch.Edit { public partial class CatchBlueprintContainer : ComposeBlueprintContainer { + public new CatchHitObjectComposer Composer => (CatchHitObjectComposer)base.Composer; + public CatchBlueprintContainer(CatchHitObjectComposer composer) : base(composer) { @@ -36,5 +42,28 @@ namespace osu.Game.Rulesets.Catch.Edit } protected sealed override DragBox CreateDragBox() => new ScrollingDragBox(Composer.Playfield); + + protected override bool TryMoveBlueprints(DragEvent e, IList<(SelectionBlueprint blueprint, Vector2[] originalSnapPositions)> blueprints) + { + Vector2 distanceTravelled = e.ScreenSpaceMousePosition - e.ScreenSpaceMouseDownPosition; + + // The final movement position, relative to movementBlueprintOriginalPosition. + Vector2 movePosition = blueprints.First().originalSnapPositions.First() + distanceTravelled; + + // Retrieve a snapped position. + var gridSnapResult = Composer.FindSnappedPositionAndTime(movePosition); + gridSnapResult.ScreenSpacePosition.X = movePosition.X; + var distanceSnapResult = Composer.TryDistanceSnap(gridSnapResult.ScreenSpacePosition); + + var result = distanceSnapResult != null && Vector2.Distance(gridSnapResult.ScreenSpacePosition, distanceSnapResult.ScreenSpacePosition) < CatchHitObjectComposer.DISTANCE_SNAP_RADIUS + ? distanceSnapResult + : gridSnapResult; + + var referenceBlueprint = blueprints.First().blueprint; + bool moved = SelectionHandler.HandleMovement(new MoveSelectionEvent(referenceBlueprint, result.ScreenSpacePosition - referenceBlueprint.ScreenSpaceSelectionPoint)); + if (moved) + ApplySnapResultTime(result, referenceBlueprint.Item.StartTime); + return moved; + } } } diff --git a/osu.Game.Rulesets.Catch/Edit/CatchHitObjectComposer.cs b/osu.Game.Rulesets.Catch/Edit/CatchHitObjectComposer.cs index 7bb5539963..9618eb28a9 100644 --- a/osu.Game.Rulesets.Catch/Edit/CatchHitObjectComposer.cs +++ b/osu.Game.Rulesets.Catch/Edit/CatchHitObjectComposer.cs @@ -25,7 +25,7 @@ namespace osu.Game.Rulesets.Catch.Edit { public partial class CatchHitObjectComposer : ScrollingHitObjectComposer, IKeyBindingHandler { - private const float distance_snap_radius = 50; + public const float DISTANCE_SNAP_RADIUS = 50; private CatchDistanceSnapGrid distanceSnapGrid = null!; @@ -135,22 +135,12 @@ namespace osu.Game.Rulesets.Catch.Edit DistanceSnapProvider.HandleToggleViaKey(key); } - public override SnapResult FindSnappedPositionAndTime(Vector2 screenSpacePosition, SnapType snapType = SnapType.All) + public SnapResult? TryDistanceSnap(Vector2 screenSpacePosition) { - var result = base.FindSnappedPositionAndTime(screenSpacePosition, snapType); + if (distanceSnapGrid.IsPresent && distanceSnapGrid.GetSnappedPosition(screenSpacePosition) is SnapResult snapResult) + return snapResult; - result.ScreenSpacePosition.X = screenSpacePosition.X; - - if (snapType.HasFlag(SnapType.RelativeGrids)) - { - if (distanceSnapGrid.IsPresent && distanceSnapGrid.GetSnappedPosition(result.ScreenSpacePosition) is SnapResult snapResult && - Vector2.Distance(snapResult.ScreenSpacePosition, result.ScreenSpacePosition) < distance_snap_radius) - { - result = snapResult; - } - } - - return result; + return null; } private PalpableCatchHitObject? getLastSnappableHitObject(double time) diff --git a/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneManiaBeatSnapGrid.cs b/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneManiaBeatSnapGrid.cs index 127beed83e..19ff13e216 100644 --- a/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneManiaBeatSnapGrid.cs +++ b/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneManiaBeatSnapGrid.cs @@ -20,7 +20,6 @@ using osu.Game.Rulesets.UI.Scrolling; using osu.Game.Screens.Edit; using osu.Game.Screens.Edit.Compose.Components; using osu.Game.Tests.Visual; -using osuTK; namespace osu.Game.Rulesets.Mania.Tests.Editor { @@ -100,10 +99,5 @@ namespace osu.Game.Rulesets.Mania.Tests.Editor { set => InternalChild = value; } - - public override SnapResult FindSnappedPositionAndTime(Vector2 screenSpacePosition, SnapType snapType = SnapType.All) - { - throw new NotImplementedException(); - } } } diff --git a/osu.Game.Rulesets.Mania/Edit/Blueprints/HoldNoteSelectionBlueprint.cs b/osu.Game.Rulesets.Mania/Edit/Blueprints/HoldNoteSelectionBlueprint.cs index 915706c044..ff29154f87 100644 --- a/osu.Game.Rulesets.Mania/Edit/Blueprints/HoldNoteSelectionBlueprint.cs +++ b/osu.Game.Rulesets.Mania/Edit/Blueprints/HoldNoteSelectionBlueprint.cs @@ -5,7 +5,6 @@ using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Primitives; -using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Mania.Edit.Blueprints.Components; using osu.Game.Rulesets.Mania.Objects; using osu.Game.Rulesets.Mania.Objects.Drawables; @@ -24,7 +23,7 @@ namespace osu.Game.Rulesets.Mania.Edit.Blueprints private EditorBeatmap? editorBeatmap { get; set; } [Resolved] - private IPositionSnapProvider? positionSnapProvider { get; set; } + private ManiaHitObjectComposer? positionSnapProvider { get; set; } private EditBodyPiece body = null!; private EditHoldNoteEndPiece head = null!; diff --git a/osu.Game.Rulesets.Mania/Edit/ManiaBlueprintContainer.cs b/osu.Game.Rulesets.Mania/Edit/ManiaBlueprintContainer.cs index d0eb8c1e6e..4eb54e6366 100644 --- a/osu.Game.Rulesets.Mania/Edit/ManiaBlueprintContainer.cs +++ b/osu.Game.Rulesets.Mania/Edit/ManiaBlueprintContainer.cs @@ -1,17 +1,23 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System.Collections.Generic; +using System.Linq; +using osu.Framework.Input.Events; using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Mania.Edit.Blueprints; using osu.Game.Rulesets.Mania.Objects; using osu.Game.Rulesets.Objects; using osu.Game.Screens.Edit.Compose.Components; +using osuTK; namespace osu.Game.Rulesets.Mania.Edit { public partial class ManiaBlueprintContainer : ComposeBlueprintContainer { - public ManiaBlueprintContainer(HitObjectComposer composer) + public new ManiaHitObjectComposer Composer => (ManiaHitObjectComposer)base.Composer; + + public ManiaBlueprintContainer(ManiaHitObjectComposer composer) : base(composer) { } @@ -33,5 +39,22 @@ namespace osu.Game.Rulesets.Mania.Edit protected override SelectionHandler CreateSelectionHandler() => new ManiaSelectionHandler(); protected sealed override DragBox CreateDragBox() => new ScrollingDragBox(Composer.Playfield); + + protected override bool TryMoveBlueprints(DragEvent e, IList<(SelectionBlueprint blueprint, Vector2[] originalSnapPositions)> blueprints) + { + Vector2 distanceTravelled = e.ScreenSpaceMousePosition - e.ScreenSpaceMouseDownPosition; + + // The final movement position, relative to movementBlueprintOriginalPosition. + Vector2 movePosition = blueprints.First().originalSnapPositions.First() + distanceTravelled; + + // Retrieve a snapped position. + var result = Composer.FindSnappedPositionAndTime(movePosition); + + var referenceBlueprint = blueprints.First().blueprint; + bool moved = SelectionHandler.HandleMovement(new MoveSelectionEvent(referenceBlueprint, result.ScreenSpacePosition - referenceBlueprint.ScreenSpaceSelectionPoint)); + if (moved) + ApplySnapResultTime(result, referenceBlueprint.Item.StartTime); + return moved; + } } } diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs index f98117c0fa..bac5f0101c 100644 --- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs +++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs @@ -48,7 +48,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components public Action> SplitControlPointsRequested; [Resolved(CanBeNull = true)] - private IPositionSnapProvider positionSnapProvider { get; set; } + private OsuHitObjectComposer positionSnapProvider { get; set; } [Resolved(CanBeNull = true)] private IDistanceSnapProvider distanceSnapProvider { get; set; } @@ -433,7 +433,9 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components { // Special handling for selections containing head control point - the position of the hit object changes which means the snapped position and time have to be taken into account Vector2 newHeadPosition = Parent!.ToScreenSpace(e.MousePosition + (dragStartPositions[0] - dragStartPositions[draggedControlPointIndex])); - SnapResult result = positionSnapProvider?.FindSnappedPositionAndTime(newHeadPosition); + SnapResult result = positionSnapProvider?.TrySnapToNearbyObjects(newHeadPosition) + ?? positionSnapProvider?.TrySnapToDistanceGrid(newHeadPosition) + ?? positionSnapProvider?.TrySnapToPositionGrid(newHeadPosition); Vector2 movementDelta = Parent!.ToLocalSpace(result?.ScreenSpacePosition ?? newHeadPosition) - hitObject.Position; @@ -453,7 +455,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components } else { - SnapResult result = positionSnapProvider?.FindSnappedPositionAndTime(Parent!.ToScreenSpace(e.MousePosition), SnapType.GlobalGrids); + SnapResult result = positionSnapProvider?.TrySnapToPositionGrid(Parent!.ToScreenSpace(e.MousePosition)); Vector2 movementDelta = Parent!.ToLocalSpace(result?.ScreenSpacePosition ?? Parent!.ToScreenSpace(e.MousePosition)) - dragStartPositions[draggedControlPointIndex] - hitObject.Position; diff --git a/osu.Game.Rulesets.Osu/Edit/OsuBlueprintContainer.cs b/osu.Game.Rulesets.Osu/Edit/OsuBlueprintContainer.cs index 54c54fca17..235368e552 100644 --- a/osu.Game.Rulesets.Osu/Edit/OsuBlueprintContainer.cs +++ b/osu.Game.Rulesets.Osu/Edit/OsuBlueprintContainer.cs @@ -1,6 +1,9 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System.Collections.Generic; +using System.Linq; +using osu.Framework.Input.Events; using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Osu.Edit.Blueprints.HitCircles; @@ -8,12 +11,15 @@ using osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders; using osu.Game.Rulesets.Osu.Edit.Blueprints.Spinners; using osu.Game.Rulesets.Osu.Objects; using osu.Game.Screens.Edit.Compose.Components; +using osuTK; namespace osu.Game.Rulesets.Osu.Edit { public partial class OsuBlueprintContainer : ComposeBlueprintContainer { - public OsuBlueprintContainer(HitObjectComposer composer) + public new OsuHitObjectComposer Composer => (OsuHitObjectComposer)base.Composer; + + public OsuBlueprintContainer(OsuHitObjectComposer composer) : base(composer) { } @@ -36,5 +42,64 @@ namespace osu.Game.Rulesets.Osu.Edit return base.CreateHitObjectBlueprintFor(hitObject); } + + protected override bool TryMoveBlueprints(DragEvent e, IList<(SelectionBlueprint blueprint, Vector2[] originalSnapPositions)> blueprints) + { + Vector2 distanceTravelled = e.ScreenSpaceMousePosition - e.ScreenSpaceMouseDownPosition; + + for (int i = 0; i < blueprints.Count; i++) + { + if (checkSnappingBlueprintToNearbyObjects(blueprints[i].blueprint, distanceTravelled, blueprints[i].originalSnapPositions)) + return true; + } + + // if no positional snapping could be performed, try unrestricted snapping from the earliest + // item in the selection. + + // The final movement position, relative to movementBlueprintOriginalPosition. + Vector2 movePosition = blueprints.First().originalSnapPositions.First() + distanceTravelled; + + // Retrieve a snapped position. + var result = Composer.TrySnapToDistanceGrid(movePosition) ?? Composer.TrySnapToPositionGrid(movePosition) ?? new SnapResult(movePosition, null); + + var referenceBlueprint = blueprints.First().blueprint; + bool moved = SelectionHandler.HandleMovement(new MoveSelectionEvent(referenceBlueprint, result.ScreenSpacePosition - referenceBlueprint.ScreenSpaceSelectionPoint)); + if (moved) + ApplySnapResultTime(result, referenceBlueprint.Item.StartTime); + return moved; + } + + /// + /// Check for positional snap for given blueprint. + /// + /// The blueprint to check for snapping. + /// Distance travelled since start of dragging action. + /// The snap positions of blueprint before start of dragging action. + /// Whether an object to snap to was found. + private bool checkSnappingBlueprintToNearbyObjects(SelectionBlueprint blueprint, Vector2 distanceTravelled, Vector2[] originalPositions) + { + var currentPositions = blueprint.ScreenSpaceSnapPoints; + + for (int i = 0; i < originalPositions.Length; i++) + { + Vector2 originalPosition = originalPositions[i]; + var testPosition = originalPosition + distanceTravelled; + + var positionalResult = Composer.TrySnapToNearbyObjects(testPosition); + + if (positionalResult == null || positionalResult.ScreenSpacePosition == testPosition) continue; + + var delta = positionalResult.ScreenSpacePosition - currentPositions[i]; + + // attempt to move the objects, and apply any time based snapping if we can. + if (SelectionHandler.HandleMovement(new MoveSelectionEvent(blueprint, delta))) + { + ApplySnapResultTime(positionalResult, blueprint.Item.StartTime); + return true; + } + } + + return false; + } } } diff --git a/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs b/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs index aad3d0c93b..06a74fb631 100644 --- a/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs +++ b/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs @@ -7,6 +7,7 @@ using System; using System.Collections.Generic; using System.Linq; using System.Text.RegularExpressions; +using JetBrains.Annotations; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Caching; @@ -222,56 +223,55 @@ namespace osu.Game.Rulesets.Osu.Edit } } - public override SnapResult FindSnappedPositionAndTime(Vector2 screenSpacePosition, SnapType snapType = SnapType.All) + [CanBeNull] + public SnapResult TrySnapToNearbyObjects(Vector2 screenSpacePosition) { - if (snapType.HasFlag(SnapType.NearbyObjects) && snapToVisibleBlueprints(screenSpacePosition, out var snapResult)) - { - // In the case of snapping to nearby objects, a time value is not provided. - // This matches the stable editor (which also uses current time), but with the introduction of time-snapping distance snap - // this could result in unexpected behaviour when distance snapping is turned on and a user attempts to place an object that is - // BOTH on a valid distance snap ring, and also at the same position as a previous object. - // - // We want to ensure that in this particular case, the time-snapping component of distance snap is still applied. - // The easiest way to ensure this is to attempt application of distance snap after a nearby object is found, and copy over - // the time value if the proposed positions are roughly the same. - if (snapType.HasFlag(SnapType.RelativeGrids) && DistanceSnapProvider.DistanceSnapToggle.Value == TernaryState.True && distanceSnapGrid != null) - { - (Vector2 distanceSnappedPosition, double distanceSnappedTime) = distanceSnapGrid.GetSnappedPosition(distanceSnapGrid.ToLocalSpace(snapResult.ScreenSpacePosition)); - if (Precision.AlmostEquals(distanceSnapGrid.ToScreenSpace(distanceSnappedPosition), snapResult.ScreenSpacePosition, 1)) - snapResult.Time = distanceSnappedTime; - } + if (!snapToVisibleBlueprints(screenSpacePosition, out var snapResult)) + return null; + if (DistanceSnapProvider.DistanceSnapToggle.Value != TernaryState.True || distanceSnapGrid == null) return snapResult; - } - SnapResult result = base.FindSnappedPositionAndTime(screenSpacePosition, snapType); + // In the case of snapping to nearby objects, a time value is not provided. + // This matches the stable editor (which also uses current time), but with the introduction of time-snapping distance snap + // this could result in unexpected behaviour when distance snapping is turned on and a user attempts to place an object that is + // BOTH on a valid distance snap ring, and also at the same position as a previous object. + // + // We want to ensure that in this particular case, the time-snapping component of distance snap is still applied. + // The easiest way to ensure this is to attempt application of distance snap after a nearby object is found, and copy over + // the time value if the proposed positions are roughly the same. + (Vector2 distanceSnappedPosition, double distanceSnappedTime) = distanceSnapGrid.GetSnappedPosition(distanceSnapGrid.ToLocalSpace(snapResult.ScreenSpacePosition)); + if (Precision.AlmostEquals(distanceSnapGrid.ToScreenSpace(distanceSnappedPosition), snapResult.ScreenSpacePosition, 1)) + snapResult.Time = distanceSnappedTime; - if (snapType.HasFlag(SnapType.RelativeGrids)) - { - if (DistanceSnapProvider.DistanceSnapToggle.Value == TernaryState.True && distanceSnapGrid != null) - { - (Vector2 pos, double time) = distanceSnapGrid.GetSnappedPosition(distanceSnapGrid.ToLocalSpace(screenSpacePosition)); + return snapResult; + } - result.ScreenSpacePosition = distanceSnapGrid.ToScreenSpace(pos); - result.Time = time; - } - } + [CanBeNull] + public SnapResult TrySnapToDistanceGrid(Vector2 screenSpacePosition) + { + if (DistanceSnapProvider.DistanceSnapToggle.Value != TernaryState.True || distanceSnapGrid == null) + return null; - if (snapType.HasFlag(SnapType.GlobalGrids)) - { - if (rectangularGridSnapToggle.Value == TernaryState.True) - { - Vector2 pos = positionSnapGrid.GetSnappedPosition(positionSnapGrid.ToLocalSpace(result.ScreenSpacePosition)); + var playfield = PlayfieldAtScreenSpacePosition(screenSpacePosition); + (Vector2 pos, double time) = distanceSnapGrid.GetSnappedPosition(distanceSnapGrid.ToLocalSpace(screenSpacePosition)); + return new SnapResult(distanceSnapGrid.ToScreenSpace(pos), time, playfield); + } - // A grid which doesn't perfectly fit the playfield can produce a position that is outside of the playfield. - // We need to clamp the position to the playfield bounds to ensure that the snapped position is always in bounds. - pos = Vector2.Clamp(pos, Vector2.Zero, OsuPlayfield.BASE_SIZE); + [CanBeNull] + public SnapResult TrySnapToPositionGrid(Vector2 screenSpacePosition) + { + if (rectangularGridSnapToggle.Value != TernaryState.True) + return null; - result.ScreenSpacePosition = positionSnapGrid.ToScreenSpace(pos); - } - } + Vector2 pos = positionSnapGrid.GetSnappedPosition(positionSnapGrid.ToLocalSpace(screenSpacePosition)); - return result; + // A grid which doesn't perfectly fit the playfield can produce a position that is outside of the playfield. + // We need to clamp the position to the playfield bounds to ensure that the snapped position is always in bounds. + pos = Vector2.Clamp(pos, Vector2.Zero, OsuPlayfield.BASE_SIZE); + + var playfield = PlayfieldAtScreenSpacePosition(screenSpacePosition); + return new SnapResult(positionSnapGrid.ToScreenSpace(pos), null, playfield); } private bool snapToVisibleBlueprints(Vector2 screenSpacePosition, out SnapResult snapResult) diff --git a/osu.Game.Rulesets.Taiko/Edit/TaikoBlueprintContainer.cs b/osu.Game.Rulesets.Taiko/Edit/TaikoBlueprintContainer.cs index 027723c02c..f0c3eec044 100644 --- a/osu.Game.Rulesets.Taiko/Edit/TaikoBlueprintContainer.cs +++ b/osu.Game.Rulesets.Taiko/Edit/TaikoBlueprintContainer.cs @@ -1,16 +1,22 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System.Collections.Generic; +using System.Linq; +using osu.Framework.Input.Events; using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Taiko.Edit.Blueprints; using osu.Game.Screens.Edit.Compose.Components; +using osuTK; namespace osu.Game.Rulesets.Taiko.Edit { public partial class TaikoBlueprintContainer : ComposeBlueprintContainer { - public TaikoBlueprintContainer(HitObjectComposer composer) + public new TaikoHitObjectComposer Composer => (TaikoHitObjectComposer)base.Composer; + + public TaikoBlueprintContainer(TaikoHitObjectComposer composer) : base(composer) { } @@ -19,5 +25,22 @@ namespace osu.Game.Rulesets.Taiko.Edit public override HitObjectSelectionBlueprint CreateHitObjectBlueprintFor(HitObject hitObject) => new TaikoSelectionBlueprint(hitObject); + + protected override bool TryMoveBlueprints(DragEvent e, IList<(SelectionBlueprint blueprint, Vector2[] originalSnapPositions)> blueprints) + { + Vector2 distanceTravelled = e.ScreenSpaceMousePosition - e.ScreenSpaceMouseDownPosition; + + // The final movement position, relative to movementBlueprintOriginalPosition. + Vector2 movePosition = blueprints.First().originalSnapPositions.First() + distanceTravelled; + + // Retrieve a snapped position. + var result = Composer.FindSnappedPositionAndTime(movePosition); + + var referenceBlueprint = blueprints.First().blueprint; + bool moved = SelectionHandler.HandleMovement(new MoveSelectionEvent(referenceBlueprint, result.ScreenSpacePosition - referenceBlueprint.ScreenSpaceSelectionPoint)); + if (moved) + ApplySnapResultTime(result, referenceBlueprint.Item.StartTime); + return moved; + } } } diff --git a/osu.Game/Overlays/SkinEditor/SkinBlueprintContainer.cs b/osu.Game/Overlays/SkinEditor/SkinBlueprintContainer.cs index 3f8d9f80d4..8f831a6f18 100644 --- a/osu.Game/Overlays/SkinEditor/SkinBlueprintContainer.cs +++ b/osu.Game/Overlays/SkinEditor/SkinBlueprintContainer.cs @@ -111,6 +111,11 @@ namespace osu.Game.Overlays.SkinEditor SelectedItems.AddRange(targetComponents.SelectMany(list => list).Except(SelectedItems).ToArray()); } + protected override bool TryMoveBlueprints(DragEvent e, IList<(SelectionBlueprint blueprint, Vector2[] originalSnapPositions)> blueprints) + { + throw new System.NotImplementedException(); + } + /// /// Move the current selection spatially by the specified delta, in screen coordinates (ie. the same coordinates as the blueprints). /// diff --git a/osu.Game/Rulesets/Edit/HitObjectComposer.cs b/osu.Game/Rulesets/Edit/HitObjectComposer.cs index 15b60114af..b38b0291e8 100644 --- a/osu.Game/Rulesets/Edit/HitObjectComposer.cs +++ b/osu.Game/Rulesets/Edit/HitObjectComposer.cs @@ -376,7 +376,7 @@ namespace osu.Game.Rulesets.Edit /// /// Construct a relevant blueprint container. This will manage hitobject selection/placement input handling and display logic. /// - protected virtual ComposeBlueprintContainer CreateBlueprintContainer() => new ComposeBlueprintContainer(this); + protected abstract ComposeBlueprintContainer CreateBlueprintContainer(); protected virtual Drawable CreateHitObjectInspector() => new HitObjectInspector(); @@ -566,28 +566,6 @@ namespace osu.Game.Rulesets.Edit /// The most relevant . protected virtual Playfield PlayfieldAtScreenSpacePosition(Vector2 screenSpacePosition) => drawableRulesetWrapper.Playfield; - public override SnapResult FindSnappedPositionAndTime(Vector2 screenSpacePosition, SnapType snapType = SnapType.All) - { - var playfield = PlayfieldAtScreenSpacePosition(screenSpacePosition); - double? targetTime = null; - - if (snapType.HasFlag(SnapType.GlobalGrids)) - { - if (playfield is ScrollingPlayfield scrollingPlayfield) - { - targetTime = scrollingPlayfield.TimeAtScreenSpacePosition(screenSpacePosition); - - // apply beat snapping - targetTime = BeatSnapProvider.SnapTime(targetTime.Value); - - // convert back to screen space - screenSpacePosition = scrollingPlayfield.ScreenSpacePositionAtTime(targetTime.Value); - } - } - - return new SnapResult(screenSpacePosition, targetTime, playfield); - } - #endregion } @@ -596,7 +574,7 @@ namespace osu.Game.Rulesets.Edit /// Generally used to access certain methods without requiring a generic type for . /// [Cached] - public abstract partial class HitObjectComposer : CompositeDrawable, IPositionSnapProvider + public abstract partial class HitObjectComposer : CompositeDrawable { public const float TOOLBOX_CONTRACTED_SIZE_LEFT = 60; public const float TOOLBOX_CONTRACTED_SIZE_RIGHT = 120; @@ -639,11 +617,5 @@ namespace osu.Game.Rulesets.Edit /// The time instant to seek to, in milliseconds. /// The ruleset-specific description of objects to select at the given timestamp. public virtual void SelectFromTimestamp(double timestamp, string objectDescription) { } - - #region IPositionSnapProvider - - public abstract SnapResult FindSnappedPositionAndTime(Vector2 screenSpacePosition, SnapType snapType = SnapType.All); - - #endregion } } diff --git a/osu.Game/Rulesets/Edit/ScrollingHitObjectComposer.cs b/osu.Game/Rulesets/Edit/ScrollingHitObjectComposer.cs index e7161ce36c..3671724042 100644 --- a/osu.Game/Rulesets/Edit/ScrollingHitObjectComposer.cs +++ b/osu.Game/Rulesets/Edit/ScrollingHitObjectComposer.cs @@ -117,6 +117,23 @@ namespace osu.Game.Rulesets.Edit } } + public SnapResult FindSnappedPositionAndTime(Vector2 screenSpacePosition) + { + var scrollingPlayfield = PlayfieldAtScreenSpacePosition(screenSpacePosition) as ScrollingPlayfield; + if (scrollingPlayfield == null) + return new SnapResult(screenSpacePosition, null); + + double? targetTime = scrollingPlayfield.TimeAtScreenSpacePosition(screenSpacePosition); + + // apply beat snapping + targetTime = BeatSnapProvider.SnapTime(targetTime.Value); + + // convert back to screen space + screenSpacePosition = scrollingPlayfield.ScreenSpacePositionAtTime(targetTime.Value); + + return new SnapResult(screenSpacePosition, targetTime, scrollingPlayfield); + } + protected override void Dispose(bool isDisposing) { base.Dispose(isDisposing); diff --git a/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs b/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs index 4a321f4a81..dc04561242 100644 --- a/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs +++ b/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs @@ -43,9 +43,6 @@ namespace osu.Game.Screens.Edit.Compose.Components private readonly Dictionary> blueprintMap = new Dictionary>(); - [Resolved(canBeNull: true)] - private IPositionSnapProvider snapProvider { get; set; } - [Resolved(CanBeNull = true)] private IEditorChangeHandler changeHandler { get; set; } @@ -333,19 +330,19 @@ namespace osu.Game.Screens.Edit.Compose.Components protected void RemoveBlueprintFor(T item) { - if (!blueprintMap.Remove(item, out var blueprint)) + if (!blueprintMap.Remove(item, out var blueprintToRemove)) return; - blueprint.Deselect(); - blueprint.Selected -= OnBlueprintSelected; - blueprint.Deselected -= OnBlueprintDeselected; + blueprintToRemove.Deselect(); + blueprintToRemove.Selected -= OnBlueprintSelected; + blueprintToRemove.Deselected -= OnBlueprintDeselected; - SelectionBlueprints.Remove(blueprint, true); + SelectionBlueprints.Remove(blueprintToRemove, true); - if (movementBlueprints?.Contains(blueprint) == true) + if (movementBlueprints?.Any(m => m.blueprint == blueprintToRemove) == true) finishSelectionMovement(); - OnBlueprintRemoved(blueprint.Item); + OnBlueprintRemoved(blueprintToRemove.Item); } /// @@ -538,8 +535,7 @@ namespace osu.Game.Screens.Edit.Compose.Components #region Selection Movement - private Vector2[][] movementBlueprintsOriginalPositions; - private SelectionBlueprint[] movementBlueprints; + private (SelectionBlueprint blueprint, Vector2[] originalSnapPositions)[] movementBlueprints; /// /// Whether a blueprint is currently being dragged. @@ -572,8 +568,7 @@ namespace osu.Game.Screens.Edit.Compose.Components return false; // Movement is tracked from the blueprint of the earliest item, since it only makes sense to distance snap from that item - movementBlueprints = SortForMovement(SelectionHandler.SelectedBlueprints).ToArray(); - movementBlueprintsOriginalPositions = movementBlueprints.Select(m => m.ScreenSpaceSnapPoints).ToArray(); + movementBlueprints = SortForMovement(SelectionHandler.SelectedBlueprints).Select(b => (b, b.ScreenSpaceSnapPoints)).ToArray(); return true; } @@ -594,68 +589,10 @@ namespace osu.Game.Screens.Edit.Compose.Components if (movementBlueprints == null) return false; - Debug.Assert(movementBlueprintsOriginalPositions != null); - - Vector2 distanceTravelled = e.ScreenSpaceMousePosition - e.ScreenSpaceMouseDownPosition; - - if (snapProvider != null) - { - for (int i = 0; i < movementBlueprints.Length; i++) - { - if (checkSnappingBlueprintToNearbyObjects(movementBlueprints[i], distanceTravelled, movementBlueprintsOriginalPositions[i])) - return true; - } - } - - // if no positional snapping could be performed, try unrestricted snapping from the earliest - // item in the selection. - - // The final movement position, relative to movementBlueprintOriginalPosition. - Vector2 movePosition = movementBlueprintsOriginalPositions.First().First() + distanceTravelled; - - // Retrieve a snapped position. - var result = snapProvider?.FindSnappedPositionAndTime(movePosition, ~SnapType.NearbyObjects); - - if (result == null) - { - return SelectionHandler.HandleMovement(new MoveSelectionEvent(movementBlueprints.First(), movePosition - movementBlueprints.First().ScreenSpaceSelectionPoint)); - } - - return ApplySnapResult(movementBlueprints, result); + return TryMoveBlueprints(e, movementBlueprints); } - /// - /// Check for positional snap for given blueprint. - /// - /// The blueprint to check for snapping. - /// Distance travelled since start of dragging action. - /// The snap positions of blueprint before start of dragging action. - /// Whether an object to snap to was found. - private bool checkSnappingBlueprintToNearbyObjects(SelectionBlueprint blueprint, Vector2 distanceTravelled, Vector2[] originalPositions) - { - var currentPositions = blueprint.ScreenSpaceSnapPoints; - - for (int i = 0; i < originalPositions.Length; i++) - { - Vector2 originalPosition = originalPositions[i]; - var testPosition = originalPosition + distanceTravelled; - - var positionalResult = snapProvider.FindSnappedPositionAndTime(testPosition, SnapType.NearbyObjects); - - if (positionalResult.ScreenSpacePosition == testPosition) continue; - - var delta = positionalResult.ScreenSpacePosition - currentPositions[i]; - - // attempt to move the objects, and abort any time based snapping if we can. - if (SelectionHandler.HandleMovement(new MoveSelectionEvent(blueprint, delta))) - return true; - } - - return false; - } - - protected virtual bool ApplySnapResult(SelectionBlueprint[] blueprints, SnapResult result) => - SelectionHandler.HandleMovement(new MoveSelectionEvent(blueprints.First(), result.ScreenSpacePosition - blueprints.First().ScreenSpaceSelectionPoint)); + protected abstract bool TryMoveBlueprints(DragEvent e, IList<(SelectionBlueprint blueprint, Vector2[] originalSnapPositions)> blueprints); /// /// Finishes the current movement of selected blueprints. @@ -666,7 +603,6 @@ namespace osu.Game.Screens.Edit.Compose.Components if (movementBlueprints == null) return false; - movementBlueprintsOriginalPositions = null; movementBlueprints = null; return true; diff --git a/osu.Game/Screens/Edit/Compose/Components/ComposeBlueprintContainer.cs b/osu.Game/Screens/Edit/Compose/Components/ComposeBlueprintContainer.cs index 15bbddd97e..27d6656c69 100644 --- a/osu.Game/Screens/Edit/Compose/Components/ComposeBlueprintContainer.cs +++ b/osu.Game/Screens/Edit/Compose/Components/ComposeBlueprintContainer.cs @@ -33,7 +33,7 @@ namespace osu.Game.Screens.Edit.Compose.Components /// /// A blueprint container generally displayed as an overlay to a ruleset's playfield. /// - public partial class ComposeBlueprintContainer : EditorBlueprintContainer + public abstract partial class ComposeBlueprintContainer : EditorBlueprintContainer { private readonly Container placementBlueprintContainer; @@ -52,7 +52,7 @@ namespace osu.Game.Screens.Edit.Compose.Components /// public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => editorScreen?.MainContent.ReceivePositionalInputAt(screenSpacePos) ?? base.ReceivePositionalInputAt(screenSpacePos); - public ComposeBlueprintContainer(HitObjectComposer composer) + protected ComposeBlueprintContainer(HitObjectComposer composer) : base(composer) { placementBlueprintContainer = new Container @@ -340,7 +340,7 @@ namespace osu.Game.Screens.Edit.Compose.Components private void updatePlacementTimeAndPosition() { - var snapResult = Composer.FindSnappedPositionAndTime(InputManager.CurrentState.Mouse.Position, CurrentPlacement.SnapType); + SnapResult snapResult = new SnapResult(InputManager.CurrentState.Mouse.Position, null); // Composer.FindSnappedPositionAndTime(InputManager.CurrentState.Mouse.Position, CurrentPlacement.SnapType); TODO // if no time was found from positional snapping, we should still quantize to the beat. snapResult.Time ??= Beatmap.SnapTime(EditorClock.CurrentTime, null); diff --git a/osu.Game/Screens/Edit/Compose/Components/EditorBlueprintContainer.cs b/osu.Game/Screens/Edit/Compose/Components/EditorBlueprintContainer.cs index 7b046251e0..f1811dd84f 100644 --- a/osu.Game/Screens/Edit/Compose/Components/EditorBlueprintContainer.cs +++ b/osu.Game/Screens/Edit/Compose/Components/EditorBlueprintContainer.cs @@ -17,7 +17,7 @@ using osu.Game.Rulesets.Objects.Drawables; namespace osu.Game.Screens.Edit.Compose.Components { - public partial class EditorBlueprintContainer : BlueprintContainer + public abstract partial class EditorBlueprintContainer : BlueprintContainer { [Resolved] protected EditorClock EditorClock { get; private set; } @@ -73,27 +73,22 @@ namespace osu.Game.Screens.Edit.Compose.Components protected override IEnumerable> SortForMovement(IReadOnlyList> blueprints) => blueprints.OrderBy(b => b.Item.StartTime); - protected override bool ApplySnapResult(SelectionBlueprint[] blueprints, SnapResult result) + protected void ApplySnapResultTime(SnapResult result, double referenceTime) { - if (!base.ApplySnapResult(blueprints, result)) - return false; + if (!result.Time.HasValue) + return; - if (result.Time.HasValue) + // Apply the start time at the newly snapped-to position + double offset = result.Time.Value - referenceTime; + + if (offset != 0) { - // Apply the start time at the newly snapped-to position - double offset = result.Time.Value - blueprints.First().Item.StartTime; - - if (offset != 0) + Beatmap.PerformOnSelection(obj => { - Beatmap.PerformOnSelection(obj => - { - obj.StartTime += offset; - Beatmap.Update(obj); - }); - } + obj.StartTime += offset; + Beatmap.Update(obj); + }); } - - return true; } protected override void AddBlueprintFor(HitObject item) diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/Timeline.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/Timeline.cs index 5f46b3d937..cbf49e62e7 100644 --- a/osu.Game/Screens/Edit/Compose/Components/Timeline/Timeline.cs +++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/Timeline.cs @@ -22,7 +22,7 @@ using osuTK.Input; namespace osu.Game.Screens.Edit.Compose.Components.Timeline { [Cached] - public partial class Timeline : ZoomableScrollContainer, IPositionSnapProvider + public partial class Timeline : ZoomableScrollContainer { private const float timeline_height = 80; @@ -332,7 +332,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline return (float)(time / editorClock.TrackLength * Content.DrawWidth); } - public SnapResult FindSnappedPositionAndTime(Vector2 screenSpacePosition, SnapType snapType = SnapType.All) + public SnapResult FindSnappedPositionAndTime(Vector2 screenSpacePosition) { double time = TimeAtPosition(Content.ToLocalSpace(screenSpacePosition).X); return new SnapResult(screenSpacePosition, beatSnapProvider.SnapTime(time)); diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineBlueprintContainer.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineBlueprintContainer.cs index 2b5667ff9c..011ff17b30 100644 --- a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineBlueprintContainer.cs +++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineBlueprintContainer.cs @@ -107,6 +107,23 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline return base.OnDragStart(e); } + protected override bool TryMoveBlueprints(DragEvent e, IList<(SelectionBlueprint blueprint, Vector2[] originalSnapPositions)> blueprints) + { + Vector2 distanceTravelled = e.ScreenSpaceMousePosition - e.ScreenSpaceMouseDownPosition; + + // The final movement position, relative to movementBlueprintOriginalPosition. + Vector2 movePosition = blueprints.First().originalSnapPositions.First() + distanceTravelled; + + // Retrieve a snapped position. + var result = timeline?.FindSnappedPositionAndTime(movePosition) ?? new SnapResult(movePosition, null); + + var referenceBlueprint = blueprints.First().blueprint; + bool moved = SelectionHandler.HandleMovement(new MoveSelectionEvent(referenceBlueprint, result.ScreenSpacePosition - referenceBlueprint.ScreenSpaceSelectionPoint)); + if (moved) + ApplySnapResultTime(result, referenceBlueprint.Item.StartTime); + return moved; + } + private float dragTimeAccumulated; protected override void Update() From a6987f5c95373ac90c8305b39442847f15e42d06 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 21 Jan 2025 13:49:29 +0100 Subject: [PATCH 0749/1275] Remove dependence of placement blueprints on `IPositionSnapProvider` --- .../BananaShowerPlacementBlueprint.cs | 8 +++-- .../Blueprints/CatchPlacementBlueprint.cs | 7 +++-- .../Blueprints/FruitPlacementBlueprint.cs | 14 +++++++-- .../JuiceStreamPlacementBlueprint.cs | 13 +++++++-- .../Edit/CatchHitObjectComposer.cs | 1 + .../Blueprints/HoldNotePlacementBlueprint.cs | 6 ++-- .../Blueprints/ManiaPlacementBlueprint.cs | 21 ++++++++++---- .../Edit/Blueprints/NotePlacementBlueprint.cs | 7 +++-- .../Edit/ManiaHitObjectComposer.cs | 1 + .../Edit/Blueprints/GridPlacementBlueprint.cs | 12 ++++---- .../HitCircles/HitCirclePlacementBlueprint.cs | 15 ++++++++-- .../Sliders/SliderPlacementBlueprint.cs | 29 +++++++++++++++---- .../Spinners/SpinnerPlacementBlueprint.cs | 8 +++++ .../Edit/OsuHitObjectComposer.cs | 1 + .../Edit/Blueprints/HitPlacementBlueprint.cs | 10 +++++-- .../Blueprints/TaikoSpanPlacementBlueprint.cs | 16 ++++++---- .../Edit/TaikoHitObjectComposer.cs | 2 ++ .../Edit/HitObjectPlacementBlueprint.cs | 2 +- osu.Game/Rulesets/Edit/PlacementBlueprint.cs | 14 ++------- .../Components/ComposeBlueprintContainer.cs | 7 +---- 20 files changed, 137 insertions(+), 57 deletions(-) diff --git a/osu.Game.Rulesets.Catch/Edit/Blueprints/BananaShowerPlacementBlueprint.cs b/osu.Game.Rulesets.Catch/Edit/Blueprints/BananaShowerPlacementBlueprint.cs index 6902f78172..85b7624f1b 100644 --- a/osu.Game.Rulesets.Catch/Edit/Blueprints/BananaShowerPlacementBlueprint.cs +++ b/osu.Game.Rulesets.Catch/Edit/Blueprints/BananaShowerPlacementBlueprint.cs @@ -7,6 +7,7 @@ using osu.Framework.Utils; using osu.Game.Rulesets.Catch.Edit.Blueprints.Components; using osu.Game.Rulesets.Catch.Objects; using osu.Game.Rulesets.Edit; +using osuTK; using osuTK.Input; namespace osu.Game.Rulesets.Catch.Edit.Blueprints @@ -59,11 +60,13 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints return base.OnMouseDown(e); } - public override void UpdateTimeAndPosition(SnapResult result) + public override SnapResult UpdateTimeAndPosition(Vector2 screenSpacePosition, double fallbackTime) { + var result = Composer?.FindSnappedPositionAndTime(screenSpacePosition) ?? new SnapResult(screenSpacePosition, fallbackTime); + base.UpdateTimeAndPosition(result); - if (!(result.Time is double time)) return; + if (!(result.Time is double time)) return result; switch (PlacementActive) { @@ -78,6 +81,7 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints HitObject.StartTime = Math.Min(placementStartTime, placementEndTime); HitObject.EndTime = Math.Max(placementStartTime, placementEndTime); + return result; } } } diff --git a/osu.Game.Rulesets.Catch/Edit/Blueprints/CatchPlacementBlueprint.cs b/osu.Game.Rulesets.Catch/Edit/Blueprints/CatchPlacementBlueprint.cs index aa862375c5..90b7fa172c 100644 --- a/osu.Game.Rulesets.Catch/Edit/Blueprints/CatchPlacementBlueprint.cs +++ b/osu.Game.Rulesets.Catch/Edit/Blueprints/CatchPlacementBlueprint.cs @@ -9,7 +9,7 @@ using osu.Game.Rulesets.UI.Scrolling; namespace osu.Game.Rulesets.Catch.Edit.Blueprints { - public partial class CatchPlacementBlueprint : HitObjectPlacementBlueprint + public abstract partial class CatchPlacementBlueprint : HitObjectPlacementBlueprint where THitObject : CatchHitObject, new() { protected new THitObject HitObject => (THitObject)base.HitObject; @@ -19,7 +19,10 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints [Resolved] private Playfield playfield { get; set; } = null!; - public CatchPlacementBlueprint() + [Resolved] + protected CatchHitObjectComposer? Composer { get; private set; } + + protected CatchPlacementBlueprint() : base(new THitObject()) { } diff --git a/osu.Game.Rulesets.Catch/Edit/Blueprints/FruitPlacementBlueprint.cs b/osu.Game.Rulesets.Catch/Edit/Blueprints/FruitPlacementBlueprint.cs index 72592891fb..83f75771ad 100644 --- a/osu.Game.Rulesets.Catch/Edit/Blueprints/FruitPlacementBlueprint.cs +++ b/osu.Game.Rulesets.Catch/Edit/Blueprints/FruitPlacementBlueprint.cs @@ -5,6 +5,7 @@ using osu.Framework.Input.Events; using osu.Game.Rulesets.Catch.Edit.Blueprints.Components; using osu.Game.Rulesets.Catch.Objects; using osu.Game.Rulesets.Edit; +using osuTK; using osuTK.Input; namespace osu.Game.Rulesets.Catch.Edit.Blueprints @@ -41,11 +42,20 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints return true; } - public override void UpdateTimeAndPosition(SnapResult result) + public override SnapResult UpdateTimeAndPosition(Vector2 screenSpacePosition, double fallbackTime) { - base.UpdateTimeAndPosition(result); + var gridSnapResult = Composer?.FindSnappedPositionAndTime(screenSpacePosition) ?? new SnapResult(screenSpacePosition, fallbackTime); + gridSnapResult.ScreenSpacePosition.X = screenSpacePosition.X; + var distanceSnapResult = Composer?.TryDistanceSnap(gridSnapResult.ScreenSpacePosition); + + var result = distanceSnapResult != null && Vector2.Distance(gridSnapResult.ScreenSpacePosition, distanceSnapResult.ScreenSpacePosition) < CatchHitObjectComposer.DISTANCE_SNAP_RADIUS + ? distanceSnapResult + : gridSnapResult; + + UpdateTimeAndPosition(result); HitObject.X = ToLocalSpace(result.ScreenSpacePosition).X; + return result; } } } diff --git a/osu.Game.Rulesets.Catch/Edit/Blueprints/JuiceStreamPlacementBlueprint.cs b/osu.Game.Rulesets.Catch/Edit/Blueprints/JuiceStreamPlacementBlueprint.cs index 21cc260462..292175353a 100644 --- a/osu.Game.Rulesets.Catch/Edit/Blueprints/JuiceStreamPlacementBlueprint.cs +++ b/osu.Game.Rulesets.Catch/Edit/Blueprints/JuiceStreamPlacementBlueprint.cs @@ -83,8 +83,16 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints return base.OnMouseDown(e); } - public override void UpdateTimeAndPosition(SnapResult result) + public override SnapResult UpdateTimeAndPosition(Vector2 screenSpacePosition, double fallbackTime) { + var gridSnapResult = Composer?.FindSnappedPositionAndTime(screenSpacePosition) ?? new SnapResult(screenSpacePosition, fallbackTime); + gridSnapResult.ScreenSpacePosition.X = screenSpacePosition.X; + var distanceSnapResult = Composer?.TryDistanceSnap(gridSnapResult.ScreenSpacePosition); + + var result = distanceSnapResult != null && Vector2.Distance(gridSnapResult.ScreenSpacePosition, distanceSnapResult.ScreenSpacePosition) < CatchHitObjectComposer.DISTANCE_SNAP_RADIUS + ? distanceSnapResult + : gridSnapResult; + switch (PlacementActive) { case PlacementState.Waiting: @@ -99,7 +107,7 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints break; default: - return; + return result; } // Make sure the up-to-date position is used for outlines. @@ -113,6 +121,7 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints ApplyDefaultsToHitObject(); scrollingPath.UpdatePathFrom(HitObjectContainer, HitObject); nestedOutlineContainer.UpdateNestedObjectsFrom(HitObjectContainer, HitObject); + return result; } private double positionToTime(float relativeYPosition) diff --git a/osu.Game.Rulesets.Catch/Edit/CatchHitObjectComposer.cs b/osu.Game.Rulesets.Catch/Edit/CatchHitObjectComposer.cs index 9618eb28a9..dfe9dc9dd8 100644 --- a/osu.Game.Rulesets.Catch/Edit/CatchHitObjectComposer.cs +++ b/osu.Game.Rulesets.Catch/Edit/CatchHitObjectComposer.cs @@ -23,6 +23,7 @@ using osuTK; namespace osu.Game.Rulesets.Catch.Edit { + [Cached] public partial class CatchHitObjectComposer : ScrollingHitObjectComposer, IKeyBindingHandler { public const float DISTANCE_SNAP_RADIUS = 50; diff --git a/osu.Game.Rulesets.Mania/Edit/Blueprints/HoldNotePlacementBlueprint.cs b/osu.Game.Rulesets.Mania/Edit/Blueprints/HoldNotePlacementBlueprint.cs index 13cfc5f691..094c59da46 100644 --- a/osu.Game.Rulesets.Mania/Edit/Blueprints/HoldNotePlacementBlueprint.cs +++ b/osu.Game.Rulesets.Mania/Edit/Blueprints/HoldNotePlacementBlueprint.cs @@ -98,9 +98,9 @@ namespace osu.Game.Rulesets.Mania.Edit.Blueprints private double originalStartTime; - public override void UpdateTimeAndPosition(SnapResult result) + public override SnapResult UpdateTimeAndPosition(Vector2 screenSpacePosition, double fallbackTime) { - base.UpdateTimeAndPosition(result); + var result = base.UpdateTimeAndPosition(screenSpacePosition, fallbackTime); if (PlacementActive == PlacementState.Active) { @@ -121,6 +121,8 @@ namespace osu.Game.Rulesets.Mania.Edit.Blueprints if (result.Time is double startTime) originalStartTime = HitObject.StartTime = startTime; } + + return result; } } } diff --git a/osu.Game.Rulesets.Mania/Edit/Blueprints/ManiaPlacementBlueprint.cs b/osu.Game.Rulesets.Mania/Edit/Blueprints/ManiaPlacementBlueprint.cs index a68bd5d6d6..359a952755 100644 --- a/osu.Game.Rulesets.Mania/Edit/Blueprints/ManiaPlacementBlueprint.cs +++ b/osu.Game.Rulesets.Mania/Edit/Blueprints/ManiaPlacementBlueprint.cs @@ -1,8 +1,8 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - +using System; +using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Input.Events; using osu.Game.Rulesets.Edit; @@ -20,13 +20,18 @@ namespace osu.Game.Rulesets.Mania.Edit.Blueprints { protected new T HitObject => (T)base.HitObject; - private Column column; + [Resolved] + private ManiaHitObjectComposer? composer { get; set; } - public Column Column + private Column? column; + + public Column? Column { get => column; set { + ArgumentNullException.ThrowIfNull(value); + if (value == column) return; @@ -53,9 +58,11 @@ namespace osu.Game.Rulesets.Mania.Edit.Blueprints return true; } - public override void UpdateTimeAndPosition(SnapResult result) + public override SnapResult UpdateTimeAndPosition(Vector2 screenSpacePosition, double fallbackTime) { - base.UpdateTimeAndPosition(result); + var result = composer?.FindSnappedPositionAndTime(screenSpacePosition) ?? new SnapResult(screenSpacePosition, fallbackTime); + + UpdateTimeAndPosition(result); if (result.Playfield is Column col) { @@ -76,6 +83,8 @@ namespace osu.Game.Rulesets.Mania.Edit.Blueprints if (PlacementActive == PlacementState.Waiting) Column = col; } + + return result; } private float getNoteHeight(Column resultPlayfield) => diff --git a/osu.Game.Rulesets.Mania/Edit/Blueprints/NotePlacementBlueprint.cs b/osu.Game.Rulesets.Mania/Edit/Blueprints/NotePlacementBlueprint.cs index 422215db57..a8cccfb067 100644 --- a/osu.Game.Rulesets.Mania/Edit/Blueprints/NotePlacementBlueprint.cs +++ b/osu.Game.Rulesets.Mania/Edit/Blueprints/NotePlacementBlueprint.cs @@ -8,6 +8,7 @@ using osu.Framework.Input.Events; using osu.Game.Graphics; using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Mania.Objects; +using osuTK; using osuTK.Input; namespace osu.Game.Rulesets.Mania.Edit.Blueprints @@ -35,15 +36,17 @@ namespace osu.Game.Rulesets.Mania.Edit.Blueprints }; } - public override void UpdateTimeAndPosition(SnapResult result) + public override SnapResult UpdateTimeAndPosition(Vector2 screenSpacePosition, double referenceTime) { - base.UpdateTimeAndPosition(result); + var result = base.UpdateTimeAndPosition(screenSpacePosition, referenceTime); if (result.Playfield != null) { piece.Width = result.Playfield.DrawWidth; piece.Position = ToLocalSpace(result.ScreenSpacePosition); } + + return result; } protected override bool OnMouseDown(MouseDownEvent e) diff --git a/osu.Game.Rulesets.Mania/Edit/ManiaHitObjectComposer.cs b/osu.Game.Rulesets.Mania/Edit/ManiaHitObjectComposer.cs index 9062c32b7b..bc20456722 100644 --- a/osu.Game.Rulesets.Mania/Edit/ManiaHitObjectComposer.cs +++ b/osu.Game.Rulesets.Mania/Edit/ManiaHitObjectComposer.cs @@ -19,6 +19,7 @@ using osuTK; namespace osu.Game.Rulesets.Mania.Edit { + [Cached] public partial class ManiaHitObjectComposer : ScrollingHitObjectComposer { private DrawableManiaEditorRuleset drawableRuleset = null!; diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/GridPlacementBlueprint.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/GridPlacementBlueprint.cs index 163b42bcfd..d3e780df9a 100644 --- a/osu.Game.Rulesets.Osu/Edit/Blueprints/GridPlacementBlueprint.cs +++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/GridPlacementBlueprint.cs @@ -13,7 +13,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints public partial class GridPlacementBlueprint : PlacementBlueprint { [Resolved] - private HitObjectComposer? hitObjectComposer { get; set; } + private OsuHitObjectComposer? hitObjectComposer { get; set; } private OsuGridToolboxGroup gridToolboxGroup = null!; private Vector2 originalOrigin; @@ -95,12 +95,12 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints base.OnDragEnd(e); } - public override SnapType SnapType => ~SnapType.GlobalGrids; - - public override void UpdateTimeAndPosition(SnapResult result) + public override SnapResult UpdateTimeAndPosition(Vector2 screenSpacePosition, double referenceTime) { if (State.Value == Visibility.Hidden) - return; + return new SnapResult(screenSpacePosition, referenceTime); + + var result = hitObjectComposer?.TrySnapToNearbyObjects(screenSpacePosition) ?? new SnapResult(screenSpacePosition, referenceTime); var pos = ToLocalSpace(result.ScreenSpacePosition); @@ -120,6 +120,8 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints gridToolboxGroup.SetGridFromPoints(gridToolboxGroup.StartPosition.Value, pos); } } + + return result; } protected override void PopOut() diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/HitCircles/HitCirclePlacementBlueprint.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/HitCircles/HitCirclePlacementBlueprint.cs index 78a0e36dc2..dad7bd5f0e 100644 --- a/osu.Game.Rulesets.Osu/Edit/Blueprints/HitCircles/HitCirclePlacementBlueprint.cs +++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/HitCircles/HitCirclePlacementBlueprint.cs @@ -1,10 +1,12 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using osu.Framework.Allocation; using osu.Framework.Input.Events; using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Osu.Edit.Blueprints.HitCircles.Components; using osu.Game.Rulesets.Osu.Objects; +using osuTK; using osuTK.Input; namespace osu.Game.Rulesets.Osu.Edit.Blueprints.HitCircles @@ -15,6 +17,9 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.HitCircles private readonly HitCirclePiece circlePiece; + [Resolved] + private OsuHitObjectComposer? composer { get; set; } + public HitCirclePlacementBlueprint() : base(new HitCircle()) { @@ -45,10 +50,16 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.HitCircles return base.OnMouseDown(e); } - public override void UpdateTimeAndPosition(SnapResult result) + public override SnapResult UpdateTimeAndPosition(Vector2 screenSpacePosition, double fallbackTime) { - base.UpdateTimeAndPosition(result); + var result = composer?.TrySnapToNearbyObjects(screenSpacePosition) + ?? composer?.TrySnapToDistanceGrid(screenSpacePosition) + ?? composer?.TrySnapToPositionGrid(screenSpacePosition) + ?? new SnapResult(screenSpacePosition, fallbackTime); + + UpdateTimeAndPosition(result); HitObject.Position = ToLocalSpace(result.ScreenSpacePosition); + return result; } } } diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderPlacementBlueprint.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderPlacementBlueprint.cs index 4f2f6516a8..f5fe00e8b6 100644 --- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderPlacementBlueprint.cs +++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderPlacementBlueprint.cs @@ -25,6 +25,9 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders { public new Slider HitObject => (Slider)base.HitObject; + [Resolved] + private OsuHitObjectComposer? composer { get; set; } + private SliderBodyPiece bodyPiece = null!; private HitCirclePiece headCirclePiece = null!; private HitCirclePiece tailCirclePiece = null!; @@ -40,9 +43,6 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders private int currentSegmentLength; private bool usingCustomSegmentType; - [Resolved] - private IPositionSnapProvider? positionSnapProvider { get; set; } - [Resolved] private IDistanceSnapProvider? distanceSnapProvider { get; set; } @@ -106,9 +106,14 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders [Resolved] private EditorBeatmap editorBeatmap { get; set; } = null!; - public override void UpdateTimeAndPosition(SnapResult result) + public override SnapResult UpdateTimeAndPosition(Vector2 screenSpacePosition, double fallbackTime) { - base.UpdateTimeAndPosition(result); + var result = composer?.TrySnapToNearbyObjects(screenSpacePosition) + ?? composer?.TrySnapToDistanceGrid(screenSpacePosition) + ?? composer?.TrySnapToPositionGrid(screenSpacePosition) + ?? new SnapResult(screenSpacePosition, fallbackTime); + + UpdateTimeAndPosition(result); switch (state) { @@ -131,6 +136,8 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders updateCursor(); break; } + + return result; } protected override bool OnMouseDown(MouseDownEvent e) @@ -375,7 +382,17 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders private Vector2 getCursorPosition() { - var result = positionSnapProvider?.FindSnappedPositionAndTime(inputManager.CurrentState.Mouse.Position, state == SliderPlacementState.ControlPoints ? SnapType.GlobalGrids : SnapType.All); + SnapResult? result = null; + var mousePosition = inputManager.CurrentState.Mouse.Position; + + if (state != SliderPlacementState.ControlPoints) + { + result ??= composer?.TrySnapToNearbyObjects(mousePosition); + result ??= composer?.TrySnapToDistanceGrid(mousePosition); + } + + result ??= composer?.TrySnapToPositionGrid(mousePosition); + return ToLocalSpace(result?.ScreenSpacePosition ?? inputManager.CurrentState.Mouse.Position) - HitObject.Position; } diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Spinners/SpinnerPlacementBlueprint.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Spinners/SpinnerPlacementBlueprint.cs index 17d2dcd75c..6c4847cada 100644 --- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Spinners/SpinnerPlacementBlueprint.cs +++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Spinners/SpinnerPlacementBlueprint.cs @@ -9,6 +9,7 @@ using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Osu.Edit.Blueprints.Spinners.Components; using osu.Game.Rulesets.Osu.Objects; using osu.Game.Rulesets.Osu.UI; +using osuTK; using osuTK.Input; namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Spinners @@ -70,5 +71,12 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Spinners ? Math.Max(HitObject.StartTime, EditorClock.CurrentTime) : Math.Max(HitObject.StartTime + beatSnapProvider.GetBeatLengthAtTime(HitObject.StartTime), beatSnapProvider.SnapTime(EditorClock.CurrentTime)); } + + public override SnapResult UpdateTimeAndPosition(Vector2 screenSpacePosition, double fallbackTime) + { + var result = new SnapResult(screenSpacePosition, fallbackTime); + UpdateTimeAndPosition(result); + return result; + } } } diff --git a/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs b/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs index 06a74fb631..faed599fa5 100644 --- a/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs +++ b/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs @@ -32,6 +32,7 @@ using osuTK; namespace osu.Game.Rulesets.Osu.Edit { + [Cached] public partial class OsuHitObjectComposer : HitObjectComposer { public OsuHitObjectComposer(Ruleset ruleset) diff --git a/osu.Game.Rulesets.Taiko/Edit/Blueprints/HitPlacementBlueprint.cs b/osu.Game.Rulesets.Taiko/Edit/Blueprints/HitPlacementBlueprint.cs index 7f45123bd6..b887fac42a 100644 --- a/osu.Game.Rulesets.Taiko/Edit/Blueprints/HitPlacementBlueprint.cs +++ b/osu.Game.Rulesets.Taiko/Edit/Blueprints/HitPlacementBlueprint.cs @@ -1,6 +1,7 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using osu.Framework.Allocation; using osu.Framework.Input.Events; using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Taiko.Objects; @@ -16,6 +17,9 @@ namespace osu.Game.Rulesets.Taiko.Edit.Blueprints public new Hit HitObject => (Hit)base.HitObject; + [Resolved] + private TaikoHitObjectComposer? composer { get; set; } + public HitPlacementBlueprint() : base(new Hit()) { @@ -40,10 +44,12 @@ namespace osu.Game.Rulesets.Taiko.Edit.Blueprints return true; } - public override void UpdateTimeAndPosition(SnapResult result) + public override SnapResult UpdateTimeAndPosition(Vector2 screenSpacePosition, double fallbackTime) { + var result = composer?.FindSnappedPositionAndTime(screenSpacePosition) ?? new SnapResult(screenSpacePosition, fallbackTime); piece.Position = ToLocalSpace(result.ScreenSpacePosition); - base.UpdateTimeAndPosition(result); + UpdateTimeAndPosition(result); + return result; } } } diff --git a/osu.Game.Rulesets.Taiko/Edit/Blueprints/TaikoSpanPlacementBlueprint.cs b/osu.Game.Rulesets.Taiko/Edit/Blueprints/TaikoSpanPlacementBlueprint.cs index de3a4d96eb..7263c1ef2c 100644 --- a/osu.Game.Rulesets.Taiko/Edit/Blueprints/TaikoSpanPlacementBlueprint.cs +++ b/osu.Game.Rulesets.Taiko/Edit/Blueprints/TaikoSpanPlacementBlueprint.cs @@ -1,9 +1,8 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System; +using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Input.Events; using osu.Framework.Utils; @@ -26,12 +25,15 @@ namespace osu.Game.Rulesets.Taiko.Edit.Blueprints private readonly IHasDuration spanPlacementObject; + [Resolved] + private TaikoHitObjectComposer? composer { get; set; } + protected override bool IsValidForPlacement => Precision.DefinitelyBigger(spanPlacementObject.Duration, 0); public TaikoSpanPlacementBlueprint(HitObject hitObject) : base(hitObject) { - spanPlacementObject = hitObject as IHasDuration; + spanPlacementObject = (hitObject as IHasDuration)!; RelativeSizeAxes = Axes.Both; @@ -79,9 +81,11 @@ namespace osu.Game.Rulesets.Taiko.Edit.Blueprints EndPlacement(true); } - public override void UpdateTimeAndPosition(SnapResult result) + public override SnapResult UpdateTimeAndPosition(Vector2 screenSpacePosition, double fallbackTime) { - base.UpdateTimeAndPosition(result); + var result = composer?.FindSnappedPositionAndTime(screenSpacePosition) ?? new SnapResult(screenSpacePosition, fallbackTime); + + UpdateTimeAndPosition(result); if (PlacementActive == PlacementState.Active) { @@ -116,6 +120,8 @@ namespace osu.Game.Rulesets.Taiko.Edit.Blueprints originalPosition = ToLocalSpace(result.ScreenSpacePosition); } } + + return result; } } } diff --git a/osu.Game.Rulesets.Taiko/Edit/TaikoHitObjectComposer.cs b/osu.Game.Rulesets.Taiko/Edit/TaikoHitObjectComposer.cs index d97a854ff7..54031f0c9f 100644 --- a/osu.Game.Rulesets.Taiko/Edit/TaikoHitObjectComposer.cs +++ b/osu.Game.Rulesets.Taiko/Edit/TaikoHitObjectComposer.cs @@ -2,6 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using System.Collections.Generic; +using osu.Framework.Allocation; using osu.Game.Beatmaps; using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Edit.Tools; @@ -12,6 +13,7 @@ using osu.Game.Screens.Edit.Compose.Components; namespace osu.Game.Rulesets.Taiko.Edit { + [Cached] public partial class TaikoHitObjectComposer : ScrollingHitObjectComposer { protected override bool ApplyHorizontalCentering => false; diff --git a/osu.Game/Rulesets/Edit/HitObjectPlacementBlueprint.cs b/osu.Game/Rulesets/Edit/HitObjectPlacementBlueprint.cs index 4df2a52743..0bfda94f44 100644 --- a/osu.Game/Rulesets/Edit/HitObjectPlacementBlueprint.cs +++ b/osu.Game/Rulesets/Edit/HitObjectPlacementBlueprint.cs @@ -90,7 +90,7 @@ namespace osu.Game.Rulesets.Edit /// Updates the time and position of this based on the provided snap information. /// /// The snap result information. - public override void UpdateTimeAndPosition(SnapResult result) + public void UpdateTimeAndPosition(SnapResult result) { if (PlacementActive == PlacementState.Waiting) { diff --git a/osu.Game/Rulesets/Edit/PlacementBlueprint.cs b/osu.Game/Rulesets/Edit/PlacementBlueprint.cs index 52b8a5c796..f2d501d1c4 100644 --- a/osu.Game/Rulesets/Edit/PlacementBlueprint.cs +++ b/osu.Game/Rulesets/Edit/PlacementBlueprint.cs @@ -7,6 +7,7 @@ using osu.Framework.Input.Bindings; using osu.Framework.Input.Events; using osu.Game.Input.Bindings; using osu.Game.Rulesets.Objects; +using osuTK; using osuTK.Input; namespace osu.Game.Rulesets.Edit @@ -75,18 +76,7 @@ namespace osu.Game.Rulesets.Edit PlacementActive = PlacementState.Finished; } - /// - /// Determines which objects to snap to for the snap result in . - /// - public virtual SnapType SnapType => SnapType.All; - - /// - /// Updates the time and position of this based on the provided snap information. - /// - /// The snap result information. - public virtual void UpdateTimeAndPosition(SnapResult result) - { - } + public abstract SnapResult UpdateTimeAndPosition(Vector2 screenSpacePosition, double fallbackTime); public bool OnPressed(KeyBindingPressEvent e) { diff --git a/osu.Game/Screens/Edit/Compose/Components/ComposeBlueprintContainer.cs b/osu.Game/Screens/Edit/Compose/Components/ComposeBlueprintContainer.cs index 27d6656c69..de1f589135 100644 --- a/osu.Game/Screens/Edit/Compose/Components/ComposeBlueprintContainer.cs +++ b/osu.Game/Screens/Edit/Compose/Components/ComposeBlueprintContainer.cs @@ -340,12 +340,7 @@ namespace osu.Game.Screens.Edit.Compose.Components private void updatePlacementTimeAndPosition() { - SnapResult snapResult = new SnapResult(InputManager.CurrentState.Mouse.Position, null); // Composer.FindSnappedPositionAndTime(InputManager.CurrentState.Mouse.Position, CurrentPlacement.SnapType); TODO - - // if no time was found from positional snapping, we should still quantize to the beat. - snapResult.Time ??= Beatmap.SnapTime(EditorClock.CurrentTime, null); - - CurrentPlacement.UpdateTimeAndPosition(snapResult); + CurrentPlacement.UpdateTimeAndPosition(InputManager.CurrentState.Mouse.Position, Beatmap.SnapTime(EditorClock.CurrentTime, null)); } #endregion From 32d341a46855d9116aa12ed8f79e1864e3bb6b78 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 21 Jan 2025 13:49:48 +0100 Subject: [PATCH 0750/1275] Remove `IPositionSnapProvider` --- .../Rulesets/Edit/IPositionSnapProvider.cs | 23 ------------- osu.Game/Rulesets/Edit/SnapType.cs | 32 ------------------- 2 files changed, 55 deletions(-) delete mode 100644 osu.Game/Rulesets/Edit/IPositionSnapProvider.cs delete mode 100644 osu.Game/Rulesets/Edit/SnapType.cs diff --git a/osu.Game/Rulesets/Edit/IPositionSnapProvider.cs b/osu.Game/Rulesets/Edit/IPositionSnapProvider.cs deleted file mode 100644 index 002a0aafe6..0000000000 --- a/osu.Game/Rulesets/Edit/IPositionSnapProvider.cs +++ /dev/null @@ -1,23 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -using osu.Framework.Allocation; -using osuTK; - -namespace osu.Game.Rulesets.Edit -{ - /// - /// A snap provider which given a proposed position for a hit object, potentially offers a more correct position and time value inferred from the context of the beatmap. - /// - [Cached] - public interface IPositionSnapProvider - { - /// - /// Given a position, find a valid time and position snap. - /// - /// The screen-space position to be snapped. - /// The type of snapping to apply. - /// The time and position post-snapping. - SnapResult FindSnappedPositionAndTime(Vector2 screenSpacePosition, SnapType snapType = SnapType.All); - } -} diff --git a/osu.Game/Rulesets/Edit/SnapType.cs b/osu.Game/Rulesets/Edit/SnapType.cs deleted file mode 100644 index cf743f6ace..0000000000 --- a/osu.Game/Rulesets/Edit/SnapType.cs +++ /dev/null @@ -1,32 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -using System; - -namespace osu.Game.Rulesets.Edit -{ - [Flags] - public enum SnapType - { - None = 0, - - /// - /// Snapping to visible nearby objects. - /// - NearbyObjects = 1 << 0, - - /// - /// Grids which are global to the playfield. - /// - GlobalGrids = 1 << 1, - - /// - /// Grids which are relative to other nearby hit objects. - /// - RelativeGrids = 1 << 2, - - AllGrids = RelativeGrids | GlobalGrids, - - All = NearbyObjects | GlobalGrids | RelativeGrids, - } -} From 269ade178e4513d2873c4caf6e9aacafc9118097 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 21 Jan 2025 14:59:12 +0100 Subject: [PATCH 0751/1275] Fix tests --- .../Editor/CatchPlacementBlueprintTestScene.cs | 9 ++++----- .../Editor/ManiaPlacementBlueprintTestScene.cs | 6 ++---- .../Edit/Blueprints/GridPlacementBlueprint.cs | 6 +++--- .../HitCircles/HitCirclePlacementBlueprint.cs | 2 +- .../Sliders/Components/PathControlPointVisualiser.cs | 2 +- .../Blueprints/Sliders/SliderPlacementBlueprint.cs | 2 +- osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs | 4 +++- .../Overlays/SkinEditor/SkinBlueprintContainer.cs | 7 ++++++- osu.Game/Rulesets/Edit/HitObjectPlacementBlueprint.cs | 2 +- osu.Game/Tests/Visual/PlacementBlueprintTestScene.cs | 11 ++++------- 10 files changed, 26 insertions(+), 25 deletions(-) diff --git a/osu.Game.Rulesets.Catch.Tests/Editor/CatchPlacementBlueprintTestScene.cs b/osu.Game.Rulesets.Catch.Tests/Editor/CatchPlacementBlueprintTestScene.cs index 0578010c25..a327e6d4c9 100644 --- a/osu.Game.Rulesets.Catch.Tests/Editor/CatchPlacementBlueprintTestScene.cs +++ b/osu.Game.Rulesets.Catch.Tests/Editor/CatchPlacementBlueprintTestScene.cs @@ -12,7 +12,6 @@ using osu.Framework.Testing; using osu.Framework.Timing; using osu.Game.Rulesets.Catch.Edit.Blueprints.Components; using osu.Game.Rulesets.Catch.Objects.Drawables; -using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.UI.Scrolling; using osu.Game.Tests.Visual; @@ -71,11 +70,11 @@ namespace osu.Game.Rulesets.Catch.Tests.Editor contentContainer.Playfield.HitObjectContainer.Add(hitObject); } - protected override SnapResult SnapForBlueprint(HitObjectPlacementBlueprint blueprint) + protected override void UpdatePlacementTimeAndPosition() { - var result = base.SnapForBlueprint(blueprint); - result.Time = Math.Round(HitObjectContainer.TimeAtScreenSpacePosition(result.ScreenSpacePosition) / TIME_SNAP) * TIME_SNAP; - return result; + var position = InputManager.CurrentState.Mouse.Position; + double time = Math.Round(HitObjectContainer.TimeAtScreenSpacePosition(position) / TIME_SNAP) * TIME_SNAP; + CurrentBlueprint.UpdateTimeAndPosition(position, time); } } } diff --git a/osu.Game.Rulesets.Mania.Tests/Editor/ManiaPlacementBlueprintTestScene.cs b/osu.Game.Rulesets.Mania.Tests/Editor/ManiaPlacementBlueprintTestScene.cs index 5e633c3161..0f913a6a7d 100644 --- a/osu.Game.Rulesets.Mania.Tests/Editor/ManiaPlacementBlueprintTestScene.cs +++ b/osu.Game.Rulesets.Mania.Tests/Editor/ManiaPlacementBlueprintTestScene.cs @@ -9,7 +9,6 @@ using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Timing; -using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Mania.Beatmaps; using osu.Game.Rulesets.Mania.Objects.Drawables; using osu.Game.Rulesets.Mania.UI; @@ -47,12 +46,11 @@ namespace osu.Game.Rulesets.Mania.Tests.Editor }); } - protected override SnapResult SnapForBlueprint(HitObjectPlacementBlueprint blueprint) + protected override void UpdatePlacementTimeAndPosition() { double time = column.TimeAtScreenSpacePosition(InputManager.CurrentState.Mouse.Position); var pos = column.ScreenSpacePositionAtTime(time); - - return new SnapResult(pos, time, column); + CurrentBlueprint.UpdateTimeAndPosition(pos, time); } protected override Container CreateHitObjectContainer() => new ScrollingTestContainer(ScrollingDirection.Down) { RelativeSizeAxes = Axes.Both }; diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/GridPlacementBlueprint.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/GridPlacementBlueprint.cs index d3e780df9a..d9edc8dbd4 100644 --- a/osu.Game.Rulesets.Osu/Edit/Blueprints/GridPlacementBlueprint.cs +++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/GridPlacementBlueprint.cs @@ -95,12 +95,12 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints base.OnDragEnd(e); } - public override SnapResult UpdateTimeAndPosition(Vector2 screenSpacePosition, double referenceTime) + public override SnapResult UpdateTimeAndPosition(Vector2 screenSpacePosition, double fallbackTime) { if (State.Value == Visibility.Hidden) - return new SnapResult(screenSpacePosition, referenceTime); + return new SnapResult(screenSpacePosition, fallbackTime); - var result = hitObjectComposer?.TrySnapToNearbyObjects(screenSpacePosition) ?? new SnapResult(screenSpacePosition, referenceTime); + var result = hitObjectComposer?.TrySnapToNearbyObjects(screenSpacePosition) ?? new SnapResult(screenSpacePosition, fallbackTime); var pos = ToLocalSpace(result.ScreenSpacePosition); diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/HitCircles/HitCirclePlacementBlueprint.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/HitCircles/HitCirclePlacementBlueprint.cs index dad7bd5f0e..53784a7f08 100644 --- a/osu.Game.Rulesets.Osu/Edit/Blueprints/HitCircles/HitCirclePlacementBlueprint.cs +++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/HitCircles/HitCirclePlacementBlueprint.cs @@ -52,7 +52,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.HitCircles public override SnapResult UpdateTimeAndPosition(Vector2 screenSpacePosition, double fallbackTime) { - var result = composer?.TrySnapToNearbyObjects(screenSpacePosition) + var result = composer?.TrySnapToNearbyObjects(screenSpacePosition, fallbackTime) ?? composer?.TrySnapToDistanceGrid(screenSpacePosition) ?? composer?.TrySnapToPositionGrid(screenSpacePosition) ?? new SnapResult(screenSpacePosition, fallbackTime); diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs index bac5f0101c..a3bb0b868a 100644 --- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs +++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs @@ -433,7 +433,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components { // Special handling for selections containing head control point - the position of the hit object changes which means the snapped position and time have to be taken into account Vector2 newHeadPosition = Parent!.ToScreenSpace(e.MousePosition + (dragStartPositions[0] - dragStartPositions[draggedControlPointIndex])); - SnapResult result = positionSnapProvider?.TrySnapToNearbyObjects(newHeadPosition) + SnapResult result = positionSnapProvider?.TrySnapToNearbyObjects(newHeadPosition, oldStartTime) ?? positionSnapProvider?.TrySnapToDistanceGrid(newHeadPosition) ?? positionSnapProvider?.TrySnapToPositionGrid(newHeadPosition); diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderPlacementBlueprint.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderPlacementBlueprint.cs index f5fe00e8b6..fd72f18b12 100644 --- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderPlacementBlueprint.cs +++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderPlacementBlueprint.cs @@ -108,7 +108,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders public override SnapResult UpdateTimeAndPosition(Vector2 screenSpacePosition, double fallbackTime) { - var result = composer?.TrySnapToNearbyObjects(screenSpacePosition) + var result = composer?.TrySnapToNearbyObjects(screenSpacePosition, fallbackTime) ?? composer?.TrySnapToDistanceGrid(screenSpacePosition) ?? composer?.TrySnapToPositionGrid(screenSpacePosition) ?? new SnapResult(screenSpacePosition, fallbackTime); diff --git a/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs b/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs index faed599fa5..7a93a26e45 100644 --- a/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs +++ b/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs @@ -225,11 +225,13 @@ namespace osu.Game.Rulesets.Osu.Edit } [CanBeNull] - public SnapResult TrySnapToNearbyObjects(Vector2 screenSpacePosition) + public SnapResult TrySnapToNearbyObjects(Vector2 screenSpacePosition, double? fallbackTime = null) { if (!snapToVisibleBlueprints(screenSpacePosition, out var snapResult)) return null; + snapResult.Time ??= fallbackTime; + if (DistanceSnapProvider.DistanceSnapToggle.Value != TernaryState.True || distanceSnapGrid == null) return snapResult; diff --git a/osu.Game/Overlays/SkinEditor/SkinBlueprintContainer.cs b/osu.Game/Overlays/SkinEditor/SkinBlueprintContainer.cs index 8f831a6f18..df8cb33a71 100644 --- a/osu.Game/Overlays/SkinEditor/SkinBlueprintContainer.cs +++ b/osu.Game/Overlays/SkinEditor/SkinBlueprintContainer.cs @@ -113,7 +113,12 @@ namespace osu.Game.Overlays.SkinEditor protected override bool TryMoveBlueprints(DragEvent e, IList<(SelectionBlueprint blueprint, Vector2[] originalSnapPositions)> blueprints) { - throw new System.NotImplementedException(); + Vector2 distanceTravelled = e.ScreenSpaceMousePosition - e.ScreenSpaceMouseDownPosition; + + // The final movement position, relative to movementBlueprintOriginalPosition. + var referenceBlueprint = blueprints.First().blueprint; + Vector2 movePosition = blueprints.First().originalSnapPositions.First() + distanceTravelled; + return SelectionHandler.HandleMovement(new MoveSelectionEvent(referenceBlueprint, movePosition - referenceBlueprint.ScreenSpaceSelectionPoint)); } /// diff --git a/osu.Game/Rulesets/Edit/HitObjectPlacementBlueprint.cs b/osu.Game/Rulesets/Edit/HitObjectPlacementBlueprint.cs index 0bfda94f44..3119680272 100644 --- a/osu.Game/Rulesets/Edit/HitObjectPlacementBlueprint.cs +++ b/osu.Game/Rulesets/Edit/HitObjectPlacementBlueprint.cs @@ -90,7 +90,7 @@ namespace osu.Game.Rulesets.Edit /// Updates the time and position of this based on the provided snap information. /// /// The snap result information. - public void UpdateTimeAndPosition(SnapResult result) + protected void UpdateTimeAndPosition(SnapResult result) { if (PlacementActive == PlacementState.Waiting) { diff --git a/osu.Game/Tests/Visual/PlacementBlueprintTestScene.cs b/osu.Game/Tests/Visual/PlacementBlueprintTestScene.cs index aa8aff3adc..baf614d1c8 100644 --- a/osu.Game/Tests/Visual/PlacementBlueprintTestScene.cs +++ b/osu.Game/Tests/Visual/PlacementBlueprintTestScene.cs @@ -28,7 +28,7 @@ namespace osu.Game.Tests.Visual base.Content.Add(HitObjectContainer = CreateHitObjectContainer().With(c => c.Clock = new FramedClock(new StopwatchClock()))); base.Content.Add(new MouseMovementInterceptor { - MouseMoved = updatePlacementTimeAndPosition, + MouseMoved = UpdatePlacementTimeAndPosition, }); } @@ -93,13 +93,10 @@ namespace osu.Game.Tests.Visual if (CurrentBlueprint.PlacementActive == PlacementBlueprint.PlacementState.Finished) ResetPlacement(); - updatePlacementTimeAndPosition(); + UpdatePlacementTimeAndPosition(); } - private void updatePlacementTimeAndPosition() => CurrentBlueprint.UpdateTimeAndPosition(SnapForBlueprint(CurrentBlueprint)); - - protected virtual SnapResult SnapForBlueprint(HitObjectPlacementBlueprint blueprint) => - new SnapResult(InputManager.CurrentState.Mouse.Position, null); + protected virtual void UpdatePlacementTimeAndPosition() => CurrentBlueprint.UpdateTimeAndPosition(InputManager.CurrentState.Mouse.Position, 0); public override void Add(Drawable drawable) { @@ -108,7 +105,7 @@ namespace osu.Game.Tests.Visual if (drawable is HitObjectPlacementBlueprint blueprint) { blueprint.Show(); - blueprint.UpdateTimeAndPosition(SnapForBlueprint(blueprint)); + UpdatePlacementTimeAndPosition(); } } From 0164a2e4dca86fed1f3ea016eb9b1e4084eebba1 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 24 Jan 2025 19:02:31 +0900 Subject: [PATCH 0752/1275] Move pool item preparation / cleanup duties to `Carousel` --- osu.Game/Screens/SelectV2/Carousel.cs | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/SelectV2/Carousel.cs b/osu.Game/Screens/SelectV2/Carousel.cs index 6ff27c6198..648c2d090a 100644 --- a/osu.Game/Screens/SelectV2/Carousel.cs +++ b/osu.Game/Screens/SelectV2/Carousel.cs @@ -540,11 +540,13 @@ namespace osu.Game.Screens.SelectV2 { var c = (ICarouselPanel)panel; + // panel in the process of expiring, ignore it. + if (c.Item == null) + continue; + if (panel.Depth != c.DrawYPosition) scroll.Panels.ChangeChildDepth(panel, (float)c.DrawYPosition); - Debug.Assert(c.Item != null); - if (c.DrawYPosition != c.Item.CarouselYPosition) c.DrawYPosition = Interpolation.DampContinuously(c.DrawYPosition, c.Item.CarouselYPosition, 50, Time.Elapsed); @@ -631,7 +633,9 @@ namespace osu.Game.Screens.SelectV2 if (drawable is not ICarouselPanel carouselPanel) throw new InvalidOperationException($"Carousel panel drawables must implement {typeof(ICarouselPanel)}"); + carouselPanel.DrawYPosition = item.CarouselYPosition; carouselPanel.Item = item; + scroll.Add(drawable); } @@ -650,6 +654,12 @@ namespace osu.Game.Screens.SelectV2 { panel.FinishTransforms(); panel.Expire(); + + var carouselPanel = (ICarouselPanel)panel; + + carouselPanel.Item = null; + carouselPanel.Selected.Value = false; + carouselPanel.KeyboardSelected.Value = false; } #endregion From 175eb82ccfed30fed57bbbeea02d687eb0a4794c Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 24 Jan 2025 19:02:47 +0900 Subject: [PATCH 0753/1275] Split out beatmaps and set panels into two separate classes --- .../SongSelect/BeatmapCarouselV2TestScene.cs | 2 +- .../TestSceneBeatmapCarouselV2Basics.cs | 10 +- .../TestSceneBeatmapCarouselV2Selection.cs | 2 +- osu.Game/Screens/SelectV2/BeatmapCarousel.cs | 20 ++- .../SelectV2/BeatmapCarouselFilterGrouping.cs | 2 +- ...eatmapCarouselPanel.cs => BeatmapPanel.cs} | 58 +++------ osu.Game/Screens/SelectV2/BeatmapSetPanel.cs | 114 ++++++++++++++++++ 7 files changed, 158 insertions(+), 50 deletions(-) rename osu.Game/Screens/SelectV2/{BeatmapCarouselPanel.cs => BeatmapPanel.cs} (69%) create mode 100644 osu.Game/Screens/SelectV2/BeatmapSetPanel.cs diff --git a/osu.Game.Tests/Visual/SongSelect/BeatmapCarouselV2TestScene.cs b/osu.Game.Tests/Visual/SongSelect/BeatmapCarouselV2TestScene.cs index 3aa9f60181..4c85cf8fcd 100644 --- a/osu.Game.Tests/Visual/SongSelect/BeatmapCarouselV2TestScene.cs +++ b/osu.Game.Tests/Visual/SongSelect/BeatmapCarouselV2TestScene.cs @@ -131,7 +131,7 @@ namespace osu.Game.Tests.Visual.SongSelect protected void SortBy(FilterCriteria criteria) => AddStep($"sort by {criteria.Sort}", () => Carousel.Filter(criteria)); - protected void WaitForDrawablePanels() => AddUntilStep("drawable panels loaded", () => Carousel.ChildrenOfType().Count(), () => Is.GreaterThan(0)); + protected void WaitForDrawablePanels() => AddUntilStep("drawable panels loaded", () => Carousel.ChildrenOfType().Count(), () => Is.GreaterThan(0)); protected void WaitForSorting() => AddUntilStep("sorting finished", () => Carousel.IsFiltering, () => Is.False); protected void WaitForScrolling() => AddUntilStep("scroll finished", () => Scroll.Current, () => Is.EqualTo(Scroll.Target)); diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2Basics.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2Basics.cs index 748831bf7b..3a516ea762 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2Basics.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2Basics.cs @@ -56,16 +56,16 @@ namespace osu.Game.Tests.Visual.SongSelect WaitForDrawablePanels(); AddStep("select middle beatmap", () => Carousel.CurrentSelection = BeatmapSets.ElementAt(BeatmapSets.Count - 2)); - AddStep("scroll to selected item", () => Scroll.ScrollTo(Scroll.ChildrenOfType().Single(p => p.Selected.Value))); + AddStep("scroll to selected item", () => Scroll.ScrollTo(Scroll.ChildrenOfType().Single(p => p.Selected.Value))); WaitForScrolling(); - AddStep("save selected screen position", () => positionBefore = Carousel.ChildrenOfType().FirstOrDefault(p => p.Selected.Value)!.ScreenSpaceDrawQuad); + AddStep("save selected screen position", () => positionBefore = Carousel.ChildrenOfType().FirstOrDefault(p => p.Selected.Value)!.ScreenSpaceDrawQuad); RemoveFirstBeatmap(); WaitForSorting(); - AddAssert("select screen position unchanged", () => Carousel.ChildrenOfType().Single(p => p.Selected.Value).ScreenSpaceDrawQuad, + AddAssert("select screen position unchanged", () => Carousel.ChildrenOfType().Single(p => p.Selected.Value).ScreenSpaceDrawQuad, () => Is.EqualTo(positionBefore)); } @@ -83,11 +83,11 @@ namespace osu.Game.Tests.Visual.SongSelect WaitForScrolling(); - AddStep("save selected screen position", () => positionBefore = Carousel.ChildrenOfType().FirstOrDefault(p => p.Selected.Value)!.ScreenSpaceDrawQuad); + AddStep("save selected screen position", () => positionBefore = Carousel.ChildrenOfType().FirstOrDefault(p => p.Selected.Value)!.ScreenSpaceDrawQuad); RemoveFirstBeatmap(); WaitForSorting(); - AddAssert("select screen position unchanged", () => Carousel.ChildrenOfType().Single(p => p.Selected.Value).ScreenSpaceDrawQuad, + AddAssert("select screen position unchanged", () => Carousel.ChildrenOfType().Single(p => p.Selected.Value).ScreenSpaceDrawQuad, () => Is.EqualTo(positionBefore)); } diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2Selection.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2Selection.cs index 305774b7d3..3c42969d8c 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2Selection.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2Selection.cs @@ -97,7 +97,7 @@ namespace osu.Game.Tests.Visual.SongSelect AddUntilStep("drawable selection restored", () => getSelectedPanel()?.Item?.Model, () => Is.EqualTo(selection)); AddAssert("drawable selection matches carousel selection", () => selection, () => Is.EqualTo(Carousel.CurrentSelection)); - BeatmapCarouselPanel? getSelectedPanel() => Carousel.ChildrenOfType().SingleOrDefault(p => p.Selected.Value); + BeatmapPanel? getSelectedPanel() => Carousel.ChildrenOfType().SingleOrDefault(p => p.Selected.Value); } [Test] diff --git a/osu.Game/Screens/SelectV2/BeatmapCarousel.cs b/osu.Game/Screens/SelectV2/BeatmapCarousel.cs index 630f7b6583..bb13c7449d 100644 --- a/osu.Game/Screens/SelectV2/BeatmapCarousel.cs +++ b/osu.Game/Screens/SelectV2/BeatmapCarousel.cs @@ -141,14 +141,28 @@ namespace osu.Game.Screens.SelectV2 #region Drawable pooling - private readonly DrawablePool carouselPanelPool = new DrawablePool(100); + private readonly DrawablePool beatmapPanelPool = new DrawablePool(100); + private readonly DrawablePool setPanelPool = new DrawablePool(100); private void setupPools() { - AddInternal(carouselPanelPool); + AddInternal(beatmapPanelPool); + AddInternal(setPanelPool); } - protected override Drawable GetDrawableForDisplay(CarouselItem item) => carouselPanelPool.Get(); + protected override Drawable GetDrawableForDisplay(CarouselItem item) + { + switch (item.Model) + { + case BeatmapInfo: + return beatmapPanelPool.Get(); + + case BeatmapSetInfo: + return setPanelPool.Get(); + } + + throw new InvalidOperationException(); + } #endregion } diff --git a/osu.Game/Screens/SelectV2/BeatmapCarouselFilterGrouping.cs b/osu.Game/Screens/SelectV2/BeatmapCarouselFilterGrouping.cs index 4f0767048a..0658263a8c 100644 --- a/osu.Game/Screens/SelectV2/BeatmapCarouselFilterGrouping.cs +++ b/osu.Game/Screens/SelectV2/BeatmapCarouselFilterGrouping.cs @@ -57,7 +57,7 @@ namespace osu.Game.Screens.SelectV2 { newItems.Add(new CarouselItem(b.BeatmapSet!) { - DrawHeight = 80, + DrawHeight = BeatmapSetPanel.HEIGHT, IsGroupSelectionTarget = true }); } diff --git a/osu.Game/Screens/SelectV2/BeatmapCarouselPanel.cs b/osu.Game/Screens/SelectV2/BeatmapPanel.cs similarity index 69% rename from osu.Game/Screens/SelectV2/BeatmapCarouselPanel.cs rename to osu.Game/Screens/SelectV2/BeatmapPanel.cs index 398ec7bf4c..4a9e406def 100644 --- a/osu.Game/Screens/SelectV2/BeatmapCarouselPanel.cs +++ b/osu.Game/Screens/SelectV2/BeatmapPanel.cs @@ -16,22 +16,25 @@ using osuTK.Graphics; namespace osu.Game.Screens.SelectV2 { - public partial class BeatmapCarouselPanel : PoolableDrawable, ICarouselPanel + public partial class BeatmapPanel : PoolableDrawable, ICarouselPanel { [Resolved] private BeatmapCarousel carousel { get; set; } = null!; private Box activationFlash = null!; - private Box background = null!; private OsuSpriteText text = null!; [BackgroundDependencyLoader] private void load() { + Size = new Vector2(500, CarouselItem.DEFAULT_HEIGHT); + Masking = true; + InternalChildren = new Drawable[] { - background = new Box + new Box { + Colour = Color4.Aqua.Darken(5), Alpha = 0.8f, RelativeSizeAxes = Axes.Both, }, @@ -69,63 +72,40 @@ namespace osu.Game.Screens.SelectV2 }); } - protected override void FreeAfterUse() - { - base.FreeAfterUse(); - Item = null; - Selected.Value = false; - KeyboardSelected.Value = false; - } - protected override void PrepareForUse() { base.PrepareForUse(); Debug.Assert(Item != null); + var beatmap = (BeatmapInfo)Item.Model; - DrawYPosition = Item.CarouselYPosition; - - Size = new Vector2(500, Item.DrawHeight); - Masking = true; - - background.Colour = (Item.Model is BeatmapInfo ? Color4.Aqua : Color4.Yellow).Darken(5); - text.Text = getTextFor(Item.Model); + text.Text = $"Difficulty: {beatmap.DifficultyName} ({beatmap.StarRating:N1}*)"; this.FadeInFromZero(500, Easing.OutQuint); } - private string getTextFor(object item) - { - switch (item) - { - case BeatmapInfo bi: - return $"Difficulty: {bi.DifficultyName} ({bi.StarRating:N1}*)"; - - case BeatmapSetInfo si: - return $"{si.Metadata}"; - } - - return "unknown"; - } - protected override bool OnClick(ClickEvent e) { - if (carousel.CurrentSelection == Item!.Model) - carousel.TryActivateSelection(); - else + if (carousel.CurrentSelection != Item!.Model) + { carousel.CurrentSelection = Item!.Model; + return true; + } + + carousel.TryActivateSelection(); return true; } + #region ICarouselPanel + public CarouselItem? Item { get; set; } public BindableBool Selected { get; } = new BindableBool(); public BindableBool KeyboardSelected { get; } = new BindableBool(); public double DrawYPosition { get; set; } - public void Activated() - { - activationFlash.FadeOutFromOne(500, Easing.OutQuint); - } + public void Activated() => activationFlash.FadeOutFromOne(500, Easing.OutQuint); + + #endregion } } diff --git a/osu.Game/Screens/SelectV2/BeatmapSetPanel.cs b/osu.Game/Screens/SelectV2/BeatmapSetPanel.cs new file mode 100644 index 0000000000..0b95f94365 --- /dev/null +++ b/osu.Game/Screens/SelectV2/BeatmapSetPanel.cs @@ -0,0 +1,114 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using System.Diagnostics; +using osu.Framework.Allocation; +using osu.Framework.Bindables; +using osu.Framework.Extensions.Color4Extensions; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Pooling; +using osu.Framework.Graphics.Shapes; +using osu.Framework.Input.Events; +using osu.Game.Beatmaps; +using osu.Game.Graphics.Sprites; +using osuTK; +using osuTK.Graphics; + +namespace osu.Game.Screens.SelectV2 +{ + public partial class BeatmapSetPanel : PoolableDrawable, ICarouselPanel + { + public const float HEIGHT = CarouselItem.DEFAULT_HEIGHT * 2; + + [Resolved] + private BeatmapCarousel carousel { get; set; } = null!; + + private Box activationFlash = null!; + private OsuSpriteText text = null!; + + [BackgroundDependencyLoader] + private void load() + { + Size = new Vector2(500, HEIGHT); + Masking = true; + + InternalChildren = new Drawable[] + { + new Box + { + Colour = Color4.Yellow.Darken(5), + Alpha = 0.8f, + RelativeSizeAxes = Axes.Both, + }, + activationFlash = new Box + { + Colour = Color4.White, + Blending = BlendingParameters.Additive, + Alpha = 0, + RelativeSizeAxes = Axes.Both, + }, + text = new OsuSpriteText + { + Padding = new MarginPadding(5), + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + } + }; + + Selected.BindValueChanged(value => + { + activationFlash.FadeTo(value.NewValue ? 0.2f : 0, 500, Easing.OutQuint); + }); + + KeyboardSelected.BindValueChanged(value => + { + if (value.NewValue) + { + BorderThickness = 5; + BorderColour = Color4.Pink; + } + else + { + BorderThickness = 0; + } + }); + } + + protected override void PrepareForUse() + { + base.PrepareForUse(); + + Debug.Assert(Item != null); + Debug.Assert(Item.IsGroupSelectionTarget); + + var beatmapSetInfo = (BeatmapSetInfo)Item.Model; + + text.Text = $"{beatmapSetInfo.Metadata}"; + + this.FadeInFromZero(500, Easing.OutQuint); + } + + protected override bool OnClick(ClickEvent e) + { + carousel.CurrentSelection = Item!.Model; + return true; + } + + #region ICarouselPanel + + public CarouselItem? Item { get; set; } + public BindableBool Selected { get; } = new BindableBool(); + public BindableBool KeyboardSelected { get; } = new BindableBool(); + + public double DrawYPosition { get; set; } + + public void Activated() + { + // sets should never be activated. + throw new InvalidOperationException(); + } + + #endregion + } +} From da762384f8450c709c8319ec2a82ba32d29528f8 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 24 Jan 2025 20:20:18 +0900 Subject: [PATCH 0754/1275] Fix breakage from reordering co-reliant variable sets (and guard against it) --- osu.Game/Screens/Play/MasterGameplayClockContainer.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Game/Screens/Play/MasterGameplayClockContainer.cs b/osu.Game/Screens/Play/MasterGameplayClockContainer.cs index 0b47d8ed85..c20d461526 100644 --- a/osu.Game/Screens/Play/MasterGameplayClockContainer.cs +++ b/osu.Game/Screens/Play/MasterGameplayClockContainer.cs @@ -72,17 +72,17 @@ namespace osu.Game.Screens.Play track = beatmap.Track; - StartTime = findEarliestStartTime(); GameplayStartTime = gameplayStartTime; + StartTime = findEarliestStartTime(gameplayStartTime, beatmap); } - private double findEarliestStartTime() + private static double findEarliestStartTime(double gameplayStartTime, WorkingBeatmap beatmap) { // here we are trying to find the time to start playback from the "zero" point. // generally this is either zero, or some point earlier than zero in the case of storyboards, lead-ins etc. // start with the originally provided latest time (if before zero). - double time = Math.Min(0, GameplayStartTime); + double time = Math.Min(0, gameplayStartTime); // if a storyboard is present, it may dictate the appropriate start time by having events in negative time space. // this is commonly used to display an intro before the audio track start. From 589035c5348aa16c586c7d28ae04cc598e3410c0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 24 Jan 2025 12:34:05 +0100 Subject: [PATCH 0755/1275] Simplify code --- osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs b/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs index 7a93a26e45..2a7ec79e55 100644 --- a/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs +++ b/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs @@ -230,8 +230,6 @@ namespace osu.Game.Rulesets.Osu.Edit if (!snapToVisibleBlueprints(screenSpacePosition, out var snapResult)) return null; - snapResult.Time ??= fallbackTime; - if (DistanceSnapProvider.DistanceSnapToggle.Value != TernaryState.True || distanceSnapGrid == null) return snapResult; @@ -244,8 +242,9 @@ namespace osu.Game.Rulesets.Osu.Edit // The easiest way to ensure this is to attempt application of distance snap after a nearby object is found, and copy over // the time value if the proposed positions are roughly the same. (Vector2 distanceSnappedPosition, double distanceSnappedTime) = distanceSnapGrid.GetSnappedPosition(distanceSnapGrid.ToLocalSpace(snapResult.ScreenSpacePosition)); - if (Precision.AlmostEquals(distanceSnapGrid.ToScreenSpace(distanceSnappedPosition), snapResult.ScreenSpacePosition, 1)) - snapResult.Time = distanceSnappedTime; + snapResult.Time = Precision.AlmostEquals(distanceSnapGrid.ToScreenSpace(distanceSnappedPosition), snapResult.ScreenSpacePosition, 1) + ? distanceSnappedTime + : fallbackTime; return snapResult; } From b04144df5489465e59b5305f65cd8b450f84fbc4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 24 Jan 2025 12:50:46 +0100 Subject: [PATCH 0756/1275] Fix behavioural change in interaction between grid & distance snap --- .../HitCircles/HitCirclePlacementBlueprint.cs | 9 +++++---- .../Components/PathControlPointVisualiser.cs | 15 ++++++++++----- .../Sliders/SliderPlacementBlueprint.cs | 9 +++++---- .../Edit/OsuBlueprintContainer.cs | 6 +++++- .../Edit/OsuHitObjectComposer.cs | 2 +- 5 files changed, 26 insertions(+), 15 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/HitCircles/HitCirclePlacementBlueprint.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/HitCircles/HitCirclePlacementBlueprint.cs index 53784a7f08..0e1ede4d4c 100644 --- a/osu.Game.Rulesets.Osu/Edit/Blueprints/HitCircles/HitCirclePlacementBlueprint.cs +++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/HitCircles/HitCirclePlacementBlueprint.cs @@ -52,10 +52,11 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.HitCircles public override SnapResult UpdateTimeAndPosition(Vector2 screenSpacePosition, double fallbackTime) { - var result = composer?.TrySnapToNearbyObjects(screenSpacePosition, fallbackTime) - ?? composer?.TrySnapToDistanceGrid(screenSpacePosition) - ?? composer?.TrySnapToPositionGrid(screenSpacePosition) - ?? new SnapResult(screenSpacePosition, fallbackTime); + var result = composer?.TrySnapToNearbyObjects(screenSpacePosition, fallbackTime); + result ??= composer?.TrySnapToDistanceGrid(screenSpacePosition); + if (composer?.TrySnapToPositionGrid(result?.ScreenSpacePosition ?? screenSpacePosition, result?.Time ?? fallbackTime) is SnapResult gridSnapResult) + result = gridSnapResult; + result ??= new SnapResult(screenSpacePosition, fallbackTime); UpdateTimeAndPosition(result); HitObject.Position = ToLocalSpace(result.ScreenSpacePosition); diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs index a3bb0b868a..189bb005a7 100644 --- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs +++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs @@ -9,6 +9,7 @@ using System.Collections.Specialized; using System.Diagnostics; using System.Linq; using Humanizer; +using JetBrains.Annotations; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; @@ -48,6 +49,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components public Action> SplitControlPointsRequested; [Resolved(CanBeNull = true)] + [CanBeNull] private OsuHitObjectComposer positionSnapProvider { get; set; } [Resolved(CanBeNull = true)] @@ -433,14 +435,17 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components { // Special handling for selections containing head control point - the position of the hit object changes which means the snapped position and time have to be taken into account Vector2 newHeadPosition = Parent!.ToScreenSpace(e.MousePosition + (dragStartPositions[0] - dragStartPositions[draggedControlPointIndex])); - SnapResult result = positionSnapProvider?.TrySnapToNearbyObjects(newHeadPosition, oldStartTime) - ?? positionSnapProvider?.TrySnapToDistanceGrid(newHeadPosition) - ?? positionSnapProvider?.TrySnapToPositionGrid(newHeadPosition); - Vector2 movementDelta = Parent!.ToLocalSpace(result?.ScreenSpacePosition ?? newHeadPosition) - hitObject.Position; + var result = positionSnapProvider?.TrySnapToNearbyObjects(newHeadPosition, oldStartTime); + result ??= positionSnapProvider?.TrySnapToDistanceGrid(newHeadPosition); + if (positionSnapProvider?.TrySnapToPositionGrid(result?.ScreenSpacePosition ?? newHeadPosition, result?.Time ?? oldStartTime) is SnapResult gridSnapResult) + result = gridSnapResult; + result ??= new SnapResult(newHeadPosition, oldStartTime); + + Vector2 movementDelta = Parent!.ToLocalSpace(result.ScreenSpacePosition) - hitObject.Position; hitObject.Position += movementDelta; - hitObject.StartTime = result?.Time ?? hitObject.StartTime; + hitObject.StartTime = result.Time ?? hitObject.StartTime; for (int i = 1; i < hitObject.Path.ControlPoints.Count; i++) { diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderPlacementBlueprint.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderPlacementBlueprint.cs index fd72f18b12..2d38e83b2e 100644 --- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderPlacementBlueprint.cs +++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderPlacementBlueprint.cs @@ -108,10 +108,11 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders public override SnapResult UpdateTimeAndPosition(Vector2 screenSpacePosition, double fallbackTime) { - var result = composer?.TrySnapToNearbyObjects(screenSpacePosition, fallbackTime) - ?? composer?.TrySnapToDistanceGrid(screenSpacePosition) - ?? composer?.TrySnapToPositionGrid(screenSpacePosition) - ?? new SnapResult(screenSpacePosition, fallbackTime); + var result = composer?.TrySnapToNearbyObjects(screenSpacePosition, fallbackTime); + result ??= composer?.TrySnapToDistanceGrid(screenSpacePosition); + if (composer?.TrySnapToPositionGrid(result?.ScreenSpacePosition ?? screenSpacePosition, result?.Time ?? fallbackTime) is SnapResult gridSnapResult) + result = gridSnapResult; + result ??= new SnapResult(screenSpacePosition, fallbackTime); UpdateTimeAndPosition(result); diff --git a/osu.Game.Rulesets.Osu/Edit/OsuBlueprintContainer.cs b/osu.Game.Rulesets.Osu/Edit/OsuBlueprintContainer.cs index 235368e552..5eff95adec 100644 --- a/osu.Game.Rulesets.Osu/Edit/OsuBlueprintContainer.cs +++ b/osu.Game.Rulesets.Osu/Edit/OsuBlueprintContainer.cs @@ -60,7 +60,11 @@ namespace osu.Game.Rulesets.Osu.Edit Vector2 movePosition = blueprints.First().originalSnapPositions.First() + distanceTravelled; // Retrieve a snapped position. - var result = Composer.TrySnapToDistanceGrid(movePosition) ?? Composer.TrySnapToPositionGrid(movePosition) ?? new SnapResult(movePosition, null); + var result = Composer.TrySnapToNearbyObjects(movePosition); + result ??= Composer.TrySnapToDistanceGrid(movePosition); + if (Composer.TrySnapToPositionGrid(result?.ScreenSpacePosition ?? movePosition, result?.Time) is SnapResult gridSnapResult) + result = gridSnapResult; + result ??= new SnapResult(movePosition, null); var referenceBlueprint = blueprints.First().blueprint; bool moved = SelectionHandler.HandleMovement(new MoveSelectionEvent(referenceBlueprint, result.ScreenSpacePosition - referenceBlueprint.ScreenSpaceSelectionPoint)); diff --git a/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs b/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs index 2a7ec79e55..194276baf9 100644 --- a/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs +++ b/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs @@ -261,7 +261,7 @@ namespace osu.Game.Rulesets.Osu.Edit } [CanBeNull] - public SnapResult TrySnapToPositionGrid(Vector2 screenSpacePosition) + public SnapResult TrySnapToPositionGrid(Vector2 screenSpacePosition, double? fallbackTime = null) { if (rectangularGridSnapToggle.Value != TernaryState.True) return null; From daec91f61d5410ad2ab879443aea0ce7a757c01c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 24 Jan 2025 13:05:38 +0100 Subject: [PATCH 0757/1275] Refactor further to avoid weird non-virtual common method --- .../Edit/Blueprints/BananaShowerPlacementBlueprint.cs | 2 +- .../Edit/Blueprints/FruitPlacementBlueprint.cs | 2 +- .../Edit/Blueprints/ManiaPlacementBlueprint.cs | 2 +- .../HitCircles/HitCirclePlacementBlueprint.cs | 2 +- .../Blueprints/Sliders/SliderPlacementBlueprint.cs | 2 +- .../Blueprints/Spinners/SpinnerPlacementBlueprint.cs | 8 -------- .../Edit/Blueprints/HitPlacementBlueprint.cs | 2 +- .../Edit/Blueprints/TaikoSpanPlacementBlueprint.cs | 2 +- osu.Game/Rulesets/Edit/HitObjectPlacementBlueprint.cs | 10 ++++++---- 9 files changed, 13 insertions(+), 19 deletions(-) diff --git a/osu.Game.Rulesets.Catch/Edit/Blueprints/BananaShowerPlacementBlueprint.cs b/osu.Game.Rulesets.Catch/Edit/Blueprints/BananaShowerPlacementBlueprint.cs index 85b7624f1b..971c98cafd 100644 --- a/osu.Game.Rulesets.Catch/Edit/Blueprints/BananaShowerPlacementBlueprint.cs +++ b/osu.Game.Rulesets.Catch/Edit/Blueprints/BananaShowerPlacementBlueprint.cs @@ -64,7 +64,7 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints { var result = Composer?.FindSnappedPositionAndTime(screenSpacePosition) ?? new SnapResult(screenSpacePosition, fallbackTime); - base.UpdateTimeAndPosition(result); + base.UpdateTimeAndPosition(result.ScreenSpacePosition, result.Time ?? fallbackTime); if (!(result.Time is double time)) return result; diff --git a/osu.Game.Rulesets.Catch/Edit/Blueprints/FruitPlacementBlueprint.cs b/osu.Game.Rulesets.Catch/Edit/Blueprints/FruitPlacementBlueprint.cs index 83f75771ad..96cfbcb046 100644 --- a/osu.Game.Rulesets.Catch/Edit/Blueprints/FruitPlacementBlueprint.cs +++ b/osu.Game.Rulesets.Catch/Edit/Blueprints/FruitPlacementBlueprint.cs @@ -52,7 +52,7 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints ? distanceSnapResult : gridSnapResult; - UpdateTimeAndPosition(result); + base.UpdateTimeAndPosition(result.ScreenSpacePosition, result.Time ?? fallbackTime); HitObject.X = ToLocalSpace(result.ScreenSpacePosition).X; return result; diff --git a/osu.Game.Rulesets.Mania/Edit/Blueprints/ManiaPlacementBlueprint.cs b/osu.Game.Rulesets.Mania/Edit/Blueprints/ManiaPlacementBlueprint.cs index 359a952755..423f14b092 100644 --- a/osu.Game.Rulesets.Mania/Edit/Blueprints/ManiaPlacementBlueprint.cs +++ b/osu.Game.Rulesets.Mania/Edit/Blueprints/ManiaPlacementBlueprint.cs @@ -62,7 +62,7 @@ namespace osu.Game.Rulesets.Mania.Edit.Blueprints { var result = composer?.FindSnappedPositionAndTime(screenSpacePosition) ?? new SnapResult(screenSpacePosition, fallbackTime); - UpdateTimeAndPosition(result); + base.UpdateTimeAndPosition(result.ScreenSpacePosition, result.Time ?? fallbackTime); if (result.Playfield is Column col) { diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/HitCircles/HitCirclePlacementBlueprint.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/HitCircles/HitCirclePlacementBlueprint.cs index 0e1ede4d4c..93d79a50ab 100644 --- a/osu.Game.Rulesets.Osu/Edit/Blueprints/HitCircles/HitCirclePlacementBlueprint.cs +++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/HitCircles/HitCirclePlacementBlueprint.cs @@ -58,7 +58,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.HitCircles result = gridSnapResult; result ??= new SnapResult(screenSpacePosition, fallbackTime); - UpdateTimeAndPosition(result); + base.UpdateTimeAndPosition(result.ScreenSpacePosition, result.Time ?? fallbackTime); HitObject.Position = ToLocalSpace(result.ScreenSpacePosition); return result; } diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderPlacementBlueprint.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderPlacementBlueprint.cs index 2d38e83b2e..1012578375 100644 --- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderPlacementBlueprint.cs +++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderPlacementBlueprint.cs @@ -114,7 +114,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders result = gridSnapResult; result ??= new SnapResult(screenSpacePosition, fallbackTime); - UpdateTimeAndPosition(result); + base.UpdateTimeAndPosition(result.ScreenSpacePosition, result.Time ?? fallbackTime); switch (state) { diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Spinners/SpinnerPlacementBlueprint.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Spinners/SpinnerPlacementBlueprint.cs index 6c4847cada..17d2dcd75c 100644 --- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Spinners/SpinnerPlacementBlueprint.cs +++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Spinners/SpinnerPlacementBlueprint.cs @@ -9,7 +9,6 @@ using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Osu.Edit.Blueprints.Spinners.Components; using osu.Game.Rulesets.Osu.Objects; using osu.Game.Rulesets.Osu.UI; -using osuTK; using osuTK.Input; namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Spinners @@ -71,12 +70,5 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Spinners ? Math.Max(HitObject.StartTime, EditorClock.CurrentTime) : Math.Max(HitObject.StartTime + beatSnapProvider.GetBeatLengthAtTime(HitObject.StartTime), beatSnapProvider.SnapTime(EditorClock.CurrentTime)); } - - public override SnapResult UpdateTimeAndPosition(Vector2 screenSpacePosition, double fallbackTime) - { - var result = new SnapResult(screenSpacePosition, fallbackTime); - UpdateTimeAndPosition(result); - return result; - } } } diff --git a/osu.Game.Rulesets.Taiko/Edit/Blueprints/HitPlacementBlueprint.cs b/osu.Game.Rulesets.Taiko/Edit/Blueprints/HitPlacementBlueprint.cs index b887fac42a..ce2a674e92 100644 --- a/osu.Game.Rulesets.Taiko/Edit/Blueprints/HitPlacementBlueprint.cs +++ b/osu.Game.Rulesets.Taiko/Edit/Blueprints/HitPlacementBlueprint.cs @@ -48,7 +48,7 @@ namespace osu.Game.Rulesets.Taiko.Edit.Blueprints { var result = composer?.FindSnappedPositionAndTime(screenSpacePosition) ?? new SnapResult(screenSpacePosition, fallbackTime); piece.Position = ToLocalSpace(result.ScreenSpacePosition); - UpdateTimeAndPosition(result); + base.UpdateTimeAndPosition(result.ScreenSpacePosition, result.Time ?? fallbackTime); return result; } } diff --git a/osu.Game.Rulesets.Taiko/Edit/Blueprints/TaikoSpanPlacementBlueprint.cs b/osu.Game.Rulesets.Taiko/Edit/Blueprints/TaikoSpanPlacementBlueprint.cs index 7263c1ef2c..3d5c95e1e8 100644 --- a/osu.Game.Rulesets.Taiko/Edit/Blueprints/TaikoSpanPlacementBlueprint.cs +++ b/osu.Game.Rulesets.Taiko/Edit/Blueprints/TaikoSpanPlacementBlueprint.cs @@ -85,7 +85,7 @@ namespace osu.Game.Rulesets.Taiko.Edit.Blueprints { var result = composer?.FindSnappedPositionAndTime(screenSpacePosition) ?? new SnapResult(screenSpacePosition, fallbackTime); - UpdateTimeAndPosition(result); + base.UpdateTimeAndPosition(result.ScreenSpacePosition, result.Time ?? fallbackTime); if (PlacementActive == PlacementState.Active) { diff --git a/osu.Game/Rulesets/Edit/HitObjectPlacementBlueprint.cs b/osu.Game/Rulesets/Edit/HitObjectPlacementBlueprint.cs index 3119680272..6720540ec2 100644 --- a/osu.Game/Rulesets/Edit/HitObjectPlacementBlueprint.cs +++ b/osu.Game/Rulesets/Edit/HitObjectPlacementBlueprint.cs @@ -13,6 +13,7 @@ using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects.Types; using osu.Game.Screens.Edit; using osu.Game.Screens.Edit.Compose; +using osuTK; namespace osu.Game.Rulesets.Edit { @@ -87,14 +88,13 @@ namespace osu.Game.Rulesets.Edit } /// - /// Updates the time and position of this based on the provided snap information. + /// Updates the time and position of this . /// - /// The snap result information. - protected void UpdateTimeAndPosition(SnapResult result) + public override SnapResult UpdateTimeAndPosition(Vector2 screenSpacePosition, double time) { if (PlacementActive == PlacementState.Waiting) { - HitObject.StartTime = result.Time ?? EditorClock.CurrentTime; + HitObject.StartTime = time; if (HitObject is IHasComboInformation comboInformation) comboInformation.UpdateComboInformation(getPreviousHitObject() as IHasComboInformation); @@ -129,6 +129,8 @@ namespace osu.Game.Rulesets.Edit for (int i = 0; i < hasRepeats.NodeSamples.Count; i++) hasRepeats.NodeSamples[i] = HitObject.Samples.Select(o => o.With()).ToList(); } + + return new SnapResult(screenSpacePosition, time); } /// From b0136f98a9f19bd61d3e0519cc200184908b31fc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 24 Jan 2025 14:24:16 +0100 Subject: [PATCH 0758/1275] Fix test failures --- osu.Game.Tests/Visual/SongSelect/BeatmapCarouselV2TestScene.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Tests/Visual/SongSelect/BeatmapCarouselV2TestScene.cs b/osu.Game.Tests/Visual/SongSelect/BeatmapCarouselV2TestScene.cs index 4c85cf8fcd..281be924a1 100644 --- a/osu.Game.Tests/Visual/SongSelect/BeatmapCarouselV2TestScene.cs +++ b/osu.Game.Tests/Visual/SongSelect/BeatmapCarouselV2TestScene.cs @@ -131,7 +131,7 @@ namespace osu.Game.Tests.Visual.SongSelect protected void SortBy(FilterCriteria criteria) => AddStep($"sort by {criteria.Sort}", () => Carousel.Filter(criteria)); - protected void WaitForDrawablePanels() => AddUntilStep("drawable panels loaded", () => Carousel.ChildrenOfType().Count(), () => Is.GreaterThan(0)); + protected void WaitForDrawablePanels() => AddUntilStep("drawable panels loaded", () => Carousel.ChildrenOfType().Count(), () => Is.GreaterThan(0)); protected void WaitForSorting() => AddUntilStep("sorting finished", () => Carousel.IsFiltering, () => Is.False); protected void WaitForScrolling() => AddUntilStep("scroll finished", () => Scroll.Current, () => Is.EqualTo(Scroll.Target)); From 82c5f37c2cf005e330a5525892246d5a70358174 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 24 Jan 2025 22:45:05 +0900 Subject: [PATCH 0759/1275] Remove selection animation on set panel --- osu.Game/Screens/SelectV2/BeatmapSetPanel.cs | 5 ----- 1 file changed, 5 deletions(-) diff --git a/osu.Game/Screens/SelectV2/BeatmapSetPanel.cs b/osu.Game/Screens/SelectV2/BeatmapSetPanel.cs index 0b95f94365..483869cad2 100644 --- a/osu.Game/Screens/SelectV2/BeatmapSetPanel.cs +++ b/osu.Game/Screens/SelectV2/BeatmapSetPanel.cs @@ -56,11 +56,6 @@ namespace osu.Game.Screens.SelectV2 } }; - Selected.BindValueChanged(value => - { - activationFlash.FadeTo(value.NewValue ? 0.2f : 0, 500, Easing.OutQuint); - }); - KeyboardSelected.BindValueChanged(value => { if (value.NewValue) From 55ab3c72f6acce20144a12ae6258138742969fe6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 24 Jan 2025 15:15:50 +0100 Subject: [PATCH 0760/1275] Remove unused field --- osu.Game/Screens/SelectV2/BeatmapSetPanel.cs | 8 -------- 1 file changed, 8 deletions(-) diff --git a/osu.Game/Screens/SelectV2/BeatmapSetPanel.cs b/osu.Game/Screens/SelectV2/BeatmapSetPanel.cs index 483869cad2..37e8b88f71 100644 --- a/osu.Game/Screens/SelectV2/BeatmapSetPanel.cs +++ b/osu.Game/Screens/SelectV2/BeatmapSetPanel.cs @@ -24,7 +24,6 @@ namespace osu.Game.Screens.SelectV2 [Resolved] private BeatmapCarousel carousel { get; set; } = null!; - private Box activationFlash = null!; private OsuSpriteText text = null!; [BackgroundDependencyLoader] @@ -41,13 +40,6 @@ namespace osu.Game.Screens.SelectV2 Alpha = 0.8f, RelativeSizeAxes = Axes.Both, }, - activationFlash = new Box - { - Colour = Color4.White, - Blending = BlendingParameters.Additive, - Alpha = 0, - RelativeSizeAxes = Axes.Both, - }, text = new OsuSpriteText { Padding = new MarginPadding(5), From 79df094f17b65c5276d317bc84563d0afbe21e67 Mon Sep 17 00:00:00 2001 From: Jamie Taylor Date: Fri, 24 Jan 2025 23:20:04 +0900 Subject: [PATCH 0761/1275] Add unique samples for friend online/offline notifications --- osu.Game/Online/FriendPresenceNotifier.cs | 4 ++-- .../Notifications/FriendOfflineNotification.cs | 10 ++++++++++ .../Overlays/Notifications/FriendOnlineNotification.cs | 10 ++++++++++ 3 files changed, 22 insertions(+), 2 deletions(-) create mode 100644 osu.Game/Overlays/Notifications/FriendOfflineNotification.cs create mode 100644 osu.Game/Overlays/Notifications/FriendOnlineNotification.cs diff --git a/osu.Game/Online/FriendPresenceNotifier.cs b/osu.Game/Online/FriendPresenceNotifier.cs index 75b487384a..229ad4f734 100644 --- a/osu.Game/Online/FriendPresenceNotifier.cs +++ b/osu.Game/Online/FriendPresenceNotifier.cs @@ -167,7 +167,7 @@ namespace osu.Game.Online APIUser? singleUser = onlineAlertQueue.Count == 1 ? onlineAlertQueue.Single() : null; - notifications.Post(new SimpleNotification + notifications.Post(new FriendOnlineNotification { Transient = true, IsImportant = false, @@ -204,7 +204,7 @@ namespace osu.Game.Online return; } - notifications.Post(new SimpleNotification + notifications.Post(new FriendOfflineNotification { Transient = true, IsImportant = false, diff --git a/osu.Game/Overlays/Notifications/FriendOfflineNotification.cs b/osu.Game/Overlays/Notifications/FriendOfflineNotification.cs new file mode 100644 index 0000000000..147fd4ba6f --- /dev/null +++ b/osu.Game/Overlays/Notifications/FriendOfflineNotification.cs @@ -0,0 +1,10 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +namespace osu.Game.Overlays.Notifications +{ + public partial class FriendOfflineNotification : SimpleNotification + { + public override string PopInSampleName => "UI/notification-friend-offline"; + } +} diff --git a/osu.Game/Overlays/Notifications/FriendOnlineNotification.cs b/osu.Game/Overlays/Notifications/FriendOnlineNotification.cs new file mode 100644 index 0000000000..6a5cf3b517 --- /dev/null +++ b/osu.Game/Overlays/Notifications/FriendOnlineNotification.cs @@ -0,0 +1,10 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +namespace osu.Game.Overlays.Notifications +{ + public partial class FriendOnlineNotification : SimpleNotification + { + public override string PopInSampleName => "UI/notification-friend-online"; + } +} From 354126b7f7684a052389fff61715118a6fe3d885 Mon Sep 17 00:00:00 2001 From: ThePooN Date: Fri, 24 Jan 2025 18:14:55 +0100 Subject: [PATCH 0762/1275] =?UTF-8?q?=F0=9F=94=A7=20Specify=20we're=20not?= =?UTF-8?q?=20using=20non-exempt=20encryption?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- osu.iOS/Info.plist | 2 ++ 1 file changed, 2 insertions(+) diff --git a/osu.iOS/Info.plist b/osu.iOS/Info.plist index 70747fc9c8..120e8caecc 100644 --- a/osu.iOS/Info.plist +++ b/osu.iOS/Info.plist @@ -153,6 +153,8 @@ Editor + ITSAppUsesNonExemptEncryption + LSApplicationCategoryType public.app-category.music-games LSSupportsOpeningDocumentsInPlace From ab4162e2aafc4e246ba070870e4967ab7a6e00cb Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Sat, 25 Jan 2025 19:27:21 +0900 Subject: [PATCH 0763/1275] Various refactorings and cleanups --- .../TestSceneMultiplayerLoungeSubScreen.cs | 28 +++++-------------- .../TestScenePlaylistsLoungeSubScreen.cs | 28 +++---------------- .../Multiplayer/IMultiplayerLoungeServer.cs | 5 ++++ .../Online/Multiplayer/MultiplayerClient.cs | 3 +- osu.Game/Online/Rooms/CreateRoomRequest.cs | 2 ++ osu.Game/Online/Rooms/JoinRoomRequest.cs | 2 ++ .../OnlinePlay/Lounge/DrawableLoungeRoom.cs | 2 +- .../OnlinePlay/Lounge/IOnlinePlayLounge.cs | 6 ++-- .../OnlinePlay/Lounge/LoungeSubScreen.cs | 6 ++-- .../Screens/OnlinePlay/Match/RoomSubScreen.cs | 3 ++ .../Multiplayer/MultiplayerLoungeSubScreen.cs | 2 +- .../Playlists/PlaylistsLoungeSubScreen.cs | 2 +- 12 files changed, 34 insertions(+), 55 deletions(-) diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerLoungeSubScreen.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerLoungeSubScreen.cs index 4a259149e2..eb649acd2d 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerLoungeSubScreen.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerLoungeSubScreen.cs @@ -40,7 +40,7 @@ namespace osu.Game.Tests.Visual.Multiplayer [Test] public void TestJoinRoomWithoutPassword() { - addRoom(false); + AddStep("add room", () => RoomManager.AddRooms(1, withPassword: false)); AddStep("select room", () => InputManager.Key(Key.Down)); AddStep("join room", () => InputManager.Key(Key.Enter)); @@ -50,7 +50,7 @@ namespace osu.Game.Tests.Visual.Multiplayer [Test] public void TestPopoverHidesOnBackButton() { - addRoom(true); + AddStep("add room", () => RoomManager.AddRooms(1, withPassword: true)); AddStep("select room", () => InputManager.Key(Key.Down)); AddStep("attempt join room", () => InputManager.Key(Key.Enter)); @@ -70,7 +70,7 @@ namespace osu.Game.Tests.Visual.Multiplayer [Test] public void TestPopoverHidesOnLeavingScreen() { - addRoom(true); + AddStep("add room", () => RoomManager.AddRooms(1, withPassword: true)); AddStep("select room", () => InputManager.Key(Key.Down)); AddStep("attempt join room", () => InputManager.Key(Key.Enter)); @@ -86,7 +86,7 @@ namespace osu.Game.Tests.Visual.Multiplayer { DrawableLoungeRoom.PasswordEntryPopover? passwordEntryPopover = null; - addRoom(true); + AddStep("add room", () => RoomManager.AddRooms(1, withPassword: true)); AddStep("select room", () => InputManager.Key(Key.Down)); AddStep("attempt join room", () => InputManager.Key(Key.Enter)); AddUntilStep("password prompt appeared", () => (passwordEntryPopover = InputManager.ChildrenOfType().FirstOrDefault()) != null); @@ -105,7 +105,7 @@ namespace osu.Game.Tests.Visual.Multiplayer { DrawableLoungeRoom.PasswordEntryPopover? passwordEntryPopover = null; - addRoom(true); + AddStep("add room", () => RoomManager.AddRooms(1, withPassword: true)); AddStep("select room", () => InputManager.Key(Key.Down)); AddStep("attempt join room", () => InputManager.Key(Key.Enter)); AddUntilStep("password prompt appeared", () => (passwordEntryPopover = InputManager.ChildrenOfType().FirstOrDefault()) != null); @@ -124,7 +124,7 @@ namespace osu.Game.Tests.Visual.Multiplayer { DrawableLoungeRoom.PasswordEntryPopover? passwordEntryPopover = null; - addRoom(true); + AddStep("add room", () => RoomManager.AddRooms(1, withPassword: true)); AddStep("select room", () => InputManager.Key(Key.Down)); AddStep("attempt join room", () => InputManager.Key(Key.Enter)); AddUntilStep("password prompt appeared", () => (passwordEntryPopover = InputManager.ChildrenOfType().FirstOrDefault()) != null); @@ -139,7 +139,7 @@ namespace osu.Game.Tests.Visual.Multiplayer { DrawableLoungeRoom.PasswordEntryPopover? passwordEntryPopover = null; - addRoom(true); + AddStep("add room", () => RoomManager.AddRooms(1, withPassword: true)); AddStep("select room", () => InputManager.Key(Key.Down)); AddStep("attempt join room", () => InputManager.Key(Key.Enter)); AddUntilStep("password prompt appeared", () => (passwordEntryPopover = InputManager.ChildrenOfType().FirstOrDefault()) != null); @@ -149,20 +149,6 @@ namespace osu.Game.Tests.Visual.Multiplayer AddAssert("room joined", () => MultiplayerClient.RoomJoined); } - private void addRoom(bool withPassword) - { - int initialRoomCount = 0; - - AddStep("add room", () => - { - initialRoomCount = roomsContainer.Rooms.Count; - RoomManager.AddRooms(1, withPassword: withPassword); - loungeScreen.RefreshRooms(); - }); - - AddUntilStep("wait for room to appear", () => roomsContainer.Rooms.Count == initialRoomCount + 1); - } - protected override OnlinePlayTestSceneDependencies CreateOnlinePlayDependencies() => new MultiplayerTestSceneDependencies(); } } diff --git a/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsLoungeSubScreen.cs b/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsLoungeSubScreen.cs index 0897a3b2f5..53c7873de5 100644 --- a/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsLoungeSubScreen.cs +++ b/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsLoungeSubScreen.cs @@ -35,12 +35,7 @@ namespace osu.Game.Tests.Visual.Playlists [Test] public void TestManyRooms() { - AddStep("add rooms", () => - { - RoomManager.AddRooms(500); - loungeScreen.RefreshRooms(); - }); - + AddStep("add rooms", () => RoomManager.AddRooms(500)); AddUntilStep("wait for rooms", () => roomsContainer.Rooms.Count == 500); } @@ -49,12 +44,7 @@ namespace osu.Game.Tests.Visual.Playlists { AddStep("reset mouse", () => InputManager.ReleaseButton(MouseButton.Left)); - AddStep("add rooms", () => - { - RoomManager.AddRooms(30); - loungeScreen.RefreshRooms(); - }); - + AddStep("add rooms", () => RoomManager.AddRooms(30)); AddUntilStep("wait for rooms", () => roomsContainer.Rooms.Count == 30); AddUntilStep("first room is not masked", () => checkRoomVisible(roomsContainer.Rooms[0])); @@ -71,12 +61,7 @@ namespace osu.Game.Tests.Visual.Playlists [Test] public void TestScrollSelectedIntoView() { - AddStep("add rooms", () => - { - RoomManager.AddRooms(30); - loungeScreen.RefreshRooms(); - }); - + AddStep("add rooms", () => RoomManager.AddRooms(30)); AddUntilStep("wait for rooms", () => roomsContainer.Rooms.Count == 30); AddUntilStep("first room is not masked", () => checkRoomVisible(roomsContainer.Rooms[0])); @@ -90,12 +75,7 @@ namespace osu.Game.Tests.Visual.Playlists [Test] public void TestEnteringRoomTakesLeaseOnSelection() { - AddStep("add rooms", () => - { - RoomManager.AddRooms(1); - loungeScreen.RefreshRooms(); - }); - + AddStep("add rooms", () => RoomManager.AddRooms(1)); AddUntilStep("wait for rooms", () => roomsContainer.Rooms.Count == 1); AddAssert("selected room is not disabled", () => !loungeScreen.SelectedRoom.Disabled); diff --git a/osu.Game/Online/Multiplayer/IMultiplayerLoungeServer.cs b/osu.Game/Online/Multiplayer/IMultiplayerLoungeServer.cs index c5eb6f9b36..0ee9fa54cd 100644 --- a/osu.Game/Online/Multiplayer/IMultiplayerLoungeServer.cs +++ b/osu.Game/Online/Multiplayer/IMultiplayerLoungeServer.cs @@ -10,6 +10,11 @@ namespace osu.Game.Online.Multiplayer /// public interface IMultiplayerLoungeServer { + /// + /// Request to create a multiplayer room. + /// + /// The room to create. + /// The created multiplayer room. Task CreateRoom(MultiplayerRoom room); /// diff --git a/osu.Game/Online/Multiplayer/MultiplayerClient.cs b/osu.Game/Online/Multiplayer/MultiplayerClient.cs index a8f314d372..6749ed9535 100644 --- a/osu.Game/Online/Multiplayer/MultiplayerClient.cs +++ b/osu.Game/Online/Multiplayer/MultiplayerClient.cs @@ -168,7 +168,7 @@ namespace osu.Game.Online.Multiplayer public async Task CreateRoom(Room room) { if (Room != null) - throw new InvalidOperationException("Cannot join a multiplayer room while already in one."); + throw new InvalidOperationException("Cannot create a multiplayer room while already in one."); var cancellationSource = joinCancellationSource = new CancellationTokenSource(); await initRoom(room, r => CreateRoomInternal(new MultiplayerRoom(room)), cancellationSource.Token).ConfigureAwait(false); @@ -212,6 +212,7 @@ namespace osu.Game.Online.Multiplayer APIRoom.RoomID = joinedRoom.RoomID; APIRoom.Playlist = joinedRoom.Playlist.Select(item => new PlaylistItem(item)).ToArray(); APIRoom.CurrentPlaylistItem = APIRoom.Playlist.Single(item => item.ID == joinedRoom.Settings.PlaylistItemId); + // The server will null out the end date upon the host joining the room, but the null value is never communicated to the client. APIRoom.EndDate = null; Debug.Assert(LocalUser != null); diff --git a/osu.Game/Online/Rooms/CreateRoomRequest.cs b/osu.Game/Online/Rooms/CreateRoomRequest.cs index 9773bb5e7d..5b2ea77aad 100644 --- a/osu.Game/Online/Rooms/CreateRoomRequest.cs +++ b/osu.Game/Online/Rooms/CreateRoomRequest.cs @@ -15,6 +15,8 @@ namespace osu.Game.Online.Rooms public CreateRoomRequest(Room room) { Room = room; + + // Also copy back to the source model, since it is likely to have been stored elsewhere. Success += r => Room.CopyFrom(r); } diff --git a/osu.Game/Online/Rooms/JoinRoomRequest.cs b/osu.Game/Online/Rooms/JoinRoomRequest.cs index 13e7ac8c84..610e887242 100644 --- a/osu.Game/Online/Rooms/JoinRoomRequest.cs +++ b/osu.Game/Online/Rooms/JoinRoomRequest.cs @@ -16,6 +16,8 @@ namespace osu.Game.Online.Rooms { Room = room; Password = password; + + // Also copy back to the source model, since it is likely to have been stored elsewhere. Success += r => Room.CopyFrom(r); } diff --git a/osu.Game/Screens/OnlinePlay/Lounge/DrawableLoungeRoom.cs b/osu.Game/Screens/OnlinePlay/Lounge/DrawableLoungeRoom.cs index 032a231ad3..5de35ef101 100644 --- a/osu.Game/Screens/OnlinePlay/Lounge/DrawableLoungeRoom.cs +++ b/osu.Game/Screens/OnlinePlay/Lounge/DrawableLoungeRoom.cs @@ -162,7 +162,7 @@ namespace osu.Game.Screens.OnlinePlay.Lounge { new OsuMenuItem("Create copy", MenuItemType.Standard, () => { - lounge?.Clone(Room); + lounge?.OpenCopy(Room); }) }; diff --git a/osu.Game/Screens/OnlinePlay/Lounge/IOnlinePlayLounge.cs b/osu.Game/Screens/OnlinePlay/Lounge/IOnlinePlayLounge.cs index 8fa7d0751f..73ab84af13 100644 --- a/osu.Game/Screens/OnlinePlay/Lounge/IOnlinePlayLounge.cs +++ b/osu.Game/Screens/OnlinePlay/Lounge/IOnlinePlayLounge.cs @@ -18,10 +18,10 @@ namespace osu.Game.Screens.OnlinePlay.Lounge void Join(Room room, string? password, Action? onSuccess = null, Action? onFailure = null); /// - /// Clones the given room and opens it as a fresh (not-yet-created) one. + /// Copies the given room and opens it as a fresh (not-yet-created) one. /// - /// The room to clone. - void Clone(Room room); + /// The room to copy. + void OpenCopy(Room room); /// /// Closes the given room. diff --git a/osu.Game/Screens/OnlinePlay/Lounge/LoungeSubScreen.cs b/osu.Game/Screens/OnlinePlay/Lounge/LoungeSubScreen.cs index df17063fdf..0e08e398a4 100644 --- a/osu.Game/Screens/OnlinePlay/Lounge/LoungeSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Lounge/LoungeSubScreen.cs @@ -309,7 +309,7 @@ namespace osu.Game.Screens.OnlinePlay.Lounge joiningRoomOperation = ongoingOperationTracker?.BeginOperation(); - TryJoin(room, password, r => + JoinInternal(room, password, r => { Open(room); joiningRoomOperation?.Dispose(); @@ -323,9 +323,9 @@ namespace osu.Game.Screens.OnlinePlay.Lounge }); }); - protected abstract void TryJoin(Room room, string? password, Action onSuccess, Action onFailure); + protected abstract void JoinInternal(Room room, string? password, Action onSuccess, Action onFailure); - public void Clone(Room room) + public void OpenCopy(Room room) { Debug.Assert(room.RoomID != null); diff --git a/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs b/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs index d37f3b877c..80b3961f44 100644 --- a/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs @@ -353,6 +353,9 @@ namespace osu.Game.Screens.OnlinePlay.Match return base.OnExiting(e); } + /// + /// Parts from the current room. + /// protected abstract void PartRoom(); private bool ensureExitConfirmed() diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerLoungeSubScreen.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerLoungeSubScreen.cs index e901ecbdce..873a9cde88 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerLoungeSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerLoungeSubScreen.cs @@ -81,7 +81,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer protected override ListingPollingComponent CreatePollingComponent() => new MultiplayerListingPollingComponent(); - protected override void TryJoin(Room room, string? password, Action onSuccess, Action onFailure) + protected override void JoinInternal(Room room, string? password, Action onSuccess, Action onFailure) { client.JoinRoom(room, password).ContinueWith(result => { diff --git a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsLoungeSubScreen.cs b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsLoungeSubScreen.cs index 92415e0eb1..6ed367328c 100644 --- a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsLoungeSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsLoungeSubScreen.cs @@ -60,7 +60,7 @@ namespace osu.Game.Screens.OnlinePlay.Playlists return criteria; } - protected override void TryJoin(Room room, string? password, Action onSuccess, Action onFailure) + protected override void JoinInternal(Room room, string? password, Action onSuccess, Action onFailure) { var joinRoomRequest = new JoinRoomRequest(room, password); From dac7d21302cbd9b7094ba7fc0d5989a9f254d46d Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Sat, 25 Jan 2025 18:12:44 -0500 Subject: [PATCH 0764/1275] Be explicit on nullability in `RequiresPortraitOrientation` Co-authored-by: Dean Herbert --- osu.Game/Screens/Play/Player.cs | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs index b3274766b2..92c483b24a 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -68,7 +68,16 @@ namespace osu.Game.Screens.Play public override bool HideMenuCursorOnNonMouseInput => true; - public override bool RequiresPortraitOrientation => DrawableRuleset?.RequiresPortraitOrientation == true; + public override bool RequiresPortraitOrientation + { + get + { + if (!LoadedBeatmapSuccessfully) + return false; + + return DrawableRuleset!.RequiresPortraitOrientation; + } + } protected override OverlayActivation InitialOverlayActivationMode => OverlayActivation.UserTriggered; From 8151c3095ddfc6389516054c4ae66ead80f5b605 Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Sat, 25 Jan 2025 18:21:20 -0500 Subject: [PATCH 0765/1275] Revert unnecessary inheritance Everyone is right, too much inheritance and polymorphism backfires very badly. --- .../Skinning/TestSceneColumnHitObjectArea.cs | 10 +++--- .../Mods/ManiaModWithPlayfieldCover.cs | 4 +-- osu.Game.Rulesets.Mania/UI/Column.cs | 6 +++- .../UI/Components/ColumnHitObjectArea.cs | 15 ++++---- .../Components/HitPositionPaddedContainer.cs | 35 ++++++------------- osu.Game.Rulesets.Mania/UI/Stage.cs | 12 ++++--- 6 files changed, 39 insertions(+), 43 deletions(-) diff --git a/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneColumnHitObjectArea.cs b/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneColumnHitObjectArea.cs index d4bbc8acb6..bf67d2d6a9 100644 --- a/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneColumnHitObjectArea.cs +++ b/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneColumnHitObjectArea.cs @@ -28,18 +28,20 @@ namespace osu.Game.Rulesets.Mania.Tests.Skinning { RelativeSizeAxes = Axes.Both, Width = 0.5f, - Child = new ColumnHitObjectArea(new HitObjectContainer()) + Child = new ColumnHitObjectArea { - RelativeSizeAxes = Axes.Both + RelativeSizeAxes = Axes.Both, + Child = new HitObjectContainer(), } }, new ColumnTestContainer(1, ManiaAction.Key2) { RelativeSizeAxes = Axes.Both, Width = 0.5f, - Child = new ColumnHitObjectArea(new HitObjectContainer()) + Child = new ColumnHitObjectArea { - RelativeSizeAxes = Axes.Both + RelativeSizeAxes = Axes.Both, + Child = new HitObjectContainer(), } } } diff --git a/osu.Game.Rulesets.Mania/Mods/ManiaModWithPlayfieldCover.cs b/osu.Game.Rulesets.Mania/Mods/ManiaModWithPlayfieldCover.cs index b6e6ee7481..1bc16112c5 100644 --- a/osu.Game.Rulesets.Mania/Mods/ManiaModWithPlayfieldCover.cs +++ b/osu.Game.Rulesets.Mania/Mods/ManiaModWithPlayfieldCover.cs @@ -5,9 +5,9 @@ using System; using System.Linq; using osu.Framework.Bindables; using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; using osu.Game.Rulesets.Mania.Objects; using osu.Game.Rulesets.Mania.UI; -using osu.Game.Rulesets.Mania.UI.Components; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.UI; @@ -35,7 +35,7 @@ namespace osu.Game.Rulesets.Mania.Mods foreach (Column column in maniaPlayfield.Stages.SelectMany(stage => stage.Columns)) { HitObjectContainer hoc = column.HitObjectContainer; - ColumnHitObjectArea hocParent = (ColumnHitObjectArea)hoc.Parent!; + Container hocParent = (Container)hoc.Parent!; hocParent.Remove(hoc, false); hocParent.Add(CreateCover(hoc).With(c => diff --git a/osu.Game.Rulesets.Mania/UI/Column.cs b/osu.Game.Rulesets.Mania/UI/Column.cs index 99d952ef1f..81f4d79281 100644 --- a/osu.Game.Rulesets.Mania/UI/Column.cs +++ b/osu.Game.Rulesets.Mania/UI/Column.cs @@ -67,7 +67,11 @@ namespace osu.Game.Rulesets.Mania.UI Width = COLUMN_WIDTH; hitPolicy = new OrderedHitPolicy(HitObjectContainer); - HitObjectArea = new ColumnHitObjectArea(HitObjectContainer) { RelativeSizeAxes = Axes.Both }; + HitObjectArea = new ColumnHitObjectArea + { + RelativeSizeAxes = Axes.Both, + Child = HitObjectContainer, + }; } [Resolved] diff --git a/osu.Game.Rulesets.Mania/UI/Components/ColumnHitObjectArea.cs b/osu.Game.Rulesets.Mania/UI/Components/ColumnHitObjectArea.cs index 2d719ef764..46b6ef86f7 100644 --- a/osu.Game.Rulesets.Mania/UI/Components/ColumnHitObjectArea.cs +++ b/osu.Game.Rulesets.Mania/UI/Components/ColumnHitObjectArea.cs @@ -3,7 +3,6 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; -using osu.Game.Rulesets.UI; using osu.Game.Rulesets.UI.Scrolling; using osu.Game.Skinning; @@ -17,25 +16,29 @@ namespace osu.Game.Rulesets.Mania.UI.Components private readonly Drawable hitTarget; - public ColumnHitObjectArea(HitObjectContainer hitObjectContainer) - : base(hitObjectContainer) + protected override Container Content => content; + + private readonly Container content; + + public ColumnHitObjectArea() { AddRangeInternal(new[] { UnderlayElements = new Container { RelativeSizeAxes = Axes.Both, - Depth = 2, }, hitTarget = new SkinnableDrawable(new ManiaSkinComponentLookup(ManiaSkinComponents.HitTarget), _ => new DefaultHitTarget()) { RelativeSizeAxes = Axes.X, - Depth = 1 + }, + content = new Container + { + RelativeSizeAxes = Axes.Both, }, Explosions = new Container { RelativeSizeAxes = Axes.Both, - Depth = -1, } }); } diff --git a/osu.Game.Rulesets.Mania/UI/Components/HitPositionPaddedContainer.cs b/osu.Game.Rulesets.Mania/UI/Components/HitPositionPaddedContainer.cs index f550e3b241..ae91be1c67 100644 --- a/osu.Game.Rulesets.Mania/UI/Components/HitPositionPaddedContainer.cs +++ b/osu.Game.Rulesets.Mania/UI/Components/HitPositionPaddedContainer.cs @@ -4,52 +4,37 @@ using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; using osu.Game.Rulesets.Mania.Skinning; using osu.Game.Rulesets.UI.Scrolling; using osu.Game.Skinning; namespace osu.Game.Rulesets.Mania.UI.Components { - public partial class HitPositionPaddedContainer : SkinReloadableDrawable + public partial class HitPositionPaddedContainer : Container { protected readonly IBindable Direction = new Bindable(); - public HitPositionPaddedContainer(Drawable child) - { - InternalChild = child; - } - - internal void Add(Drawable drawable) - { - base.AddInternal(drawable); - } - - internal void Remove(Drawable drawable, bool disposeImmediately = true) - { - base.RemoveInternal(drawable, disposeImmediately); - } + [Resolved] + private ISkinSource skin { get; set; } = null!; [BackgroundDependencyLoader] private void load(IScrollingInfo scrollingInfo) { Direction.BindTo(scrollingInfo.Direction); - Direction.BindValueChanged(onDirectionChanged, true); - } + Direction.BindValueChanged(onDirectionChanged); + + skin.SourceChanged += onSkinChanged; - protected override void SkinChanged(ISkinSource skin) - { - base.SkinChanged(skin); UpdateHitPosition(); } - private void onDirectionChanged(ValueChangedEvent direction) - { - UpdateHitPosition(); - } + private void onSkinChanged() => UpdateHitPosition(); + private void onDirectionChanged(ValueChangedEvent direction) => UpdateHitPosition(); protected virtual void UpdateHitPosition() { - float hitPosition = CurrentSkin.GetConfig( + float hitPosition = skin.GetConfig( new ManiaSkinConfigurationLookup(LegacyManiaSkinConfigurationLookups.HitPosition))?.Value ?? Stage.HIT_TARGET_POSITION; diff --git a/osu.Game.Rulesets.Mania/UI/Stage.cs b/osu.Game.Rulesets.Mania/UI/Stage.cs index 2d73e7bcbe..fb9671c14d 100644 --- a/osu.Game.Rulesets.Mania/UI/Stage.cs +++ b/osu.Game.Rulesets.Mania/UI/Stage.cs @@ -103,12 +103,13 @@ namespace osu.Game.Rulesets.Mania.UI Width = 1366, // Bar lines should only be masked on the vertical axis BypassAutoSizeAxes = Axes.Both, Masking = true, - Child = barLineContainer = new HitPositionPaddedContainer(HitObjectContainer) + Child = barLineContainer = new HitPositionPaddedContainer { Name = "Bar lines", Anchor = Anchor.TopCentre, Origin = Anchor.TopCentre, RelativeSizeAxes = Axes.Y, + Child = HitObjectContainer, } }, columnFlow = new ColumnFlow(definition) @@ -119,12 +120,13 @@ namespace osu.Game.Rulesets.Mania.UI { RelativeSizeAxes = Axes.Both }, - new HitPositionPaddedContainer(judgements = new JudgementContainer - { - RelativeSizeAxes = Axes.Both, - }) + new HitPositionPaddedContainer { RelativeSizeAxes = Axes.Both, + Child = judgements = new JudgementContainer + { + RelativeSizeAxes = Axes.Both, + }, }, topLevelContainer = new Container { RelativeSizeAxes = Axes.Both } } From ffc37cece0483c9bcdea0962abc8bfbe1dd9b0f1 Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Sat, 25 Jan 2025 18:50:54 -0500 Subject: [PATCH 0766/1275] Avoid extra unnecessary DI Co-authored-by: Dean Herbert --- .../UI/DrawableManiaRuleset.cs | 2 +- .../UI/ManiaPlayfieldAdjustmentContainer.cs | 11 ++++------ .../Edit/DrawableEditorRulesetWrapper.cs | 22 +++++++++---------- osu.Game/Rulesets/Edit/HitObjectComposer.cs | 1 - 4 files changed, 16 insertions(+), 20 deletions(-) diff --git a/osu.Game.Rulesets.Mania/UI/DrawableManiaRuleset.cs b/osu.Game.Rulesets.Mania/UI/DrawableManiaRuleset.cs index a186d9aa7d..e33cf092c3 100644 --- a/osu.Game.Rulesets.Mania/UI/DrawableManiaRuleset.cs +++ b/osu.Game.Rulesets.Mania/UI/DrawableManiaRuleset.cs @@ -161,7 +161,7 @@ namespace osu.Game.Rulesets.Mania.UI /// The scroll time. public static double ComputeScrollTime(double scrollSpeed) => MAX_TIME_RANGE / scrollSpeed; - public override PlayfieldAdjustmentContainer CreatePlayfieldAdjustmentContainer() => new ManiaPlayfieldAdjustmentContainer(); + public override PlayfieldAdjustmentContainer CreatePlayfieldAdjustmentContainer() => new ManiaPlayfieldAdjustmentContainer(this); protected override Playfield CreatePlayfield() => new ManiaPlayfield(Beatmap.Stages); diff --git a/osu.Game.Rulesets.Mania/UI/ManiaPlayfieldAdjustmentContainer.cs b/osu.Game.Rulesets.Mania/UI/ManiaPlayfieldAdjustmentContainer.cs index b0203643b0..feb75b9f1e 100644 --- a/osu.Game.Rulesets.Mania/UI/ManiaPlayfieldAdjustmentContainer.cs +++ b/osu.Game.Rulesets.Mania/UI/ManiaPlayfieldAdjustmentContainer.cs @@ -2,7 +2,6 @@ // See the LICENCE file in the repository root for full licence text. using System; -using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Game.Rulesets.UI; @@ -16,8 +15,11 @@ namespace osu.Game.Rulesets.Mania.UI private readonly DrawSizePreservingFillContainer scalingContainer; - public ManiaPlayfieldAdjustmentContainer() + private readonly DrawableManiaRuleset drawableManiaRuleset; + + public ManiaPlayfieldAdjustmentContainer(DrawableManiaRuleset drawableManiaRuleset) { + this.drawableManiaRuleset = drawableManiaRuleset; InternalChild = scalingContainer = new DrawSizePreservingFillContainer { Anchor = Anchor.Centre, @@ -30,9 +32,6 @@ namespace osu.Game.Rulesets.Mania.UI }; } - [Resolved] - private DrawableRuleset drawableRuleset { get; set; } = null!; - protected override void Update() { base.Update(); @@ -40,8 +39,6 @@ namespace osu.Game.Rulesets.Mania.UI float aspectRatio = DrawWidth / DrawHeight; bool isPortrait = aspectRatio < 1f; - var drawableManiaRuleset = (DrawableManiaRuleset)drawableRuleset; - if (isPortrait && drawableManiaRuleset.Beatmap.Stages.Count == 1) { // Scale playfield up by 25% to become playable on mobile devices, diff --git a/osu.Game/Rulesets/Edit/DrawableEditorRulesetWrapper.cs b/osu.Game/Rulesets/Edit/DrawableEditorRulesetWrapper.cs index 573eb8c42f..174b278d89 100644 --- a/osu.Game/Rulesets/Edit/DrawableEditorRulesetWrapper.cs +++ b/osu.Game/Rulesets/Edit/DrawableEditorRulesetWrapper.cs @@ -19,16 +19,16 @@ namespace osu.Game.Rulesets.Edit internal partial class DrawableEditorRulesetWrapper : CompositeDrawable where TObject : HitObject { - public Playfield Playfield => DrawableRuleset.Playfield; + public Playfield Playfield => drawableRuleset.Playfield; - public readonly DrawableRuleset DrawableRuleset; + private readonly DrawableRuleset drawableRuleset; [Resolved] private EditorBeatmap beatmap { get; set; } = null!; public DrawableEditorRulesetWrapper(DrawableRuleset drawableRuleset) { - DrawableRuleset = drawableRuleset; + this.drawableRuleset = drawableRuleset; RelativeSizeAxes = Axes.Both; @@ -38,7 +38,7 @@ namespace osu.Game.Rulesets.Edit [BackgroundDependencyLoader] private void load() { - DrawableRuleset.FrameStablePlayback = false; + drawableRuleset.FrameStablePlayback = false; Playfield.DisplayJudgements.Value = false; } @@ -67,27 +67,27 @@ namespace osu.Game.Rulesets.Edit private void regenerateAutoplay() { - var autoplayMod = DrawableRuleset.Mods.OfType().Single(); - DrawableRuleset.SetReplayScore(autoplayMod.CreateScoreFromReplayData(DrawableRuleset.Beatmap, DrawableRuleset.Mods)); + var autoplayMod = drawableRuleset.Mods.OfType().Single(); + drawableRuleset.SetReplayScore(autoplayMod.CreateScoreFromReplayData(drawableRuleset.Beatmap, drawableRuleset.Mods)); } private void addHitObject(HitObject hitObject) { - DrawableRuleset.AddHitObject((TObject)hitObject); - DrawableRuleset.Playfield.PostProcess(); + drawableRuleset.AddHitObject((TObject)hitObject); + drawableRuleset.Playfield.PostProcess(); } private void removeHitObject(HitObject hitObject) { - DrawableRuleset.RemoveHitObject((TObject)hitObject); - DrawableRuleset.Playfield.PostProcess(); + drawableRuleset.RemoveHitObject((TObject)hitObject); + drawableRuleset.Playfield.PostProcess(); } public override bool PropagatePositionalInputSubTree => false; public override bool PropagateNonPositionalInputSubTree => false; - public PlayfieldAdjustmentContainer CreatePlayfieldAdjustmentContainer() => DrawableRuleset.CreatePlayfieldAdjustmentContainer(); + public PlayfieldAdjustmentContainer CreatePlayfieldAdjustmentContainer() => drawableRuleset.CreatePlayfieldAdjustmentContainer(); protected override void Dispose(bool isDisposing) { diff --git a/osu.Game/Rulesets/Edit/HitObjectComposer.cs b/osu.Game/Rulesets/Edit/HitObjectComposer.cs index 8882d55b42..15b60114af 100644 --- a/osu.Game/Rulesets/Edit/HitObjectComposer.cs +++ b/osu.Game/Rulesets/Edit/HitObjectComposer.cs @@ -133,7 +133,6 @@ namespace osu.Game.Rulesets.Edit if (DrawableRuleset is IDrawableScrollingRuleset scrollingRuleset) dependencies.CacheAs(scrollingRuleset.ScrollingInfo); - dependencies.CacheAs(drawableRulesetWrapper.DrawableRuleset); dependencies.CacheAs(Playfield); InternalChildren = new[] From bb7daae08063fb06e16934b7542a14b65a1f189d Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Sat, 25 Jan 2025 19:08:01 -0500 Subject: [PATCH 0767/1275] Simplify orientation locking code magnificently --- osu.Game/Mobile/OrientationManager.cs | 30 ++++++++++----------------- 1 file changed, 11 insertions(+), 19 deletions(-) diff --git a/osu.Game/Mobile/OrientationManager.cs b/osu.Game/Mobile/OrientationManager.cs index 0f9b56d434..964b40e2af 100644 --- a/osu.Game/Mobile/OrientationManager.cs +++ b/osu.Game/Mobile/OrientationManager.cs @@ -50,30 +50,22 @@ namespace osu.Game.Mobile bool lockCurrentOrientation = localUserPlaying.Value == LocalUserPlayingState.Playing; bool lockToPortraitOnPhone = requiresPortraitOrientation.Value; - if (lockCurrentOrientation) + if (IsTablet) { - if (!IsTablet && lockToPortraitOnPhone && !IsCurrentOrientationPortrait) - SetAllowedOrientations(GameOrientation.Portrait); - else if (!IsTablet && !lockToPortraitOnPhone && IsCurrentOrientationPortrait) - SetAllowedOrientations(GameOrientation.Landscape); - else - { - // if the orientation is already portrait/landscape according to the game's specifications, - // then use Locked instead of Portrait/Landscape to handle the case where the device is - // in landscape-left or reverse-portrait. + if (lockCurrentOrientation) SetAllowedOrientations(GameOrientation.Locked); - } - - return; + else + SetAllowedOrientations(null); } - - if (!IsTablet && lockToPortraitOnPhone) + else { - SetAllowedOrientations(GameOrientation.Portrait); - return; + if (lockToPortraitOnPhone) + SetAllowedOrientations(GameOrientation.Portrait); + else if (lockCurrentOrientation) + SetAllowedOrientations(GameOrientation.Locked); + else + SetAllowedOrientations(null); } - - SetAllowedOrientations(null); } /// From c18128e97419ea1f7c9a4086f1b19de8f9c6022e Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Sat, 25 Jan 2025 20:01:12 -0500 Subject: [PATCH 0768/1275] Remove `OrientationManager` and the entire mobile namespace --- osu.Android/AndroidOrientationManager.cs | 39 ------------ osu.Android/OsuGameAndroid.cs | 32 +++++++++- osu.Game/Mobile/GameOrientation.cs | 34 ----------- osu.Game/Mobile/OrientationManager.cs | 77 ------------------------ osu.Game/OsuGame.cs | 63 ++++++++----------- osu.Game/Utils/MobileUtils.cs | 49 +++++++++++++++ osu.iOS/IOSOrientationManager.cs | 41 ------------- osu.iOS/OsuGameIOS.cs | 33 +++++++++- 8 files changed, 138 insertions(+), 230 deletions(-) delete mode 100644 osu.Android/AndroidOrientationManager.cs delete mode 100644 osu.Game/Mobile/GameOrientation.cs delete mode 100644 osu.Game/Mobile/OrientationManager.cs create mode 100644 osu.Game/Utils/MobileUtils.cs delete mode 100644 osu.iOS/IOSOrientationManager.cs diff --git a/osu.Android/AndroidOrientationManager.cs b/osu.Android/AndroidOrientationManager.cs deleted file mode 100644 index 76d2fc24cb..0000000000 --- a/osu.Android/AndroidOrientationManager.cs +++ /dev/null @@ -1,39 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -using Android.Content.PM; -using Android.Content.Res; -using osu.Framework.Allocation; -using osu.Game.Mobile; - -namespace osu.Android -{ - public partial class AndroidOrientationManager : OrientationManager - { - [Resolved] - private OsuGameActivity gameActivity { get; set; } = null!; - - protected override bool IsCurrentOrientationPortrait => gameActivity.Resources!.Configuration!.Orientation == Orientation.Portrait; - protected override bool IsTablet => gameActivity.IsTablet; - - protected override void SetAllowedOrientations(GameOrientation? orientation) - => gameActivity.RequestedOrientation = orientation == null ? gameActivity.DefaultOrientation : toScreenOrientation(orientation.Value); - - private static ScreenOrientation toScreenOrientation(GameOrientation orientation) - { - if (orientation == GameOrientation.Locked) - return ScreenOrientation.Locked; - - if (orientation == GameOrientation.Portrait) - return ScreenOrientation.Portrait; - - if (orientation == GameOrientation.Landscape) - return ScreenOrientation.Landscape; - - if (orientation == GameOrientation.FullPortrait) - return ScreenOrientation.SensorPortrait; - - return ScreenOrientation.SensorLandscape; - } - } -} diff --git a/osu.Android/OsuGameAndroid.cs b/osu.Android/OsuGameAndroid.cs index 4143c8cae6..0f2451f0a0 100644 --- a/osu.Android/OsuGameAndroid.cs +++ b/osu.Android/OsuGameAndroid.cs @@ -3,11 +3,13 @@ using System; using Android.App; +using Android.Content.PM; using Microsoft.Maui.Devices; using osu.Framework.Allocation; using osu.Framework.Extensions.ObjectExtensions; using osu.Framework.Platform; using osu.Game; +using osu.Game.Screens; using osu.Game.Updater; using osu.Game.Utils; @@ -71,7 +73,35 @@ namespace osu.Android protected override void LoadComplete() { base.LoadComplete(); - LoadComponentAsync(new AndroidOrientationManager(), Add); + UserPlayingState.BindValueChanged(_ => updateOrientation()); + } + + protected override void ScreenChanged(IOsuScreen? current, IOsuScreen? newScreen) + { + base.ScreenChanged(current, newScreen); + + if (newScreen != null) + updateOrientation(); + } + + private void updateOrientation() + { + var orientation = MobileUtils.GetOrientation(this, (IOsuScreen)ScreenStack.CurrentScreen, gameActivity.IsTablet); + + switch (orientation) + { + case MobileUtils.Orientation.Locked: + gameActivity.RequestedOrientation = ScreenOrientation.Locked; + break; + + case MobileUtils.Orientation.Portrait: + gameActivity.RequestedOrientation = ScreenOrientation.Portrait; + break; + + case MobileUtils.Orientation.Default: + gameActivity.RequestedOrientation = gameActivity.DefaultOrientation; + break; + } } public override void SetHost(GameHost host) diff --git a/osu.Game/Mobile/GameOrientation.cs b/osu.Game/Mobile/GameOrientation.cs deleted file mode 100644 index 0022c8fefb..0000000000 --- a/osu.Game/Mobile/GameOrientation.cs +++ /dev/null @@ -1,34 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -namespace osu.Game.Mobile -{ - public enum GameOrientation - { - /// - /// Lock the game orientation. - /// - Locked, - - /// - /// Display the game in regular portrait orientation. - /// - Portrait, - - /// - /// Display the game in landscape-right orientation. - /// - Landscape, - - /// - /// Display the game in landscape-right/landscape-left orientations. - /// - FullLandscape, - - /// - /// Display the game in portrait/portrait-upside-down orientations. - /// This is exclusive to tablet mobile devices. - /// - FullPortrait, - } -} diff --git a/osu.Game/Mobile/OrientationManager.cs b/osu.Game/Mobile/OrientationManager.cs deleted file mode 100644 index 964b40e2af..0000000000 --- a/osu.Game/Mobile/OrientationManager.cs +++ /dev/null @@ -1,77 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -using osu.Framework.Allocation; -using osu.Framework.Bindables; -using osu.Framework.Graphics; -using osu.Game.Screens.Play; - -namespace osu.Game.Mobile -{ - /// - /// A that manages the device orientations a game can display in. - /// - public abstract partial class OrientationManager : Component - { - /// - /// Whether the current orientation of the game is portrait. - /// - protected abstract bool IsCurrentOrientationPortrait { get; } - - /// - /// Whether the mobile device is considered a tablet. - /// - protected abstract bool IsTablet { get; } - - [Resolved] - private OsuGame game { get; set; } = null!; - - [Resolved] - private ILocalUserPlayInfo localUserPlayInfo { get; set; } = null!; - - private IBindable requiresPortraitOrientation = null!; - private IBindable localUserPlaying = null!; - - protected override void LoadComplete() - { - base.LoadComplete(); - - requiresPortraitOrientation = game.RequiresPortraitOrientation.GetBoundCopy(); - requiresPortraitOrientation.BindValueChanged(_ => updateOrientations()); - - localUserPlaying = localUserPlayInfo.PlayingState.GetBoundCopy(); - localUserPlaying.BindValueChanged(_ => updateOrientations()); - - updateOrientations(); - } - - private void updateOrientations() - { - bool lockCurrentOrientation = localUserPlaying.Value == LocalUserPlayingState.Playing; - bool lockToPortraitOnPhone = requiresPortraitOrientation.Value; - - if (IsTablet) - { - if (lockCurrentOrientation) - SetAllowedOrientations(GameOrientation.Locked); - else - SetAllowedOrientations(null); - } - else - { - if (lockToPortraitOnPhone) - SetAllowedOrientations(GameOrientation.Portrait); - else if (lockCurrentOrientation) - SetAllowedOrientations(GameOrientation.Locked); - else - SetAllowedOrientations(null); - } - } - - /// - /// Sets the allowed orientations the device can rotate to. - /// - /// The allowed orientations, or null to return back to default. - protected abstract void SetAllowedOrientations(GameOrientation? orientation); - } -} diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs index cc6613da89..89aba818a3 100644 --- a/osu.Game/OsuGame.cs +++ b/osu.Game/OsuGame.cs @@ -173,25 +173,14 @@ namespace osu.Game /// public readonly IBindable OverlayActivationMode = new Bindable(); - /// - /// On mobile phones, this specifies whether the device should be set and locked to portrait orientation. - /// Tablet devices are unaffected by this property. - /// - /// - /// Implementations can be viewed in mobile projects. - /// - public IBindable RequiresPortraitOrientation => requiresPortraitOrientation; - - private readonly Bindable requiresPortraitOrientation = new BindableBool(); - /// /// Whether the back button is currently displayed. /// private readonly IBindable backButtonVisibility = new Bindable(); - IBindable ILocalUserPlayInfo.PlayingState => playingState; + IBindable ILocalUserPlayInfo.PlayingState => UserPlayingState; - private readonly Bindable playingState = new Bindable(); + protected readonly Bindable UserPlayingState = new Bindable(); protected OsuScreenStack ScreenStack; @@ -319,7 +308,7 @@ namespace osu.Game protected override UserInputManager CreateUserInputManager() { var userInputManager = base.CreateUserInputManager(); - (userInputManager as OsuUserInputManager)?.PlayingState.BindTo(playingState); + (userInputManager as OsuUserInputManager)?.PlayingState.BindTo(UserPlayingState); return userInputManager; } @@ -414,7 +403,7 @@ namespace osu.Game // Transfer any runtime changes back to configuration file. SkinManager.CurrentSkinInfo.ValueChanged += skin => configSkin.Value = skin.NewValue.ID.ToString(); - playingState.BindValueChanged(p => + UserPlayingState.BindValueChanged(p => { BeatmapManager.PauseImports = p.NewValue != LocalUserPlayingState.NotPlaying; SkinManager.PauseImports = p.NewValue != LocalUserPlayingState.NotPlaying; @@ -1555,7 +1544,7 @@ namespace osu.Game GlobalCursorDisplay.ShowCursor = (ScreenStack.CurrentScreen as IOsuScreen)?.CursorVisible ?? false; } - private void screenChanged(IScreen current, IScreen newScreen) + protected virtual void ScreenChanged([CanBeNull] IOsuScreen current, [CanBeNull] IOsuScreen newScreen) { SentrySdk.ConfigureScope(scope => { @@ -1571,10 +1560,10 @@ namespace osu.Game switch (current) { case Player player: - player.PlayingState.UnbindFrom(playingState); + player.PlayingState.UnbindFrom(UserPlayingState); // reset for sanity. - playingState.Value = LocalUserPlayingState.NotPlaying; + UserPlayingState.Value = LocalUserPlayingState.NotPlaying; break; } @@ -1591,7 +1580,7 @@ namespace osu.Game break; case Player player: - player.PlayingState.BindTo(playingState); + player.PlayingState.BindTo(UserPlayingState); break; default: @@ -1599,32 +1588,32 @@ namespace osu.Game break; } - if (current is IOsuScreen currentOsuScreen) + if (current != null) { - backButtonVisibility.UnbindFrom(currentOsuScreen.BackButtonVisibility); - OverlayActivationMode.UnbindFrom(currentOsuScreen.OverlayActivationMode); - configUserActivity.UnbindFrom(currentOsuScreen.Activity); + backButtonVisibility.UnbindFrom(current.BackButtonVisibility); + OverlayActivationMode.UnbindFrom(current.OverlayActivationMode); + configUserActivity.UnbindFrom(current.Activity); } - if (newScreen is IOsuScreen newOsuScreen) + // Bind to new screen. + if (newScreen != null) { - backButtonVisibility.BindTo(newOsuScreen.BackButtonVisibility); - OverlayActivationMode.BindTo(newOsuScreen.OverlayActivationMode); - configUserActivity.BindTo(newOsuScreen.Activity); + backButtonVisibility.BindTo(newScreen.BackButtonVisibility); + OverlayActivationMode.BindTo(newScreen.OverlayActivationMode); + configUserActivity.BindTo(newScreen.Activity); - GlobalCursorDisplay.MenuCursor.HideCursorOnNonMouseInput = newOsuScreen.HideMenuCursorOnNonMouseInput; + // Handle various configuration updates based on new screen settings. + GlobalCursorDisplay.MenuCursor.HideCursorOnNonMouseInput = newScreen.HideMenuCursorOnNonMouseInput; - requiresPortraitOrientation.Value = newOsuScreen.RequiresPortraitOrientation; - - if (newOsuScreen.HideOverlaysOnEnter) + if (newScreen.HideOverlaysOnEnter) CloseAllOverlays(); else Toolbar.Show(); - if (newOsuScreen.ShowFooter) + if (newScreen.ShowFooter) { BackButton.Hide(); - ScreenFooter.SetButtons(newOsuScreen.CreateFooterButtons()); + ScreenFooter.SetButtons(newScreen.CreateFooterButtons()); ScreenFooter.Show(); } else @@ -1632,16 +1621,16 @@ namespace osu.Game ScreenFooter.SetButtons(Array.Empty()); ScreenFooter.Hide(); } - } - skinEditor.SetTarget((OsuScreen)newScreen); + skinEditor.SetTarget((OsuScreen)newScreen); + } } - private void screenPushed(IScreen lastScreen, IScreen newScreen) => screenChanged(lastScreen, newScreen); + private void screenPushed(IScreen lastScreen, IScreen newScreen) => ScreenChanged((OsuScreen)lastScreen, (OsuScreen)newScreen); private void screenExited(IScreen lastScreen, IScreen newScreen) { - screenChanged(lastScreen, newScreen); + ScreenChanged((OsuScreen)lastScreen, (OsuScreen)newScreen); if (newScreen == null) Exit(); diff --git a/osu.Game/Utils/MobileUtils.cs b/osu.Game/Utils/MobileUtils.cs new file mode 100644 index 0000000000..6e59efb71c --- /dev/null +++ b/osu.Game/Utils/MobileUtils.cs @@ -0,0 +1,49 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Game.Screens; +using osu.Game.Screens.Play; + +namespace osu.Game.Utils +{ + public static class MobileUtils + { + /// + /// Determines the correct state which a mobile device should be put into for the given information. + /// + /// Information about whether the user is currently playing. + /// The current screen which the user is at. + /// Whether the user is playing on a mobile tablet device instead of a phone. + public static Orientation GetOrientation(ILocalUserPlayInfo userPlayInfo, IOsuScreen currentScreen, bool isTablet) + { + bool lockCurrentOrientation = userPlayInfo.PlayingState.Value == LocalUserPlayingState.Playing; + bool lockToPortraitOnPhone = currentScreen.RequiresPortraitOrientation; + + if (lockToPortraitOnPhone && !isTablet) + return Orientation.Portrait; + + if (lockCurrentOrientation) + return Orientation.Locked; + + return Orientation.Default; + } + + public enum Orientation + { + /// + /// Lock the game orientation. + /// + Locked, + + /// + /// Lock the game to portrait orientation (does not include upside-down portrait). + /// + Portrait, + + /// + /// Use the application's default settings. + /// + Default, + } + } +} diff --git a/osu.iOS/IOSOrientationManager.cs b/osu.iOS/IOSOrientationManager.cs deleted file mode 100644 index 6d5bb990c2..0000000000 --- a/osu.iOS/IOSOrientationManager.cs +++ /dev/null @@ -1,41 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -using osu.Game.Mobile; -using UIKit; - -namespace osu.iOS -{ - public partial class IOSOrientationManager : OrientationManager - { - private readonly AppDelegate appDelegate; - - protected override bool IsCurrentOrientationPortrait => appDelegate.CurrentOrientation.IsPortrait(); - protected override bool IsTablet => UIDevice.CurrentDevice.UserInterfaceIdiom == UIUserInterfaceIdiom.Pad; - - public IOSOrientationManager(AppDelegate appDelegate) - { - this.appDelegate = appDelegate; - } - - protected override void SetAllowedOrientations(GameOrientation? orientation) - => appDelegate.Orientations = orientation == null ? null : toUIInterfaceOrientationMask(orientation.Value); - - private UIInterfaceOrientationMask toUIInterfaceOrientationMask(GameOrientation orientation) - { - if (orientation == GameOrientation.Locked) - return (UIInterfaceOrientationMask)(1 << (int)appDelegate.CurrentOrientation); - - if (orientation == GameOrientation.Portrait) - return UIInterfaceOrientationMask.Portrait; - - if (orientation == GameOrientation.Landscape) - return UIInterfaceOrientationMask.LandscapeRight; - - if (orientation == GameOrientation.FullPortrait) - return UIInterfaceOrientationMask.Portrait | UIInterfaceOrientationMask.PortraitUpsideDown; - - return UIInterfaceOrientationMask.Landscape; - } - } -} diff --git a/osu.iOS/OsuGameIOS.cs b/osu.iOS/OsuGameIOS.cs index ed47a1e8b8..a5a42c1e66 100644 --- a/osu.iOS/OsuGameIOS.cs +++ b/osu.iOS/OsuGameIOS.cs @@ -8,8 +8,10 @@ using osu.Framework.Graphics; using osu.Framework.iOS; using osu.Framework.Platform; using osu.Game; +using osu.Game.Screens; using osu.Game.Updater; using osu.Game.Utils; +using UIKit; namespace osu.iOS { @@ -28,7 +30,36 @@ namespace osu.iOS protected override void LoadComplete() { base.LoadComplete(); - LoadComponentAsync(new IOSOrientationManager(appDelegate), Add); + UserPlayingState.BindValueChanged(_ => updateOrientation()); + } + + protected override void ScreenChanged(IOsuScreen? current, IOsuScreen? newScreen) + { + base.ScreenChanged(current, newScreen); + + if (newScreen != null) + updateOrientation(); + } + + private void updateOrientation() + { + bool iPad = UIDevice.CurrentDevice.UserInterfaceIdiom == UIUserInterfaceIdiom.Pad; + var orientation = MobileUtils.GetOrientation(this, (IOsuScreen)ScreenStack.CurrentScreen, iPad); + + switch (orientation) + { + case MobileUtils.Orientation.Locked: + appDelegate.Orientations = (UIInterfaceOrientationMask)(1 << (int)appDelegate.CurrentOrientation); + break; + + case MobileUtils.Orientation.Portrait: + appDelegate.Orientations = UIInterfaceOrientationMask.Portrait; + break; + + case MobileUtils.Orientation.Default: + appDelegate.Orientations = null; + break; + } } protected override UpdateManager CreateUpdateManager() => new MobileUpdateNotifier(); From 4d7b0710275f2e41317d988f516322cc2c06c45f Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Sat, 25 Jan 2025 23:58:56 -0500 Subject: [PATCH 0769/1275] Specifiy second-factor authentication code text box with `Code` type --- osu.Game/Overlays/Login/SecondFactorAuthForm.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/osu.Game/Overlays/Login/SecondFactorAuthForm.cs b/osu.Game/Overlays/Login/SecondFactorAuthForm.cs index 3022233e9c..506cb70d09 100644 --- a/osu.Game/Overlays/Login/SecondFactorAuthForm.cs +++ b/osu.Game/Overlays/Login/SecondFactorAuthForm.cs @@ -5,6 +5,7 @@ using System.Threading.Tasks; using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; +using osu.Framework.Input; using osu.Framework.Input.Events; using osu.Framework.Logging; using osu.Game.Graphics; @@ -62,6 +63,7 @@ namespace osu.Game.Overlays.Login }, codeTextBox = new OsuTextBox { + InputProperties = new TextInputProperties(TextInputType.Code), PlaceholderText = "Enter code", RelativeSizeAxes = Axes.X, TabbableContentContainer = this, From a7aa553445738068eb8075043cb64187ed6b73dc Mon Sep 17 00:00:00 2001 From: tsunyoku Date: Sun, 26 Jan 2025 16:21:07 +0000 Subject: [PATCH 0770/1275] Fix incorrect `startTime` calculation --- .../Difficulty/Preprocessing/TaikoDifficultyHitObject.cs | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/TaikoDifficultyHitObject.cs b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/TaikoDifficultyHitObject.cs index 0c668797cd..486841b995 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/TaikoDifficultyHitObject.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/TaikoDifficultyHitObject.cs @@ -118,13 +118,11 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing noteObjects.Add(this); } - double startTime = hitObject.StartTime * clockRate; - // Retrieve the timing point at the note's start time - TimingControlPoint currentControlPoint = controlPointInfo.TimingPointAt(startTime); + TimingControlPoint currentControlPoint = controlPointInfo.TimingPointAt(hitObject.StartTime); // Calculate the slider velocity at the note's start time. - double currentSliderVelocity = calculateSliderVelocity(controlPointInfo, globalSliderVelocity, startTime, clockRate); + double currentSliderVelocity = calculateSliderVelocity(controlPointInfo, globalSliderVelocity, hitObject.StartTime, clockRate); CurrentSliderVelocity = currentSliderVelocity; EffectiveBPM = currentControlPoint.BPM * currentSliderVelocity; From 13c956c2482ee8ff81e83f283de9f17910ad189d Mon Sep 17 00:00:00 2001 From: tsunyoku Date: Sun, 26 Jan 2025 20:15:13 +0000 Subject: [PATCH 0771/1275] Account for floating point errors --- .../Difficulty/Preprocessing/TaikoDifficultyHitObject.cs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/TaikoDifficultyHitObject.cs b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/TaikoDifficultyHitObject.cs index 486841b995..f9ca2707ab 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/TaikoDifficultyHitObject.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/TaikoDifficultyHitObject.cs @@ -118,11 +118,14 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing noteObjects.Add(this); } + // Using `hitObject.StartTime` causes floating point error differences + double normalizedStartTime = StartTime * clockRate; + // Retrieve the timing point at the note's start time - TimingControlPoint currentControlPoint = controlPointInfo.TimingPointAt(hitObject.StartTime); + TimingControlPoint currentControlPoint = controlPointInfo.TimingPointAt(normalizedStartTime); // Calculate the slider velocity at the note's start time. - double currentSliderVelocity = calculateSliderVelocity(controlPointInfo, globalSliderVelocity, hitObject.StartTime, clockRate); + double currentSliderVelocity = calculateSliderVelocity(controlPointInfo, globalSliderVelocity, normalizedStartTime, clockRate); CurrentSliderVelocity = currentSliderVelocity; EffectiveBPM = currentControlPoint.BPM * currentSliderVelocity; From 836a9e5c2518dab2d130e6148c17568f02bcd819 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 27 Jan 2025 09:40:20 +0100 Subject: [PATCH 0772/1275] Remove explicit beatmap set from list of bundled beatmap sets --- osu.Game/Beatmaps/Drawables/BundledBeatmapDownloader.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/osu.Game/Beatmaps/Drawables/BundledBeatmapDownloader.cs b/osu.Game/Beatmaps/Drawables/BundledBeatmapDownloader.cs index 3aa34a5580..61aa9ef921 100644 --- a/osu.Game/Beatmaps/Drawables/BundledBeatmapDownloader.cs +++ b/osu.Game/Beatmaps/Drawables/BundledBeatmapDownloader.cs @@ -345,7 +345,6 @@ namespace osu.Game.Beatmaps.Drawables "1971951 James Landino - Shiba Paradise.osz", "1972518 Toromaru - Sleight of Hand.osz", "1982302 KINEMA106 - INVITE.osz", - "1983475 KNOWER - The Government Knows.osz", "2010165 Junk - Yellow Smile (bms edit).osz", "2022737 Andora - Euphoria (feat. WaMi).osz", "2025023 tephe - Genjitsu Escape.osz", From e24af4b341d36e13c1b897a43a5d7d2d13fd94c8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 27 Jan 2025 09:40:53 +0100 Subject: [PATCH 0773/1275] Add inline comments for sets that are not marked FA but should be --- osu.Game/Beatmaps/Drawables/BundledBeatmapDownloader.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Game/Beatmaps/Drawables/BundledBeatmapDownloader.cs b/osu.Game/Beatmaps/Drawables/BundledBeatmapDownloader.cs index 61aa9ef921..16e143f9dc 100644 --- a/osu.Game/Beatmaps/Drawables/BundledBeatmapDownloader.cs +++ b/osu.Game/Beatmaps/Drawables/BundledBeatmapDownloader.cs @@ -292,7 +292,7 @@ namespace osu.Game.Beatmaps.Drawables "1407228 II-L - VANGUARD-1.osz", "1422686 II-L - VANGUARD-2.osz", "1429217 Street - Phi.osz", - "1442235 2ToneDisco x Cosmicosmo - Shoelaces (feat. Puniden).osz", + "1442235 2ToneDisco x Cosmicosmo - Shoelaces (feat. Puniden).osz", // set is not marked as FA, but track is listed in https://osu.ppy.sh/beatmaps/artists/157 "1447478 Cres. - End Time.osz", "1449942 m108 - Crescent Sakura.osz", "1463778 MuryokuP - A tree without a branch.osz", @@ -336,8 +336,8 @@ namespace osu.Game.Beatmaps.Drawables "1854710 Blaster & Extra Terra - Spacecraft (Cut Ver.).osz", "1859322 Hino Isuka - Delightness Brightness.osz", "1884102 Maduk - Go (feat. Lachi) (Cut Ver.).osz", - "1884578 Neko Hacker - People People feat. Nanahira.osz", - "1897902 uma vs. Morimori Atsushi - Re: End of a Dream.osz", + "1884578 Neko Hacker - People People feat. Nanahira.osz", // set is not marked as FA, but track is listed in https://osu.ppy.sh/beatmaps/artists/266 + "1897902 uma vs. Morimori Atsushi - Re: End of a Dream.osz", // set is not marked as FA, but track is listed in https://osu.ppy.sh/beatmaps/artists/108 "1905582 KINEMA106 - Fly Away (Cut Ver.).osz", "1934686 ARForest - Rainbow Magic!!.osz", "1963076 METAROOM - S.N.U.F.F.Y.osz", From 01ae1a58f12d2268c3aa12cda499824cad0e184e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 27 Jan 2025 10:25:22 +0100 Subject: [PATCH 0774/1275] Catch and display user-friendly errors regarding corrupted audio files Addresses lack of user feedback as indicated by https://github.com/ppy/osu/issues/31693. --- osu.Game/Screens/Edit/Setup/ResourcesSection.cs | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/osu.Game/Screens/Edit/Setup/ResourcesSection.cs b/osu.Game/Screens/Edit/Setup/ResourcesSection.cs index 408292c2d0..2eda232b9f 100644 --- a/osu.Game/Screens/Edit/Setup/ResourcesSection.cs +++ b/osu.Game/Screens/Edit/Setup/ResourcesSection.cs @@ -8,6 +8,7 @@ using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Localisation; +using osu.Framework.Logging; using osu.Game.Beatmaps; using osu.Game.Overlays; using osu.Game.Localisation; @@ -97,7 +98,17 @@ namespace osu.Game.Screens.Edit.Setup if (!source.Exists) return false; - var tagSource = TagLib.File.Create(source.FullName); + TagLib.File? tagSource; + + try + { + tagSource = TagLib.File.Create(source.FullName); + } + catch (Exception e) + { + Logger.Error(e, "The selected audio track appears to be corrupted. Please select another one."); + return false; + } changeResource(source, applyToAllDifficulties, @"audio", metadata => metadata.AudioFile, From be9c96c041b4dc9179b00a1ec13e3eaf0f7b414f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 27 Jan 2025 10:25:53 +0100 Subject: [PATCH 0775/1275] Fix infinite loop when switching audio tracks fails on an existing beatmap Bit ugly, but appears to work in practice... --- .../Screens/Edit/Setup/ResourcesSection.cs | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/osu.Game/Screens/Edit/Setup/ResourcesSection.cs b/osu.Game/Screens/Edit/Setup/ResourcesSection.cs index 2eda232b9f..cab6eddaa4 100644 --- a/osu.Game/Screens/Edit/Setup/ResourcesSection.cs +++ b/osu.Game/Screens/Edit/Setup/ResourcesSection.cs @@ -203,16 +203,40 @@ namespace osu.Game.Screens.Edit.Setup editor?.Save(); } + // to avoid scaring users, both background & audio choosers use fake `FileInfo`s with user-friendly filenames + // when displaying an imported beatmap rather than the actual SHA-named file in storage. + // however, that means that when a background or audio file is chosen that is broken or doesn't exist on disk when switching away from the fake files, + // the rollback could enter an infinite loop, because the fake `FileInfo`s *also* don't exist on disk - at least not in the fake location they indicate. + // to circumvent this issue, just allow rollback to proceed always without actually running any of the change logic to ensure visual consistency. + // note that this means that `Change{BackgroundImage,AudioTrack}()` are required to not have made any modifications to the beatmap files + // (or at least cleaned them up properly themselves) if they return `false`. + private bool rollingBackBackgroundChange; + private bool rollingBackAudioChange; + private void backgroundChanged(ValueChangedEvent file) { + if (rollingBackBackgroundChange) + return; + if (file.NewValue == null || !ChangeBackgroundImage(file.NewValue, backgroundChooser.ApplyToAllDifficulties.Value)) + { + rollingBackBackgroundChange = true; backgroundChooser.Current.Value = file.OldValue; + rollingBackBackgroundChange = false; + } } private void audioTrackChanged(ValueChangedEvent file) { + if (rollingBackAudioChange) + return; + if (file.NewValue == null || !ChangeAudioTrack(file.NewValue, audioTrackChooser.ApplyToAllDifficulties.Value)) + { + rollingBackAudioChange = true; audioTrackChooser.Current.Value = file.OldValue; + rollingBackAudioChange = false; + } } } } From ca979d35423265017435e4cd44b3c3e5c3a92630 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Mon, 27 Jan 2025 18:32:12 +0900 Subject: [PATCH 0776/1275] Adjust xmldocs --- osu.Game/Online/Multiplayer/MultiplayerRoomUser.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Online/Multiplayer/MultiplayerRoomUser.cs b/osu.Game/Online/Multiplayer/MultiplayerRoomUser.cs index 8142873fd5..499e84ce80 100644 --- a/osu.Game/Online/Multiplayer/MultiplayerRoomUser.cs +++ b/osu.Game/Online/Multiplayer/MultiplayerRoomUser.cs @@ -38,13 +38,13 @@ namespace osu.Game.Online.Multiplayer public MatchUserState? MatchState { get; set; } /// - /// Any ruleset applicable only to the local user. + /// If not-null, a local override for this user's ruleset selection. /// [Key(5)] public int? RulesetId; /// - /// Any beatmap applicable only to the local user. + /// If not-null, a local override for this user's beatmap selection. /// [Key(6)] public int? BeatmapId; From fc73037d9f0373f8914e389efc1202900580195f Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Mon, 27 Jan 2025 18:45:52 +0900 Subject: [PATCH 0777/1275] Add pill displaying current freestyle status --- .../Lounge/Components/DrawableRoom.cs | 5 ++ .../Lounge/Components/FreeStyleStatusPill.cs | 64 +++++++++++++++++++ 2 files changed, 69 insertions(+) create mode 100644 osu.Game/Screens/OnlinePlay/Lounge/Components/FreeStyleStatusPill.cs diff --git a/osu.Game/Screens/OnlinePlay/Lounge/Components/DrawableRoom.cs b/osu.Game/Screens/OnlinePlay/Lounge/Components/DrawableRoom.cs index c39ca347c7..7bc0b612f1 100644 --- a/osu.Game/Screens/OnlinePlay/Lounge/Components/DrawableRoom.cs +++ b/osu.Game/Screens/OnlinePlay/Lounge/Components/DrawableRoom.cs @@ -169,6 +169,11 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components Anchor = Anchor.CentreLeft, Origin = Anchor.CentreLeft }, + new FreeStyleStatusPill(Room) + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft + }, endDateInfo = new EndDateInfo(Room) { Anchor = Anchor.CentreLeft, diff --git a/osu.Game/Screens/OnlinePlay/Lounge/Components/FreeStyleStatusPill.cs b/osu.Game/Screens/OnlinePlay/Lounge/Components/FreeStyleStatusPill.cs new file mode 100644 index 0000000000..1f3149d788 --- /dev/null +++ b/osu.Game/Screens/OnlinePlay/Lounge/Components/FreeStyleStatusPill.cs @@ -0,0 +1,64 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.ComponentModel; +using osu.Framework.Allocation; +using osu.Framework.Graphics.Sprites; +using osu.Game.Graphics; +using osu.Game.Online.Rooms; +using osuTK.Graphics; + +namespace osu.Game.Screens.OnlinePlay.Lounge.Components +{ + public class FreeStyleStatusPill : OnlinePlayPill + { + private readonly Room room; + + [Resolved] + private OsuColour colours { get; set; } = null!; + + protected override FontUsage Font => base.Font.With(weight: FontWeight.SemiBold); + + public FreeStyleStatusPill(Room room) + { + this.room = room; + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + Pill.Background.Alpha = 1; + Pill.Background.Colour = colours.Yellow; + + TextFlow.Text = "Freestyle"; + TextFlow.Colour = Color4.Black; + + room.PropertyChanged += onRoomPropertyChanged; + updateFreeStyleStatus(); + } + + private void onRoomPropertyChanged(object? sender, PropertyChangedEventArgs e) + { + switch (e.PropertyName) + { + case nameof(Room.CurrentPlaylistItem): + case nameof(Room.Playlist): + updateFreeStyleStatus(); + break; + } + } + + private void updateFreeStyleStatus() + { + PlaylistItem? currentItem = room.Playlist.GetCurrentItem() ?? room.CurrentPlaylistItem; + Alpha = currentItem?.FreeStyle == true ? 1 : 0; + } + + protected override void Dispose(bool isDisposing) + { + base.Dispose(isDisposing); + room.PropertyChanged -= onRoomPropertyChanged; + } + } +} From bb8f58f6d6db344499f50e64f1463cc8ca84e35c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 27 Jan 2025 12:28:53 +0100 Subject: [PATCH 0778/1275] Work around rare sharpcompress failure to extract certain archives Closes https://github.com/ppy/osu/issues/31667. See https://github.com/ppy/osu/issues/31667#issuecomment-2615483900 for explanation. For whatever it's worth, I see rejecting this change and telling upstream to fix it as an equally agreeable outcome, but after I spent an hour+ tracking this down, writing this diff was nothing in comparison. --- osu.Game/IO/Archives/ZipArchiveReader.cs | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/osu.Game/IO/Archives/ZipArchiveReader.cs b/osu.Game/IO/Archives/ZipArchiveReader.cs index 6bb2a314e7..8b9ecc7462 100644 --- a/osu.Game/IO/Archives/ZipArchiveReader.cs +++ b/osu.Game/IO/Archives/ZipArchiveReader.cs @@ -9,6 +9,7 @@ using System.IO; using System.Linq; using System.Text; using Microsoft.Toolkit.HighPerformance; +using osu.Framework.Extensions; using osu.Framework.IO.Stores; using SharpCompress.Archives.Zip; using SharpCompress.Common; @@ -54,12 +55,22 @@ namespace osu.Game.IO.Archives if (entry == null) return null; - var owner = MemoryAllocator.Default.Allocate((int)entry.Size); - using (Stream s = entry.OpenEntryStream()) - s.ReadExactly(owner.Memory.Span); + { + if (entry.Size > 0) + { + var owner = MemoryAllocator.Default.Allocate((int)entry.Size); + s.ReadExactly(owner.Memory.Span); + return new MemoryOwnerMemoryStream(owner); + } - return new MemoryOwnerMemoryStream(owner); + // due to a sharpcompress bug (https://github.com/adamhathcock/sharpcompress/issues/88), + // in rare instances the `ZipArchiveEntry` will not contain a correct `Size` but instead report 0. + // this would lead to the block above reading nothing, and the game basically seeing an archive full of empty files. + // since the bug is years old now, and this is a rather rare situation anyways (reported once in years), + // work around this locally by falling back to reading as many bytes as possible and using a standard non-pooled memory stream. + return new MemoryStream(s.ReadAllRemainingBytesToArray()); + } } public override void Dispose() From 71b89c390fe7d672ec8f1f61bbea31352315a4fb Mon Sep 17 00:00:00 2001 From: tsunyoku Date: Mon, 27 Jan 2025 12:54:22 +0000 Subject: [PATCH 0779/1275] Rename class, rename children to hit objects and groups, make fields un-settable --- .../Difficulty/Evaluators/RhythmEvaluator.cs | 12 ++++---- .../Data/SamePatternsGroupedHitObjects.cs | 22 +++++++------- ...ects.cs => SameRhythmHitObjectGrouping.cs} | 30 +++++++++---------- .../Rhythm/TaikoDifficultyHitObjectRhythm.cs | 2 +- .../TaikoRhythmDifficultyPreprocessor.cs | 10 +++---- 5 files changed, 38 insertions(+), 38 deletions(-) rename osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/Data/{SameRhythmGroupedHitObjects.cs => SameRhythmHitObjectGrouping.cs} (65%) diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Evaluators/RhythmEvaluator.cs b/osu.Game.Rulesets.Taiko/Difficulty/Evaluators/RhythmEvaluator.cs index 8accc6124c..f4686f2fe3 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Evaluators/RhythmEvaluator.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Evaluators/RhythmEvaluator.cs @@ -39,7 +39,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Evaluators return difficulty; } - private static double evaluateDifficultyOf(SameRhythmGroupedHitObjects sameRhythmGroupedHitObjects, double hitWindow) + private static double evaluateDifficultyOf(SameRhythmHitObjectGrouping sameRhythmGroupedHitObjects, double hitWindow) { double intervalDifficulty = ratioDifficulty(sameRhythmGroupedHitObjects.HitObjectIntervalRatio); double? previousInterval = sameRhythmGroupedHitObjects.Previous?.HitObjectInterval; @@ -47,9 +47,9 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Evaluators intervalDifficulty *= repeatedIntervalPenalty(sameRhythmGroupedHitObjects, hitWindow); // If a previous interval exists and there are multiple hit objects in the sequence: - if (previousInterval != null && sameRhythmGroupedHitObjects.Children.Count > 1) + if (previousInterval != null && sameRhythmGroupedHitObjects.HitObjects.Count > 1) { - double expectedDurationFromPrevious = (double)previousInterval * sameRhythmGroupedHitObjects.Children.Count; + double expectedDurationFromPrevious = (double)previousInterval * sameRhythmGroupedHitObjects.HitObjects.Count; double durationDifference = sameRhythmGroupedHitObjects.Duration - expectedDurationFromPrevious; if (durationDifference > 0) @@ -75,11 +75,11 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Evaluators /// /// Determines if the changes in hit object intervals is consistent based on a given threshold. /// - private static double repeatedIntervalPenalty(SameRhythmGroupedHitObjects sameRhythmGroupedHitObjects, double hitWindow, double threshold = 0.1) + private static double repeatedIntervalPenalty(SameRhythmHitObjectGrouping sameRhythmGroupedHitObjects, double hitWindow, double threshold = 0.1) { double longIntervalPenalty = sameInterval(sameRhythmGroupedHitObjects, 3); - double shortIntervalPenalty = sameRhythmGroupedHitObjects.Children.Count < 6 + double shortIntervalPenalty = sameRhythmGroupedHitObjects.HitObjects.Count < 6 ? sameInterval(sameRhythmGroupedHitObjects, 4) : 1.0; // Returns a non-penalty if there are 6 or more notes within an interval. @@ -88,7 +88,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Evaluators return Math.Min(longIntervalPenalty, shortIntervalPenalty) * durationPenalty; - double sameInterval(SameRhythmGroupedHitObjects startObject, int intervalCount) + double sameInterval(SameRhythmHitObjectGrouping startObject, int intervalCount) { List intervals = new List(); var currentObject = startObject; diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/Data/SamePatternsGroupedHitObjects.cs b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/Data/SamePatternsGroupedHitObjects.cs index cb22b2ef82..938cb4670f 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/Data/SamePatternsGroupedHitObjects.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/Data/SamePatternsGroupedHitObjects.cs @@ -7,34 +7,34 @@ using System.Linq; namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Rhythm.Data { /// - /// Represents grouped by their 's interval. + /// Represents grouped by their 's interval. /// public class SamePatternsGroupedHitObjects { - public IReadOnlyList Children { get; } + public IReadOnlyList Groups { get; } public SamePatternsGroupedHitObjects? Previous { get; } /// - /// The between children within this group. - /// If there is only one child, this will have the value of the first child's . + /// The between groups . + /// If there is only one group, this will have the value of the first group's . /// - public double ChildrenInterval => Children.Count > 1 ? Children[1].Interval : Children[0].Interval; + public double GroupInterval => Groups.Count > 1 ? Groups[1].Interval : Groups[0].Interval; /// - /// The ratio of between this and the previous . In the + /// The ratio of between this and the previous . In the /// case where there is no previous , this will have a value of 1. /// - public double IntervalRatio => ChildrenInterval / Previous?.ChildrenInterval ?? 1.0d; + public double IntervalRatio => GroupInterval / Previous?.GroupInterval ?? 1.0d; - public TaikoDifficultyHitObject FirstHitObject => Children[0].FirstHitObject; + public TaikoDifficultyHitObject FirstHitObject => Groups[0].FirstHitObject; - public IEnumerable AllHitObjects => Children.SelectMany(child => child.Children); + public IEnumerable AllHitObjects => Groups.SelectMany(hitObject => hitObject.HitObjects); - public SamePatternsGroupedHitObjects(SamePatternsGroupedHitObjects? previous, List children) + public SamePatternsGroupedHitObjects(SamePatternsGroupedHitObjects? previous, List groups) { Previous = previous; - Children = children; + Groups = groups; } } } diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/Data/SameRhythmGroupedHitObjects.cs b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/Data/SameRhythmHitObjectGrouping.cs similarity index 65% rename from osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/Data/SameRhythmGroupedHitObjects.cs rename to osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/Data/SameRhythmHitObjectGrouping.cs index b77176b49d..9caa9b9958 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/Data/SameRhythmGroupedHitObjects.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/Data/SameRhythmHitObjectGrouping.cs @@ -10,46 +10,46 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Rhythm.Data /// /// Represents a group of s with no rhythm variation. /// - public class SameRhythmGroupedHitObjects : IHasInterval + public class SameRhythmHitObjectGrouping : IHasInterval { - public List Children { get; private set; } + public readonly List HitObjects; - public TaikoDifficultyHitObject FirstHitObject => Children[0]; + public TaikoDifficultyHitObject FirstHitObject => HitObjects[0]; - public SameRhythmGroupedHitObjects? Previous; + public readonly SameRhythmHitObjectGrouping? Previous; /// /// of the first hit object. /// - public double StartTime => Children[0].StartTime; + public double StartTime => HitObjects[0].StartTime; /// /// The interval between the first and final hit object within this group. /// - public double Duration => Children[^1].StartTime - Children[0].StartTime; + public double Duration => HitObjects[^1].StartTime - HitObjects[0].StartTime; /// - /// The interval in ms of each hit object in this . This is only defined if there is - /// more than two hit objects in this . + /// The interval in ms of each hit object in this . This is only defined if there is + /// more than two hit objects in this . /// - public double? HitObjectInterval; + public readonly double? HitObjectInterval; /// - /// The ratio of between this and the previous . In the + /// The ratio of between this and the previous . In the /// case where one or both of the is undefined, this will have a value of 1. /// - public double HitObjectIntervalRatio; + public readonly double HitObjectIntervalRatio; /// - public double Interval { get; private set; } + public double Interval { get; } - public SameRhythmGroupedHitObjects(SameRhythmGroupedHitObjects? previous, List children) + public SameRhythmHitObjectGrouping(SameRhythmHitObjectGrouping? previous, List hitObjects) { Previous = previous; - Children = children; + HitObjects = hitObjects; // Calculate the average interval between hitobjects, or null if there are fewer than two - HitObjectInterval = Children.Count < 2 ? null : Duration / (Children.Count - 1); + HitObjectInterval = HitObjects.Count < 2 ? null : Duration / (HitObjects.Count - 1); // Calculate the ratio between this group's interval and the previous group's interval HitObjectIntervalRatio = Previous?.HitObjectInterval != null && HitObjectInterval != null diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/TaikoDifficultyHitObjectRhythm.cs b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/TaikoDifficultyHitObjectRhythm.cs index 351015ae08..3503a836fa 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/TaikoDifficultyHitObjectRhythm.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/TaikoDifficultyHitObjectRhythm.cs @@ -15,7 +15,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Rhythm /// /// The group of hit objects with consistent rhythm that this object belongs to. /// - public SameRhythmGroupedHitObjects? SameRhythmGroupedHitObjects; + public SameRhythmHitObjectGrouping? SameRhythmGroupedHitObjects; /// /// The larger pattern of rhythm groups that this object is part of. diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/TaikoRhythmDifficultyPreprocessor.cs b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/TaikoRhythmDifficultyPreprocessor.cs index cd56d835dc..3ebc0c25b7 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/TaikoRhythmDifficultyPreprocessor.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/TaikoRhythmDifficultyPreprocessor.cs @@ -15,7 +15,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Rhythm foreach (var rhythmGroup in rhythmGroups) { - foreach (var hitObject in rhythmGroup.Children) + foreach (var hitObject in rhythmGroup.HitObjects) { hitObject.Rhythm.SameRhythmGroupedHitObjects = rhythmGroup; hitObject.HitObjectInterval = rhythmGroup.HitObjectInterval; @@ -33,21 +33,21 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Rhythm } } - private static List createSameRhythmGroupedHitObjects(List hitObjects) + private static List createSameRhythmGroupedHitObjects(List hitObjects) { - var rhythmGroups = new List(); + var rhythmGroups = new List(); var groups = IntervalGroupingUtils.GroupByInterval(hitObjects); foreach (var group in groups) { var previous = rhythmGroups.Count > 0 ? rhythmGroups[^1] : null; - rhythmGroups.Add(new SameRhythmGroupedHitObjects(previous, group)); + rhythmGroups.Add(new SameRhythmHitObjectGrouping(previous, group)); } return rhythmGroups; } - private static List createSamePatternGroupedHitObjects(List rhythmGroups) + private static List createSamePatternGroupedHitObjects(List rhythmGroups) { var patternGroups = new List(); var groups = IntervalGroupingUtils.GroupByInterval(rhythmGroups); From f3c17f1c2b73e4f12fd00b130bd8326ca17a74e6 Mon Sep 17 00:00:00 2001 From: tsunyoku Date: Mon, 27 Jan 2025 12:56:33 +0000 Subject: [PATCH 0780/1275] Use correct English --- .../Difficulty/Preprocessing/TaikoDifficultyHitObject.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/TaikoDifficultyHitObject.cs b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/TaikoDifficultyHitObject.cs index f9ca2707ab..d6a2d5874e 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/TaikoDifficultyHitObject.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/TaikoDifficultyHitObject.cs @@ -119,13 +119,13 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing } // Using `hitObject.StartTime` causes floating point error differences - double normalizedStartTime = StartTime * clockRate; + double normalisedStartTime = StartTime * clockRate; // Retrieve the timing point at the note's start time - TimingControlPoint currentControlPoint = controlPointInfo.TimingPointAt(normalizedStartTime); + TimingControlPoint currentControlPoint = controlPointInfo.TimingPointAt(normalisedStartTime); // Calculate the slider velocity at the note's start time. - double currentSliderVelocity = calculateSliderVelocity(controlPointInfo, globalSliderVelocity, normalizedStartTime, clockRate); + double currentSliderVelocity = calculateSliderVelocity(controlPointInfo, globalSliderVelocity, normalisedStartTime, clockRate); CurrentSliderVelocity = currentSliderVelocity; EffectiveBPM = currentControlPoint.BPM * currentSliderVelocity; From 1aa1137b09cc649b1e99d2f0eb18b846feb249ae Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 28 Jan 2025 21:22:51 +0900 Subject: [PATCH 0781/1275] Remove "Accuracy" and "Stack Leniency" from osu!catch editor setup --- osu.Game.Rulesets.Catch/CatchRuleset.cs | 3 +- .../Edit/Setup/CatchDifficultySection.cs | 125 ++++++++++++++++++ 2 files changed, 127 insertions(+), 1 deletion(-) create mode 100644 osu.Game.Rulesets.Catch/Edit/Setup/CatchDifficultySection.cs diff --git a/osu.Game.Rulesets.Catch/CatchRuleset.cs b/osu.Game.Rulesets.Catch/CatchRuleset.cs index 5bd7a0ff00..d253b9893f 100644 --- a/osu.Game.Rulesets.Catch/CatchRuleset.cs +++ b/osu.Game.Rulesets.Catch/CatchRuleset.cs @@ -14,6 +14,7 @@ using osu.Game.Graphics; using osu.Game.Rulesets.Catch.Beatmaps; using osu.Game.Rulesets.Catch.Difficulty; using osu.Game.Rulesets.Catch.Edit; +using osu.Game.Rulesets.Catch.Edit.Setup; using osu.Game.Rulesets.Catch.Mods; using osu.Game.Rulesets.Catch.Objects; using osu.Game.Rulesets.Catch.Replays; @@ -228,7 +229,7 @@ namespace osu.Game.Rulesets.Catch public override IEnumerable CreateEditorSetupSections() => [ new MetadataSection(), - new DifficultySection(), + new CatchDifficultySection(), new FillFlowContainer { AutoSizeAxes = Axes.Y, diff --git a/osu.Game.Rulesets.Catch/Edit/Setup/CatchDifficultySection.cs b/osu.Game.Rulesets.Catch/Edit/Setup/CatchDifficultySection.cs new file mode 100644 index 0000000000..6ae60c4d24 --- /dev/null +++ b/osu.Game.Rulesets.Catch/Edit/Setup/CatchDifficultySection.cs @@ -0,0 +1,125 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Linq; +using osu.Framework.Allocation; +using osu.Framework.Bindables; +using osu.Framework.Graphics; +using osu.Framework.Localisation; +using osu.Game.Beatmaps; +using osu.Game.Graphics.UserInterfaceV2; +using osu.Game.Localisation; +using osu.Game.Resources.Localisation.Web; +using osu.Game.Screens.Edit.Setup; + +namespace osu.Game.Rulesets.Catch.Edit.Setup +{ + public partial class CatchDifficultySection : SetupSection + { + private FormSliderBar circleSizeSlider { get; set; } = null!; + private FormSliderBar healthDrainSlider { get; set; } = null!; + private FormSliderBar approachRateSlider { get; set; } = null!; + private FormSliderBar baseVelocitySlider { get; set; } = null!; + private FormSliderBar tickRateSlider { get; set; } = null!; + + public override LocalisableString Title => EditorSetupStrings.DifficultyHeader; + + [BackgroundDependencyLoader] + private void load() + { + Children = new Drawable[] + { + circleSizeSlider = new FormSliderBar + { + Caption = BeatmapsetsStrings.ShowStatsCs, + HintText = EditorSetupStrings.CircleSizeDescription, + Current = new BindableFloat(Beatmap.Difficulty.CircleSize) + { + Default = BeatmapDifficulty.DEFAULT_DIFFICULTY, + MinValue = 0, + MaxValue = 10, + Precision = 0.1f, + }, + TransferValueOnCommit = true, + TabbableContentContainer = this, + }, + healthDrainSlider = new FormSliderBar + { + Caption = BeatmapsetsStrings.ShowStatsDrain, + HintText = EditorSetupStrings.DrainRateDescription, + Current = new BindableFloat(Beatmap.Difficulty.DrainRate) + { + Default = BeatmapDifficulty.DEFAULT_DIFFICULTY, + MinValue = 0, + MaxValue = 10, + Precision = 0.1f, + }, + TransferValueOnCommit = true, + TabbableContentContainer = this, + }, + approachRateSlider = new FormSliderBar + { + Caption = BeatmapsetsStrings.ShowStatsAr, + HintText = EditorSetupStrings.ApproachRateDescription, + Current = new BindableFloat(Beatmap.Difficulty.ApproachRate) + { + Default = BeatmapDifficulty.DEFAULT_DIFFICULTY, + MinValue = 0, + MaxValue = 10, + Precision = 0.1f, + }, + TransferValueOnCommit = true, + TabbableContentContainer = this, + }, + baseVelocitySlider = new FormSliderBar + { + Caption = EditorSetupStrings.BaseVelocity, + HintText = EditorSetupStrings.BaseVelocityDescription, + Current = new BindableDouble(Beatmap.Difficulty.SliderMultiplier) + { + Default = 1.4, + MinValue = 0.4, + MaxValue = 3.6, + Precision = 0.01f, + }, + TransferValueOnCommit = true, + TabbableContentContainer = this, + }, + tickRateSlider = new FormSliderBar + { + Caption = EditorSetupStrings.TickRate, + HintText = EditorSetupStrings.TickRateDescription, + Current = new BindableDouble(Beatmap.Difficulty.SliderTickRate) + { + Default = 1, + MinValue = 1, + MaxValue = 4, + Precision = 1, + }, + TransferValueOnCommit = true, + TabbableContentContainer = this, + }, + }; + + foreach (var item in Children.OfType>()) + item.Current.ValueChanged += _ => updateValues(); + + foreach (var item in Children.OfType>()) + item.Current.ValueChanged += _ => updateValues(); + } + + private void updateValues() + { + // for now, update these on commit rather than making BeatmapMetadata bindables. + // after switching database engines we can reconsider if switching to bindables is a good direction. + Beatmap.Difficulty.CircleSize = circleSizeSlider.Current.Value; + Beatmap.Difficulty.DrainRate = healthDrainSlider.Current.Value; + Beatmap.Difficulty.ApproachRate = approachRateSlider.Current.Value; + Beatmap.Difficulty.SliderMultiplier = baseVelocitySlider.Current.Value; + Beatmap.Difficulty.SliderTickRate = tickRateSlider.Current.Value; + + Beatmap.UpdateAllHitObjects(); + Beatmap.SaveState(); + } + } +} From 017d38af3d0f8af13155cf049e27c5371fd6f3bf Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 28 Jan 2025 21:29:17 +0900 Subject: [PATCH 0782/1275] Change friend online notifications' icon and colours The previous choices made it seem like potentially destructive actions were being performed. I've gone with neutral colours and more suiting icons to attempt to avoid this. --- Addresses concerns in https://github.com/ppy/osu/discussions/31621#discussioncomment-11948377. I chose this design even though it wasn't the #1 most popular because I personally feel that using green/red doesn't work great for these. --- osu.Game/Online/FriendPresenceNotifier.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/osu.Game/Online/FriendPresenceNotifier.cs b/osu.Game/Online/FriendPresenceNotifier.cs index 75b487384a..bc2bf344b0 100644 --- a/osu.Game/Online/FriendPresenceNotifier.cs +++ b/osu.Game/Online/FriendPresenceNotifier.cs @@ -171,9 +171,9 @@ namespace osu.Game.Online { Transient = true, IsImportant = false, - Icon = FontAwesome.Solid.UserPlus, + Icon = FontAwesome.Solid.User, Text = $"Online: {string.Join(@", ", onlineAlertQueue.Select(u => u.Username))}", - IconColour = colours.Green, + IconColour = colours.GrayD, Activated = () => { if (singleUser != null) @@ -208,9 +208,9 @@ namespace osu.Game.Online { Transient = true, IsImportant = false, - Icon = FontAwesome.Solid.UserMinus, + Icon = FontAwesome.Solid.UserSlash, Text = $"Offline: {string.Join(@", ", offlineAlertQueue.Select(u => u.Username))}", - IconColour = colours.Red + IconColour = colours.Gray3 }); offlineAlertQueue.Clear(); From a3a08832b41fd9a46c50142eb0f05b0720a20f78 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 28 Jan 2025 21:31:51 +0900 Subject: [PATCH 0783/1275] Add keywords to make lighten-during-breaks setting discoverable to stable users See https://github.com/ppy/osu/discussions/31671. --- .../Overlays/Settings/Sections/Gameplay/BackgroundSettings.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/osu.Game/Overlays/Settings/Sections/Gameplay/BackgroundSettings.cs b/osu.Game/Overlays/Settings/Sections/Gameplay/BackgroundSettings.cs index 048351b4cb..830ccec279 100644 --- a/osu.Game/Overlays/Settings/Sections/Gameplay/BackgroundSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/Gameplay/BackgroundSettings.cs @@ -35,7 +35,8 @@ namespace osu.Game.Overlays.Settings.Sections.Gameplay new SettingsCheckbox { LabelText = GameplaySettingsStrings.LightenDuringBreaks, - Current = config.GetBindable(OsuSetting.LightenDuringBreaks) + Current = config.GetBindable(OsuSetting.LightenDuringBreaks), + Keywords = new[] { "dim", "level" } }, new SettingsCheckbox { From 6c4b4166ac21324abf6b467c87a166c032cb5933 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 29 Jan 2025 17:09:42 +0900 Subject: [PATCH 0784/1275] Add fail cases to unstable rate incremental testing --- osu.Game.Tests/NonVisual/Ranking/UnstableRateTest.cs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/osu.Game.Tests/NonVisual/Ranking/UnstableRateTest.cs b/osu.Game.Tests/NonVisual/Ranking/UnstableRateTest.cs index 03dc91b5d4..18ac5b4964 100644 --- a/osu.Game.Tests/NonVisual/Ranking/UnstableRateTest.cs +++ b/osu.Game.Tests/NonVisual/Ranking/UnstableRateTest.cs @@ -36,6 +36,10 @@ namespace osu.Game.Tests.NonVisual.Ranking .Select(t => new HitEvent(t - 5, 1.0, HitResult.Great, new HitObject(), null, null)) .ToList(); + // Add some red herrings + events.Insert(4, new HitEvent(200, 1.0, HitResult.Meh, new HitObject { HitWindows = HitWindows.Empty }, null, null)); + events.Insert(8, new HitEvent(-100, 1.0, HitResult.Miss, new HitObject(), null, null)); + HitEventExtensions.UnstableRateCalculationResult result = null; for (int i = 0; i < events.Count; i++) @@ -57,6 +61,10 @@ namespace osu.Game.Tests.NonVisual.Ranking .Select(t => new HitEvent(t - 5, 1.0, HitResult.Great, new HitObject(), null, null)) .ToList(); + // Add some red herrings + events.Insert(4, new HitEvent(200, 1.0, HitResult.Meh, new HitObject { HitWindows = HitWindows.Empty }, null, null)); + events.Insert(8, new HitEvent(-100, 1.0, HitResult.Miss, new HitObject(), null, null)); + HitEventExtensions.UnstableRateCalculationResult result = null; for (int i = 0; i < events.Count; i++) From d8ec3b77e4b29ff95f90873bf0ae83fb4041c460 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 29 Jan 2025 17:06:13 +0900 Subject: [PATCH 0785/1275] Fix incremental unstable rate calculation not matching expectations The `EventCount` variable wasn't factoring in that some results do not affect unstable rate. It would therefore become more incorrect as the play continued. Closes https://github.com/ppy/osu/issues/31712. --- osu.Game/Rulesets/Scoring/HitEventExtensions.cs | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/osu.Game/Rulesets/Scoring/HitEventExtensions.cs b/osu.Game/Rulesets/Scoring/HitEventExtensions.cs index 269342460f..fed0c3b51b 100644 --- a/osu.Game/Rulesets/Scoring/HitEventExtensions.cs +++ b/osu.Game/Rulesets/Scoring/HitEventExtensions.cs @@ -28,11 +28,12 @@ namespace osu.Game.Rulesets.Scoring result ??= new UnstableRateCalculationResult(); // Handle rewinding in the simplest way possible. - if (hitEvents.Count < result.EventCount + 1) + if (hitEvents.Count < result.LastProcessedIndex + 1) result = new UnstableRateCalculationResult(); - for (int i = result.EventCount; i < hitEvents.Count; i++) + for (int i = result.LastProcessedIndex + 1; i < hitEvents.Count; i++) { + result.LastProcessedIndex = i; HitEvent e = hitEvents[i]; if (!AffectsUnstableRate(e)) @@ -84,6 +85,11 @@ namespace osu.Game.Rulesets.Scoring /// public class UnstableRateCalculationResult { + /// + /// The last result index processed. For internal incremental calculation use. + /// + public int LastProcessedIndex = -1; + /// /// Total events processed. For internal incremental calculation use. /// From fd1d90cbd93ffc0cc9be5c3d18035e78613e0d06 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 29 Jan 2025 11:55:35 +0900 Subject: [PATCH 0786/1275] Update framework Update framework --- osu.Android.props | 2 +- osu.Game/osu.Game.csproj | 2 +- osu.iOS.props | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Android.props b/osu.Android.props index 7ae16b8b70..d2682fc024 100644 --- a/osu.Android.props +++ b/osu.Android.props @@ -10,7 +10,7 @@ true - + diff --git a/osu.iOS.props b/osu.iOS.props index 7b0a027d39..309a9dcc87 100644 --- a/osu.iOS.props +++ b/osu.iOS.props @@ -17,6 +17,6 @@ -all - + From 4c83ef83eeb9b372fdbc31a624a6688f0428dca2 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 29 Jan 2025 17:34:03 +0900 Subject: [PATCH 0787/1275] Update resources --- osu.Game/osu.Game.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj index bfb6e51f93..bc4c42484d 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -36,7 +36,7 @@ - + From bf40f071eb0d17fa54957ac9c3436afe12749506 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 29 Jan 2025 17:40:52 +0900 Subject: [PATCH 0788/1275] Code quality pass --- osu.Game/Online/FriendPresenceNotifier.cs | 89 +++++++++++-------- .../FriendOfflineNotification.cs | 10 --- .../Notifications/FriendOnlineNotification.cs | 10 --- 3 files changed, 52 insertions(+), 57 deletions(-) delete mode 100644 osu.Game/Overlays/Notifications/FriendOfflineNotification.cs delete mode 100644 osu.Game/Overlays/Notifications/FriendOnlineNotification.cs diff --git a/osu.Game/Online/FriendPresenceNotifier.cs b/osu.Game/Online/FriendPresenceNotifier.cs index 229ad4f734..70d532dfeb 100644 --- a/osu.Game/Online/FriendPresenceNotifier.cs +++ b/osu.Game/Online/FriendPresenceNotifier.cs @@ -31,15 +31,6 @@ namespace osu.Game.Online [Resolved] private MetadataClient metadataClient { get; set; } = null!; - [Resolved] - private ChannelManager channelManager { get; set; } = null!; - - [Resolved] - private ChatOverlay chatOverlay { get; set; } = null!; - - [Resolved] - private OsuColour colours { get; set; } = null!; - [Resolved] private OsuConfigManager config { get; set; } = null!; @@ -165,26 +156,7 @@ namespace osu.Game.Online return; } - APIUser? singleUser = onlineAlertQueue.Count == 1 ? onlineAlertQueue.Single() : null; - - notifications.Post(new FriendOnlineNotification - { - Transient = true, - IsImportant = false, - Icon = FontAwesome.Solid.UserPlus, - Text = $"Online: {string.Join(@", ", onlineAlertQueue.Select(u => u.Username))}", - IconColour = colours.Green, - Activated = () => - { - if (singleUser != null) - { - channelManager.OpenPrivateChannel(singleUser); - chatOverlay.Show(); - } - - return true; - } - }); + notifications.Post(new FriendOnlineNotification(onlineAlertQueue)); onlineAlertQueue.Clear(); lastOnlineAlertTime = null; @@ -204,17 +176,60 @@ namespace osu.Game.Online return; } - notifications.Post(new FriendOfflineNotification - { - Transient = true, - IsImportant = false, - Icon = FontAwesome.Solid.UserMinus, - Text = $"Offline: {string.Join(@", ", offlineAlertQueue.Select(u => u.Username))}", - IconColour = colours.Red - }); + notifications.Post(new FriendOfflineNotification(offlineAlertQueue)); offlineAlertQueue.Clear(); lastOfflineAlertTime = null; } + + public partial class FriendOnlineNotification : SimpleNotification + { + private readonly ICollection users; + + public FriendOnlineNotification(ICollection users) + { + this.users = users; + Transient = true; + IsImportant = false; + Icon = FontAwesome.Solid.User; + Text = $"Online: {string.Join(@", ", users.Select(u => u.Username))}"; + } + + [BackgroundDependencyLoader] + private void load(OsuColour colours, ChannelManager channelManager, ChatOverlay chatOverlay) + { + IconColour = colours.GrayD; + Activated = () => + { + APIUser? singleUser = users.Count == 1 ? users.Single() : null; + + if (singleUser != null) + { + channelManager.OpenPrivateChannel(singleUser); + chatOverlay.Show(); + } + + return true; + }; + } + + public override string PopInSampleName => "UI/notification-friend-online"; + } + + private partial class FriendOfflineNotification : SimpleNotification + { + public FriendOfflineNotification(ICollection users) + { + Transient = true; + IsImportant = false; + Icon = FontAwesome.Solid.UserSlash; + Text = $"Offline: {string.Join(@", ", users.Select(u => u.Username))}"; + } + + [BackgroundDependencyLoader] + private void load(OsuColour colours) => IconColour = colours.Gray3; + + public override string PopInSampleName => "UI/notification-friend-offline"; + } } } diff --git a/osu.Game/Overlays/Notifications/FriendOfflineNotification.cs b/osu.Game/Overlays/Notifications/FriendOfflineNotification.cs deleted file mode 100644 index 147fd4ba6f..0000000000 --- a/osu.Game/Overlays/Notifications/FriendOfflineNotification.cs +++ /dev/null @@ -1,10 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -namespace osu.Game.Overlays.Notifications -{ - public partial class FriendOfflineNotification : SimpleNotification - { - public override string PopInSampleName => "UI/notification-friend-offline"; - } -} diff --git a/osu.Game/Overlays/Notifications/FriendOnlineNotification.cs b/osu.Game/Overlays/Notifications/FriendOnlineNotification.cs deleted file mode 100644 index 6a5cf3b517..0000000000 --- a/osu.Game/Overlays/Notifications/FriendOnlineNotification.cs +++ /dev/null @@ -1,10 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -namespace osu.Game.Overlays.Notifications -{ - public partial class FriendOnlineNotification : SimpleNotification - { - public override string PopInSampleName => "UI/notification-friend-online"; - } -} From e8d20fb4020083d85a31a16492ae3c92d2b6382d Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 29 Jan 2025 18:16:04 +0900 Subject: [PATCH 0789/1275] Fix skin `SourceChanged` event never being unbound --- .../UI/Components/HitPositionPaddedContainer.cs | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/osu.Game.Rulesets.Mania/UI/Components/HitPositionPaddedContainer.cs b/osu.Game.Rulesets.Mania/UI/Components/HitPositionPaddedContainer.cs index ae91be1c67..72daf4b21d 100644 --- a/osu.Game.Rulesets.Mania/UI/Components/HitPositionPaddedContainer.cs +++ b/osu.Game.Rulesets.Mania/UI/Components/HitPositionPaddedContainer.cs @@ -3,6 +3,7 @@ using osu.Framework.Allocation; using osu.Framework.Bindables; +using osu.Framework.Extensions.ObjectExtensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Game.Rulesets.Mania.Skinning; @@ -22,15 +23,12 @@ namespace osu.Game.Rulesets.Mania.UI.Components private void load(IScrollingInfo scrollingInfo) { Direction.BindTo(scrollingInfo.Direction); - Direction.BindValueChanged(onDirectionChanged); + Direction.BindValueChanged(_ => UpdateHitPosition(), true); skin.SourceChanged += onSkinChanged; - - UpdateHitPosition(); } private void onSkinChanged() => UpdateHitPosition(); - private void onDirectionChanged(ValueChangedEvent direction) => UpdateHitPosition(); protected virtual void UpdateHitPosition() { @@ -42,5 +40,13 @@ namespace osu.Game.Rulesets.Mania.UI.Components ? new MarginPadding { Top = hitPosition } : new MarginPadding { Bottom = hitPosition }; } + + protected override void Dispose(bool isDisposing) + { + base.Dispose(isDisposing); + + if (skin.IsNotNull()) + skin.SourceChanged -= onSkinChanged; + } } } From d3f9804ef1de2ee9e9f75df9321183bb9439da8c Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 29 Jan 2025 18:45:02 +0900 Subject: [PATCH 0790/1275] Combine more methods to simplify flow --- .../Screens/OnlinePlay/Match/RoomSubScreen.cs | 33 ++++--------------- 1 file changed, 7 insertions(+), 26 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs b/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs index 9915560a95..3e0d94e992 100644 --- a/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs @@ -277,7 +277,7 @@ namespace osu.Game.Screens.OnlinePlay.Match UserRuleset.BindValueChanged(_ => Scheduler.AddOnce(OnSelectedItemChanged)); beatmapAvailabilityTracker.SelectedItem.BindTo(SelectedItem); - beatmapAvailabilityTracker.Availability.BindValueChanged(_ => updateBeatmap()); + beatmapAvailabilityTracker.Availability.BindValueChanged(_ => updateSpecifics()); userModsSelectOverlayRegistration = overlayManager?.RegisterBlockingOverlay(UserModsSelectOverlay); @@ -346,7 +346,7 @@ namespace osu.Game.Screens.OnlinePlay.Match public override void OnSuspending(ScreenTransitionEvent e) { // Should be a noop in most cases, but let's ensure beyond doubt that the beatmap is in a correct state. - updateBeatmap(); + updateSpecifics(); onLeaving(); base.OnSuspending(e); @@ -356,7 +356,6 @@ namespace osu.Game.Screens.OnlinePlay.Match { base.OnResuming(e); - updateBeatmap(); updateSpecifics(); beginHandlingTrack(); @@ -446,8 +445,6 @@ namespace osu.Game.Screens.OnlinePlay.Match if (!this.IsCurrentScreen() || SelectedItem.Value is not PlaylistItem item) return; - updateUserMods(); - updateBeatmap(); updateSpecifics(); if (!item.AllowedMods.Any()) @@ -471,42 +468,26 @@ namespace osu.Game.Screens.OnlinePlay.Match UserStyleSection?.Hide(); } - private void updateUserMods() + private void updateSpecifics() { if (!this.IsCurrentScreen() || SelectedItem.Value is not PlaylistItem item) return; + var rulesetInstance = GetGameplayRuleset().CreateInstance(); + // Remove any user mods that are no longer allowed. - Ruleset rulesetInstance = GetGameplayRuleset().CreateInstance(); Mod[] allowedMods = item.AllowedMods.Select(m => m.ToMod(rulesetInstance)).ToArray(); Mod[] newUserMods = UserMods.Value.Where(m => allowedMods.Any(a => m.GetType() == a.GetType())).ToArray(); - - if (newUserMods.SequenceEqual(UserMods.Value)) - return; - - UserMods.Value = UserMods.Value.Where(m => allowedMods.Any(a => m.GetType() == a.GetType())).ToList(); - } - - private void updateBeatmap() - { - if (!this.IsCurrentScreen() || SelectedItem.Value is not PlaylistItem) - return; + if (!newUserMods.SequenceEqual(UserMods.Value)) + UserMods.Value = UserMods.Value.Where(m => allowedMods.Any(a => m.GetType() == a.GetType())).ToList(); // Retrieve the corresponding local beatmap, since we can't directly use the playlist's beatmap info int beatmapId = GetGameplayBeatmap().OnlineID; var localBeatmap = beatmapManager.QueryBeatmap(b => b.OnlineID == beatmapId); Beatmap.Value = beatmapManager.GetWorkingBeatmap(localBeatmap); UserModsSelectOverlay.Beatmap.Value = Beatmap.Value; - } - private void updateSpecifics() - { - if (!this.IsCurrentScreen() || SelectedItem.Value is not PlaylistItem) - return; - - var rulesetInstance = GetGameplayRuleset().CreateInstance(); Mods.Value = GetGameplayMods().Select(m => m.ToMod(rulesetInstance)).ToArray(); - Ruleset.Value = GetGameplayRuleset(); if (UserStyleDisplayContainer != null) From 05200e897057c06dc7a4e9ad0cedfbccaf6c9738 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 29 Jan 2025 19:05:28 +0900 Subject: [PATCH 0791/1275] Add missing `partial` --- .../Screens/OnlinePlay/Lounge/Components/FreeStyleStatusPill.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/OnlinePlay/Lounge/Components/FreeStyleStatusPill.cs b/osu.Game/Screens/OnlinePlay/Lounge/Components/FreeStyleStatusPill.cs index 1f3149d788..1c0135fb89 100644 --- a/osu.Game/Screens/OnlinePlay/Lounge/Components/FreeStyleStatusPill.cs +++ b/osu.Game/Screens/OnlinePlay/Lounge/Components/FreeStyleStatusPill.cs @@ -10,7 +10,7 @@ using osuTK.Graphics; namespace osu.Game.Screens.OnlinePlay.Lounge.Components { - public class FreeStyleStatusPill : OnlinePlayPill + public partial class FreeStyleStatusPill : OnlinePlayPill { private readonly Room room; From c70ff1108527a58903067eaf39cfa5a7d778b486 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 29 Jan 2025 19:06:14 +0900 Subject: [PATCH 0792/1275] Remove new bindables from `RoomSubScreen` --- .../Screens/OnlinePlay/Match/RoomSubScreen.cs | 23 +++---------------- .../Multiplayer/MultiplayerMatchSubScreen.cs | 11 +-------- .../Playlists/PlaylistsRoomSubScreen.cs | 16 +++++++++---- 3 files changed, 16 insertions(+), 34 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs b/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs index 3e0d94e992..d9e22efec5 100644 --- a/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs @@ -69,18 +69,6 @@ namespace osu.Game.Screens.OnlinePlay.Match /// protected readonly Bindable> UserMods = new Bindable>(Array.Empty()); - /// - /// When players are freely allowed to select their own gameplay style (selected item has a non-null beatmapset id), - /// a non-null value indicates a local beatmap selection from the same beatmapset as the selected item. - /// - public readonly Bindable UserBeatmap = new Bindable(); - - /// - /// When players are freely allowed to select their own gameplay style (selected item has a non-null beatmapset id), - /// a non-null value indicates a local ruleset selection. - /// - public readonly Bindable UserRuleset = new Bindable(); - [Resolved(CanBeNull = true)] private IOverlayManager? overlayManager { get; set; } @@ -273,8 +261,6 @@ namespace osu.Game.Screens.OnlinePlay.Match SelectedItem.BindValueChanged(_ => Scheduler.AddOnce(OnSelectedItemChanged)); UserMods.BindValueChanged(_ => Scheduler.AddOnce(OnSelectedItemChanged)); - UserBeatmap.BindValueChanged(_ => Scheduler.AddOnce(OnSelectedItemChanged)); - UserRuleset.BindValueChanged(_ => Scheduler.AddOnce(OnSelectedItemChanged)); beatmapAvailabilityTracker.SelectedItem.BindTo(SelectedItem); beatmapAvailabilityTracker.Availability.BindValueChanged(_ => updateSpecifics()); @@ -507,14 +493,11 @@ namespace osu.Game.Screens.OnlinePlay.Match } } - protected virtual APIMod[] GetGameplayMods() - => UserMods.Value.Select(m => new APIMod(m)).Concat(SelectedItem.Value!.RequiredMods).ToArray(); + protected virtual APIMod[] GetGameplayMods() => UserMods.Value.Select(m => new APIMod(m)).Concat(SelectedItem.Value!.RequiredMods).ToArray(); - protected virtual RulesetInfo GetGameplayRuleset() - => Rulesets.GetRuleset(UserRuleset.Value?.OnlineID ?? SelectedItem.Value!.RulesetID)!; + protected virtual RulesetInfo GetGameplayRuleset() => Rulesets.GetRuleset(SelectedItem.Value!.RulesetID)!; - protected virtual IBeatmapInfo GetGameplayBeatmap() - => UserBeatmap.Value ?? SelectedItem.Value!.Beatmap; + protected virtual IBeatmapInfo GetGameplayBeatmap() => SelectedItem.Value!.Beatmap; protected abstract void OpenStyleSelection(); diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs index b5fe8bf631..7f946a6997 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs @@ -388,7 +388,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer return; } - updateCurrentItem(); + SelectedItem.Value = Room.Playlist.SingleOrDefault(i => i.ID == client.Room.Settings.PlaylistItemId); addItemButton.Alpha = localUserCanAddItem ? 1 : 0; @@ -400,15 +400,6 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer private bool localUserCanAddItem => client.IsHost || Room.QueueMode != QueueMode.HostOnly; - private void updateCurrentItem() - { - Debug.Assert(client.Room != null); - - SelectedItem.Value = Room.Playlist.SingleOrDefault(i => i.ID == client.Room.Settings.PlaylistItemId); - UserBeatmap.Value = client.LocalUser?.BeatmapId == null ? null : UserBeatmap.Value; - UserRuleset.Value = client.LocalUser?.RulesetId == null ? null : UserRuleset.Value; - } - private void handleRoomLost() => Schedule(() => { Logger.Log($"{this} exiting due to loss of room or connection"); diff --git a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs index d1b90b18e7..2c74767f42 100644 --- a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs @@ -11,11 +11,13 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Logging; using osu.Framework.Screens; +using osu.Game.Beatmaps; using osu.Game.Graphics.Cursor; using osu.Game.Input; using osu.Game.Online.API; using osu.Game.Online.API.Requests; using osu.Game.Online.Rooms; +using osu.Game.Rulesets; using osu.Game.Screens.OnlinePlay.Components; using osu.Game.Screens.OnlinePlay.Match; using osu.Game.Screens.OnlinePlay.Match.Components; @@ -46,6 +48,9 @@ namespace osu.Game.Screens.OnlinePlay.Playlists private FillFlowContainer progressSection = null!; private DrawableRoomPlaylist drawablePlaylist = null!; + private readonly Bindable userBeatmap = new Bindable(); + private readonly Bindable userRuleset = new Bindable(); + public PlaylistsRoomSubScreen(Room room) : base(room, false) // Editing is temporarily not allowed. { @@ -78,10 +83,13 @@ namespace osu.Game.Screens.OnlinePlay.Playlists private void onSelectedItemChanged(ValueChangedEvent item) { // Simplest for now. - UserBeatmap.Value = null; - UserRuleset.Value = null; + userBeatmap.Value = null; + userRuleset.Value = null; } + protected override IBeatmapInfo GetGameplayBeatmap() => userBeatmap.Value ?? base.GetGameplayBeatmap(); + protected override RulesetInfo GetGameplayRuleset() => userRuleset.Value ?? base.GetGameplayRuleset(); + private void onRoomPropertyChanged(object? sender, PropertyChangedEventArgs e) { switch (e.PropertyName) @@ -313,8 +321,8 @@ namespace osu.Game.Screens.OnlinePlay.Playlists this.Push(new PlaylistsRoomStyleSelect(Room, item) { - Beatmap = { BindTarget = UserBeatmap }, - Ruleset = { BindTarget = UserRuleset } + Beatmap = { BindTarget = userBeatmap }, + Ruleset = { BindTarget = userRuleset } }); } From facc9a4dc3d2e0d8b3741cddd0536e7775817d86 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 29 Jan 2025 19:15:28 +0900 Subject: [PATCH 0793/1275] Fix reference hashsets getting emptied before used --- osu.Game/Online/FriendPresenceNotifier.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Online/FriendPresenceNotifier.cs b/osu.Game/Online/FriendPresenceNotifier.cs index 70d532dfeb..a73c705d76 100644 --- a/osu.Game/Online/FriendPresenceNotifier.cs +++ b/osu.Game/Online/FriendPresenceNotifier.cs @@ -156,7 +156,7 @@ namespace osu.Game.Online return; } - notifications.Post(new FriendOnlineNotification(onlineAlertQueue)); + notifications.Post(new FriendOnlineNotification(onlineAlertQueue.ToArray())); onlineAlertQueue.Clear(); lastOnlineAlertTime = null; @@ -176,7 +176,7 @@ namespace osu.Game.Online return; } - notifications.Post(new FriendOfflineNotification(offlineAlertQueue)); + notifications.Post(new FriendOfflineNotification(offlineAlertQueue.ToArray())); offlineAlertQueue.Clear(); lastOfflineAlertTime = null; From 07bff222008fb729e9a17824dd0e17a206df1c88 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 29 Jan 2025 19:30:55 +0900 Subject: [PATCH 0794/1275] Fix delay before difficulty panel displays fully --- .../Screens/OnlinePlay/DailyChallenge/DailyChallenge.cs | 2 +- osu.Game/Screens/OnlinePlay/DrawableRoomPlaylistItem.cs | 6 ++++-- osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs | 2 +- 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallenge.cs b/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallenge.cs index 13a282dd52..249cad8ca3 100644 --- a/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallenge.cs +++ b/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallenge.cs @@ -161,7 +161,7 @@ namespace osu.Game.Screens.OnlinePlay.DailyChallenge { new Drawable[] { - new DrawableRoomPlaylistItem(playlistItem) + new DrawableRoomPlaylistItem(playlistItem, true) { RelativeSizeAxes = Axes.X, AllowReordering = false, diff --git a/osu.Game/Screens/OnlinePlay/DrawableRoomPlaylistItem.cs b/osu.Game/Screens/OnlinePlay/DrawableRoomPlaylistItem.cs index 7a773bb116..1e1e79d256 100644 --- a/osu.Game/Screens/OnlinePlay/DrawableRoomPlaylistItem.cs +++ b/osu.Game/Screens/OnlinePlay/DrawableRoomPlaylistItem.cs @@ -74,7 +74,7 @@ namespace osu.Game.Screens.OnlinePlay public bool IsSelectedItem => SelectedItem.Value?.ID == Item.ID; - private readonly DelayedLoadWrapper onScreenLoader = new DelayedLoadWrapper(Empty) { RelativeSizeAxes = Axes.Both }; + private readonly DelayedLoadWrapper onScreenLoader; private readonly IBindable valid = new Bindable(); private IBeatmapInfo? beatmap; @@ -120,9 +120,11 @@ namespace osu.Game.Screens.OnlinePlay [Resolved(CanBeNull = true)] private ManageCollectionsDialog? manageCollectionsDialog { get; set; } - public DrawableRoomPlaylistItem(PlaylistItem item) + public DrawableRoomPlaylistItem(PlaylistItem item, bool loadImmediately = false) : base(item) { + onScreenLoader = new DelayedLoadWrapper(Empty, timeBeforeLoad: loadImmediately ? 0 : 500) { RelativeSizeAxes = Axes.Both }; + Item = item; valid.BindTo(item.Valid); diff --git a/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs b/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs index d9e22efec5..8f286c0f16 100644 --- a/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs @@ -484,7 +484,7 @@ namespace osu.Game.Screens.OnlinePlay.Match if (gameplayItem.Equals(currentItem)) return; - UserStyleDisplayContainer.Child = new DrawableRoomPlaylistItem(gameplayItem) + UserStyleDisplayContainer.Child = new DrawableRoomPlaylistItem(gameplayItem, true) { AllowReordering = false, AllowEditing = true, From a6814d1a8a5c86fae6eb0a587c13c2196b523434 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 29 Jan 2025 19:48:04 +0900 Subject: [PATCH 0795/1275] Make multiplayer change room settings more obvious as to what it does "Edit" felt really weird. --- osu.Game/Screens/OnlinePlay/Match/DrawableMatchRoom.cs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/Match/DrawableMatchRoom.cs b/osu.Game/Screens/OnlinePlay/Match/DrawableMatchRoom.cs index 0c993f4abf..0eb8cc3706 100644 --- a/osu.Game/Screens/OnlinePlay/Match/DrawableMatchRoom.cs +++ b/osu.Game/Screens/OnlinePlay/Match/DrawableMatchRoom.cs @@ -49,8 +49,10 @@ namespace osu.Game.Screens.OnlinePlay.Match ButtonsContainer.Add(editButton = new PurpleRoundedButton { RelativeSizeAxes = Axes.Y, - Size = new Vector2(100, 1), - Text = CommonStrings.ButtonsEdit, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Size = new Vector2(120, 0.7f), + Text = "Change settings", Action = () => OnEdit?.Invoke() }); } From e8d0d2a1d9ebaa21bd408a8976902b40827e6cc4 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 29 Jan 2025 19:56:36 +0900 Subject: [PATCH 0796/1275] Combine more methods to simplify flow futher --- .../Screens/OnlinePlay/Match/RoomSubScreen.cs | 81 +++++++++---------- .../Multiplayer/MultiplayerMatchSubScreen.cs | 3 - 2 files changed, 36 insertions(+), 48 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs b/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs index 8f286c0f16..428f0e9ed8 100644 --- a/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs @@ -259,8 +259,8 @@ namespace osu.Game.Screens.OnlinePlay.Match { base.LoadComplete(); - SelectedItem.BindValueChanged(_ => Scheduler.AddOnce(OnSelectedItemChanged)); - UserMods.BindValueChanged(_ => Scheduler.AddOnce(OnSelectedItemChanged)); + SelectedItem.BindValueChanged(_ => updateSpecifics()); + UserMods.BindValueChanged(_ => updateSpecifics()); beatmapAvailabilityTracker.SelectedItem.BindTo(SelectedItem); beatmapAvailabilityTracker.Availability.BindValueChanged(_ => updateSpecifics()); @@ -426,35 +426,7 @@ namespace osu.Game.Screens.OnlinePlay.Match /// The screen to enter. protected abstract Screen CreateGameplayScreen(PlaylistItem selectedItem); - protected void OnSelectedItemChanged() - { - if (!this.IsCurrentScreen() || SelectedItem.Value is not PlaylistItem item) - return; - - updateSpecifics(); - - if (!item.AllowedMods.Any()) - { - UserModsSection?.Hide(); - UserModsSelectOverlay.Hide(); - UserModsSelectOverlay.IsValidMod = _ => false; - } - else - { - UserModsSection?.Show(); - - var rulesetInstance = GetGameplayRuleset().CreateInstance(); - var allowedMods = item.AllowedMods.Select(m => m.ToMod(rulesetInstance)); - UserModsSelectOverlay.IsValidMod = m => allowedMods.Any(a => a.GetType() == m.GetType()); - } - - if (item.FreeStyle) - UserStyleSection?.Show(); - else - UserStyleSection?.Hide(); - } - - private void updateSpecifics() + private void updateSpecifics() => Scheduler.AddOnce(() => { if (!this.IsCurrentScreen() || SelectedItem.Value is not PlaylistItem item) return; @@ -476,22 +448,41 @@ namespace osu.Game.Screens.OnlinePlay.Match Mods.Value = GetGameplayMods().Select(m => m.ToMod(rulesetInstance)).ToArray(); Ruleset.Value = GetGameplayRuleset(); - if (UserStyleDisplayContainer != null) + if (!item.AllowedMods.Any()) { - PlaylistItem gameplayItem = SelectedItem.Value.With(ruleset: GetGameplayRuleset().OnlineID, beatmap: new Optional(GetGameplayBeatmap())); - PlaylistItem? currentItem = UserStyleDisplayContainer.SingleOrDefault()?.Item; - - if (gameplayItem.Equals(currentItem)) - return; - - UserStyleDisplayContainer.Child = new DrawableRoomPlaylistItem(gameplayItem, true) - { - AllowReordering = false, - AllowEditing = true, - RequestEdit = _ => OpenStyleSelection() - }; + UserModsSection?.Hide(); + UserModsSelectOverlay.Hide(); + UserModsSelectOverlay.IsValidMod = _ => false; } - } + else + { + UserModsSection?.Show(); + UserModsSelectOverlay.IsValidMod = m => allowedMods.Any(a => a.GetType() == m.GetType()); + } + + if (item.FreeStyle) + { + UserStyleSection?.Show(); + + if (UserStyleDisplayContainer != null) + { + PlaylistItem gameplayItem = SelectedItem.Value.With(ruleset: GetGameplayRuleset().OnlineID, beatmap: new Optional(GetGameplayBeatmap())); + PlaylistItem? currentItem = UserStyleDisplayContainer.SingleOrDefault()?.Item; + + if (gameplayItem.Equals(currentItem)) + return; + + UserStyleDisplayContainer.Child = new DrawableRoomPlaylistItem(gameplayItem, true) + { + AllowReordering = false, + AllowEditing = item.FreeStyle, + RequestEdit = _ => OpenStyleSelection() + }; + } + } + else + UserStyleSection?.Hide(); + }); protected virtual APIMod[] GetGameplayMods() => UserMods.Value.Select(m => new APIMod(m)).Concat(SelectedItem.Value!.RequiredMods).ToArray(); diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs index 7f946a6997..f882fb7f89 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs @@ -392,9 +392,6 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer addItemButton.Alpha = localUserCanAddItem ? 1 : 0; - // Forcefully update the selected item so that the user state is applied. - Scheduler.AddOnce(OnSelectedItemChanged); - Activity.Value = new UserActivity.InLobby(Room); } From bc930e8fd32eab12f1bcdf6e57236433ad7ebe40 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 29 Jan 2025 20:02:01 +0900 Subject: [PATCH 0797/1275] Minimal clean-up to get things bearable I plan to do a full refactor of `RoomSubScreen` at first opportunity. --- .../Screens/OnlinePlay/Match/RoomSubScreen.cs | 59 ++++++++++--------- 1 file changed, 32 insertions(+), 27 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs b/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs index 428f0e9ed8..c9c9c3eca7 100644 --- a/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Generic; using System.ComponentModel; +using System.Diagnostics; using System.Linq; using osu.Framework.Allocation; using osu.Framework.Audio; @@ -49,18 +50,18 @@ namespace osu.Game.Screens.OnlinePlay.Match /// A container that provides controls for selection of user mods. /// This will be shown/hidden automatically when applicable. /// - protected Drawable? UserModsSection; + protected Drawable UserModsSection = null!; /// /// A container that provides controls for selection of the user style. /// This will be shown/hidden automatically when applicable. /// - protected Drawable? UserStyleSection; + protected Drawable UserStyleSection = null!; /// /// A container that will display the user's style. /// - protected Container? UserStyleDisplayContainer; + protected Container UserStyleDisplayContainer = null!; private Sample? sampleStart; @@ -448,40 +449,44 @@ namespace osu.Game.Screens.OnlinePlay.Match Mods.Value = GetGameplayMods().Select(m => m.ToMod(rulesetInstance)).ToArray(); Ruleset.Value = GetGameplayRuleset(); - if (!item.AllowedMods.Any()) + bool freeMod = item.AllowedMods.Any(); + bool freeStyle = item.FreeStyle; + + // For now, the game can never be in a state where freemod and freestyle are on at the same time. + // This will change, but due to the current implementation if this was to occur drawables will overlap so let's assert. + Debug.Assert(!freeMod || !freeStyle); + + if (freeMod) { - UserModsSection?.Hide(); + UserModsSection.Show(); + UserModsSelectOverlay.IsValidMod = m => allowedMods.Any(a => a.GetType() == m.GetType()); + } + else + { + UserModsSection.Hide(); UserModsSelectOverlay.Hide(); UserModsSelectOverlay.IsValidMod = _ => false; } - else - { - UserModsSection?.Show(); - UserModsSelectOverlay.IsValidMod = m => allowedMods.Any(a => a.GetType() == m.GetType()); - } - if (item.FreeStyle) + if (freeStyle) { - UserStyleSection?.Show(); + UserStyleSection.Show(); - if (UserStyleDisplayContainer != null) + PlaylistItem gameplayItem = SelectedItem.Value.With(ruleset: GetGameplayRuleset().OnlineID, beatmap: new Optional(GetGameplayBeatmap())); + PlaylistItem? currentItem = UserStyleDisplayContainer.SingleOrDefault()?.Item; + + if (gameplayItem.Equals(currentItem)) + return; + + UserStyleDisplayContainer.Child = new DrawableRoomPlaylistItem(gameplayItem, true) { - PlaylistItem gameplayItem = SelectedItem.Value.With(ruleset: GetGameplayRuleset().OnlineID, beatmap: new Optional(GetGameplayBeatmap())); - PlaylistItem? currentItem = UserStyleDisplayContainer.SingleOrDefault()?.Item; - - if (gameplayItem.Equals(currentItem)) - return; - - UserStyleDisplayContainer.Child = new DrawableRoomPlaylistItem(gameplayItem, true) - { - AllowReordering = false, - AllowEditing = item.FreeStyle, - RequestEdit = _ => OpenStyleSelection() - }; - } + AllowReordering = false, + AllowEditing = freeStyle, + RequestEdit = _ => OpenStyleSelection() + }; } else - UserStyleSection?.Hide(); + UserStyleSection.Hide(); }); protected virtual APIMod[] GetGameplayMods() => UserMods.Value.Select(m => new APIMod(m)).Concat(SelectedItem.Value!.RequiredMods).ToArray(); From ca7a36d3d6739d8aee75d937cd7544ea7a071983 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Wed, 29 Jan 2025 23:32:44 +0900 Subject: [PATCH 0798/1275] Remove unused usings --- osu.Game/Screens/OnlinePlay/Match/DrawableMatchRoom.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/osu.Game/Screens/OnlinePlay/Match/DrawableMatchRoom.cs b/osu.Game/Screens/OnlinePlay/Match/DrawableMatchRoom.cs index 0eb8cc3706..08bcf32edf 100644 --- a/osu.Game/Screens/OnlinePlay/Match/DrawableMatchRoom.cs +++ b/osu.Game/Screens/OnlinePlay/Match/DrawableMatchRoom.cs @@ -9,7 +9,6 @@ using osu.Framework.Graphics; using osu.Game.Beatmaps.Drawables; using osu.Game.Online.API; using osu.Game.Online.Rooms; -using osu.Game.Resources.Localisation.Web; using osu.Game.Screens.OnlinePlay.Lounge.Components; using osu.Game.Screens.OnlinePlay.Match.Components; using osuTK; From 46144960e50fc49867bccaed2ee035e983a05718 Mon Sep 17 00:00:00 2001 From: "Rian (Reza Mouna Hendrian)" <52914632+Rian8337@users.noreply.github.com> Date: Thu, 30 Jan 2025 03:06:05 +0800 Subject: [PATCH 0799/1275] Remove unnecessary strain sorting in difficult slider count (#31724) --- osu.Game.Rulesets.Osu/Difficulty/Skills/Aim.cs | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Difficulty/Skills/Aim.cs b/osu.Game.Rulesets.Osu/Difficulty/Skills/Aim.cs index 89adda302c..6f1b680211 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/Skills/Aim.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/Skills/Aim.cs @@ -53,13 +53,11 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills if (sliderStrains.Count == 0) return 0; - double[] sortedStrains = sliderStrains.OrderDescending().ToArray(); - - double maxSliderStrain = sortedStrains.Max(); + double maxSliderStrain = sliderStrains.Max(); if (maxSliderStrain == 0) return 0; - return sortedStrains.Sum(strain => 1.0 / (1.0 + Math.Exp(-(strain / maxSliderStrain * 12.0 - 6.0)))); + return sliderStrains.Sum(strain => 1.0 / (1.0 + Math.Exp(-(strain / maxSliderStrain * 12.0 - 6.0)))); } } } From bad2959d5ba6f74d3ab76d32a6110ebccedde922 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 30 Jan 2025 08:54:42 +0100 Subject: [PATCH 0800/1275] Change mirror mod direction setting tooltip to hopefully be less confusing See https://github.com/ppy/osu/issues/29720, https://discord.com/channels/188630481301012481/188630652340404224/1334294048541904906. This removes the tooltip due to being zero or negative information, and also changes the description of the setting to not contain the word "mirror", which will hopefully quash the "this is where I would place a mirror to my screen to achieve what I want" interpretation which seems to be a minority interpretation. The first time this was complained about I figured this was probably a one guy issue, but now it's happened twice, and I never want to see this conversation again. --- osu.Game.Rulesets.Osu/Mods/OsuModMirror.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModMirror.cs b/osu.Game.Rulesets.Osu/Mods/OsuModMirror.cs index 6d01808fb5..4af88caee4 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModMirror.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModMirror.cs @@ -17,7 +17,7 @@ namespace osu.Game.Rulesets.Osu.Mods public override LocalisableString Description => "Flip objects on the chosen axes."; public override Type[] IncompatibleMods => new[] { typeof(ModHardRock) }; - [SettingSource("Mirrored axes", "Choose which axes objects are mirrored over.")] + [SettingSource("Flipped axes")] public Bindable Reflection { get; } = new Bindable(); public void ApplyToHitObject(HitObject hitObject) From ec99fc114103f5fd2bc696bcf1bd75ad3bd37241 Mon Sep 17 00:00:00 2001 From: Marvin Helstein Date: Thu, 30 Jan 2025 10:15:16 +0200 Subject: [PATCH 0801/1275] Move `ApplySelectionOrder` override from `EditorBlueprintContainer` to `ComposeBlueprintContainer` --- .../Edit/Compose/Components/ComposeBlueprintContainer.cs | 5 +++++ .../Edit/Compose/Components/EditorBlueprintContainer.cs | 5 ----- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/osu.Game/Screens/Edit/Compose/Components/ComposeBlueprintContainer.cs b/osu.Game/Screens/Edit/Compose/Components/ComposeBlueprintContainer.cs index de1f589135..e82f6395d0 100644 --- a/osu.Game/Screens/Edit/Compose/Components/ComposeBlueprintContainer.cs +++ b/osu.Game/Screens/Edit/Compose/Components/ComposeBlueprintContainer.cs @@ -3,6 +3,7 @@ #nullable disable +using System; using System.Collections.Generic; using System.Linq; using Humanizer; @@ -52,6 +53,10 @@ namespace osu.Game.Screens.Edit.Compose.Components /// public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => editorScreen?.MainContent.ReceivePositionalInputAt(screenSpacePos) ?? base.ReceivePositionalInputAt(screenSpacePos); + protected override IEnumerable> ApplySelectionOrder(IEnumerable> blueprints) => + base.ApplySelectionOrder(blueprints) + .OrderBy(b => Math.Min(Math.Abs(EditorClock.CurrentTime - b.Item.GetEndTime()), Math.Abs(EditorClock.CurrentTime - b.Item.StartTime))); + protected ComposeBlueprintContainer(HitObjectComposer composer) : base(composer) { diff --git a/osu.Game/Screens/Edit/Compose/Components/EditorBlueprintContainer.cs b/osu.Game/Screens/Edit/Compose/Components/EditorBlueprintContainer.cs index f1811dd84f..e67644baaa 100644 --- a/osu.Game/Screens/Edit/Compose/Components/EditorBlueprintContainer.cs +++ b/osu.Game/Screens/Edit/Compose/Components/EditorBlueprintContainer.cs @@ -3,7 +3,6 @@ #nullable disable -using System; using System.Collections.Generic; using System.Collections.Specialized; using System.Linq; @@ -126,10 +125,6 @@ namespace osu.Game.Screens.Edit.Compose.Components return true; } - protected override IEnumerable> ApplySelectionOrder(IEnumerable> blueprints) => - base.ApplySelectionOrder(blueprints) - .OrderBy(b => Math.Min(Math.Abs(EditorClock.CurrentTime - b.Item.GetEndTime()), Math.Abs(EditorClock.CurrentTime - b.Item.StartTime))); - protected override SelectionBlueprintContainer CreateSelectionBlueprintContainer() => new HitObjectOrderedSelectionContainer { RelativeSizeAxes = Axes.Both }; protected override SelectionHandler CreateSelectionHandler() => new EditorSelectionHandler(); From 31c4461fbb1167d3a1c93910b0f5c4263ab348dd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 17 Oct 2024 11:04:39 +0200 Subject: [PATCH 0802/1275] Abstract out `WizardOverlay` for multi-step wizard type screens To be used in the editor, for the beatmap submission wizard. I've recently been on record for hating "abstract" as a rationale to do anything, but seeing this commit ~3 months after I originally made it, it still feels okay to do for me in this particular case. I think the abstraction is loose enough, makes sense from a code reuse and UX consistency standpoint, and doesn't seem to leak any particular implementation details. That said, it is both a huge diffstat and also potentially controversial, which is why I'm PRing first separately. --- .../Overlays/FirstRunSetup/ScreenBeatmaps.cs | 2 +- .../Overlays/FirstRunSetup/ScreenBehaviour.cs | 2 +- .../FirstRunSetup/ScreenImportFromStable.cs | 2 +- .../Overlays/FirstRunSetup/ScreenUIScale.cs | 2 +- .../Overlays/FirstRunSetup/ScreenWelcome.cs | 2 +- osu.Game/Overlays/FirstRunSetupOverlay.cs | 268 +--------------- osu.Game/Overlays/WizardOverlay.cs | 288 ++++++++++++++++++ ...FirstRunSetupScreen.cs => WizardScreen.cs} | 4 +- 8 files changed, 305 insertions(+), 265 deletions(-) create mode 100644 osu.Game/Overlays/WizardOverlay.cs rename osu.Game/Overlays/{FirstRunSetup/FirstRunSetupScreen.cs => WizardScreen.cs} (96%) diff --git a/osu.Game/Overlays/FirstRunSetup/ScreenBeatmaps.cs b/osu.Game/Overlays/FirstRunSetup/ScreenBeatmaps.cs index da60951ab6..392b170ad2 100644 --- a/osu.Game/Overlays/FirstRunSetup/ScreenBeatmaps.cs +++ b/osu.Game/Overlays/FirstRunSetup/ScreenBeatmaps.cs @@ -21,7 +21,7 @@ using Realms; namespace osu.Game.Overlays.FirstRunSetup { [LocalisableDescription(typeof(FirstRunSetupBeatmapScreenStrings), nameof(FirstRunSetupBeatmapScreenStrings.Header))] - public partial class ScreenBeatmaps : FirstRunSetupScreen + public partial class ScreenBeatmaps : WizardScreen { private ProgressRoundedButton downloadBundledButton = null!; private ProgressRoundedButton downloadTutorialButton = null!; diff --git a/osu.Game/Overlays/FirstRunSetup/ScreenBehaviour.cs b/osu.Game/Overlays/FirstRunSetup/ScreenBehaviour.cs index d31ce7ea18..a583ba5f6b 100644 --- a/osu.Game/Overlays/FirstRunSetup/ScreenBehaviour.cs +++ b/osu.Game/Overlays/FirstRunSetup/ScreenBehaviour.cs @@ -20,7 +20,7 @@ using osu.Game.Overlays.Settings.Sections; namespace osu.Game.Overlays.FirstRunSetup { [LocalisableDescription(typeof(FirstRunSetupOverlayStrings), nameof(FirstRunSetupOverlayStrings.Behaviour))] - public partial class ScreenBehaviour : FirstRunSetupScreen + public partial class ScreenBehaviour : WizardScreen { private SearchContainer searchContainer; diff --git a/osu.Game/Overlays/FirstRunSetup/ScreenImportFromStable.cs b/osu.Game/Overlays/FirstRunSetup/ScreenImportFromStable.cs index 5eb38b6e11..5bdcd8e850 100644 --- a/osu.Game/Overlays/FirstRunSetup/ScreenImportFromStable.cs +++ b/osu.Game/Overlays/FirstRunSetup/ScreenImportFromStable.cs @@ -31,7 +31,7 @@ using osuTK; namespace osu.Game.Overlays.FirstRunSetup { [LocalisableDescription(typeof(FirstRunOverlayImportFromStableScreenStrings), nameof(FirstRunOverlayImportFromStableScreenStrings.Header))] - public partial class ScreenImportFromStable : FirstRunSetupScreen + public partial class ScreenImportFromStable : WizardScreen { private static readonly Vector2 button_size = new Vector2(400, 50); diff --git a/osu.Game/Overlays/FirstRunSetup/ScreenUIScale.cs b/osu.Game/Overlays/FirstRunSetup/ScreenUIScale.cs index d0eefa55c5..fc64408775 100644 --- a/osu.Game/Overlays/FirstRunSetup/ScreenUIScale.cs +++ b/osu.Game/Overlays/FirstRunSetup/ScreenUIScale.cs @@ -32,7 +32,7 @@ using osuTK; namespace osu.Game.Overlays.FirstRunSetup { [LocalisableDescription(typeof(GraphicsSettingsStrings), nameof(GraphicsSettingsStrings.UIScaling))] - public partial class ScreenUIScale : FirstRunSetupScreen + public partial class ScreenUIScale : WizardScreen { [BackgroundDependencyLoader] private void load(OsuConfigManager config) diff --git a/osu.Game/Overlays/FirstRunSetup/ScreenWelcome.cs b/osu.Game/Overlays/FirstRunSetup/ScreenWelcome.cs index 68c6c78986..93cf555bc9 100644 --- a/osu.Game/Overlays/FirstRunSetup/ScreenWelcome.cs +++ b/osu.Game/Overlays/FirstRunSetup/ScreenWelcome.cs @@ -23,7 +23,7 @@ using osuTK; namespace osu.Game.Overlays.FirstRunSetup { [LocalisableDescription(typeof(FirstRunSetupOverlayStrings), nameof(FirstRunSetupOverlayStrings.WelcomeTitle))] - public partial class ScreenWelcome : FirstRunSetupScreen + public partial class ScreenWelcome : WizardScreen { [BackgroundDependencyLoader] private void load(FrameworkConfigManager frameworkConfig) diff --git a/osu.Game/Overlays/FirstRunSetupOverlay.cs b/osu.Game/Overlays/FirstRunSetupOverlay.cs index 1a302cf51d..c2e89f32f1 100644 --- a/osu.Game/Overlays/FirstRunSetupOverlay.cs +++ b/osu.Game/Overlays/FirstRunSetupOverlay.cs @@ -1,38 +1,22 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using System; -using System.Collections.Generic; -using System.Diagnostics; using osu.Framework.Allocation; using osu.Framework.Bindables; -using osu.Framework.Extensions; -using osu.Framework.Graphics; -using osu.Framework.Graphics.Containers; -using osu.Framework.Graphics.Cursor; -using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Sprites; -using osu.Framework.Input.Events; -using osu.Framework.Localisation; -using osu.Framework.Screens; -using osu.Framework.Threading; using osu.Game.Configuration; using osu.Game.Database; using osu.Game.Graphics; -using osu.Game.Graphics.UserInterface; -using osu.Game.Input.Bindings; using osu.Game.Localisation; using osu.Game.Overlays.FirstRunSetup; -using osu.Game.Overlays.Mods; using osu.Game.Overlays.Notifications; using osu.Game.Screens; -using osu.Game.Screens.Footer; using osu.Game.Screens.Menu; namespace osu.Game.Overlays { [Cached] - public partial class FirstRunSetupOverlay : ShearedOverlayContainer + public partial class FirstRunSetupOverlay : WizardOverlay { [Resolved] private IPerformFromScreenRunner performer { get; set; } = null!; @@ -43,28 +27,8 @@ namespace osu.Game.Overlays [Resolved] private OsuConfigManager config { get; set; } = null!; - private ScreenStack? stack; - - public ShearedButton? NextButton => DisplayedFooterContent?.NextButton; - private readonly Bindable showFirstRunSetup = new Bindable(); - private int? currentStepIndex; - - /// - /// The currently displayed screen, if any. - /// - public FirstRunSetupScreen? CurrentScreen => (FirstRunSetupScreen?)stack?.CurrentScreen; - - private readonly List steps = new List(); - - private Container screenContent = null!; - - private Container content = null!; - - private LoadingSpinner loading = null!; - private ScheduledDelegate? loadingShowDelegate; - public FirstRunSetupOverlay() : base(OverlayColourScheme.Purple) { @@ -73,67 +37,15 @@ namespace osu.Game.Overlays [BackgroundDependencyLoader(permitNulls: true)] private void load(OsuColour colours, LegacyImportManager? legacyImportManager) { - steps.Add(typeof(ScreenWelcome)); - steps.Add(typeof(ScreenUIScale)); - steps.Add(typeof(ScreenBeatmaps)); + AddStep(); + AddStep(); + AddStep(); if (legacyImportManager?.SupportsImportFromStable == true) - steps.Add(typeof(ScreenImportFromStable)); - steps.Add(typeof(ScreenBehaviour)); + AddStep(); + AddStep(); Header.Title = FirstRunSetupOverlayStrings.FirstRunSetupTitle; Header.Description = FirstRunSetupOverlayStrings.FirstRunSetupDescription; - - MainAreaContent.AddRange(new Drawable[] - { - content = new PopoverContainer - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - RelativeSizeAxes = Axes.Both, - Padding = new MarginPadding { Bottom = 20 }, - Child = new GridContainer - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - RelativeSizeAxes = Axes.Both, - ColumnDimensions = new[] - { - new Dimension(), - new Dimension(minSize: 640, maxSize: 800), - new Dimension(), - }, - Content = new[] - { - new[] - { - Empty(), - new InputBlockingContainer - { - Masking = true, - CornerRadius = 14, - RelativeSizeAxes = Axes.Both, - Children = new Drawable[] - { - new Box - { - RelativeSizeAxes = Axes.Both, - Colour = ColourProvider.Background6, - }, - loading = new LoadingSpinner(), - new Container - { - RelativeSizeAxes = Axes.Both, - Padding = new MarginPadding { Vertical = 20 }, - Child = screenContent = new Container { RelativeSizeAxes = Axes.Both, }, - }, - }, - }, - Empty(), - }, - } - } - }, - }); } protected override void LoadComplete() @@ -145,55 +57,6 @@ namespace osu.Game.Overlays if (showFirstRunSetup.Value) Show(); } - [Resolved] - private ScreenFooter footer { get; set; } = null!; - - public new FirstRunSetupFooterContent? DisplayedFooterContent => base.DisplayedFooterContent as FirstRunSetupFooterContent; - - public override VisibilityContainer CreateFooterContent() - { - var footerContent = new FirstRunSetupFooterContent - { - ShowNextStep = showNextStep, - }; - - footerContent.OnLoadComplete += _ => updateButtons(); - return footerContent; - } - - public override bool OnBackButton() - { - if (currentStepIndex == 0) - return false; - - Debug.Assert(stack != null); - - stack.CurrentScreen.Exit(); - currentStepIndex--; - - updateButtons(); - return true; - } - - public override bool OnPressed(KeyBindingPressEvent e) - { - if (!e.Repeat) - { - switch (e.Action) - { - case GlobalAction.Select: - DisplayedFooterContent?.NextButton.TriggerClick(); - return true; - - case GlobalAction.Back: - footer.BackButton.TriggerClick(); - return false; - } - } - - return base.OnPressed(e); - } - public override void Show() { // if we are valid for display, only do so after reaching the main menu. @@ -207,24 +70,11 @@ namespace osu.Game.Overlays }, new[] { typeof(MainMenu) }); } - protected override void PopIn() - { - base.PopIn(); - - content.ScaleTo(0.99f) - .ScaleTo(1, 400, Easing.OutQuint); - - if (currentStepIndex == null) - showFirstStep(); - } - protected override void PopOut() { base.PopOut(); - content.ScaleTo(0.99f, 400, Easing.OutQuint); - - if (currentStepIndex != null) + if (CurrentStepIndex != null) { notificationOverlay.Post(new SimpleNotification { @@ -237,112 +87,14 @@ namespace osu.Game.Overlays }, }); } - else - { - stack?.FadeOut(100) - .Expire(); - } } - private void showFirstStep() + protected override void ShowNextStep() { - Debug.Assert(currentStepIndex == null); + base.ShowNextStep(); - screenContent.Child = stack = new ScreenStack - { - RelativeSizeAxes = Axes.Both, - }; - - currentStepIndex = -1; - showNextStep(); - } - - private void showNextStep() - { - Debug.Assert(currentStepIndex != null); - Debug.Assert(stack != null); - - currentStepIndex++; - - if (currentStepIndex < steps.Count) - { - var nextScreen = (Screen)Activator.CreateInstance(steps[currentStepIndex.Value])!; - - loadingShowDelegate = Scheduler.AddDelayed(() => loading.Show(), 200); - nextScreen.OnLoadComplete += _ => - { - loadingShowDelegate?.Cancel(); - loading.Hide(); - }; - - stack.Push(nextScreen); - } - else - { + if (CurrentStepIndex == null) showFirstRunSetup.Value = false; - currentStepIndex = null; - Hide(); - } - - updateButtons(); - } - - private void updateButtons() => DisplayedFooterContent?.UpdateButtons(currentStepIndex, steps); - - public partial class FirstRunSetupFooterContent : VisibilityContainer - { - public ShearedButton NextButton { get; private set; } = null!; - - public Action? ShowNextStep; - - [BackgroundDependencyLoader] - private void load(OverlayColourProvider colourProvider) - { - RelativeSizeAxes = Axes.Both; - - InternalChild = NextButton = new ShearedButton(0) - { - Anchor = Anchor.BottomLeft, - Origin = Anchor.BottomLeft, - Margin = new MarginPadding { Right = 12f }, - RelativeSizeAxes = Axes.X, - Width = 1, - Text = FirstRunSetupOverlayStrings.GetStarted, - DarkerColour = colourProvider.Colour2, - LighterColour = colourProvider.Colour1, - Action = () => ShowNextStep?.Invoke(), - }; - } - - public void UpdateButtons(int? currentStep, IReadOnlyList steps) - { - NextButton.Enabled.Value = currentStep != null; - - if (currentStep == null) - return; - - bool isFirstStep = currentStep == 0; - bool isLastStep = currentStep == steps.Count - 1; - - if (isFirstStep) - NextButton.Text = FirstRunSetupOverlayStrings.GetStarted; - else - { - NextButton.Text = isLastStep - ? CommonStrings.Finish - : LocalisableString.Interpolate($@"{CommonStrings.Next} ({steps[currentStep.Value + 1].GetLocalisableDescription()})"); - } - } - - protected override void PopIn() - { - this.FadeIn(); - } - - protected override void PopOut() - { - this.Delay(400).FadeOut(); - } } } } diff --git a/osu.Game/Overlays/WizardOverlay.cs b/osu.Game/Overlays/WizardOverlay.cs new file mode 100644 index 0000000000..38701efc96 --- /dev/null +++ b/osu.Game/Overlays/WizardOverlay.cs @@ -0,0 +1,288 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using osu.Framework.Allocation; +using osu.Framework.Extensions; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Cursor; +using osu.Framework.Graphics.Shapes; +using osu.Framework.Input.Events; +using osu.Framework.Localisation; +using osu.Framework.Screens; +using osu.Framework.Threading; +using osu.Game.Graphics; +using osu.Game.Graphics.UserInterface; +using osu.Game.Input.Bindings; +using osu.Game.Localisation; +using osu.Game.Overlays.Mods; +using osu.Game.Screens.Footer; + +namespace osu.Game.Overlays +{ + public partial class WizardOverlay : ShearedOverlayContainer + { + private ScreenStack? stack; + + public ShearedButton? NextButton => DisplayedFooterContent?.NextButton; + + protected int? CurrentStepIndex { get; private set; } + + /// + /// The currently displayed screen, if any. + /// + public WizardScreen? CurrentScreen => (WizardScreen?)stack?.CurrentScreen; + + private readonly List steps = new List(); + + private Container screenContent = null!; + + private Container content = null!; + + private LoadingSpinner loading = null!; + private ScheduledDelegate? loadingShowDelegate; + + protected WizardOverlay(OverlayColourScheme scheme) + : base(scheme) + { + } + + [BackgroundDependencyLoader] + private void load() + { + MainAreaContent.AddRange(new Drawable[] + { + content = new PopoverContainer + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + RelativeSizeAxes = Axes.Both, + Padding = new MarginPadding { Bottom = 20 }, + Child = new GridContainer + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + RelativeSizeAxes = Axes.Both, + ColumnDimensions = new[] + { + new Dimension(), + new Dimension(minSize: 640, maxSize: 800), + new Dimension(), + }, + Content = new[] + { + new[] + { + Empty(), + new InputBlockingContainer + { + Masking = true, + CornerRadius = 14, + RelativeSizeAxes = Axes.Both, + Children = new Drawable[] + { + new Box + { + RelativeSizeAxes = Axes.Both, + Colour = ColourProvider.Background6, + }, + loading = new LoadingSpinner(), + new Container + { + RelativeSizeAxes = Axes.Both, + Padding = new MarginPadding { Vertical = 20 }, + Child = screenContent = new Container { RelativeSizeAxes = Axes.Both, }, + }, + }, + }, + Empty(), + }, + } + } + }, + }); + } + + [Resolved] + private ScreenFooter footer { get; set; } = null!; + + public new WizardFooterContent? DisplayedFooterContent => base.DisplayedFooterContent as WizardFooterContent; + + public override VisibilityContainer CreateFooterContent() + { + var footerContent = new WizardFooterContent + { + ShowNextStep = ShowNextStep, + }; + + footerContent.OnLoadComplete += _ => updateButtons(); + return footerContent; + } + + public override bool OnBackButton() + { + if (CurrentStepIndex == 0) + return false; + + Debug.Assert(stack != null); + + stack.CurrentScreen.Exit(); + CurrentStepIndex--; + + updateButtons(); + return true; + } + + public override bool OnPressed(KeyBindingPressEvent e) + { + if (!e.Repeat) + { + switch (e.Action) + { + case GlobalAction.Select: + DisplayedFooterContent?.NextButton.TriggerClick(); + return true; + + case GlobalAction.Back: + footer.BackButton.TriggerClick(); + return false; + } + } + + return base.OnPressed(e); + } + + protected override void PopIn() + { + base.PopIn(); + + content.ScaleTo(0.99f) + .ScaleTo(1, 400, Easing.OutQuint); + + if (CurrentStepIndex == null) + showFirstStep(); + } + + protected override void PopOut() + { + base.PopOut(); + + content.ScaleTo(0.99f, 400, Easing.OutQuint); + + if (CurrentStepIndex == null) + { + stack?.FadeOut(100) + .Expire(); + } + } + + protected void AddStep() + where T : WizardScreen + { + steps.Add(typeof(T)); + } + + private void showFirstStep() + { + Debug.Assert(CurrentStepIndex == null); + + screenContent.Child = stack = new ScreenStack + { + RelativeSizeAxes = Axes.Both, + }; + + CurrentStepIndex = -1; + ShowNextStep(); + } + + protected virtual void ShowNextStep() + { + Debug.Assert(CurrentStepIndex != null); + Debug.Assert(stack != null); + + CurrentStepIndex++; + + if (CurrentStepIndex < steps.Count) + { + var nextScreen = (Screen)Activator.CreateInstance(steps[CurrentStepIndex.Value])!; + + loadingShowDelegate = Scheduler.AddDelayed(() => loading.Show(), 200); + nextScreen.OnLoadComplete += _ => + { + loadingShowDelegate?.Cancel(); + loading.Hide(); + }; + + stack.Push(nextScreen); + } + else + { + CurrentStepIndex = null; + Hide(); + } + + updateButtons(); + } + + private void updateButtons() => DisplayedFooterContent?.UpdateButtons(CurrentStepIndex, steps); + + public partial class WizardFooterContent : VisibilityContainer + { + public ShearedButton NextButton { get; private set; } = null!; + + public Action? ShowNextStep; + + [BackgroundDependencyLoader] + private void load(OverlayColourProvider colourProvider) + { + RelativeSizeAxes = Axes.Both; + + InternalChild = NextButton = new ShearedButton(0) + { + Anchor = Anchor.BottomLeft, + Origin = Anchor.BottomLeft, + Margin = new MarginPadding { Right = 12f }, + RelativeSizeAxes = Axes.X, + Width = 1, + Text = FirstRunSetupOverlayStrings.GetStarted, + DarkerColour = colourProvider.Colour2, + LighterColour = colourProvider.Colour1, + Action = () => ShowNextStep?.Invoke(), + }; + } + + public void UpdateButtons(int? currentStep, IReadOnlyList steps) + { + NextButton.Enabled.Value = currentStep != null; + + if (currentStep == null) + return; + + bool isFirstStep = currentStep == 0; + bool isLastStep = currentStep == steps.Count - 1; + + if (isFirstStep) + NextButton.Text = FirstRunSetupOverlayStrings.GetStarted; + else + { + NextButton.Text = isLastStep + ? CommonStrings.Finish + : LocalisableString.Interpolate($@"{CommonStrings.Next} ({steps[currentStep.Value + 1].GetLocalisableDescription()})"); + } + } + + protected override void PopIn() + { + this.FadeIn(); + } + + protected override void PopOut() + { + this.Delay(400).FadeOut(); + } + } + } +} diff --git a/osu.Game/Overlays/FirstRunSetup/FirstRunSetupScreen.cs b/osu.Game/Overlays/WizardScreen.cs similarity index 96% rename from osu.Game/Overlays/FirstRunSetup/FirstRunSetupScreen.cs rename to osu.Game/Overlays/WizardScreen.cs index 76921718f2..7f3b1fe7f4 100644 --- a/osu.Game/Overlays/FirstRunSetup/FirstRunSetupScreen.cs +++ b/osu.Game/Overlays/WizardScreen.cs @@ -13,9 +13,9 @@ using osu.Game.Graphics.Containers; using osu.Game.Graphics.Sprites; using osuTK; -namespace osu.Game.Overlays.FirstRunSetup +namespace osu.Game.Overlays { - public abstract partial class FirstRunSetupScreen : Screen + public abstract partial class WizardScreen : Screen { private const float offset = 100; From 749704344c5fbb0d46b153d98e60798e331a3965 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 30 Jan 2025 13:11:05 +0100 Subject: [PATCH 0803/1275] Move implicit slider path segment handling logic to Bezier converter The logic in `LegacyBeatmapEncoder` that was supposed to handle the lazer-exclusive feature of supporting multiple slider segment types in a single slider was interfering rather badly with the Bezier converter. Generally it was a bit difficult to follow, too. The nice thing about `BezierConverter` is that it is *guaranteed* to only output Bezier control points. In light of this, the same double-up- -the-control-point logic that was supposed to make multiple slider segment types backwards-compatible with stable can be placed in the Bezier conversion logic, and be *much* more understandable, too. --- .../Beatmaps/Formats/LegacyBeatmapEncoder.cs | 59 +++++-------------- osu.Game/Rulesets/Objects/BezierConverter.cs | 4 ++ 2 files changed, 19 insertions(+), 44 deletions(-) diff --git a/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs b/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs index 6c855e1346..07e88ab956 100644 --- a/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs +++ b/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs @@ -447,60 +447,31 @@ namespace osu.Game.Beatmaps.Formats private void addPathData(TextWriter writer, IHasPath pathData, Vector2 position) { - PathType? lastType = null; - for (int i = 0; i < pathData.Path.ControlPoints.Count; i++) { PathControlPoint point = pathData.Path.ControlPoints[i]; + // Note that lazer's encoding format supports specifying multiple curve types for a slider path, which is not supported by stable. + // Backwards compatibility with stable is handled by `LegacyBeatmapExporter` and `BezierConverter.ConvertToModernBezier()`. if (point.Type != null) { - // We've reached a new (explicit) segment! - - // Explicit segments have a new format in which the type is injected into the middle of the control point string. - // To preserve compatibility with osu-stable as much as possible, explicit segments with the same type are converted to use implicit segments by duplicating the control point. - // One exception are consecutive perfect curves, which aren't supported in osu!stable and can lead to decoding issues if encoded as implicit segments - bool needsExplicitSegment = point.Type != lastType || point.Type == PathType.PERFECT_CURVE || i == pathData.Path.ControlPoints.Count - 1; - - // Another exception to this is when the last two control points of the last segment were duplicated. This is not a scenario supported by osu!stable. - // Lazer does not add implicit segments for the last two control points of _any_ explicit segment, so an explicit segment is forced in order to maintain consistency with the decoder. - if (i > 1) + switch (point.Type?.Type) { - // We need to use the absolute control point position to determine equality, otherwise floating point issues may arise. - Vector2 p1 = position + pathData.Path.ControlPoints[i - 1].Position; - Vector2 p2 = position + pathData.Path.ControlPoints[i - 2].Position; + case SplineType.BSpline: + writer.Write(point.Type.Value.Degree > 0 ? $"B{point.Type.Value.Degree}|" : "B|"); + break; - if ((int)p1.X == (int)p2.X && (int)p1.Y == (int)p2.Y) - needsExplicitSegment = true; - } + case SplineType.Catmull: + writer.Write("C|"); + break; - if (needsExplicitSegment) - { - switch (point.Type?.Type) - { - case SplineType.BSpline: - writer.Write(point.Type.Value.Degree > 0 ? $"B{point.Type.Value.Degree}|" : "B|"); - break; + case SplineType.PerfectCurve: + writer.Write("P|"); + break; - case SplineType.Catmull: - writer.Write("C|"); - break; - - case SplineType.PerfectCurve: - writer.Write("P|"); - break; - - case SplineType.Linear: - writer.Write("L|"); - break; - } - - lastType = point.Type; - } - else - { - // New segment with the same type - duplicate the control point - writer.Write(FormattableString.Invariant($"{position.X + point.Position.X}:{position.Y + point.Position.Y}|")); + case SplineType.Linear: + writer.Write("L|"); + break; } } diff --git a/osu.Game/Rulesets/Objects/BezierConverter.cs b/osu.Game/Rulesets/Objects/BezierConverter.cs index 638975630e..384c686167 100644 --- a/osu.Game/Rulesets/Objects/BezierConverter.cs +++ b/osu.Game/Rulesets/Objects/BezierConverter.cs @@ -136,6 +136,7 @@ namespace osu.Game.Rulesets.Objects { for (int j = 0; j < segment.Length - 1; j++) { + if (result.Count > 0 && j == 0) result.Add(new PathControlPoint(segment[j])); result.Add(new PathControlPoint(segment[j], j == 0 ? PathType.BEZIER : null)); } } @@ -147,6 +148,7 @@ namespace osu.Game.Rulesets.Objects { for (int j = 0; j < segment.Length - 1; j++) { + if (result.Count > 0 && j == 0) result.Add(new PathControlPoint(segment[j])); result.Add(new PathControlPoint(segment[j], j == 0 ? PathType.BEZIER : null)); } } @@ -158,6 +160,7 @@ namespace osu.Game.Rulesets.Objects for (int j = 0; j < circleResult.Length - 1; j++) { + if (result.Count > 0 && j == 0) result.Add(new PathControlPoint(circleResult[j])); result.Add(new PathControlPoint(circleResult[j], j == 0 ? PathType.BEZIER : null)); } @@ -170,6 +173,7 @@ namespace osu.Game.Rulesets.Objects for (int j = 0; j < bSplineResult.Length - 1; j++) { + if (result.Count > 0 && j == 0) result.Add(new PathControlPoint(bSplineResult[j])); result.Add(new PathControlPoint(bSplineResult[j], j == 0 ? PathType.BEZIER : null)); } From 64b67252a2edc1b762c4f4cca311738effe2df68 Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Thu, 30 Jan 2025 08:22:28 -0500 Subject: [PATCH 0804/1275] Enable NRT on `Column` --- osu.Game.Rulesets.Mania/ManiaInputManager.cs | 2 +- osu.Game.Rulesets.Mania/UI/Column.cs | 19 +++++++++---------- 2 files changed, 10 insertions(+), 11 deletions(-) diff --git a/osu.Game.Rulesets.Mania/ManiaInputManager.cs b/osu.Game.Rulesets.Mania/ManiaInputManager.cs index 36ccf68d76..e8c993a91b 100644 --- a/osu.Game.Rulesets.Mania/ManiaInputManager.cs +++ b/osu.Game.Rulesets.Mania/ManiaInputManager.cs @@ -8,7 +8,7 @@ using osu.Game.Rulesets.UI; namespace osu.Game.Rulesets.Mania { - [Cached] // Used for touch input, see ColumnTouchInputArea. + [Cached] // Used for touch input, see Column.OnTouchDown/OnTouchUp. public partial class ManiaInputManager : RulesetInputManager { public ManiaInputManager(RulesetInfo ruleset, int variant) diff --git a/osu.Game.Rulesets.Mania/UI/Column.cs b/osu.Game.Rulesets.Mania/UI/Column.cs index 81f4d79281..5425965897 100644 --- a/osu.Game.Rulesets.Mania/UI/Column.cs +++ b/osu.Game.Rulesets.Mania/UI/Column.cs @@ -1,10 +1,9 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Framework.Allocation; using osu.Framework.Bindables; +using osu.Framework.Extensions.ObjectExtensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Pooling; @@ -45,11 +44,11 @@ namespace osu.Game.Rulesets.Mania.UI internal readonly Container TopLevelContainer = new Container { RelativeSizeAxes = Axes.Both }; - private DrawablePool hitExplosionPool; + private DrawablePool hitExplosionPool = null!; private readonly OrderedHitPolicy hitPolicy; public Container UnderlayElements => HitObjectArea.UnderlayElements; - private GameplaySampleTriggerSource sampleTriggerSource; + private GameplaySampleTriggerSource sampleTriggerSource = null!; /// /// Whether this is a special (ie. scratch) column. @@ -75,7 +74,7 @@ namespace osu.Game.Rulesets.Mania.UI } [Resolved] - private ISkinSource skin { get; set; } + private ISkinSource skin { get; set; } = null!; [BackgroundDependencyLoader] private void load(GameHost host) @@ -136,7 +135,7 @@ namespace osu.Game.Rulesets.Mania.UI base.Dispose(isDisposing); - if (skin != null) + if (skin.IsNotNull()) skin.SourceChanged -= onSourceChanged; } @@ -187,14 +186,14 @@ namespace osu.Game.Rulesets.Mania.UI #region Touch Input - [Resolved(canBeNull: true)] - private ManiaInputManager maniaInputManager { get; set; } + [Resolved] + private ManiaInputManager? maniaInputManager { get; set; } private int touchActivationCount; protected override bool OnTouchDown(TouchDownEvent e) { - maniaInputManager.KeyBindingContainer.TriggerPressed(Action.Value); + maniaInputManager?.KeyBindingContainer.TriggerPressed(Action.Value); touchActivationCount++; return true; } @@ -204,7 +203,7 @@ namespace osu.Game.Rulesets.Mania.UI touchActivationCount--; if (touchActivationCount == 0) - maniaInputManager.KeyBindingContainer.TriggerReleased(Action.Value); + maniaInputManager?.KeyBindingContainer.TriggerReleased(Action.Value); } #endregion From 261a7e537b0451f34725c376af345ff8fdd131f0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 30 Jan 2025 14:42:44 +0100 Subject: [PATCH 0805/1275] Fix distance snap time part ceasing to work when grid snap is also active As pointed out in https://github.com/ppy/osu/pull/31655#discussion_r1935536934. --- osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs b/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs index 194276baf9..e08968e1aa 100644 --- a/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs +++ b/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs @@ -273,7 +273,7 @@ namespace osu.Game.Rulesets.Osu.Edit pos = Vector2.Clamp(pos, Vector2.Zero, OsuPlayfield.BASE_SIZE); var playfield = PlayfieldAtScreenSpacePosition(screenSpacePosition); - return new SnapResult(positionSnapGrid.ToScreenSpace(pos), null, playfield); + return new SnapResult(positionSnapGrid.ToScreenSpace(pos), fallbackTime, playfield); } private bool snapToVisibleBlueprints(Vector2 screenSpacePosition, out SnapResult snapResult) From 2ee480c442436bb442b8b6171e2f42b86c3cbfa8 Mon Sep 17 00:00:00 2001 From: James Wilson Date: Thu, 30 Jan 2025 13:58:38 +0000 Subject: [PATCH 0806/1275] Clamp `estimateImproperlyFollowedDifficultSliders` between 0 and `attributes.AimDifficultSliderCount` (#31736) --- osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs b/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs index f191180630..dc2df39cdb 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs @@ -161,7 +161,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty { // We add tick misses here since they too mean that the player didn't follow the slider properly // We however aren't adding misses here because missing slider heads has a harsh penalty by itself and doesn't mean that the rest of the slider wasn't followed properly - estimateImproperlyFollowedDifficultSliders = Math.Min(countSliderEndsDropped + countSliderTickMiss, attributes.AimDifficultSliderCount); + estimateImproperlyFollowedDifficultSliders = Math.Clamp(countSliderEndsDropped + countSliderTickMiss, 0, attributes.AimDifficultSliderCount); } double sliderNerfFactor = (1 - attributes.SliderFactor) * Math.Pow(1 - estimateImproperlyFollowedDifficultSliders / attributes.AimDifficultSliderCount, 3) + attributes.SliderFactor; From b4f63da048e16c9f0fd0d339ea13f33637dade9c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 30 Jan 2025 15:23:22 +0100 Subject: [PATCH 0807/1275] Move control point double-up logic to `LegacyBeatmapExporter` Done for two reasons: - During review it was requested for the logic to be moved out of `BezierConverter` as `BezierConverter` was intended to produce "lazer style" sliders with per-control-point curve types, as a future usability / code layering concern. - It is also relevant for encode-decode stability. With how the logic was structured between the Bezier converter and the legacy beatmap encoder, the encoder would leave behind per-control-point Bezier curve specs that stable ignored, but subsequent encodes and decodes in lazer would end up multiplying the doubled-up control points ad nauseam. Instead, it is sufficient to only specify the curve type for the head control point as Bezier, not specify any further curve types later on, and instead just keep the double-up-control-point for new implicit segment logic which is enough to make stable cooperate (and also as close to outputting the slider exactly as stable would have produced it as we've ever been) --- osu.Game/Database/LegacyBeatmapExporter.cs | 32 ++++++++++++++------ osu.Game/Rulesets/Objects/BezierConverter.cs | 4 --- 2 files changed, 22 insertions(+), 14 deletions(-) diff --git a/osu.Game/Database/LegacyBeatmapExporter.cs b/osu.Game/Database/LegacyBeatmapExporter.cs index 24e752da31..9bb90ab461 100644 --- a/osu.Game/Database/LegacyBeatmapExporter.cs +++ b/osu.Game/Database/LegacyBeatmapExporter.cs @@ -120,18 +120,30 @@ namespace osu.Game.Database if (BezierConverter.CountSegments(hasPath.Path.ControlPoints) <= 1 && hasPath.Path.ControlPoints[0].Type!.Value.Degree == null) continue; - var newControlPoints = BezierConverter.ConvertToModernBezier(hasPath.Path.ControlPoints); - - // Truncate control points to integer positions - foreach (var pathControlPoint in newControlPoints) - { - pathControlPoint.Position = new Vector2( - (float)Math.Floor(pathControlPoint.Position.X), - (float)Math.Floor(pathControlPoint.Position.Y)); - } + var convertedToBezier = BezierConverter.ConvertToModernBezier(hasPath.Path.ControlPoints); hasPath.Path.ControlPoints.Clear(); - hasPath.Path.ControlPoints.AddRange(newControlPoints); + + for (int i = 0; i < convertedToBezier.Count; i++) + { + var convertedPoint = convertedToBezier[i]; + + // Truncate control points to integer positions + var position = new Vector2( + (float)Math.Floor(convertedPoint.Position.X), + (float)Math.Floor(convertedPoint.Position.Y)); + + // stable only supports a single curve type specification per slider. + // we exploit the fact that the converted-to-Bézier path only has Bézier segments, + // and thus we specify the Bézier curve type once ever at the start of the slider. + hasPath.Path.ControlPoints.Add(new PathControlPoint(position, i == 0 ? PathType.BEZIER : null)); + + // however, the Bézier path as output by the converter has multiple segments. + // `LegacyBeatmapEncoder` will attempt to encode this by emitting per-control-point curve type specs which don't do anything for stable. + // instead, stable expects control points that start a segment to be present in the path twice in succession. + if (convertedPoint.Type == PathType.BEZIER) + hasPath.Path.ControlPoints.Add(new PathControlPoint(position)); + } } // Encode to legacy format diff --git a/osu.Game/Rulesets/Objects/BezierConverter.cs b/osu.Game/Rulesets/Objects/BezierConverter.cs index 384c686167..638975630e 100644 --- a/osu.Game/Rulesets/Objects/BezierConverter.cs +++ b/osu.Game/Rulesets/Objects/BezierConverter.cs @@ -136,7 +136,6 @@ namespace osu.Game.Rulesets.Objects { for (int j = 0; j < segment.Length - 1; j++) { - if (result.Count > 0 && j == 0) result.Add(new PathControlPoint(segment[j])); result.Add(new PathControlPoint(segment[j], j == 0 ? PathType.BEZIER : null)); } } @@ -148,7 +147,6 @@ namespace osu.Game.Rulesets.Objects { for (int j = 0; j < segment.Length - 1; j++) { - if (result.Count > 0 && j == 0) result.Add(new PathControlPoint(segment[j])); result.Add(new PathControlPoint(segment[j], j == 0 ? PathType.BEZIER : null)); } } @@ -160,7 +158,6 @@ namespace osu.Game.Rulesets.Objects for (int j = 0; j < circleResult.Length - 1; j++) { - if (result.Count > 0 && j == 0) result.Add(new PathControlPoint(circleResult[j])); result.Add(new PathControlPoint(circleResult[j], j == 0 ? PathType.BEZIER : null)); } @@ -173,7 +170,6 @@ namespace osu.Game.Rulesets.Objects for (int j = 0; j < bSplineResult.Length - 1; j++) { - if (result.Count > 0 && j == 0) result.Add(new PathControlPoint(bSplineResult[j])); result.Add(new PathControlPoint(bSplineResult[j], j == 0 ? PathType.BEZIER : null)); } From 4a164b7b149ff7c8f78d15f904fcb61673ac9ff8 Mon Sep 17 00:00:00 2001 From: Joppe27 Date: Sun, 11 Dec 2022 02:17:50 +0100 Subject: [PATCH 0808/1275] Add legacy taiko swell --- .../Objects/Drawables/DrawableSwell.cs | 113 ++------------ .../Objects/ISkinnableSwell.cs | 22 +++ .../Argon/TaikoArgonSkinTransformer.cs | 2 +- .../Skinning/Default/DefaultSwell.cs | 142 ++++++++++++++++++ .../Skinning/Legacy/LegacySwell.cs | 136 +++++++++++++++++ .../Skinning/Legacy/LegacySwellCirclePiece.cs | 23 +++ .../Legacy/TaikoLegacySkinTransformer.cs | 10 +- .../TaikoSkinComponents.cs | 1 + 8 files changed, 348 insertions(+), 101 deletions(-) create mode 100644 osu.Game.Rulesets.Taiko/Objects/ISkinnableSwell.cs create mode 100644 osu.Game.Rulesets.Taiko/Skinning/Default/DefaultSwell.cs create mode 100644 osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacySwell.cs create mode 100644 osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacySwellCirclePiece.cs diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableSwell.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableSwell.cs index 28617b35f6..cba044959c 100644 --- a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableSwell.cs +++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableSwell.cs @@ -6,14 +6,9 @@ using System; using System.Linq; using JetBrains.Annotations; -using osu.Framework.Allocation; -using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; -using osu.Game.Graphics; using osu.Game.Rulesets.Objects.Drawables; -using osuTK.Graphics; -using osu.Framework.Graphics.Shapes; using osu.Framework.Input.Events; using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Taiko.Skinning.Default; @@ -25,11 +20,6 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables { public partial class DrawableSwell : DrawableTaikoHitObject { - private const float target_ring_thick_border = 1.4f; - private const float target_ring_thin_border = 1f; - private const float target_ring_scale = 5f; - private const float inner_ring_alpha = 0.65f; - /// /// Offset away from the start time of the swell at which the ring starts appearing. /// @@ -37,10 +27,9 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables private Vector2 baseSize; + private readonly SkinnableDrawable spinnerBody; + private readonly Container ticks; - private readonly Container bodyContainer; - private readonly CircularContainer targetRing; - private readonly CircularContainer expandingRing; private double? lastPressHandleTime; @@ -61,82 +50,17 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables { FillMode = FillMode.Fit; - Content.Add(bodyContainer = new Container + Content.Add(spinnerBody = new SkinnableDrawable(new TaikoSkinComponentLookup(TaikoSkinComponents.Swell), _ => new DefaultSwell()) { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, RelativeSizeAxes = Axes.Both, - Depth = 1, - Children = new Drawable[] - { - expandingRing = new CircularContainer - { - Name = "Expanding ring", - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Alpha = 0, - RelativeSizeAxes = Axes.Both, - Blending = BlendingParameters.Additive, - Masking = true, - Children = new[] - { - new Box - { - RelativeSizeAxes = Axes.Both, - Alpha = inner_ring_alpha, - } - } - }, - targetRing = new CircularContainer - { - Name = "Target ring (thick border)", - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - RelativeSizeAxes = Axes.Both, - Masking = true, - BorderThickness = target_ring_thick_border, - Blending = BlendingParameters.Additive, - Children = new Drawable[] - { - new Box - { - RelativeSizeAxes = Axes.Both, - Alpha = 0, - AlwaysPresent = true - }, - new CircularContainer - { - Name = "Target ring (thin border)", - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - RelativeSizeAxes = Axes.Both, - Masking = true, - BorderThickness = target_ring_thin_border, - BorderColour = Color4.White, - Children = new[] - { - new Box - { - RelativeSizeAxes = Axes.Both, - Alpha = 0, - AlwaysPresent = true - } - } - } - } - } - } }); AddInternal(ticks = new Container { RelativeSizeAxes = Axes.Both }); } - [BackgroundDependencyLoader] - private void load(OsuColour colours) - { - expandingRing.Colour = colours.YellowLight; - targetRing.BorderColour = colours.YellowDark.Opacity(0.25f); - } - - protected override SkinnableDrawable CreateMainPiece() => new SkinnableDrawable(new TaikoSkinComponentLookup(TaikoSkinComponents.Swell), + protected override SkinnableDrawable CreateMainPiece() => new SkinnableDrawable(new TaikoSkinComponentLookup(TaikoSkinComponents.SwellCirclePiece), _ => new SwellCirclePiece { // to allow for rotation transform @@ -208,16 +132,7 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables int numHits = ticks.Count(r => r.IsHit); - float completion = (float)numHits / HitObject.RequiredHits; - - expandingRing - .FadeTo(expandingRing.Alpha + Math.Clamp(completion / 16, 0.1f, 0.6f), 50) - .Then() - .FadeTo(completion / 8, 2000, Easing.OutQuint); - - MainPiece.Drawable.RotateTo((float)(completion * HitObject.Duration / 8), 4000, Easing.OutQuint); - - expandingRing.ScaleTo(1f + Math.Min(target_ring_scale - 1f, (target_ring_scale - 1f) * completion * 1.3f), 260, Easing.OutQuint); + (spinnerBody.Drawable as ISkinnableSwell)?.OnUserInput(this, numHits, MainPiece); if (numHits == HitObject.RequiredHits) ApplyMaxResult(); @@ -252,24 +167,24 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables { base.UpdateStartTimeStateTransforms(); - using (BeginDelayedSequence(-ring_appear_offset)) - targetRing.ScaleTo(target_ring_scale, 400, Easing.OutQuint); + (spinnerBody.Drawable as ISkinnableSwell)?.ApplyPassiveTransforms(this, MainPiece); } protected override void UpdateHitStateTransforms(ArmedState state) { - const double transition_duration = 300; - switch (state) { case ArmedState.Idle: - expandingRing.FadeTo(0); + HandleUserInput = true; break; case ArmedState.Miss: case ArmedState.Hit: - this.FadeOut(transition_duration, Easing.Out); - bodyContainer.ScaleTo(1.4f, transition_duration); + // Postpone drawable hitobject expiration until it has animated/faded out. Inputs on the object are disallowed during this delay. + LifetimeEnd = Time.Current + 1200; + HandleUserInput = false; + + (spinnerBody.Drawable as ISkinnableSwell)?.OnHitObjectEnd(state, MainPiece); break; } } diff --git a/osu.Game.Rulesets.Taiko/Objects/ISkinnableSwell.cs b/osu.Game.Rulesets.Taiko/Objects/ISkinnableSwell.cs new file mode 100644 index 0000000000..18feff5bb9 --- /dev/null +++ b/osu.Game.Rulesets.Taiko/Objects/ISkinnableSwell.cs @@ -0,0 +1,22 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Game.Rulesets.Objects.Drawables; +using osu.Game.Rulesets.Taiko.Objects.Drawables; +using osu.Game.Skinning; + +namespace osu.Game.Rulesets.Taiko.Objects +{ + public interface ISkinnableSwell + { + void OnUserInput(DrawableTaikoHitObject swell, int numHits, SkinnableDrawable mainPiece); + + void OnHitObjectEnd(ArmedState state, SkinnableDrawable mainPiece); + + /// + /// Applies passive transforms on HitObject start. Gets called every time DrawableTaikoHitobject + /// changes state. This happens on creation, and when the object is completed (as in hit or missed). + /// + void ApplyPassiveTransforms(DrawableTaikoHitObject swell, SkinnableDrawable mainPiece); + } +} diff --git a/osu.Game.Rulesets.Taiko/Skinning/Argon/TaikoArgonSkinTransformer.cs b/osu.Game.Rulesets.Taiko/Skinning/Argon/TaikoArgonSkinTransformer.cs index bfc9e8648d..cfd30dd628 100644 --- a/osu.Game.Rulesets.Taiko/Skinning/Argon/TaikoArgonSkinTransformer.cs +++ b/osu.Game.Rulesets.Taiko/Skinning/Argon/TaikoArgonSkinTransformer.cs @@ -68,7 +68,7 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Argon case TaikoSkinComponents.TaikoExplosionOk: return new ArgonHitExplosion(taikoComponent.Component); - case TaikoSkinComponents.Swell: + case TaikoSkinComponents.SwellCirclePiece: return new ArgonSwellCirclePiece(); } diff --git a/osu.Game.Rulesets.Taiko/Skinning/Default/DefaultSwell.cs b/osu.Game.Rulesets.Taiko/Skinning/Default/DefaultSwell.cs new file mode 100644 index 0000000000..e525e9873d --- /dev/null +++ b/osu.Game.Rulesets.Taiko/Skinning/Default/DefaultSwell.cs @@ -0,0 +1,142 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using osu.Framework.Allocation; +using osu.Framework.Extensions.Color4Extensions; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; +using osu.Game.Graphics; +using osu.Game.Rulesets.Objects.Drawables; +using osu.Game.Rulesets.Taiko.Objects; +using osu.Game.Rulesets.Taiko.Objects.Drawables; +using osu.Game.Skinning; +using osuTK.Graphics; + +namespace osu.Game.Rulesets.Taiko.Skinning.Default +{ + public partial class DefaultSwell : Container, ISkinnableSwell + { + private const float target_ring_thick_border = 1.4f; + private const float target_ring_thin_border = 1f; + private const float target_ring_scale = 5f; + private const float inner_ring_alpha = 0.65f; + + private readonly Container bodyContainer; + private readonly CircularContainer targetRing; + private readonly CircularContainer expandingRing; + + public DefaultSwell() + { + Anchor = Anchor.Centre; + Origin = Anchor.Centre; + RelativeSizeAxes = Axes.Both; + + Content.Add(bodyContainer = new Container + { + RelativeSizeAxes = Axes.Both, + Depth = 1, + Children = new Drawable[] + { + expandingRing = new CircularContainer + { + Name = "Expanding ring", + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Alpha = 0, + RelativeSizeAxes = Axes.Both, + Blending = BlendingParameters.Additive, + Masking = true, + Children = new[] + { + new Box + { + RelativeSizeAxes = Axes.Both, + Alpha = inner_ring_alpha, + } + } + }, + targetRing = new CircularContainer + { + Name = "Target ring (thick border)", + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + RelativeSizeAxes = Axes.Both, + Masking = true, + BorderThickness = target_ring_thick_border, + Blending = BlendingParameters.Additive, + Children = new Drawable[] + { + new Box + { + RelativeSizeAxes = Axes.Both, + Alpha = 0, + AlwaysPresent = true + }, + new CircularContainer + { + Name = "Target ring (thin border)", + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + RelativeSizeAxes = Axes.Both, + Masking = true, + BorderThickness = target_ring_thin_border, + BorderColour = Color4.White, + Children = new[] + { + new Box + { + RelativeSizeAxes = Axes.Both, + Alpha = 0, + AlwaysPresent = true + } + } + } + } + } + } + }); + } + + [BackgroundDependencyLoader] + private void load(OsuColour colours) + { + expandingRing.Colour = colours.YellowLight; + targetRing.BorderColour = colours.YellowDark.Opacity(0.25f); + } + + public void OnUserInput(DrawableTaikoHitObject swell, int numHits, SkinnableDrawable mainPiece) + { + float completion = (float)numHits / swell.HitObject.RequiredHits; + + mainPiece.Drawable.RotateTo((float)(completion * swell.HitObject.Duration / 8), 4000, Easing.OutQuint); + + expandingRing.ScaleTo(1f + Math.Min(target_ring_scale - 1f, (target_ring_scale - 1f) * completion * 1.3f), 260, Easing.OutQuint); + + expandingRing + .FadeTo(expandingRing.Alpha + Math.Clamp(completion / 16, 0.1f, 0.6f), 50) + .Then() + .FadeTo(completion / 8, 2000, Easing.OutQuint); + } + + public void OnHitObjectEnd(ArmedState state, SkinnableDrawable mainPiece) + { + const double transition_duration = 300; + + bodyContainer.FadeOut(transition_duration, Easing.OutQuad); + bodyContainer.ScaleTo(1.4f, transition_duration); + mainPiece.FadeOut(transition_duration, Easing.OutQuad); + } + + public void ApplyPassiveTransforms(DrawableTaikoHitObject swell, SkinnableDrawable mainPiece) + { + if (swell.IsHit == false) + expandingRing.FadeTo(0); + + const double ring_appear_offset = 100; + + targetRing.Delay(ring_appear_offset).ScaleTo(target_ring_scale, 400, Easing.OutQuint); + } + } +} diff --git a/osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacySwell.cs b/osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacySwell.cs new file mode 100644 index 0000000000..240ec71f94 --- /dev/null +++ b/osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacySwell.cs @@ -0,0 +1,136 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Graphics; +using osu.Framework.Allocation; +using osu.Framework.Graphics.Containers; +using osu.Game.Skinning; +using osu.Framework.Graphics.Sprites; +using osu.Game.Rulesets.Taiko.Objects.Drawables; +using osu.Game.Rulesets.Taiko.Objects; +using osu.Framework.Audio.Sample; +using osu.Game.Audio; +using osuTK; +using osu.Game.Rulesets.Objects.Drawables; + +namespace osu.Game.Rulesets.Taiko.Skinning.Legacy +{ + public partial class LegacySwell : Container, ISkinnableSwell + { + private Container bodyContainer = null!; + private Sprite spinnerCircle = null!; + private Sprite shrinkingRing = null!; + private Sprite clearAnimation = null!; + private ISample? clearSample; + private LegacySpriteText remainingHitsCountdown = null!; + + private bool samplePlayed; + + public LegacySwell() + { + Anchor = Anchor.Centre; + Origin = Anchor.Centre; + RelativeSizeAxes = Axes.Both; + } + + [BackgroundDependencyLoader] + private void load(ISkinSource skin, SkinManager skinManager) + { + Child = new Container + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Position = new Vector2(200f, 100f), + + Children = new Drawable[] + { + bodyContainer = new Container + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Alpha = 0, + + Children = new Drawable[] + { + spinnerCircle = new Sprite + { + Texture = skin.GetTexture("spinner-circle"), + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Scale = new Vector2(0.8f), + }, + shrinkingRing = new Sprite + { + Texture = skin.GetTexture("spinner-approachcircle") ?? skinManager.DefaultClassicSkin.GetTexture("spinner-approachcircle"), + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Scale = Vector2.One, + }, + remainingHitsCountdown = new LegacySpriteText(LegacyFont.Combo) + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Position = new Vector2(0f, 165f), + Scale = Vector2.One, + }, + } + }, + clearAnimation = new Sprite + { + // File extension is included here because of a GetTexture limitation, see #21543 + Texture = skin.GetTexture("spinner-osu.png"), + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Position = new Vector2(0f, -165f), + Scale = new Vector2(0.3f), + Alpha = 0, + }, + } + }; + + clearSample = skin.GetSample(new SampleInfo("spinner-osu")); + } + + public void OnUserInput(DrawableTaikoHitObject swell, int numHits, SkinnableDrawable mainPiece) + { + remainingHitsCountdown.Text = $"{swell.HitObject.RequiredHits - numHits}"; + spinnerCircle.RotateTo(180f * numHits, 1000, Easing.OutQuint); + } + + public void OnHitObjectEnd(ArmedState state, SkinnableDrawable mainPiece) + { + const double clear_transition_duration = 300; + + bodyContainer.FadeOut(clear_transition_duration, Easing.OutQuad); + + if (state == ArmedState.Hit) + { + if (!samplePlayed) + { + clearSample?.Play(); + samplePlayed = true; + } + + clearAnimation + .FadeIn(clear_transition_duration, Easing.InQuad) + .ScaleTo(0.8f, clear_transition_duration, Easing.InQuad) + .Delay(700).FadeOut(200, Easing.OutQuad); + } + } + + public void ApplyPassiveTransforms(DrawableTaikoHitObject swell, SkinnableDrawable mainPiece) + { + if (swell.IsHit == false) + { + remainingHitsCountdown.Text = $"{swell.HitObject.RequiredHits}"; + samplePlayed = false; + } + + const double body_transition_duration = 100; + + mainPiece.FadeOut(body_transition_duration); + bodyContainer.FadeIn(body_transition_duration); + shrinkingRing.ResizeTo(0.1f, swell.HitObject.Duration); + } + } +} diff --git a/osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacySwellCirclePiece.cs b/osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacySwellCirclePiece.cs new file mode 100644 index 0000000000..40501d1d40 --- /dev/null +++ b/osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacySwellCirclePiece.cs @@ -0,0 +1,23 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Allocation; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Sprites; +using osu.Game.Skinning; +using osuTK; + +namespace osu.Game.Rulesets.Taiko.Skinning.Legacy +{ + internal partial class LegacySwellCirclePiece : Sprite + { + [BackgroundDependencyLoader] + private void load(ISkinSource skin, SkinManager skinManager) + { + Texture = skin.GetTexture("spinner-warning") ?? skinManager.DefaultClassicSkin.GetTexture("spinner-circle"); + Anchor = Anchor.Centre; + Origin = Anchor.Centre; + Scale = skin.GetTexture("spinner-warning") != null ? Vector2.One : new Vector2(0.18f); + } + } +} diff --git a/osu.Game.Rulesets.Taiko/Skinning/Legacy/TaikoLegacySkinTransformer.cs b/osu.Game.Rulesets.Taiko/Skinning/Legacy/TaikoLegacySkinTransformer.cs index 5bdb824f1c..243d975216 100644 --- a/osu.Game.Rulesets.Taiko/Skinning/Legacy/TaikoLegacySkinTransformer.cs +++ b/osu.Game.Rulesets.Taiko/Skinning/Legacy/TaikoLegacySkinTransformer.cs @@ -66,7 +66,15 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy return this.GetAnimation("sliderscorepoint", false, false); case TaikoSkinComponents.Swell: - // todo: support taiko legacy swell (https://github.com/ppy/osu/issues/13601). + if (GetTexture("spinner-circle") != null) + return new LegacySwell(); + + return null; + + case TaikoSkinComponents.SwellCirclePiece: + if (GetTexture("spinner-circle") != null) + return new LegacySwellCirclePiece(); + return null; case TaikoSkinComponents.HitTarget: diff --git a/osu.Game.Rulesets.Taiko/TaikoSkinComponents.cs b/osu.Game.Rulesets.Taiko/TaikoSkinComponents.cs index 28133ffcb2..aa7e4686d8 100644 --- a/osu.Game.Rulesets.Taiko/TaikoSkinComponents.cs +++ b/osu.Game.Rulesets.Taiko/TaikoSkinComponents.cs @@ -11,6 +11,7 @@ namespace osu.Game.Rulesets.Taiko DrumRollBody, DrumRollTick, Swell, + SwellCirclePiece, HitTarget, PlayfieldBackgroundLeft, PlayfieldBackgroundRight, From fe84e6e5f53d5a3264b1fcbe68fb698b7c039f48 Mon Sep 17 00:00:00 2001 From: Joppe27 Date: Sun, 11 Dec 2022 02:19:06 +0100 Subject: [PATCH 0809/1275] Adjust existing test to accommodate swell size --- osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneDrawableSwell.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneDrawableSwell.cs b/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneDrawableSwell.cs index c130b5f366..286b16aa34 100644 --- a/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneDrawableSwell.cs +++ b/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneDrawableSwell.cs @@ -20,6 +20,7 @@ namespace osu.Game.Rulesets.Taiko.Tests.Skinning { Anchor = Anchor.Centre, Origin = Anchor.Centre, + Scale = new osuTK.Vector2(0.5f), })); } From 988450a2c4f8244d1ef1bc572d711ee781eaaa09 Mon Sep 17 00:00:00 2001 From: Joppe27 Date: Sun, 11 Dec 2022 02:23:48 +0100 Subject: [PATCH 0810/1275] Add test for expire delay Delaying the expiry of the drawable hitobject can potentially be dangerous and gameplay-altering when user inputs are accidentally handled. This is why I found a test necessary. --- .../TestSceneDrawableSwellExpireDelay.cs | 41 +++++++++++++++++++ 1 file changed, 41 insertions(+) create mode 100644 osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneDrawableSwellExpireDelay.cs diff --git a/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneDrawableSwellExpireDelay.cs b/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneDrawableSwellExpireDelay.cs new file mode 100644 index 0000000000..ad78ed3b20 --- /dev/null +++ b/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneDrawableSwellExpireDelay.cs @@ -0,0 +1,41 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Collections.Generic; +using NUnit.Framework; +using osu.Game.Rulesets.Replays; +using osu.Game.Rulesets.Scoring; +using osu.Game.Rulesets.Taiko.Objects; +using osu.Game.Rulesets.Taiko.Replays; +using osu.Game.Rulesets.Taiko.Tests.Judgements; + +namespace osu.Game.Rulesets.Taiko.Tests.Skinning +{ + public partial class TestSceneDrawableSwellExpireDelay : JudgementTest + { + [Test] + public void TestExpireDelay() + { + const double swell_start = 1000; + const double swell_duration = 1000; + + Swell swell = new Swell + { + StartTime = swell_start, + Duration = swell_duration, + }; + + Hit hit = new Hit { StartTime = swell_start + swell_duration + 50 }; + + List frames = new List + { + new TaikoReplayFrame(0), + new TaikoReplayFrame(2100, TaikoAction.LeftCentre), + }; + + PerformTest(frames, CreateBeatmap(swell, hit)); + + AssertResult(0, HitResult.Ok); + } + } +} From e2196e8b9b97f447863b61124f7bd3454a505e60 Mon Sep 17 00:00:00 2001 From: Joppe27 Date: Tue, 13 Dec 2022 19:32:05 +0100 Subject: [PATCH 0811/1275] Rename methods and skin component + add comments --- .../Objects/Drawables/DrawableSwell.cs | 13 ++++++++----- osu.Game.Rulesets.Taiko/Objects/ISkinnableSwell.cs | 10 +++------- .../Skinning/Default/DefaultSwell.cs | 6 +++--- .../Skinning/Legacy/LegacySwell.cs | 6 +++--- .../Skinning/Legacy/TaikoLegacySkinTransformer.cs | 2 +- osu.Game.Rulesets.Taiko/TaikoSkinComponents.cs | 2 +- 6 files changed, 19 insertions(+), 20 deletions(-) diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableSwell.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableSwell.cs index cba044959c..54a609f7d3 100644 --- a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableSwell.cs +++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableSwell.cs @@ -50,7 +50,7 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables { FillMode = FillMode.Fit; - Content.Add(spinnerBody = new SkinnableDrawable(new TaikoSkinComponentLookup(TaikoSkinComponents.Swell), _ => new DefaultSwell()) + Content.Add(spinnerBody = new SkinnableDrawable(new TaikoSkinComponentLookup(TaikoSkinComponents.SwellBody), _ => new DefaultSwell()) { Anchor = Anchor.Centre, Origin = Anchor.Centre, @@ -132,7 +132,7 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables int numHits = ticks.Count(r => r.IsHit); - (spinnerBody.Drawable as ISkinnableSwell)?.OnUserInput(this, numHits, MainPiece); + (spinnerBody.Drawable as ISkinnableSwell)?.AnimateSwellProgress(this, numHits, MainPiece); if (numHits == HitObject.RequiredHits) ApplyMaxResult(); @@ -167,7 +167,7 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables { base.UpdateStartTimeStateTransforms(); - (spinnerBody.Drawable as ISkinnableSwell)?.ApplyPassiveTransforms(this, MainPiece); + (spinnerBody.Drawable as ISkinnableSwell)?.AnimateSwellStart(this, MainPiece); } protected override void UpdateHitStateTransforms(ArmedState state) @@ -175,16 +175,19 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables switch (state) { case ArmedState.Idle: + // Only for rewind support. Reallows user inputs if swell is rewound from being hit/missed to being idle. HandleUserInput = true; break; case ArmedState.Miss: case ArmedState.Hit: + const int clear_animation_duration = 1200; + // Postpone drawable hitobject expiration until it has animated/faded out. Inputs on the object are disallowed during this delay. - LifetimeEnd = Time.Current + 1200; + LifetimeEnd = Time.Current + clear_animation_duration; HandleUserInput = false; - (spinnerBody.Drawable as ISkinnableSwell)?.OnHitObjectEnd(state, MainPiece); + (spinnerBody.Drawable as ISkinnableSwell)?.AnimateSwellCompletion(state, MainPiece); break; } } diff --git a/osu.Game.Rulesets.Taiko/Objects/ISkinnableSwell.cs b/osu.Game.Rulesets.Taiko/Objects/ISkinnableSwell.cs index 18feff5bb9..3cdb3566fb 100644 --- a/osu.Game.Rulesets.Taiko/Objects/ISkinnableSwell.cs +++ b/osu.Game.Rulesets.Taiko/Objects/ISkinnableSwell.cs @@ -9,14 +9,10 @@ namespace osu.Game.Rulesets.Taiko.Objects { public interface ISkinnableSwell { - void OnUserInput(DrawableTaikoHitObject swell, int numHits, SkinnableDrawable mainPiece); + void AnimateSwellProgress(DrawableTaikoHitObject swell, int numHits, SkinnableDrawable mainPiece); - void OnHitObjectEnd(ArmedState state, SkinnableDrawable mainPiece); + void AnimateSwellCompletion(ArmedState state, SkinnableDrawable mainPiece); - /// - /// Applies passive transforms on HitObject start. Gets called every time DrawableTaikoHitobject - /// changes state. This happens on creation, and when the object is completed (as in hit or missed). - /// - void ApplyPassiveTransforms(DrawableTaikoHitObject swell, SkinnableDrawable mainPiece); + void AnimateSwellStart(DrawableTaikoHitObject swell, SkinnableDrawable mainPiece); } } diff --git a/osu.Game.Rulesets.Taiko/Skinning/Default/DefaultSwell.cs b/osu.Game.Rulesets.Taiko/Skinning/Default/DefaultSwell.cs index e525e9873d..cec07d8769 100644 --- a/osu.Game.Rulesets.Taiko/Skinning/Default/DefaultSwell.cs +++ b/osu.Game.Rulesets.Taiko/Skinning/Default/DefaultSwell.cs @@ -106,7 +106,7 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Default targetRing.BorderColour = colours.YellowDark.Opacity(0.25f); } - public void OnUserInput(DrawableTaikoHitObject swell, int numHits, SkinnableDrawable mainPiece) + public void AnimateSwellProgress(DrawableTaikoHitObject swell, int numHits, SkinnableDrawable mainPiece) { float completion = (float)numHits / swell.HitObject.RequiredHits; @@ -120,7 +120,7 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Default .FadeTo(completion / 8, 2000, Easing.OutQuint); } - public void OnHitObjectEnd(ArmedState state, SkinnableDrawable mainPiece) + public void AnimateSwellCompletion(ArmedState state, SkinnableDrawable mainPiece) { const double transition_duration = 300; @@ -129,7 +129,7 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Default mainPiece.FadeOut(transition_duration, Easing.OutQuad); } - public void ApplyPassiveTransforms(DrawableTaikoHitObject swell, SkinnableDrawable mainPiece) + public void AnimateSwellStart(DrawableTaikoHitObject swell, SkinnableDrawable mainPiece) { if (swell.IsHit == false) expandingRing.FadeTo(0); diff --git a/osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacySwell.cs b/osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacySwell.cs index 240ec71f94..fdddea2df5 100644 --- a/osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacySwell.cs +++ b/osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacySwell.cs @@ -91,13 +91,13 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy clearSample = skin.GetSample(new SampleInfo("spinner-osu")); } - public void OnUserInput(DrawableTaikoHitObject swell, int numHits, SkinnableDrawable mainPiece) + public void AnimateSwellProgress(DrawableTaikoHitObject swell, int numHits, SkinnableDrawable mainPiece) { remainingHitsCountdown.Text = $"{swell.HitObject.RequiredHits - numHits}"; spinnerCircle.RotateTo(180f * numHits, 1000, Easing.OutQuint); } - public void OnHitObjectEnd(ArmedState state, SkinnableDrawable mainPiece) + public void AnimateSwellCompletion(ArmedState state, SkinnableDrawable mainPiece) { const double clear_transition_duration = 300; @@ -118,7 +118,7 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy } } - public void ApplyPassiveTransforms(DrawableTaikoHitObject swell, SkinnableDrawable mainPiece) + public void AnimateSwellStart(DrawableTaikoHitObject swell, SkinnableDrawable mainPiece) { if (swell.IsHit == false) { diff --git a/osu.Game.Rulesets.Taiko/Skinning/Legacy/TaikoLegacySkinTransformer.cs b/osu.Game.Rulesets.Taiko/Skinning/Legacy/TaikoLegacySkinTransformer.cs index 243d975216..b9ebed6b80 100644 --- a/osu.Game.Rulesets.Taiko/Skinning/Legacy/TaikoLegacySkinTransformer.cs +++ b/osu.Game.Rulesets.Taiko/Skinning/Legacy/TaikoLegacySkinTransformer.cs @@ -65,7 +65,7 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy case TaikoSkinComponents.DrumRollTick: return this.GetAnimation("sliderscorepoint", false, false); - case TaikoSkinComponents.Swell: + case TaikoSkinComponents.SwellBody: if (GetTexture("spinner-circle") != null) return new LegacySwell(); diff --git a/osu.Game.Rulesets.Taiko/TaikoSkinComponents.cs b/osu.Game.Rulesets.Taiko/TaikoSkinComponents.cs index aa7e4686d8..0145fb6482 100644 --- a/osu.Game.Rulesets.Taiko/TaikoSkinComponents.cs +++ b/osu.Game.Rulesets.Taiko/TaikoSkinComponents.cs @@ -10,7 +10,7 @@ namespace osu.Game.Rulesets.Taiko RimHit, DrumRollBody, DrumRollTick, - Swell, + SwellBody, SwellCirclePiece, HitTarget, PlayfieldBackgroundLeft, From cf2d0e6911539a23f9f9ae41160b06b1bb52e91f Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Fri, 31 Jan 2025 16:22:37 +0900 Subject: [PATCH 0812/1275] Fix results screen sounds persisting after exit --- osu.Game/Screens/Ranking/ResultsScreen.cs | 107 ++++++++++++---------- 1 file changed, 57 insertions(+), 50 deletions(-) diff --git a/osu.Game/Screens/Ranking/ResultsScreen.cs b/osu.Game/Screens/Ranking/ResultsScreen.cs index 5e91171051..95dbfb2712 100644 --- a/osu.Game/Screens/Ranking/ResultsScreen.cs +++ b/osu.Game/Screens/Ranking/ResultsScreen.cs @@ -64,6 +64,7 @@ namespace osu.Game.Screens.Ranking private Drawable bottomPanel = null!; private Container detachedPanelContainer = null!; + private AudioContainer audioContainer = null!; private bool lastFetchCompleted; @@ -100,76 +101,80 @@ namespace osu.Game.Screens.Ranking popInSample = audio.Samples.Get(@"UI/overlay-pop-in"); - InternalChild = new PopoverContainer + InternalChild = audioContainer = new AudioContainer { RelativeSizeAxes = Axes.Both, - Child = new GridContainer + Child = new PopoverContainer { RelativeSizeAxes = Axes.Both, - Content = new[] + Child = new GridContainer { - new Drawable[] + RelativeSizeAxes = Axes.Both, + Content = new[] { - VerticalScrollContent = new VerticalScrollContainer + new Drawable[] { - RelativeSizeAxes = Axes.Both, - ScrollbarVisible = false, - Child = new Container + VerticalScrollContent = new VerticalScrollContainer { RelativeSizeAxes = Axes.Both, + ScrollbarVisible = false, + Child = new Container + { + RelativeSizeAxes = Axes.Both, + Children = new Drawable[] + { + StatisticsPanel = createStatisticsPanel().With(panel => + { + panel.RelativeSizeAxes = Axes.Both; + panel.Score.BindTarget = SelectedScore; + }), + ScorePanelList = new ScorePanelList + { + RelativeSizeAxes = Axes.Both, + SelectedScore = { BindTarget = SelectedScore }, + PostExpandAction = () => StatisticsPanel.ToggleVisibility() + }, + detachedPanelContainer = new Container + { + RelativeSizeAxes = Axes.Both + }, + } + } + }, + }, + new[] + { + bottomPanel = new Container + { + Anchor = Anchor.BottomLeft, + Origin = Anchor.BottomLeft, + RelativeSizeAxes = Axes.X, + Height = TwoLayerButton.SIZE_EXTENDED.Y, + Alpha = 0, Children = new Drawable[] { - StatisticsPanel = createStatisticsPanel().With(panel => - { - panel.RelativeSizeAxes = Axes.Both; - panel.Score.BindTarget = SelectedScore; - }), - ScorePanelList = new ScorePanelList + new Box { RelativeSizeAxes = Axes.Both, - SelectedScore = { BindTarget = SelectedScore }, - PostExpandAction = () => StatisticsPanel.ToggleVisibility() + Colour = Color4Extensions.FromHex("#333") }, - detachedPanelContainer = new Container + buttons = new FillFlowContainer { - RelativeSizeAxes = Axes.Both + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + AutoSizeAxes = Axes.Both, + Spacing = new Vector2(5), + Direction = FillDirection.Horizontal }, } } - }, - }, - new[] - { - bottomPanel = new Container - { - Anchor = Anchor.BottomLeft, - Origin = Anchor.BottomLeft, - RelativeSizeAxes = Axes.X, - Height = TwoLayerButton.SIZE_EXTENDED.Y, - Alpha = 0, - Children = new Drawable[] - { - new Box - { - RelativeSizeAxes = Axes.Both, - Colour = Color4Extensions.FromHex("#333") - }, - buttons = new FillFlowContainer - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - AutoSizeAxes = Axes.Both, - Spacing = new Vector2(5), - Direction = FillDirection.Horizontal - }, - } } + }, + RowDimensions = new[] + { + new Dimension(), + new Dimension(GridSizeMode.AutoSize) } - }, - RowDimensions = new[] - { - new Dimension(), - new Dimension(GridSizeMode.AutoSize) } } }; @@ -330,6 +335,8 @@ namespace osu.Game.Screens.Ranking if (!skipExitTransition) this.FadeOut(100); + + audioContainer.Volume.Value = 0; return false; } From 20280cd1959d0ceecff45f1e11a7aff3cedd5768 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 31 Jan 2025 09:01:42 +0100 Subject: [PATCH 0813/1275] Do not double up first control point of path --- osu.Game/Database/LegacyBeatmapExporter.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Database/LegacyBeatmapExporter.cs b/osu.Game/Database/LegacyBeatmapExporter.cs index 9bb90ab461..8f94fc9e63 100644 --- a/osu.Game/Database/LegacyBeatmapExporter.cs +++ b/osu.Game/Database/LegacyBeatmapExporter.cs @@ -141,7 +141,7 @@ namespace osu.Game.Database // however, the Bézier path as output by the converter has multiple segments. // `LegacyBeatmapEncoder` will attempt to encode this by emitting per-control-point curve type specs which don't do anything for stable. // instead, stable expects control points that start a segment to be present in the path twice in succession. - if (convertedPoint.Type == PathType.BEZIER) + if (convertedPoint.Type == PathType.BEZIER && i > 0) hasPath.Path.ControlPoints.Add(new PathControlPoint(position)); } } From 8718483c702e7a69a2314d9fd515297615cb6920 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 21 Jan 2025 15:18:59 +0100 Subject: [PATCH 0814/1275] Avoid moving already placed objects temporally when "limit distance snap to current time" is active --- .../HitCircles/HitCirclePlacementBlueprint.cs | 16 +++++++++++++++- .../Components/PathControlPointVisualiser.cs | 11 ++++++++++- .../Sliders/SliderPlacementBlueprint.cs | 12 ++++++++++-- .../Edit/OsuBlueprintContainer.cs | 15 +++++++++++++-- .../Edit/OsuHitObjectComposer.cs | 4 ++-- .../Editing/TestSceneDistanceSnapGrid.cs | 2 +- .../Components/CircularDistanceSnapGrid.cs | 9 +++------ .../Compose/Components/DistanceSnapGrid.cs | 19 +++++-------------- 8 files changed, 59 insertions(+), 29 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/HitCircles/HitCirclePlacementBlueprint.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/HitCircles/HitCirclePlacementBlueprint.cs index 93d79a50ab..61ed30259a 100644 --- a/osu.Game.Rulesets.Osu/Edit/Blueprints/HitCircles/HitCirclePlacementBlueprint.cs +++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/HitCircles/HitCirclePlacementBlueprint.cs @@ -2,10 +2,13 @@ // See the LICENCE file in the repository root for full licence text. using osu.Framework.Allocation; +using osu.Framework.Bindables; using osu.Framework.Input.Events; +using osu.Game.Configuration; using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Osu.Edit.Blueprints.HitCircles.Components; using osu.Game.Rulesets.Osu.Objects; +using osu.Game.Screens.Edit; using osuTK; using osuTK.Input; @@ -20,12 +23,23 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.HitCircles [Resolved] private OsuHitObjectComposer? composer { get; set; } + [Resolved] + private EditorClock? editorClock { get; set; } + + private Bindable limitedDistanceSnap { get; set; } = null!; + public HitCirclePlacementBlueprint() : base(new HitCircle()) { InternalChild = circlePiece = new HitCirclePiece(); } + [BackgroundDependencyLoader] + private void load(OsuConfigManager config) + { + limitedDistanceSnap = config.GetBindable(OsuSetting.EditorLimitedDistanceSnap); + } + protected override void LoadComplete() { base.LoadComplete(); @@ -53,7 +67,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.HitCircles public override SnapResult UpdateTimeAndPosition(Vector2 screenSpacePosition, double fallbackTime) { var result = composer?.TrySnapToNearbyObjects(screenSpacePosition, fallbackTime); - result ??= composer?.TrySnapToDistanceGrid(screenSpacePosition); + result ??= composer?.TrySnapToDistanceGrid(screenSpacePosition, limitedDistanceSnap.Value && editorClock != null ? editorClock.CurrentTime : null); if (composer?.TrySnapToPositionGrid(result?.ScreenSpacePosition ?? screenSpacePosition, result?.Time ?? fallbackTime) is SnapResult gridSnapResult) result = gridSnapResult; result ??= new SnapResult(screenSpacePosition, fallbackTime); diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs index 189bb005a7..b9938209ae 100644 --- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs +++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs @@ -21,6 +21,7 @@ using osu.Framework.Input; using osu.Framework.Input.Bindings; using osu.Framework.Input.Events; using osu.Framework.Utils; +using osu.Game.Configuration; using osu.Game.Graphics.UserInterface; using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Objects; @@ -55,6 +56,8 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components [Resolved(CanBeNull = true)] private IDistanceSnapProvider distanceSnapProvider { get; set; } + private Bindable limitedDistanceSnap { get; set; } = null!; + public PathControlPointVisualiser(T hitObject, bool allowSelection) { this.hitObject = hitObject; @@ -69,6 +72,12 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components }; } + [BackgroundDependencyLoader] + private void load(OsuConfigManager config) + { + limitedDistanceSnap = config.GetBindable(OsuSetting.EditorLimitedDistanceSnap); + } + protected override void LoadComplete() { base.LoadComplete(); @@ -437,7 +446,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components Vector2 newHeadPosition = Parent!.ToScreenSpace(e.MousePosition + (dragStartPositions[0] - dragStartPositions[draggedControlPointIndex])); var result = positionSnapProvider?.TrySnapToNearbyObjects(newHeadPosition, oldStartTime); - result ??= positionSnapProvider?.TrySnapToDistanceGrid(newHeadPosition); + result ??= positionSnapProvider?.TrySnapToDistanceGrid(newHeadPosition, limitedDistanceSnap.Value ? oldStartTime : null); if (positionSnapProvider?.TrySnapToPositionGrid(result?.ScreenSpacePosition ?? newHeadPosition, result?.Time ?? oldStartTime) is SnapResult gridSnapResult) result = gridSnapResult; result ??= new SnapResult(newHeadPosition, oldStartTime); diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderPlacementBlueprint.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderPlacementBlueprint.cs index 1012578375..21817045c4 100644 --- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderPlacementBlueprint.cs +++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderPlacementBlueprint.cs @@ -5,10 +5,12 @@ using System; using System.Collections.Generic; using System.Linq; using osu.Framework.Allocation; +using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Input; using osu.Framework.Input.Events; using osu.Framework.Utils; +using osu.Game.Configuration; using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects.Types; @@ -49,6 +51,11 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders [Resolved] private FreehandSliderToolboxGroup? freehandToolboxGroup { get; set; } + [Resolved] + private EditorClock? editorClock { get; set; } + + private Bindable limitedDistanceSnap { get; set; } = null!; + private readonly IncrementalBSplineBuilder bSplineBuilder = new IncrementalBSplineBuilder { Degree = 4 }; protected override bool IsValidForPlacement => HitObject.Path.HasValidLength; @@ -63,7 +70,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders } [BackgroundDependencyLoader] - private void load() + private void load(OsuConfigManager config) { InternalChildren = new Drawable[] { @@ -74,6 +81,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders }; state = SliderPlacementState.Initial; + limitedDistanceSnap = config.GetBindable(OsuSetting.EditorLimitedDistanceSnap); } protected override void LoadComplete() @@ -109,7 +117,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders public override SnapResult UpdateTimeAndPosition(Vector2 screenSpacePosition, double fallbackTime) { var result = composer?.TrySnapToNearbyObjects(screenSpacePosition, fallbackTime); - result ??= composer?.TrySnapToDistanceGrid(screenSpacePosition); + result ??= composer?.TrySnapToDistanceGrid(screenSpacePosition, limitedDistanceSnap.Value && editorClock != null ? editorClock.CurrentTime : null); if (composer?.TrySnapToPositionGrid(result?.ScreenSpacePosition ?? screenSpacePosition, result?.Time ?? fallbackTime) is SnapResult gridSnapResult) result = gridSnapResult; result ??= new SnapResult(screenSpacePosition, fallbackTime); diff --git a/osu.Game.Rulesets.Osu/Edit/OsuBlueprintContainer.cs b/osu.Game.Rulesets.Osu/Edit/OsuBlueprintContainer.cs index 5eff95adec..9d82046c23 100644 --- a/osu.Game.Rulesets.Osu/Edit/OsuBlueprintContainer.cs +++ b/osu.Game.Rulesets.Osu/Edit/OsuBlueprintContainer.cs @@ -3,7 +3,10 @@ using System.Collections.Generic; using System.Linq; +using osu.Framework.Allocation; +using osu.Framework.Bindables; using osu.Framework.Input.Events; +using osu.Game.Configuration; using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Osu.Edit.Blueprints.HitCircles; @@ -17,6 +20,8 @@ namespace osu.Game.Rulesets.Osu.Edit { public partial class OsuBlueprintContainer : ComposeBlueprintContainer { + private Bindable limitedDistanceSnap { get; set; } = null!; + public new OsuHitObjectComposer Composer => (OsuHitObjectComposer)base.Composer; public OsuBlueprintContainer(OsuHitObjectComposer composer) @@ -24,6 +29,12 @@ namespace osu.Game.Rulesets.Osu.Edit { } + [BackgroundDependencyLoader] + private void load(OsuConfigManager config) + { + limitedDistanceSnap = config.GetBindable(OsuSetting.EditorLimitedDistanceSnap); + } + protected override SelectionHandler CreateSelectionHandler() => new OsuSelectionHandler(); public override HitObjectSelectionBlueprint? CreateHitObjectBlueprintFor(HitObject hitObject) @@ -58,15 +69,15 @@ namespace osu.Game.Rulesets.Osu.Edit // The final movement position, relative to movementBlueprintOriginalPosition. Vector2 movePosition = blueprints.First().originalSnapPositions.First() + distanceTravelled; + var referenceBlueprint = blueprints.First().blueprint; // Retrieve a snapped position. var result = Composer.TrySnapToNearbyObjects(movePosition); - result ??= Composer.TrySnapToDistanceGrid(movePosition); + result ??= Composer.TrySnapToDistanceGrid(movePosition, limitedDistanceSnap.Value ? referenceBlueprint.Item.StartTime : null); if (Composer.TrySnapToPositionGrid(result?.ScreenSpacePosition ?? movePosition, result?.Time) is SnapResult gridSnapResult) result = gridSnapResult; result ??= new SnapResult(movePosition, null); - var referenceBlueprint = blueprints.First().blueprint; bool moved = SelectionHandler.HandleMovement(new MoveSelectionEvent(referenceBlueprint, result.ScreenSpacePosition - referenceBlueprint.ScreenSpaceSelectionPoint)); if (moved) ApplySnapResultTime(result, referenceBlueprint.Item.StartTime); diff --git a/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs b/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs index e08968e1aa..563d0b1e3e 100644 --- a/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs +++ b/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs @@ -250,13 +250,13 @@ namespace osu.Game.Rulesets.Osu.Edit } [CanBeNull] - public SnapResult TrySnapToDistanceGrid(Vector2 screenSpacePosition) + public SnapResult TrySnapToDistanceGrid(Vector2 screenSpacePosition, double? fixedTime = null) { if (DistanceSnapProvider.DistanceSnapToggle.Value != TernaryState.True || distanceSnapGrid == null) return null; var playfield = PlayfieldAtScreenSpacePosition(screenSpacePosition); - (Vector2 pos, double time) = distanceSnapGrid.GetSnappedPosition(distanceSnapGrid.ToLocalSpace(screenSpacePosition)); + (Vector2 pos, double time) = distanceSnapGrid.GetSnappedPosition(distanceSnapGrid.ToLocalSpace(screenSpacePosition), fixedTime); return new SnapResult(distanceSnapGrid.ToScreenSpace(pos), time, playfield); } diff --git a/osu.Game.Tests/Visual/Editing/TestSceneDistanceSnapGrid.cs b/osu.Game.Tests/Visual/Editing/TestSceneDistanceSnapGrid.cs index c1a788cd22..818862d958 100644 --- a/osu.Game.Tests/Visual/Editing/TestSceneDistanceSnapGrid.cs +++ b/osu.Game.Tests/Visual/Editing/TestSceneDistanceSnapGrid.cs @@ -181,7 +181,7 @@ namespace osu.Game.Tests.Visual.Editing } } - public override (Vector2 position, double time) GetSnappedPosition(Vector2 screenSpacePosition) + public override (Vector2 position, double time) GetSnappedPosition(Vector2 screenSpacePosition, double? fixedTime = null) => (Vector2.Zero, 0); } diff --git a/osu.Game/Screens/Edit/Compose/Components/CircularDistanceSnapGrid.cs b/osu.Game/Screens/Edit/Compose/Components/CircularDistanceSnapGrid.cs index bd750dac76..e84c2ebc35 100644 --- a/osu.Game/Screens/Edit/Compose/Components/CircularDistanceSnapGrid.cs +++ b/osu.Game/Screens/Edit/Compose/Components/CircularDistanceSnapGrid.cs @@ -16,9 +16,6 @@ namespace osu.Game.Screens.Edit.Compose.Components { public abstract partial class CircularDistanceSnapGrid : DistanceSnapGrid { - [Resolved] - private EditorClock editorClock { get; set; } = null!; - protected CircularDistanceSnapGrid(HitObject referenceObject, Vector2 startPosition, double startTime, double? endTime = null) : base(referenceObject, startPosition, startTime, endTime) { @@ -76,7 +73,7 @@ namespace osu.Game.Screens.Edit.Compose.Components } } - public override (Vector2 position, double time) GetSnappedPosition(Vector2 position) + public override (Vector2 position, double time) GetSnappedPosition(Vector2 position, double? fixedTime = null) { if (MaxIntervals == 0) return (StartPosition, StartTime); @@ -100,8 +97,8 @@ namespace osu.Game.Screens.Edit.Compose.Components if (travelLength < DistanceBetweenTicks) travelLength = DistanceBetweenTicks; - float snappedDistance = LimitedDistanceSnap.Value - ? SnapProvider.DurationToDistance(ReferenceObject, editorClock.CurrentTime - ReferenceObject.GetEndTime()) + float snappedDistance = fixedTime != null + ? SnapProvider.DurationToDistance(ReferenceObject, fixedTime.Value - ReferenceObject.GetEndTime()) // When interacting with the resolved snap provider, the distance spacing multiplier should first be removed // to allow for snapping at a non-multiplied ratio. : SnapProvider.FindSnappedDistance(ReferenceObject, travelLength / distanceSpacingMultiplier, DistanceSnapTarget.End); diff --git a/osu.Game/Screens/Edit/Compose/Components/DistanceSnapGrid.cs b/osu.Game/Screens/Edit/Compose/Components/DistanceSnapGrid.cs index 7003d632ca..aaf58e0f7a 100644 --- a/osu.Game/Screens/Edit/Compose/Components/DistanceSnapGrid.cs +++ b/osu.Game/Screens/Edit/Compose/Components/DistanceSnapGrid.cs @@ -10,7 +10,6 @@ using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Layout; -using osu.Game.Configuration; using osu.Game.Graphics; using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Objects; @@ -61,18 +60,6 @@ namespace osu.Game.Screens.Edit.Compose.Components [Resolved] private BindableBeatDivisor beatDivisor { get; set; } - /// - /// When enabled, distance snap should only snap to the current time (as per the editor clock). - /// This is to emulate stable behaviour. - /// - protected Bindable LimitedDistanceSnap { get; private set; } - - [BackgroundDependencyLoader] - private void load(OsuConfigManager config) - { - LimitedDistanceSnap = config.GetBindable(OsuSetting.EditorLimitedDistanceSnap); - } - private readonly LayoutValue gridCache = new LayoutValue(Invalidation.RequiredParentSizeToFit); protected readonly HitObject ReferenceObject; @@ -143,8 +130,12 @@ namespace osu.Game.Screens.Edit.Compose.Components /// Snaps a position to this grid. /// /// The original position in coordinate space local to this . + /// + /// Whether the snap operation should be temporally constrained to a particular time instant, + /// thus fixing the possible positions to a set distance from the . + /// /// A tuple containing the snapped position in coordinate space local to this and the respective time value. - public abstract (Vector2 position, double time) GetSnappedPosition(Vector2 position); + public abstract (Vector2 position, double time) GetSnappedPosition(Vector2 position, double? fixedTime = null); /// /// Retrieves the applicable colour for a beat index. From 4fd8a4dc5a6f0453767175aa706ea331bbfca7c6 Mon Sep 17 00:00:00 2001 From: Nathan Du Date: Fri, 31 Jan 2025 16:55:39 +0800 Subject: [PATCH 0815/1275] Merge taiko swell components Per , taking a variation of the "Make all swell main pieces implement ISkinnableSwellPart" path. Should clean the interface up enough for further refactors. --- .../Objects/Drawables/DrawableSwell.cs | 25 ++++-------- .../Objects/ISkinnableSwell.cs | 7 ++-- .../Skinning/Argon/ArgonSwell.cs | 20 ++++++++++ .../Argon/TaikoArgonSkinTransformer.cs | 4 +- .../Skinning/Default/DefaultSwell.cs | 26 +++++++++---- ...wellSymbolPiece.cs => SwellCirclePiece.cs} | 0 .../Skinning/Legacy/LegacySwell.cs | 38 +++++++++++-------- .../Legacy/TaikoLegacySkinTransformer.cs | 6 --- .../TaikoSkinComponents.cs | 1 - 9 files changed, 75 insertions(+), 52 deletions(-) create mode 100644 osu.Game.Rulesets.Taiko/Skinning/Argon/ArgonSwell.cs rename osu.Game.Rulesets.Taiko/Skinning/Default/{SwellSymbolPiece.cs => SwellCirclePiece.cs} (100%) diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableSwell.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableSwell.cs index 54a609f7d3..18d76d02a1 100644 --- a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableSwell.cs +++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableSwell.cs @@ -27,8 +27,6 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables private Vector2 baseSize; - private readonly SkinnableDrawable spinnerBody; - private readonly Container ticks; private double? lastPressHandleTime; @@ -50,24 +48,17 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables { FillMode = FillMode.Fit; - Content.Add(spinnerBody = new SkinnableDrawable(new TaikoSkinComponentLookup(TaikoSkinComponents.SwellBody), _ => new DefaultSwell()) + AddInternal(ticks = new Container { RelativeSizeAxes = Axes.Both }); + } + + protected override SkinnableDrawable CreateMainPiece() => new SkinnableDrawable(new TaikoSkinComponentLookup(TaikoSkinComponents.SwellBody), + _ => new DefaultSwell { Anchor = Anchor.Centre, Origin = Anchor.Centre, RelativeSizeAxes = Axes.Both, }); - AddInternal(ticks = new Container { RelativeSizeAxes = Axes.Both }); - } - - protected override SkinnableDrawable CreateMainPiece() => new SkinnableDrawable(new TaikoSkinComponentLookup(TaikoSkinComponents.SwellCirclePiece), - _ => new SwellCirclePiece - { - // to allow for rotation transform - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - }); - protected override void RecreatePieces() { base.RecreatePieces(); @@ -132,7 +123,7 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables int numHits = ticks.Count(r => r.IsHit); - (spinnerBody.Drawable as ISkinnableSwell)?.AnimateSwellProgress(this, numHits, MainPiece); + (MainPiece.Drawable as ISkinnableSwell)?.AnimateSwellProgress(this, numHits); if (numHits == HitObject.RequiredHits) ApplyMaxResult(); @@ -167,7 +158,7 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables { base.UpdateStartTimeStateTransforms(); - (spinnerBody.Drawable as ISkinnableSwell)?.AnimateSwellStart(this, MainPiece); + (MainPiece.Drawable as ISkinnableSwell)?.AnimateSwellStart(this); } protected override void UpdateHitStateTransforms(ArmedState state) @@ -187,7 +178,7 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables LifetimeEnd = Time.Current + clear_animation_duration; HandleUserInput = false; - (spinnerBody.Drawable as ISkinnableSwell)?.AnimateSwellCompletion(state, MainPiece); + (MainPiece.Drawable as ISkinnableSwell)?.AnimateSwellCompletion(state); break; } } diff --git a/osu.Game.Rulesets.Taiko/Objects/ISkinnableSwell.cs b/osu.Game.Rulesets.Taiko/Objects/ISkinnableSwell.cs index 3cdb3566fb..9bd169acd7 100644 --- a/osu.Game.Rulesets.Taiko/Objects/ISkinnableSwell.cs +++ b/osu.Game.Rulesets.Taiko/Objects/ISkinnableSwell.cs @@ -3,16 +3,15 @@ using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Taiko.Objects.Drawables; -using osu.Game.Skinning; namespace osu.Game.Rulesets.Taiko.Objects { public interface ISkinnableSwell { - void AnimateSwellProgress(DrawableTaikoHitObject swell, int numHits, SkinnableDrawable mainPiece); + void AnimateSwellProgress(DrawableTaikoHitObject swell, int numHits); - void AnimateSwellCompletion(ArmedState state, SkinnableDrawable mainPiece); + void AnimateSwellCompletion(ArmedState state); - void AnimateSwellStart(DrawableTaikoHitObject swell, SkinnableDrawable mainPiece); + void AnimateSwellStart(DrawableTaikoHitObject swell); } } diff --git a/osu.Game.Rulesets.Taiko/Skinning/Argon/ArgonSwell.cs b/osu.Game.Rulesets.Taiko/Skinning/Argon/ArgonSwell.cs new file mode 100644 index 0000000000..65cd936e38 --- /dev/null +++ b/osu.Game.Rulesets.Taiko/Skinning/Argon/ArgonSwell.cs @@ -0,0 +1,20 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Graphics; +using osu.Game.Rulesets.Taiko.Skinning.Default; + +namespace osu.Game.Rulesets.Taiko.Skinning.Argon +{ + public partial class ArgonSwell : DefaultSwell + { + protected override Drawable CreateCentreCircle() + { + return new ArgonSwellCirclePiece() + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + }; + } + } +} diff --git a/osu.Game.Rulesets.Taiko/Skinning/Argon/TaikoArgonSkinTransformer.cs b/osu.Game.Rulesets.Taiko/Skinning/Argon/TaikoArgonSkinTransformer.cs index cfd30dd628..b588a22d12 100644 --- a/osu.Game.Rulesets.Taiko/Skinning/Argon/TaikoArgonSkinTransformer.cs +++ b/osu.Game.Rulesets.Taiko/Skinning/Argon/TaikoArgonSkinTransformer.cs @@ -68,8 +68,8 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Argon case TaikoSkinComponents.TaikoExplosionOk: return new ArgonHitExplosion(taikoComponent.Component); - case TaikoSkinComponents.SwellCirclePiece: - return new ArgonSwellCirclePiece(); + case TaikoSkinComponents.SwellBody: + return new ArgonSwell(); } break; diff --git a/osu.Game.Rulesets.Taiko/Skinning/Default/DefaultSwell.cs b/osu.Game.Rulesets.Taiko/Skinning/Default/DefaultSwell.cs index cec07d8769..bdb444db90 100644 --- a/osu.Game.Rulesets.Taiko/Skinning/Default/DefaultSwell.cs +++ b/osu.Game.Rulesets.Taiko/Skinning/Default/DefaultSwell.cs @@ -11,7 +11,6 @@ using osu.Game.Graphics; using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Taiko.Objects; using osu.Game.Rulesets.Taiko.Objects.Drawables; -using osu.Game.Skinning; using osuTK.Graphics; namespace osu.Game.Rulesets.Taiko.Skinning.Default @@ -26,6 +25,7 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Default private readonly Container bodyContainer; private readonly CircularContainer targetRing; private readonly CircularContainer expandingRing; + private readonly Drawable centreCircle; public DefaultSwell() { @@ -35,6 +35,8 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Default Content.Add(bodyContainer = new Container { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, RelativeSizeAxes = Axes.Both, Depth = 1, Children = new Drawable[] @@ -94,11 +96,21 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Default } } } - } + }, + centreCircle = CreateCentreCircle(), } }); } + protected virtual Drawable CreateCentreCircle() + { + return new SwellCirclePiece() + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + }; + } + [BackgroundDependencyLoader] private void load(OsuColour colours) { @@ -106,11 +118,11 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Default targetRing.BorderColour = colours.YellowDark.Opacity(0.25f); } - public void AnimateSwellProgress(DrawableTaikoHitObject swell, int numHits, SkinnableDrawable mainPiece) + public void AnimateSwellProgress(DrawableTaikoHitObject swell, int numHits) { float completion = (float)numHits / swell.HitObject.RequiredHits; - mainPiece.Drawable.RotateTo((float)(completion * swell.HitObject.Duration / 8), 4000, Easing.OutQuint); + centreCircle.RotateTo((float)(completion * swell.HitObject.Duration / 8), 4000, Easing.OutQuint); expandingRing.ScaleTo(1f + Math.Min(target_ring_scale - 1f, (target_ring_scale - 1f) * completion * 1.3f), 260, Easing.OutQuint); @@ -120,16 +132,16 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Default .FadeTo(completion / 8, 2000, Easing.OutQuint); } - public void AnimateSwellCompletion(ArmedState state, SkinnableDrawable mainPiece) + public void AnimateSwellCompletion(ArmedState state) { const double transition_duration = 300; bodyContainer.FadeOut(transition_duration, Easing.OutQuad); bodyContainer.ScaleTo(1.4f, transition_duration); - mainPiece.FadeOut(transition_duration, Easing.OutQuad); + centreCircle.FadeOut(transition_duration, Easing.OutQuad); } - public void AnimateSwellStart(DrawableTaikoHitObject swell, SkinnableDrawable mainPiece) + public void AnimateSwellStart(DrawableTaikoHitObject swell) { if (swell.IsHit == false) expandingRing.FadeTo(0); diff --git a/osu.Game.Rulesets.Taiko/Skinning/Default/SwellSymbolPiece.cs b/osu.Game.Rulesets.Taiko/Skinning/Default/SwellCirclePiece.cs similarity index 100% rename from osu.Game.Rulesets.Taiko/Skinning/Default/SwellSymbolPiece.cs rename to osu.Game.Rulesets.Taiko/Skinning/Default/SwellCirclePiece.cs diff --git a/osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacySwell.cs b/osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacySwell.cs index fdddea2df5..e487c5e051 100644 --- a/osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacySwell.cs +++ b/osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacySwell.cs @@ -18,6 +18,7 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy public partial class LegacySwell : Container, ISkinnableSwell { private Container bodyContainer = null!; + private Sprite warning = null!; private Sprite spinnerCircle = null!; private Sprite shrinkingRing = null!; private Sprite clearAnimation = null!; @@ -40,14 +41,21 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy { Anchor = Anchor.Centre, Origin = Anchor.Centre, - Position = new Vector2(200f, 100f), Children = new Drawable[] { + warning = new Sprite + { + Texture = skin.GetTexture("spinner-warning") ?? skinManager.DefaultClassicSkin.GetTexture("spinner-circle"), + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Scale = skin.GetTexture("spinner-warning") != null ? Vector2.One : new Vector2(0.18f), + }, bodyContainer = new Container { Anchor = Anchor.Centre, Origin = Anchor.Centre, + Position = new Vector2(200f, 100f), Alpha = 0, Children = new Drawable[] @@ -73,31 +81,31 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy Position = new Vector2(0f, 165f), Scale = Vector2.One, }, + clearAnimation = new Sprite + { + // File extension is included here because of a GetTexture limitation, see #21543 + Texture = skin.GetTexture("spinner-osu.png"), + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Position = new Vector2(0f, -165f), + Scale = new Vector2(0.3f), + Alpha = 0, + }, } }, - clearAnimation = new Sprite - { - // File extension is included here because of a GetTexture limitation, see #21543 - Texture = skin.GetTexture("spinner-osu.png"), - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Position = new Vector2(0f, -165f), - Scale = new Vector2(0.3f), - Alpha = 0, - }, } }; clearSample = skin.GetSample(new SampleInfo("spinner-osu")); } - public void AnimateSwellProgress(DrawableTaikoHitObject swell, int numHits, SkinnableDrawable mainPiece) + public void AnimateSwellProgress(DrawableTaikoHitObject swell, int numHits) { remainingHitsCountdown.Text = $"{swell.HitObject.RequiredHits - numHits}"; spinnerCircle.RotateTo(180f * numHits, 1000, Easing.OutQuint); } - public void AnimateSwellCompletion(ArmedState state, SkinnableDrawable mainPiece) + public void AnimateSwellCompletion(ArmedState state) { const double clear_transition_duration = 300; @@ -118,7 +126,7 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy } } - public void AnimateSwellStart(DrawableTaikoHitObject swell, SkinnableDrawable mainPiece) + public void AnimateSwellStart(DrawableTaikoHitObject swell) { if (swell.IsHit == false) { @@ -128,7 +136,7 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy const double body_transition_duration = 100; - mainPiece.FadeOut(body_transition_duration); + warning.FadeOut(body_transition_duration); bodyContainer.FadeIn(body_transition_duration); shrinkingRing.ResizeTo(0.1f, swell.HitObject.Duration); } diff --git a/osu.Game.Rulesets.Taiko/Skinning/Legacy/TaikoLegacySkinTransformer.cs b/osu.Game.Rulesets.Taiko/Skinning/Legacy/TaikoLegacySkinTransformer.cs index b9ebed6b80..8fa4551fd4 100644 --- a/osu.Game.Rulesets.Taiko/Skinning/Legacy/TaikoLegacySkinTransformer.cs +++ b/osu.Game.Rulesets.Taiko/Skinning/Legacy/TaikoLegacySkinTransformer.cs @@ -71,12 +71,6 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy return null; - case TaikoSkinComponents.SwellCirclePiece: - if (GetTexture("spinner-circle") != null) - return new LegacySwellCirclePiece(); - - return null; - case TaikoSkinComponents.HitTarget: if (GetTexture("taikobigcircle") != null) return new TaikoLegacyHitTarget(); diff --git a/osu.Game.Rulesets.Taiko/TaikoSkinComponents.cs b/osu.Game.Rulesets.Taiko/TaikoSkinComponents.cs index 0145fb6482..05c6316a05 100644 --- a/osu.Game.Rulesets.Taiko/TaikoSkinComponents.cs +++ b/osu.Game.Rulesets.Taiko/TaikoSkinComponents.cs @@ -11,7 +11,6 @@ namespace osu.Game.Rulesets.Taiko DrumRollBody, DrumRollTick, SwellBody, - SwellCirclePiece, HitTarget, PlayfieldBackgroundLeft, PlayfieldBackgroundRight, From 2a5540b39251c19f46a2965f0226f45d7a085f3e Mon Sep 17 00:00:00 2001 From: Nathan Du Date: Fri, 31 Jan 2025 17:51:35 +0800 Subject: [PATCH 0816/1275] remove ISkinnableSwell This commit removes ISkinnableSwell for taiko swell animations. In place of it, an event named UpdateHitProgress is added to DrawableSwell, and the skin swells are converted to listen to said event and ApplyCustomUpdateState, like how spinner skinning is implemented for std. --- .../Objects/Drawables/DrawableSwell.cs | 6 +- .../Objects/ISkinnableSwell.cs | 17 ----- .../Skinning/Default/DefaultSwell.cs | 70 +++++++++++------ .../Skinning/Legacy/LegacySwell.cs | 75 ++++++++++++------- 4 files changed, 101 insertions(+), 67 deletions(-) delete mode 100644 osu.Game.Rulesets.Taiko/Objects/ISkinnableSwell.cs diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableSwell.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableSwell.cs index 18d76d02a1..e0276db911 100644 --- a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableSwell.cs +++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableSwell.cs @@ -38,6 +38,8 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables /// public bool MustAlternate { get; internal set; } = true; + public event Action UpdateHitProgress; + public DrawableSwell() : this(null) { @@ -123,7 +125,7 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables int numHits = ticks.Count(r => r.IsHit); - (MainPiece.Drawable as ISkinnableSwell)?.AnimateSwellProgress(this, numHits); + UpdateHitProgress?.Invoke(numHits, HitObject.RequiredHits); if (numHits == HitObject.RequiredHits) ApplyMaxResult(); @@ -158,7 +160,6 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables { base.UpdateStartTimeStateTransforms(); - (MainPiece.Drawable as ISkinnableSwell)?.AnimateSwellStart(this); } protected override void UpdateHitStateTransforms(ArmedState state) @@ -178,7 +179,6 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables LifetimeEnd = Time.Current + clear_animation_duration; HandleUserInput = false; - (MainPiece.Drawable as ISkinnableSwell)?.AnimateSwellCompletion(state); break; } } diff --git a/osu.Game.Rulesets.Taiko/Objects/ISkinnableSwell.cs b/osu.Game.Rulesets.Taiko/Objects/ISkinnableSwell.cs deleted file mode 100644 index 9bd169acd7..0000000000 --- a/osu.Game.Rulesets.Taiko/Objects/ISkinnableSwell.cs +++ /dev/null @@ -1,17 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -using osu.Game.Rulesets.Objects.Drawables; -using osu.Game.Rulesets.Taiko.Objects.Drawables; - -namespace osu.Game.Rulesets.Taiko.Objects -{ - public interface ISkinnableSwell - { - void AnimateSwellProgress(DrawableTaikoHitObject swell, int numHits); - - void AnimateSwellCompletion(ArmedState state); - - void AnimateSwellStart(DrawableTaikoHitObject swell); - } -} diff --git a/osu.Game.Rulesets.Taiko/Skinning/Default/DefaultSwell.cs b/osu.Game.Rulesets.Taiko/Skinning/Default/DefaultSwell.cs index bdb444db90..852116cbfe 100644 --- a/osu.Game.Rulesets.Taiko/Skinning/Default/DefaultSwell.cs +++ b/osu.Game.Rulesets.Taiko/Skinning/Default/DefaultSwell.cs @@ -4,6 +4,7 @@ using System; using osu.Framework.Allocation; using osu.Framework.Extensions.Color4Extensions; +using osu.Framework.Extensions.ObjectExtensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; @@ -15,13 +16,15 @@ using osuTK.Graphics; namespace osu.Game.Rulesets.Taiko.Skinning.Default { - public partial class DefaultSwell : Container, ISkinnableSwell + public partial class DefaultSwell : Container { private const float target_ring_thick_border = 1.4f; private const float target_ring_thin_border = 1f; private const float target_ring_scale = 5f; private const float inner_ring_alpha = 0.65f; + private DrawableSwell drawableSwell = null!; + private readonly Container bodyContainer; private readonly CircularContainer targetRing; private readonly CircularContainer expandingRing; @@ -102,6 +105,17 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Default }); } + [BackgroundDependencyLoader] + private void load(DrawableHitObject hitObject, OsuColour colours) + { + drawableSwell = (DrawableSwell)hitObject; + drawableSwell.UpdateHitProgress += animateSwellProgress; + drawableSwell.ApplyCustomUpdateState += updateStateTransforms; + + expandingRing.Colour = colours.YellowLight; + targetRing.BorderColour = colours.YellowDark.Opacity(0.25f); + } + protected virtual Drawable CreateCentreCircle() { return new SwellCirclePiece() @@ -111,18 +125,11 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Default }; } - [BackgroundDependencyLoader] - private void load(OsuColour colours) + private void animateSwellProgress(int numHits, int requiredHits) { - expandingRing.Colour = colours.YellowLight; - targetRing.BorderColour = colours.YellowDark.Opacity(0.25f); - } + float completion = (float)numHits / requiredHits; - public void AnimateSwellProgress(DrawableTaikoHitObject swell, int numHits) - { - float completion = (float)numHits / swell.HitObject.RequiredHits; - - centreCircle.RotateTo((float)(completion * swell.HitObject.Duration / 8), 4000, Easing.OutQuint); + centreCircle.RotateTo((float)(completion * drawableSwell.HitObject.Duration / 8), 4000, Easing.OutQuint); expandingRing.ScaleTo(1f + Math.Min(target_ring_scale - 1f, (target_ring_scale - 1f) * completion * 1.3f), 260, Easing.OutQuint); @@ -132,23 +139,42 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Default .FadeTo(completion / 8, 2000, Easing.OutQuint); } - public void AnimateSwellCompletion(ArmedState state) + private void updateStateTransforms(DrawableHitObject drawableHitObject, ArmedState state) { - const double transition_duration = 300; + if (!(drawableHitObject is DrawableSwell drawableSwell)) + return; - bodyContainer.FadeOut(transition_duration, Easing.OutQuad); - bodyContainer.ScaleTo(1.4f, transition_duration); - centreCircle.FadeOut(transition_duration, Easing.OutQuad); + Swell swell = drawableSwell.HitObject; + + using (BeginAbsoluteSequence(swell.StartTime)) + { + if (state == ArmedState.Idle) + expandingRing.FadeTo(0); + + const double ring_appear_offset = 100; + + targetRing.Delay(ring_appear_offset).ScaleTo(target_ring_scale, 400, Easing.OutQuint); + } + + using (BeginAbsoluteSequence(drawableSwell.HitStateUpdateTime)) + { + const double transition_duration = 300; + + bodyContainer.FadeOut(transition_duration, Easing.OutQuad); + bodyContainer.ScaleTo(1.4f, transition_duration); + centreCircle.FadeOut(transition_duration, Easing.OutQuad); + } } - public void AnimateSwellStart(DrawableTaikoHitObject swell) + protected override void Dispose(bool isDisposing) { - if (swell.IsHit == false) - expandingRing.FadeTo(0); + base.Dispose(isDisposing); - const double ring_appear_offset = 100; - - targetRing.Delay(ring_appear_offset).ScaleTo(target_ring_scale, 400, Easing.OutQuint); + if (drawableSwell.IsNotNull()) + { + drawableSwell.UpdateHitProgress -= animateSwellProgress; + drawableSwell.ApplyCustomUpdateState -= updateStateTransforms; + } } } } diff --git a/osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacySwell.cs b/osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacySwell.cs index e487c5e051..60a0b1d951 100644 --- a/osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacySwell.cs +++ b/osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacySwell.cs @@ -12,11 +12,14 @@ using osu.Framework.Audio.Sample; using osu.Game.Audio; using osuTK; using osu.Game.Rulesets.Objects.Drawables; +using osu.Framework.Extensions.ObjectExtensions; namespace osu.Game.Rulesets.Taiko.Skinning.Legacy { - public partial class LegacySwell : Container, ISkinnableSwell + public partial class LegacySwell : Container { + private DrawableSwell drawableSwell = null!; + private Container bodyContainer = null!; private Sprite warning = null!; private Sprite spinnerCircle = null!; @@ -35,7 +38,7 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy } [BackgroundDependencyLoader] - private void load(ISkinSource skin, SkinManager skinManager) + private void load(DrawableHitObject hitObject, ISkinSource skin, SkinManager skinManager) { Child = new Container { @@ -96,49 +99,71 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy } }; + drawableSwell = (DrawableSwell)hitObject; + drawableSwell.UpdateHitProgress += animateSwellProgress; + drawableSwell.ApplyCustomUpdateState += updateStateTransforms; clearSample = skin.GetSample(new SampleInfo("spinner-osu")); } - public void AnimateSwellProgress(DrawableTaikoHitObject swell, int numHits) + private void animateSwellProgress(int numHits, int requiredHits) { - remainingHitsCountdown.Text = $"{swell.HitObject.RequiredHits - numHits}"; + remainingHitsCountdown.Text = $"{requiredHits - numHits}"; spinnerCircle.RotateTo(180f * numHits, 1000, Easing.OutQuint); } - public void AnimateSwellCompletion(ArmedState state) + private void updateStateTransforms(DrawableHitObject drawableHitObject, ArmedState state) { - const double clear_transition_duration = 300; + if (!(drawableHitObject is DrawableSwell drawableSwell)) + return; - bodyContainer.FadeOut(clear_transition_duration, Easing.OutQuad); + Swell swell = drawableSwell.HitObject; - if (state == ArmedState.Hit) + using (BeginAbsoluteSequence(swell.StartTime)) { - if (!samplePlayed) + if (state == ArmedState.Idle) { - clearSample?.Play(); - samplePlayed = true; + remainingHitsCountdown.Text = $"{swell.RequiredHits}"; + samplePlayed = false; } - clearAnimation - .FadeIn(clear_transition_duration, Easing.InQuad) - .ScaleTo(0.8f, clear_transition_duration, Easing.InQuad) - .Delay(700).FadeOut(200, Easing.OutQuad); + const double body_transition_duration = 100; + + warning.FadeOut(body_transition_duration); + bodyContainer.FadeIn(body_transition_duration); + shrinkingRing.ResizeTo(0.1f, swell.Duration); + } + + using (BeginAbsoluteSequence(drawableSwell.HitStateUpdateTime)) + { + const double clear_transition_duration = 300; + + bodyContainer.FadeOut(clear_transition_duration, Easing.OutQuad); + + if (state == ArmedState.Hit) + { + if (!samplePlayed) + { + clearSample?.Play(); + samplePlayed = true; + } + + clearAnimation + .FadeIn(clear_transition_duration, Easing.InQuad) + .ScaleTo(0.8f, clear_transition_duration, Easing.InQuad) + .Delay(700).FadeOut(200, Easing.OutQuad); + } } } - public void AnimateSwellStart(DrawableTaikoHitObject swell) + protected override void Dispose(bool isDisposing) { - if (swell.IsHit == false) + base.Dispose(isDisposing); + + if (drawableSwell.IsNotNull()) { - remainingHitsCountdown.Text = $"{swell.HitObject.RequiredHits}"; - samplePlayed = false; + drawableSwell.UpdateHitProgress -= animateSwellProgress; + drawableSwell.ApplyCustomUpdateState -= updateStateTransforms; } - - const double body_transition_duration = 100; - - warning.FadeOut(body_transition_duration); - bodyContainer.FadeIn(body_transition_duration); - shrinkingRing.ResizeTo(0.1f, swell.HitObject.Duration); } } } From ad2b469b143d74da7843a42563fe3e170a53d35c Mon Sep 17 00:00:00 2001 From: Nathan Du Date: Fri, 31 Jan 2025 18:52:19 +0800 Subject: [PATCH 0817/1275] remove spinner-osu.png workaround https://github.com/ppy/osu/issues/22084 --- osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacySwell.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacySwell.cs b/osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacySwell.cs index 60a0b1d951..405b0b7692 100644 --- a/osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacySwell.cs +++ b/osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacySwell.cs @@ -86,8 +86,7 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy }, clearAnimation = new Sprite { - // File extension is included here because of a GetTexture limitation, see #21543 - Texture = skin.GetTexture("spinner-osu.png"), + Texture = skin.GetTexture("spinner-osu"), Anchor = Anchor.Centre, Origin = Anchor.Centre, Position = new Vector2(0f, -165f), From c3981f1097f1d7d3a29422261ad39d43819cf1dd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 31 Jan 2025 12:05:30 +0100 Subject: [PATCH 0818/1275] Do not reset online info on beatmap save --- .../Editing/TestSceneLocallyModifyingOnlineBeatmaps.cs | 6 +++++- osu.Game/Beatmaps/BeatmapManager.cs | 3 --- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/osu.Game.Tests/Visual/Editing/TestSceneLocallyModifyingOnlineBeatmaps.cs b/osu.Game.Tests/Visual/Editing/TestSceneLocallyModifyingOnlineBeatmaps.cs index 7f9a69833c..636b3f54d8 100644 --- a/osu.Game.Tests/Visual/Editing/TestSceneLocallyModifyingOnlineBeatmaps.cs +++ b/osu.Game.Tests/Visual/Editing/TestSceneLocallyModifyingOnlineBeatmaps.cs @@ -4,6 +4,7 @@ using System.Linq; using NUnit.Framework; using osu.Framework.Extensions; +using osu.Game.Beatmaps; using osu.Game.Database; using osu.Game.Tests.Resources; @@ -25,13 +26,16 @@ namespace osu.Game.Tests.Visual.Editing [Test] public void TestLocallyModifyingOnlineBeatmap() { + string initialHash = string.Empty; AddAssert("editor beatmap has online ID", () => EditorBeatmap.BeatmapInfo.OnlineID, () => Is.GreaterThan(0)); + AddStep("store hash for later", () => initialHash = EditorBeatmap.BeatmapInfo.MD5Hash); AddStep("delete first hitobject", () => EditorBeatmap.RemoveAt(0)); SaveEditor(); ReloadEditorToSameBeatmap(); - AddAssert("editor beatmap online ID reset", () => EditorBeatmap.BeatmapInfo.OnlineID, () => Is.EqualTo(-1)); + AddAssert("beatmap marked as locally modified", () => EditorBeatmap.BeatmapInfo.Status, () => Is.EqualTo(BeatmapOnlineStatus.LocallyModified)); + AddAssert("beatmap hash changed", () => EditorBeatmap.BeatmapInfo.MD5Hash, () => Is.Not.EqualTo(initialHash)); } } } diff --git a/osu.Game/Beatmaps/BeatmapManager.cs b/osu.Game/Beatmaps/BeatmapManager.cs index aa67d3c548..1e66b28b15 100644 --- a/osu.Game/Beatmaps/BeatmapManager.cs +++ b/osu.Game/Beatmaps/BeatmapManager.cs @@ -475,11 +475,8 @@ namespace osu.Game.Beatmaps beatmapContent.BeatmapInfo = beatmapInfo; // Since now this is a locally-modified beatmap, we also set all relevant flags to indicate this. - // Importantly, the `ResetOnlineInfo()` call must happen before encoding, as online ID is encoded into the `.osu` file, - // which influences the beatmap checksums. beatmapInfo.LastLocalUpdate = DateTimeOffset.Now; beatmapInfo.Status = BeatmapOnlineStatus.LocallyModified; - beatmapInfo.ResetOnlineInfo(); Realm.Write(r => { From 7ef861670379b42ce17ba648c5e5d016fa4a995e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 31 Jan 2025 12:22:05 +0100 Subject: [PATCH 0819/1275] Fix broken user-facing messaging when beatmap hash mismatch is detected --- osu.Game/Screens/Play/SubmittingPlayer.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/Play/SubmittingPlayer.cs b/osu.Game/Screens/Play/SubmittingPlayer.cs index 24c5b2c3d4..0a230ea00b 100644 --- a/osu.Game/Screens/Play/SubmittingPlayer.cs +++ b/osu.Game/Screens/Play/SubmittingPlayer.cs @@ -152,7 +152,7 @@ namespace osu.Game.Screens.Play Logger.Log($"Please ensure that you are using the latest version of the official game releases.\n\n{whatWillHappen}", level: LogLevel.Important); break; - case @"invalid beatmap_hash": + case @"invalid or missing beatmap_hash": Logger.Log($"This beatmap does not match the online version. Please update or redownload it.\n\n{whatWillHappen}", level: LogLevel.Important); break; From ac17b4065f06571cc3bf30cc7536e4746a78e9d3 Mon Sep 17 00:00:00 2001 From: Nathan Du Date: Fri, 31 Jan 2025 19:55:29 +0800 Subject: [PATCH 0820/1275] change legacy spinner animations to match stable Also removed a few fallbacks pointed out in code review that I don't understand. --- .../Skinning/Legacy/LegacySwell.cs | 46 +++++++++++++------ 1 file changed, 31 insertions(+), 15 deletions(-) diff --git a/osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacySwell.cs b/osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacySwell.cs index 405b0b7692..9ed21b1bb0 100644 --- a/osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacySwell.cs +++ b/osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacySwell.cs @@ -13,6 +13,7 @@ using osu.Game.Audio; using osuTK; using osu.Game.Rulesets.Objects.Drawables; using osu.Framework.Extensions.ObjectExtensions; +using System; namespace osu.Game.Rulesets.Taiko.Skinning.Legacy { @@ -23,10 +24,10 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy private Container bodyContainer = null!; private Sprite warning = null!; private Sprite spinnerCircle = null!; - private Sprite shrinkingRing = null!; + private Sprite approachCircle = null!; private Sprite clearAnimation = null!; private ISample? clearSample; - private LegacySpriteText remainingHitsCountdown = null!; + private LegacySpriteText remainingHitsText = null!; private bool samplePlayed; @@ -40,6 +41,8 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy [BackgroundDependencyLoader] private void load(DrawableHitObject hitObject, ISkinSource skin, SkinManager skinManager) { + var spinnerCircleProvider = skin.FindProvider(s => s.GetTexture("spinner-circle") != null); + Child = new Container { Anchor = Anchor.Centre, @@ -49,7 +52,7 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy { warning = new Sprite { - Texture = skin.GetTexture("spinner-warning") ?? skinManager.DefaultClassicSkin.GetTexture("spinner-circle"), + Texture = skin.GetTexture("spinner-warning"), Anchor = Anchor.Centre, Origin = Anchor.Centre, Scale = skin.GetTexture("spinner-warning") != null ? Vector2.One : new Vector2(0.18f), @@ -70,14 +73,14 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy Origin = Anchor.Centre, Scale = new Vector2(0.8f), }, - shrinkingRing = new Sprite + approachCircle = new Sprite { - Texture = skin.GetTexture("spinner-approachcircle") ?? skinManager.DefaultClassicSkin.GetTexture("spinner-approachcircle"), + Texture = skin.GetTexture("spinner-approachcircle"), Anchor = Anchor.Centre, Origin = Anchor.Centre, - Scale = Vector2.One, + Scale = new Vector2(1.86f * 0.8f), }, - remainingHitsCountdown = new LegacySpriteText(LegacyFont.Combo) + remainingHitsText = new LegacySpriteText(LegacyFont.Combo) { Anchor = Anchor.Centre, Origin = Anchor.Centre, @@ -106,8 +109,14 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy private void animateSwellProgress(int numHits, int requiredHits) { - remainingHitsCountdown.Text = $"{requiredHits - numHits}"; - spinnerCircle.RotateTo(180f * numHits, 1000, Easing.OutQuint); + remainingHitsText.Text = $"{requiredHits - numHits}"; + remainingHitsText.ScaleTo(1.6f - 0.6f * ((float)numHits / requiredHits), 60, Easing.OutQuad); + + spinnerCircle.ClearTransforms(); + spinnerCircle + .RotateTo(180f * numHits, 1000, Easing.OutQuint) + .ScaleTo(Math.Min(0.94f, spinnerCircle.Scale.X + 0.02f)) + .ScaleTo(0.8f, 400, Easing.OutQuad); } private void updateStateTransforms(DrawableHitObject drawableHitObject, ArmedState state) @@ -121,7 +130,7 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy { if (state == ArmedState.Idle) { - remainingHitsCountdown.Text = $"{swell.RequiredHits}"; + remainingHitsText.Text = $"{swell.RequiredHits}"; samplePlayed = false; } @@ -129,14 +138,17 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy warning.FadeOut(body_transition_duration); bodyContainer.FadeIn(body_transition_duration); - shrinkingRing.ResizeTo(0.1f, swell.Duration); + approachCircle.ResizeTo(0.1f * 0.8f, swell.Duration); } using (BeginAbsoluteSequence(drawableSwell.HitStateUpdateTime)) { const double clear_transition_duration = 300; + const double clear_fade_in = 120; - bodyContainer.FadeOut(clear_transition_duration, Easing.OutQuad); + bodyContainer + .FadeOut(clear_transition_duration, Easing.OutQuad) + .ScaleTo(1.05f, clear_transition_duration, Easing.OutQuad); if (state == ArmedState.Hit) { @@ -147,9 +159,13 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy } clearAnimation - .FadeIn(clear_transition_duration, Easing.InQuad) - .ScaleTo(0.8f, clear_transition_duration, Easing.InQuad) - .Delay(700).FadeOut(200, Easing.OutQuad); + .FadeIn(clear_fade_in) + .MoveTo(new Vector2(320, 240)) + .ScaleTo(0.4f) + .MoveTo(new Vector2(320, 150), clear_fade_in * 2, Easing.OutQuad) + .ScaleTo(1f, clear_fade_in * 2, Easing.Out) + .Delay(clear_fade_in * 3) + .FadeOut(clear_fade_in * 2.5); } } } From a62a84a30f7e92b9a855dfba7ddeb5c42a2bb442 Mon Sep 17 00:00:00 2001 From: Nathan Du Date: Fri, 31 Jan 2025 20:48:29 +0800 Subject: [PATCH 0821/1275] fix code style --- osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableSwell.cs | 6 ------ osu.Game.Rulesets.Taiko/Skinning/Argon/ArgonSwell.cs | 2 +- osu.Game.Rulesets.Taiko/Skinning/Default/DefaultSwell.cs | 6 +++--- osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacySwell.cs | 2 +- 4 files changed, 5 insertions(+), 11 deletions(-) diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableSwell.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableSwell.cs index e0276db911..363a6bf8e1 100644 --- a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableSwell.cs +++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableSwell.cs @@ -156,12 +156,6 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables } } - protected override void UpdateStartTimeStateTransforms() - { - base.UpdateStartTimeStateTransforms(); - - } - protected override void UpdateHitStateTransforms(ArmedState state) { switch (state) diff --git a/osu.Game.Rulesets.Taiko/Skinning/Argon/ArgonSwell.cs b/osu.Game.Rulesets.Taiko/Skinning/Argon/ArgonSwell.cs index 65cd936e38..3b3684d219 100644 --- a/osu.Game.Rulesets.Taiko/Skinning/Argon/ArgonSwell.cs +++ b/osu.Game.Rulesets.Taiko/Skinning/Argon/ArgonSwell.cs @@ -10,7 +10,7 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Argon { protected override Drawable CreateCentreCircle() { - return new ArgonSwellCirclePiece() + return new ArgonSwellCirclePiece { Anchor = Anchor.Centre, Origin = Anchor.Centre, diff --git a/osu.Game.Rulesets.Taiko/Skinning/Default/DefaultSwell.cs b/osu.Game.Rulesets.Taiko/Skinning/Default/DefaultSwell.cs index 852116cbfe..a588f866c6 100644 --- a/osu.Game.Rulesets.Taiko/Skinning/Default/DefaultSwell.cs +++ b/osu.Game.Rulesets.Taiko/Skinning/Default/DefaultSwell.cs @@ -42,7 +42,7 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Default Origin = Anchor.Centre, RelativeSizeAxes = Axes.Both, Depth = 1, - Children = new Drawable[] + Children = new[] { expandingRing = new CircularContainer { @@ -118,7 +118,7 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Default protected virtual Drawable CreateCentreCircle() { - return new SwellCirclePiece() + return new SwellCirclePiece { Anchor = Anchor.Centre, Origin = Anchor.Centre, @@ -141,7 +141,7 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Default private void updateStateTransforms(DrawableHitObject drawableHitObject, ArmedState state) { - if (!(drawableHitObject is DrawableSwell drawableSwell)) + if (!(drawableHitObject is DrawableSwell)) return; Swell swell = drawableSwell.HitObject; diff --git a/osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacySwell.cs b/osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacySwell.cs index 9ed21b1bb0..43b2d5c435 100644 --- a/osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacySwell.cs +++ b/osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacySwell.cs @@ -121,7 +121,7 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy private void updateStateTransforms(DrawableHitObject drawableHitObject, ArmedState state) { - if (!(drawableHitObject is DrawableSwell drawableSwell)) + if (!(drawableHitObject is DrawableSwell)) return; Swell swell = drawableSwell.HitObject; From e794389fe83644323a563a343338e282783b53b1 Mon Sep 17 00:00:00 2001 From: Nathan Du Date: Sat, 1 Feb 2025 13:34:52 +0800 Subject: [PATCH 0822/1275] further adjust swell behavior The outstanding visual issues of the clear animation is fixed. The HandleUserInput state management is removed as it no longer seems necessary. --- .../Objects/Drawables/DrawableSwell.cs | 14 +-- .../Skinning/Legacy/LegacySwell.cs | 109 +++++++++--------- 2 files changed, 60 insertions(+), 63 deletions(-) diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableSwell.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableSwell.cs index 363a6bf8e1..d75fdbc40a 100644 --- a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableSwell.cs +++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableSwell.cs @@ -158,21 +158,19 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables protected override void UpdateHitStateTransforms(ArmedState state) { + base.UpdateHitStateTransforms(state); + switch (state) { case ArmedState.Idle: - // Only for rewind support. Reallows user inputs if swell is rewound from being hit/missed to being idle. - HandleUserInput = true; break; case ArmedState.Miss: + this.Delay(300).FadeOut(); + break; + case ArmedState.Hit: - const int clear_animation_duration = 1200; - - // Postpone drawable hitobject expiration until it has animated/faded out. Inputs on the object are disallowed during this delay. - LifetimeEnd = Time.Current + clear_animation_duration; - HandleUserInput = false; - + this.Delay(660).FadeOut(); break; } } diff --git a/osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacySwell.cs b/osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacySwell.cs index 43b2d5c435..0eb80d333f 100644 --- a/osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacySwell.cs +++ b/osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacySwell.cs @@ -41,64 +41,63 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy [BackgroundDependencyLoader] private void load(DrawableHitObject hitObject, ISkinSource skin, SkinManager skinManager) { - var spinnerCircleProvider = skin.FindProvider(s => s.GetTexture("spinner-circle") != null); - - Child = new Container + Children = new Drawable[] { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - - Children = new Drawable[] + warning = new Sprite { - warning = new Sprite - { - Texture = skin.GetTexture("spinner-warning"), - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Scale = skin.GetTexture("spinner-warning") != null ? Vector2.One : new Vector2(0.18f), - }, - bodyContainer = new Container - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Position = new Vector2(200f, 100f), - Alpha = 0, + Texture = skin.GetTexture("spinner-warning"), + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Scale = skin.GetTexture("spinner-warning") != null ? Vector2.One : new Vector2(0.18f), + }, + new Container + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Position = new Vector2(200f, 100f), - Children = new Drawable[] + Children = new Drawable[] + { + bodyContainer = new Container { - spinnerCircle = new Sprite + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Alpha = 0, + + Children = new Drawable[] { - Texture = skin.GetTexture("spinner-circle"), - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Scale = new Vector2(0.8f), - }, - approachCircle = new Sprite - { - Texture = skin.GetTexture("spinner-approachcircle"), - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Scale = new Vector2(1.86f * 0.8f), - }, - remainingHitsText = new LegacySpriteText(LegacyFont.Combo) - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Position = new Vector2(0f, 165f), - Scale = Vector2.One, - }, - clearAnimation = new Sprite - { - Texture = skin.GetTexture("spinner-osu"), - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Position = new Vector2(0f, -165f), - Scale = new Vector2(0.3f), - Alpha = 0, - }, - } + spinnerCircle = new Sprite + { + Texture = skin.GetTexture("spinner-circle"), + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Scale = new Vector2(0.8f), + }, + approachCircle = new Sprite + { + Texture = skin.GetTexture("spinner-approachcircle"), + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Scale = new Vector2(1.86f * 0.8f), + }, + remainingHitsText = new LegacySpriteText(LegacyFont.Combo) + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Position = new Vector2(0f, 165f), + Scale = Vector2.One, + }, + } + }, + clearAnimation = new Sprite + { + Texture = skin.GetTexture("spinner-osu"), + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Alpha = 0, + }, }, - } + }, }; drawableSwell = (DrawableSwell)hitObject; @@ -110,7 +109,7 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy private void animateSwellProgress(int numHits, int requiredHits) { remainingHitsText.Text = $"{requiredHits - numHits}"; - remainingHitsText.ScaleTo(1.6f - 0.6f * ((float)numHits / requiredHits), 60, Easing.OutQuad); + remainingHitsText.ScaleTo(1.6f - (0.6f * ((float)numHits / requiredHits)), 60, Easing.OutQuad); spinnerCircle.ClearTransforms(); spinnerCircle @@ -160,9 +159,9 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy clearAnimation .FadeIn(clear_fade_in) - .MoveTo(new Vector2(320, 240)) + .MoveTo(new Vector2(0, 0)) .ScaleTo(0.4f) - .MoveTo(new Vector2(320, 150), clear_fade_in * 2, Easing.OutQuad) + .MoveTo(new Vector2(0, -90), clear_fade_in * 2, Easing.OutQuad) .ScaleTo(1f, clear_fade_in * 2, Easing.Out) .Delay(clear_fade_in * 3) .FadeOut(clear_fade_in * 2.5); From cc3bb590c97b1d818229d06d614003c20370163c Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sat, 1 Feb 2025 14:48:13 +0900 Subject: [PATCH 0823/1275] Remove pointless comment --- osu.Game.Rulesets.Mania/UI/DrawableManiaJudgement.cs | 2 -- 1 file changed, 2 deletions(-) diff --git a/osu.Game.Rulesets.Mania/UI/DrawableManiaJudgement.cs b/osu.Game.Rulesets.Mania/UI/DrawableManiaJudgement.cs index a1dabd66bc..75f56bffa4 100644 --- a/osu.Game.Rulesets.Mania/UI/DrawableManiaJudgement.cs +++ b/osu.Game.Rulesets.Mania/UI/DrawableManiaJudgement.cs @@ -59,8 +59,6 @@ namespace osu.Game.Rulesets.Mania.UI this.Delay(50) .ScaleTo(0.75f, 250) .FadeOut(200); - - // osu!mania uses a custom fade length, so the base call is intentionally omitted. break; } } From 3cde11ab773f705e4132d7f837150e1b1232c11b Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 24 Jan 2025 19:28:39 +0900 Subject: [PATCH 0824/1275] Re-enable masking by default --- .../Visual/SongSelect/TestSceneBeatmapCarouselV2Basics.cs | 7 +++++++ osu.Game/Screens/SelectV2/Carousel.cs | 1 - 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2Basics.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2Basics.cs index 3a516ea762..0e72ee4f8c 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2Basics.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2Basics.cs @@ -32,6 +32,13 @@ namespace osu.Game.Tests.Visual.SongSelect RemoveAllBeatmaps(); } + [Test] + public void TestOffScreenLoading() + { + AddStep("disable masking", () => Scroll.Masking = false); + AddStep("enable masking", () => Scroll.Masking = true); + } + [Test] public void TestAddRemoveOneByOne() { diff --git a/osu.Game/Screens/SelectV2/Carousel.cs b/osu.Game/Screens/SelectV2/Carousel.cs index 648c2d090a..811bb120e1 100644 --- a/osu.Game/Screens/SelectV2/Carousel.cs +++ b/osu.Game/Screens/SelectV2/Carousel.cs @@ -205,7 +205,6 @@ namespace osu.Game.Screens.SelectV2 InternalChild = scroll = new CarouselScrollContainer { RelativeSizeAxes = Axes.Both, - Masking = false, }; Items.BindCollectionChanged((_, _) => FilterAsync()); From d5dc55149d93cd534e3106a5997be2262d18be17 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 24 Jan 2025 19:29:14 +0900 Subject: [PATCH 0825/1275] Add initial difficulty grouping support --- osu.Game/Screens/SelectV2/BeatmapCarousel.cs | 59 ++++++--- .../SelectV2/BeatmapCarouselFilterGrouping.cs | 36 +++++- osu.Game/Screens/SelectV2/GroupPanel.cs | 113 ++++++++++++++++++ 3 files changed, 191 insertions(+), 17 deletions(-) create mode 100644 osu.Game/Screens/SelectV2/GroupPanel.cs diff --git a/osu.Game/Screens/SelectV2/BeatmapCarousel.cs b/osu.Game/Screens/SelectV2/BeatmapCarousel.cs index bb13c7449d..9a87fba140 100644 --- a/osu.Game/Screens/SelectV2/BeatmapCarousel.cs +++ b/osu.Game/Screens/SelectV2/BeatmapCarousel.cs @@ -92,34 +92,56 @@ namespace osu.Game.Screens.SelectV2 #region Selection handling + private GroupDefinition? lastSelectedGroup; + private BeatmapInfo? lastSelectedBeatmap; + protected override void HandleItemSelected(object? model) { base.HandleItemSelected(model); - // Selecting a set isn't valid – let's re-select the first difficulty. - if (model is BeatmapSetInfo setInfo) + switch (model) { - CurrentSelection = setInfo.Beatmaps.First(); - return; - } + case GroupDefinition group: + if (lastSelectedGroup != null) + setVisibilityOfGroupItems(lastSelectedGroup, false); + lastSelectedGroup = group; - if (model is BeatmapInfo beatmapInfo) - setVisibilityOfSetItems(beatmapInfo.BeatmapSet!, true); + setVisibilityOfGroupItems(group, true); + + // In stable, you can kinda select a group (expand without changing selection) + // For simplicity, let's not do that for now and handle similar to a beatmap set header. + CurrentSelection = grouping.GroupItems[group].First().Model; + return; + + case BeatmapSetInfo setInfo: + // Selecting a set isn't valid – let's re-select the first difficulty. + CurrentSelection = setInfo.Beatmaps.First(); + return; + + case BeatmapInfo beatmapInfo: + if (lastSelectedBeatmap != null) + setVisibilityOfSetItems(lastSelectedBeatmap.BeatmapSet!, false); + lastSelectedBeatmap = beatmapInfo; + + setVisibilityOfSetItems(beatmapInfo.BeatmapSet!, true); + break; + } } - protected override void HandleItemDeselected(object? model) + private void setVisibilityOfGroupItems(GroupDefinition group, bool visible) { - base.HandleItemDeselected(model); - - if (model is BeatmapInfo beatmapInfo) - setVisibilityOfSetItems(beatmapInfo.BeatmapSet!, false); + if (grouping.GroupItems.TryGetValue(group, out var items)) + { + foreach (var i in items) + i.IsVisible = visible; + } } private void setVisibilityOfSetItems(BeatmapSetInfo set, bool visible) { - if (grouping.SetItems.TryGetValue(set, out var group)) + if (grouping.SetItems.TryGetValue(set, out var items)) { - foreach (var i in group) + foreach (var i in items) i.IsVisible = visible; } } @@ -143,9 +165,11 @@ namespace osu.Game.Screens.SelectV2 private readonly DrawablePool beatmapPanelPool = new DrawablePool(100); private readonly DrawablePool setPanelPool = new DrawablePool(100); + private readonly DrawablePool groupPanelPool = new DrawablePool(100); private void setupPools() { + AddInternal(groupPanelPool); AddInternal(beatmapPanelPool); AddInternal(setPanelPool); } @@ -154,7 +178,12 @@ namespace osu.Game.Screens.SelectV2 { switch (item.Model) { + case GroupDefinition: + return groupPanelPool.Get(); + case BeatmapInfo: + // TODO: if beatmap is a group selection target, it needs to be a different drawable + // with more information attached. return beatmapPanelPool.Get(); case BeatmapSetInfo: @@ -166,4 +195,6 @@ namespace osu.Game.Screens.SelectV2 #endregion } + + public record GroupDefinition(string Title); } diff --git a/osu.Game/Screens/SelectV2/BeatmapCarouselFilterGrouping.cs b/osu.Game/Screens/SelectV2/BeatmapCarouselFilterGrouping.cs index 0658263a8c..e8384a8a2d 100644 --- a/osu.Game/Screens/SelectV2/BeatmapCarouselFilterGrouping.cs +++ b/osu.Game/Screens/SelectV2/BeatmapCarouselFilterGrouping.cs @@ -18,7 +18,13 @@ namespace osu.Game.Screens.SelectV2 /// public IDictionary> SetItems => setItems; + /// + /// Groups contain children which are group-selectable. This dictionary holds the relationships between groups-panels to allow expanding them on selection. + /// + public IDictionary> GroupItems => groupItems; + private readonly Dictionary> setItems = new Dictionary>(); + private readonly Dictionary> groupItems = new Dictionary>(); private readonly Func getCriteria; @@ -31,15 +37,40 @@ namespace osu.Game.Screens.SelectV2 { var criteria = getCriteria(); + int starGroup = int.MinValue; + if (criteria.SplitOutDifficulties) { + var diffItems = new List(items.Count()); + + GroupDefinition? group = null; + foreach (var item in items) { - item.IsVisible = true; + var b = (BeatmapInfo)item.Model; + + if (b.StarRating > starGroup) + { + starGroup = (int)Math.Floor(b.StarRating); + group = new GroupDefinition($"{starGroup} - {++starGroup} *"); + diffItems.Add(new CarouselItem(group) + { + DrawHeight = GroupPanel.HEIGHT, + IsGroupSelectionTarget = true + }); + } + + if (!groupItems.TryGetValue(group!, out var related)) + groupItems[group!] = related = new HashSet(); + related.Add(item); + + diffItems.Add(item); + + item.IsVisible = false; item.IsGroupSelectionTarget = true; } - return items; + return diffItems; } CarouselItem? lastItem = null; @@ -64,7 +95,6 @@ namespace osu.Game.Screens.SelectV2 if (!setItems.TryGetValue(b.BeatmapSet!, out var related)) setItems[b.BeatmapSet!] = related = new HashSet(); - related.Add(item); } diff --git a/osu.Game/Screens/SelectV2/GroupPanel.cs b/osu.Game/Screens/SelectV2/GroupPanel.cs new file mode 100644 index 0000000000..e837d8a32f --- /dev/null +++ b/osu.Game/Screens/SelectV2/GroupPanel.cs @@ -0,0 +1,113 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using System.Diagnostics; +using osu.Framework.Allocation; +using osu.Framework.Bindables; +using osu.Framework.Extensions.Color4Extensions; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Pooling; +using osu.Framework.Graphics.Shapes; +using osu.Framework.Input.Events; +using osu.Game.Graphics.Sprites; +using osuTK; +using osuTK.Graphics; + +namespace osu.Game.Screens.SelectV2 +{ + public partial class GroupPanel : PoolableDrawable, ICarouselPanel + { + public const float HEIGHT = CarouselItem.DEFAULT_HEIGHT * 2; + + [Resolved] + private BeatmapCarousel carousel { get; set; } = null!; + + private Box activationFlash = null!; + private OsuSpriteText text = null!; + + [BackgroundDependencyLoader] + private void load() + { + Size = new Vector2(500, HEIGHT); + Masking = true; + + InternalChildren = new Drawable[] + { + new Box + { + Colour = Color4.DarkBlue.Darken(5), + Alpha = 0.8f, + RelativeSizeAxes = Axes.Both, + }, + activationFlash = new Box + { + Colour = Color4.White, + Blending = BlendingParameters.Additive, + Alpha = 0, + RelativeSizeAxes = Axes.Both, + }, + text = new OsuSpriteText + { + Padding = new MarginPadding(5), + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + } + }; + + Selected.BindValueChanged(value => + { + activationFlash.FadeTo(value.NewValue ? 0.2f : 0, 500, Easing.OutQuint); + }); + + KeyboardSelected.BindValueChanged(value => + { + if (value.NewValue) + { + BorderThickness = 5; + BorderColour = Color4.Pink; + } + else + { + BorderThickness = 0; + } + }); + } + + protected override void PrepareForUse() + { + base.PrepareForUse(); + + Debug.Assert(Item != null); + Debug.Assert(Item.IsGroupSelectionTarget); + + GroupDefinition group = (GroupDefinition)Item.Model; + + text.Text = group.Title; + + this.FadeInFromZero(500, Easing.OutQuint); + } + + protected override bool OnClick(ClickEvent e) + { + carousel.CurrentSelection = Item!.Model; + return true; + } + + #region ICarouselPanel + + public CarouselItem? Item { get; set; } + public BindableBool Selected { get; } = new BindableBool(); + public BindableBool KeyboardSelected { get; } = new BindableBool(); + + public double DrawYPosition { get; set; } + + public void Activated() + { + // sets should never be activated. + throw new InvalidOperationException(); + } + + #endregion + } +} From 764f799dcb3aeb33cb905888d811a91e5a37640f Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 28 Jan 2025 22:53:17 +0900 Subject: [PATCH 0826/1275] Improve selection flow using early exit and invalidation --- osu.Game/Screens/SelectV2/BeatmapCarousel.cs | 31 +++++++++++--- .../SelectV2/BeatmapCarouselFilterGrouping.cs | 3 ++ osu.Game/Screens/SelectV2/Carousel.cs | 41 ++++++++++--------- 3 files changed, 50 insertions(+), 25 deletions(-) diff --git a/osu.Game/Screens/SelectV2/BeatmapCarousel.cs b/osu.Game/Screens/SelectV2/BeatmapCarousel.cs index 9a87fba140..0a7ca5a6bb 100644 --- a/osu.Game/Screens/SelectV2/BeatmapCarousel.cs +++ b/osu.Game/Screens/SelectV2/BeatmapCarousel.cs @@ -95,7 +95,7 @@ namespace osu.Game.Screens.SelectV2 private GroupDefinition? lastSelectedGroup; private BeatmapInfo? lastSelectedBeatmap; - protected override void HandleItemSelected(object? model) + protected override bool HandleItemSelected(object? model) { base.HandleItemSelected(model); @@ -104,6 +104,14 @@ namespace osu.Game.Screens.SelectV2 case GroupDefinition group: if (lastSelectedGroup != null) setVisibilityOfGroupItems(lastSelectedGroup, false); + + // Collapsing an open group. + if (lastSelectedGroup == group) + { + lastSelectedGroup = null; + return false; + } + lastSelectedGroup = group; setVisibilityOfGroupItems(group, true); @@ -111,21 +119,34 @@ namespace osu.Game.Screens.SelectV2 // In stable, you can kinda select a group (expand without changing selection) // For simplicity, let's not do that for now and handle similar to a beatmap set header. CurrentSelection = grouping.GroupItems[group].First().Model; - return; + return false; case BeatmapSetInfo setInfo: // Selecting a set isn't valid – let's re-select the first difficulty. CurrentSelection = setInfo.Beatmaps.First(); - return; + return false; case BeatmapInfo beatmapInfo: if (lastSelectedBeatmap != null) setVisibilityOfSetItems(lastSelectedBeatmap.BeatmapSet!, false); lastSelectedBeatmap = beatmapInfo; - setVisibilityOfSetItems(beatmapInfo.BeatmapSet!, true); - break; + // If we have groups, we need to account for them. + if (grouping.GroupItems.Count > 0) + { + // Find the containing group. There should never be too many groups so iterating is efficient enough. + var group = grouping.GroupItems.Single(kvp => kvp.Value.Any(i => ReferenceEquals(i.Model, beatmapInfo))).Key; + setVisibilityOfGroupItems(group, true); + } + else + setVisibilityOfSetItems(beatmapInfo.BeatmapSet!, true); + + // Ensure the group containing this beatmap is also visible. + // TODO: need to update visibility of correct group? + return true; } + + return true; } private void setVisibilityOfGroupItems(GroupDefinition group, bool visible) diff --git a/osu.Game/Screens/SelectV2/BeatmapCarouselFilterGrouping.cs b/osu.Game/Screens/SelectV2/BeatmapCarouselFilterGrouping.cs index e8384a8a2d..9ecf735980 100644 --- a/osu.Game/Screens/SelectV2/BeatmapCarouselFilterGrouping.cs +++ b/osu.Game/Screens/SelectV2/BeatmapCarouselFilterGrouping.cs @@ -35,6 +35,9 @@ namespace osu.Game.Screens.SelectV2 public async Task> Run(IEnumerable items, CancellationToken cancellationToken) => await Task.Run(() => { + setItems.Clear(); + groupItems.Clear(); + var criteria = getCriteria(); int starGroup = int.MinValue; diff --git a/osu.Game/Screens/SelectV2/Carousel.cs b/osu.Game/Screens/SelectV2/Carousel.cs index 811bb120e1..7184aaa866 100644 --- a/osu.Game/Screens/SelectV2/Carousel.cs +++ b/osu.Game/Screens/SelectV2/Carousel.cs @@ -8,6 +8,7 @@ using System.Linq; using System.Threading; using System.Threading.Tasks; using osu.Framework.Bindables; +using osu.Framework.Caching; using osu.Framework.Extensions.TypeExtensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; @@ -170,9 +171,8 @@ namespace osu.Game.Screens.SelectV2 /// /// Called when an item is "selected". /// - protected virtual void HandleItemSelected(object? model) - { - } + /// Whether the item should be selected. + protected virtual bool HandleItemSelected(object? model) => true; /// /// Called when an item is "deselected". @@ -410,6 +410,8 @@ namespace osu.Game.Screens.SelectV2 #region Selection handling + private readonly Cached selectionValid = new Cached(); + private Selection currentKeyboardSelection = new Selection(); private Selection currentSelection = new Selection(); @@ -418,29 +420,21 @@ namespace osu.Game.Screens.SelectV2 if (currentSelection.Model == model) return; - var previousSelection = currentSelection; + if (HandleItemSelected(model)) + { + if (currentSelection.Model != null) + HandleItemDeselected(currentSelection.Model); - if (previousSelection.Model != null) - HandleItemDeselected(previousSelection.Model); - - currentSelection = currentKeyboardSelection = new Selection(model); - HandleItemSelected(currentSelection.Model); - - // `HandleItemSelected` can alter `CurrentSelection`, which will recursively call `setSelection()` again. - // if that happens, the rest of this method should be a no-op. - if (currentSelection.Model != model) - return; - - refreshAfterSelection(); - scrollToSelection(); + currentKeyboardSelection = new Selection(model); + currentSelection = currentKeyboardSelection; + selectionValid.Invalidate(); + } } private void setKeyboardSelection(object? model) { currentKeyboardSelection = new Selection(model); - - refreshAfterSelection(); - scrollToSelection(); + selectionValid.Invalidate(); } /// @@ -525,6 +519,13 @@ namespace osu.Game.Screens.SelectV2 if (carouselItems == null) return; + if (!selectionValid.IsValid) + { + refreshAfterSelection(); + scrollToSelection(); + selectionValid.Validate(); + } + var range = getDisplayRange(); if (range != displayedRange) From d74939e6e983267a5bc8be37d94108d46581b02f Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 31 Jan 2025 20:58:32 +0900 Subject: [PATCH 0827/1275] Fix backwards traversal of groupings and allow toggling groups without updating selection --- osu.Game/Screens/SelectV2/BeatmapCarousel.cs | 64 +++++++++++++------ .../SelectV2/BeatmapCarouselFilterGrouping.cs | 9 +-- osu.Game/Screens/SelectV2/BeatmapSetPanel.cs | 1 - osu.Game/Screens/SelectV2/Carousel.cs | 18 +++++- osu.Game/Screens/SelectV2/CarouselItem.cs | 5 -- osu.Game/Screens/SelectV2/GroupPanel.cs | 1 - 6 files changed, 60 insertions(+), 38 deletions(-) diff --git a/osu.Game/Screens/SelectV2/BeatmapCarousel.cs b/osu.Game/Screens/SelectV2/BeatmapCarousel.cs index 0a7ca5a6bb..10bc069cfc 100644 --- a/osu.Game/Screens/SelectV2/BeatmapCarousel.cs +++ b/osu.Game/Screens/SelectV2/BeatmapCarousel.cs @@ -102,23 +102,15 @@ namespace osu.Game.Screens.SelectV2 switch (model) { case GroupDefinition group: - if (lastSelectedGroup != null) - setVisibilityOfGroupItems(lastSelectedGroup, false); - - // Collapsing an open group. + // Special case – collapsing an open group. if (lastSelectedGroup == group) { + setVisibilityOfGroupItems(lastSelectedGroup, false); lastSelectedGroup = null; return false; } - lastSelectedGroup = group; - - setVisibilityOfGroupItems(group, true); - - // In stable, you can kinda select a group (expand without changing selection) - // For simplicity, let's not do that for now and handle similar to a beatmap set header. - CurrentSelection = grouping.GroupItems[group].First().Model; + setVisibleGroup(group); return false; case BeatmapSetInfo setInfo: @@ -127,28 +119,52 @@ namespace osu.Game.Screens.SelectV2 return false; case BeatmapInfo beatmapInfo: - if (lastSelectedBeatmap != null) - setVisibilityOfSetItems(lastSelectedBeatmap.BeatmapSet!, false); - lastSelectedBeatmap = beatmapInfo; // If we have groups, we need to account for them. - if (grouping.GroupItems.Count > 0) + if (Criteria.SplitOutDifficulties) { // Find the containing group. There should never be too many groups so iterating is efficient enough. - var group = grouping.GroupItems.Single(kvp => kvp.Value.Any(i => ReferenceEquals(i.Model, beatmapInfo))).Key; - setVisibilityOfGroupItems(group, true); + GroupDefinition group = grouping.GroupItems.Single(kvp => kvp.Value.Any(i => ReferenceEquals(i.Model, beatmapInfo))).Key; + + setVisibleGroup(group); } else - setVisibilityOfSetItems(beatmapInfo.BeatmapSet!, true); + { + setVisibleSet(beatmapInfo); + } - // Ensure the group containing this beatmap is also visible. - // TODO: need to update visibility of correct group? return true; } return true; } + protected override bool CheckValidForGroupSelection(CarouselItem item) + { + switch (item.Model) + { + case BeatmapSetInfo: + return true; + + case BeatmapInfo: + return Criteria.SplitOutDifficulties; + + case GroupDefinition: + return false; + + default: + throw new ArgumentException($"Unsupported model type {item.Model}"); + } + } + + private void setVisibleGroup(GroupDefinition group) + { + if (lastSelectedGroup != null) + setVisibilityOfGroupItems(lastSelectedGroup, false); + lastSelectedGroup = group; + setVisibilityOfGroupItems(group, true); + } + private void setVisibilityOfGroupItems(GroupDefinition group, bool visible) { if (grouping.GroupItems.TryGetValue(group, out var items)) @@ -158,6 +174,14 @@ namespace osu.Game.Screens.SelectV2 } } + private void setVisibleSet(BeatmapInfo beatmapInfo) + { + if (lastSelectedBeatmap != null) + setVisibilityOfSetItems(lastSelectedBeatmap.BeatmapSet!, false); + lastSelectedBeatmap = beatmapInfo; + setVisibilityOfSetItems(beatmapInfo.BeatmapSet!, true); + } + private void setVisibilityOfSetItems(BeatmapSetInfo set, bool visible) { if (grouping.SetItems.TryGetValue(set, out var items)) diff --git a/osu.Game/Screens/SelectV2/BeatmapCarouselFilterGrouping.cs b/osu.Game/Screens/SelectV2/BeatmapCarouselFilterGrouping.cs index 9ecf735980..951b010564 100644 --- a/osu.Game/Screens/SelectV2/BeatmapCarouselFilterGrouping.cs +++ b/osu.Game/Screens/SelectV2/BeatmapCarouselFilterGrouping.cs @@ -56,11 +56,7 @@ namespace osu.Game.Screens.SelectV2 { starGroup = (int)Math.Floor(b.StarRating); group = new GroupDefinition($"{starGroup} - {++starGroup} *"); - diffItems.Add(new CarouselItem(group) - { - DrawHeight = GroupPanel.HEIGHT, - IsGroupSelectionTarget = true - }); + diffItems.Add(new CarouselItem(group) { DrawHeight = GroupPanel.HEIGHT }); } if (!groupItems.TryGetValue(group!, out var related)) @@ -70,7 +66,6 @@ namespace osu.Game.Screens.SelectV2 diffItems.Add(item); item.IsVisible = false; - item.IsGroupSelectionTarget = true; } return diffItems; @@ -92,7 +87,6 @@ namespace osu.Game.Screens.SelectV2 newItems.Add(new CarouselItem(b.BeatmapSet!) { DrawHeight = BeatmapSetPanel.HEIGHT, - IsGroupSelectionTarget = true }); } @@ -104,7 +98,6 @@ namespace osu.Game.Screens.SelectV2 newItems.Add(item); lastItem = item; - item.IsGroupSelectionTarget = false; item.IsVisible = false; } diff --git a/osu.Game/Screens/SelectV2/BeatmapSetPanel.cs b/osu.Game/Screens/SelectV2/BeatmapSetPanel.cs index 37e8b88f71..06e3ad3426 100644 --- a/osu.Game/Screens/SelectV2/BeatmapSetPanel.cs +++ b/osu.Game/Screens/SelectV2/BeatmapSetPanel.cs @@ -67,7 +67,6 @@ namespace osu.Game.Screens.SelectV2 base.PrepareForUse(); Debug.Assert(Item != null); - Debug.Assert(Item.IsGroupSelectionTarget); var beatmapSetInfo = (BeatmapSetInfo)Item.Model; diff --git a/osu.Game/Screens/SelectV2/Carousel.cs b/osu.Game/Screens/SelectV2/Carousel.cs index 7184aaa866..a76b6efee9 100644 --- a/osu.Game/Screens/SelectV2/Carousel.cs +++ b/osu.Game/Screens/SelectV2/Carousel.cs @@ -168,6 +168,13 @@ namespace osu.Game.Screens.SelectV2 protected Drawable? GetMaterialisedDrawableForItem(CarouselItem item) => scroll.Panels.SingleOrDefault(p => ((ICarouselPanel)p).Item == item); + /// + /// When a user is traversing the carousel via group selection keys, assert whether the item provided is a valid target. + /// + /// The candidate item. + /// Whether the provided item is a valid group target. If false, more panels will be checked in the user's requested direction until a valid target is found. + protected virtual bool CheckValidForGroupSelection(CarouselItem item) => true; + /// /// Called when an item is "selected". /// @@ -373,7 +380,7 @@ namespace osu.Game.Screens.SelectV2 // make sure to go back to the group header this item belongs to, so that the block below doesn't find it and stop too early. if (isGroupSelection && direction < 0) { - while (!carouselItems[selectionIndex].IsGroupSelectionTarget) + while (!CheckValidForGroupSelection(carouselItems[selectionIndex])) selectionIndex--; } @@ -394,7 +401,11 @@ namespace osu.Game.Screens.SelectV2 bool attemptSelection(CarouselItem item) { - if (!item.IsVisible || (isGroupSelection && !item.IsGroupSelectionTarget)) + // Keyboard (non-group) selection should only consider visible items. + if (!isGroupSelection && !item.IsVisible) + return false; + + if (isGroupSelection && !CheckValidForGroupSelection(item)) return false; if (isGroupSelection) @@ -427,8 +438,9 @@ namespace osu.Game.Screens.SelectV2 currentKeyboardSelection = new Selection(model); currentSelection = currentKeyboardSelection; - selectionValid.Invalidate(); } + + selectionValid.Invalidate(); } private void setKeyboardSelection(object? model) diff --git a/osu.Game/Screens/SelectV2/CarouselItem.cs b/osu.Game/Screens/SelectV2/CarouselItem.cs index 2cb96a3d7f..13d5c840cf 100644 --- a/osu.Game/Screens/SelectV2/CarouselItem.cs +++ b/osu.Game/Screens/SelectV2/CarouselItem.cs @@ -29,11 +29,6 @@ namespace osu.Game.Screens.SelectV2 /// public float DrawHeight { get; set; } = DEFAULT_HEIGHT; - /// - /// Whether this item should be a valid target for user group selection hotkeys. - /// - public bool IsGroupSelectionTarget { get; set; } - /// /// Whether this item is visible or collapsed (hidden). /// diff --git a/osu.Game/Screens/SelectV2/GroupPanel.cs b/osu.Game/Screens/SelectV2/GroupPanel.cs index e837d8a32f..882d77cb8d 100644 --- a/osu.Game/Screens/SelectV2/GroupPanel.cs +++ b/osu.Game/Screens/SelectV2/GroupPanel.cs @@ -79,7 +79,6 @@ namespace osu.Game.Screens.SelectV2 base.PrepareForUse(); Debug.Assert(Item != null); - Debug.Assert(Item.IsGroupSelectionTarget); GroupDefinition group = (GroupDefinition)Item.Model; From 645c26ca19a16e9c5b33fb66125011c806ca2d78 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sat, 1 Feb 2025 11:18:45 +0900 Subject: [PATCH 0828/1275] Simplify keyboard traversal logic --- osu.Game/Screens/SelectV2/Carousel.cs | 149 +++++++++++++------------- 1 file changed, 73 insertions(+), 76 deletions(-) diff --git a/osu.Game/Screens/SelectV2/Carousel.cs b/osu.Game/Screens/SelectV2/Carousel.cs index a76b6efee9..312dbc1bd9 100644 --- a/osu.Game/Screens/SelectV2/Carousel.cs +++ b/osu.Game/Screens/SelectV2/Carousel.cs @@ -309,19 +309,19 @@ namespace osu.Game.Screens.SelectV2 return true; case GlobalAction.SelectNext: - selectNext(1, isGroupSelection: false); - return true; - - case GlobalAction.SelectNextGroup: - selectNext(1, isGroupSelection: true); + traverseKeyboardSelection(1); return true; case GlobalAction.SelectPrevious: - selectNext(-1, isGroupSelection: false); + traverseKeyboardSelection(-1); + return true; + + case GlobalAction.SelectNextGroup: + traverseGroupSelection(1); return true; case GlobalAction.SelectPreviousGroup: - selectNext(-1, isGroupSelection: true); + traverseGroupSelection(-1); return true; } @@ -332,89 +332,86 @@ namespace osu.Game.Screens.SelectV2 { } - /// - /// Select the next valid selection relative to a current selection. - /// This is generally for keyboard based traversal. - /// - /// Positive for downwards, negative for upwards. - /// Whether the selection should traverse groups. Group selection updates the actual selection immediately, while non-group selection will only prepare a future keyboard selection. - /// Whether selection was possible. - private bool selectNext(int direction, bool isGroupSelection) + private void traverseKeyboardSelection(int direction) { - // Ensure sanity - Debug.Assert(direction != 0); - direction = direction > 0 ? 1 : -1; + if (carouselItems == null || carouselItems.Count == 0) return; - if (carouselItems == null || carouselItems.Count == 0) - return false; + int originalIndex; - // If the user has a different keyboard selection and requests - // group selection, first transfer the keyboard selection to actual selection. - if (isGroupSelection && currentSelection.CarouselItem != currentKeyboardSelection.CarouselItem) - { - TryActivateSelection(); - return true; - } + if (currentKeyboardSelection.Index != null) + originalIndex = currentKeyboardSelection.Index.Value; + else if (direction > 0) + originalIndex = carouselItems.Count - 1; + else + originalIndex = 0; - CarouselItem? selectionItem = currentKeyboardSelection.CarouselItem; - int selectionIndex = currentKeyboardSelection.Index ?? -1; - - // To keep things simple, let's first handle the cases where there's no selection yet. - if (selectionItem == null || selectionIndex < 0) - { - // Start by selecting the first item. - selectionItem = carouselItems.First(); - selectionIndex = 0; - - // In the forwards case, immediately attempt selection of this panel. - // If selection fails, continue with standard logic to find the next valid selection. - if (direction > 0 && attemptSelection(selectionItem)) - return true; - - // In the backwards direction we can just allow the selection logic to go ahead and loop around to the last valid. - } - - Debug.Assert(selectionItem != null); - - // As a second special case, if we're group selecting backwards and the current selection isn't a group, - // make sure to go back to the group header this item belongs to, so that the block below doesn't find it and stop too early. - if (isGroupSelection && direction < 0) - { - while (!CheckValidForGroupSelection(carouselItems[selectionIndex])) - selectionIndex--; - } - - CarouselItem? newItem; + int newIndex = originalIndex; // Iterate over every item back to the current selection, finding the first valid item. // The fail condition is when we reach the selection after a cyclic loop over every item. do { - selectionIndex += direction; - newItem = carouselItems[(selectionIndex + carouselItems.Count) % carouselItems.Count]; + newIndex = (newIndex + direction + carouselItems.Count) % carouselItems.Count; + var newItem = carouselItems[newIndex]; - if (attemptSelection(newItem)) - return true; - } while (newItem != selectionItem); + if (newItem.IsVisible) + { + setKeyboardSelection(newItem.Model); + return; + } + } while (newIndex != originalIndex); + } - return false; + /// + /// Select the next valid selection relative to a current selection. + /// This is generally for keyboard based traversal. + /// + /// Positive for downwards, negative for upwards. + /// Whether selection was possible. + private void traverseGroupSelection(int direction) + { + if (carouselItems == null || carouselItems.Count == 0) return; - bool attemptSelection(CarouselItem item) + // If the user has a different keyboard selection and requests + // group selection, first transfer the keyboard selection to actual selection. + if (currentSelection.CarouselItem != currentKeyboardSelection.CarouselItem) { - // Keyboard (non-group) selection should only consider visible items. - if (!isGroupSelection && !item.IsVisible) - return false; - - if (isGroupSelection && !CheckValidForGroupSelection(item)) - return false; - - if (isGroupSelection) - setSelection(item.Model); - else - setKeyboardSelection(item.Model); - - return true; + TryActivateSelection(); + return; } + + int originalIndex; + + if (currentKeyboardSelection.Index != null) + originalIndex = currentKeyboardSelection.Index.Value; + else if (direction > 0) + originalIndex = carouselItems.Count - 1; + else + originalIndex = 0; + + int newIndex = originalIndex; + + // As a second special case, if we're group selecting backwards and the current selection isn't a group, + // make sure to go back to the group header this item belongs to, so that the block below doesn't find it and stop too early. + if (direction < 0) + { + while (!CheckValidForGroupSelection(carouselItems[newIndex])) + newIndex--; + } + + // Iterate over every item back to the current selection, finding the first valid item. + // The fail condition is when we reach the selection after a cyclic loop over every item. + do + { + newIndex = (newIndex + direction + carouselItems.Count) % carouselItems.Count; + var newItem = carouselItems[newIndex]; + + if (CheckValidForGroupSelection(newItem)) + { + setSelection(newItem.Model); + return; + } + } while (newIndex != originalIndex); } #endregion From 9c34819ff4a533f8a39879dd8a5053676bff415a Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sat, 1 Feb 2025 14:55:48 +0900 Subject: [PATCH 0829/1275] Add test coverage for grouped selection --- .../SongSelect/BeatmapCarouselV2TestScene.cs | 50 +++++++- ...estSceneBeatmapCarouselV2GroupSelection.cs | 121 ++++++++++++++++++ .../TestSceneBeatmapCarouselV2Selection.cs | 112 +++++++--------- osu.Game/Screens/SelectV2/Carousel.cs | 2 +- 4 files changed, 217 insertions(+), 68 deletions(-) create mode 100644 osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2GroupSelection.cs diff --git a/osu.Game.Tests/Visual/SongSelect/BeatmapCarouselV2TestScene.cs b/osu.Game.Tests/Visual/SongSelect/BeatmapCarouselV2TestScene.cs index 281be924a1..5143d681a6 100644 --- a/osu.Game.Tests/Visual/SongSelect/BeatmapCarouselV2TestScene.cs +++ b/osu.Game.Tests/Visual/SongSelect/BeatmapCarouselV2TestScene.cs @@ -21,6 +21,7 @@ using osu.Game.Screens.SelectV2; using osu.Game.Tests.Beatmaps; using osu.Game.Tests.Resources; using osuTK.Graphics; +using osuTK.Input; using BeatmapCarousel = osu.Game.Screens.SelectV2.BeatmapCarousel; namespace osu.Game.Tests.Visual.SongSelect @@ -53,7 +54,7 @@ namespace osu.Game.Tests.Visual.SongSelect } [SetUpSteps] - public void SetUpSteps() + public virtual void SetUpSteps() { RemoveAllBeatmaps(); @@ -135,6 +136,53 @@ namespace osu.Game.Tests.Visual.SongSelect protected void WaitForSorting() => AddUntilStep("sorting finished", () => Carousel.IsFiltering, () => Is.False); protected void WaitForScrolling() => AddUntilStep("scroll finished", () => Scroll.Current, () => Is.EqualTo(Scroll.Target)); + protected void SelectNextPanel() => AddStep("select next panel", () => InputManager.Key(Key.Down)); + protected void SelectPrevPanel() => AddStep("select prev panel", () => InputManager.Key(Key.Up)); + protected void SelectNextGroup() => AddStep("select next group", () => InputManager.Key(Key.Right)); + protected void SelectPrevGroup() => AddStep("select prev group", () => InputManager.Key(Key.Left)); + + protected void Select() => AddStep("select", () => InputManager.Key(Key.Enter)); + + protected void CheckNoSelection() => AddAssert("has no selection", () => Carousel.CurrentSelection, () => Is.Null); + protected void CheckHasSelection() => AddAssert("has selection", () => Carousel.CurrentSelection, () => Is.Not.Null); + + protected void WaitForGroupSelection(int group, int panel) + { + AddUntilStep($"selected is group{group} panel{panel}", () => + { + var groupingFilter = Carousel.Filters.OfType().Single(); + + GroupDefinition g = groupingFilter.GroupItems.Keys.ElementAt(group); + CarouselItem item = groupingFilter.GroupItems[g].ElementAt(panel); + + return ReferenceEquals(Carousel.CurrentSelection, item.Model); + }); + } + + protected void WaitForSelection(int set, int? diff = null) + { + AddUntilStep($"selected is set{set}{(diff.HasValue ? $" diff{diff.Value}" : "")}", () => + { + if (diff != null) + return ReferenceEquals(Carousel.CurrentSelection, BeatmapSets[set].Beatmaps[diff.Value]); + + return BeatmapSets[set].Beatmaps.Contains(Carousel.CurrentSelection); + }); + } + + protected void ClickVisiblePanel(int index) + where T : Drawable + { + AddStep($"click panel at index {index}", () => + { + Carousel.ChildrenOfType() + .Where(p => ((ICarouselPanel)p).Item?.IsVisible == true) + .Reverse() + .ElementAt(index) + .TriggerClick(); + }); + } + /// /// Add requested beatmap sets count to list. /// diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2GroupSelection.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2GroupSelection.cs new file mode 100644 index 0000000000..bcb609500f --- /dev/null +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2GroupSelection.cs @@ -0,0 +1,121 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Linq; +using NUnit.Framework; +using osu.Framework.Testing; +using osu.Game.Beatmaps; +using osu.Game.Screens.Select; +using osu.Game.Screens.Select.Filter; +using osu.Game.Screens.SelectV2; + +namespace osu.Game.Tests.Visual.SongSelect +{ + [TestFixture] + public partial class TestSceneBeatmapCarouselV2GroupSelection : BeatmapCarouselV2TestScene + { + public override void SetUpSteps() + { + RemoveAllBeatmaps(); + + CreateCarousel(); + + SortBy(new FilterCriteria { Sort = SortMode.Difficulty }); + } + + [Test] + public void TestOpenCloseGroupWithNoSelection() + { + AddBeatmaps(10, 5); + WaitForDrawablePanels(); + + AddAssert("no beatmaps visible", () => Carousel.ChildrenOfType().Count(p => p.Alpha > 0), () => Is.Zero); + CheckNoSelection(); + + ClickVisiblePanel(0); + AddUntilStep("some beatmaps visible", () => Carousel.ChildrenOfType().Count(p => p.Alpha > 0), () => Is.GreaterThan(0)); + CheckNoSelection(); + + ClickVisiblePanel(0); + AddUntilStep("no beatmaps visible", () => Carousel.ChildrenOfType().Count(p => p.Alpha > 0), () => Is.Zero); + CheckNoSelection(); + } + + [Test] + public void TestCarouselRemembersSelection() + { + AddBeatmaps(10); + WaitForDrawablePanels(); + + SelectNextGroup(); + + object? selection = null; + + AddStep("store drawable selection", () => selection = getSelectedPanel()?.Item?.Model); + + CheckHasSelection(); + AddAssert("drawable selection non-null", () => selection, () => Is.Not.Null); + AddAssert("drawable selection matches carousel selection", () => selection, () => Is.EqualTo(Carousel.CurrentSelection)); + + RemoveAllBeatmaps(); + AddUntilStep("no drawable selection", getSelectedPanel, () => Is.Null); + + AddBeatmaps(10); + WaitForDrawablePanels(); + + CheckHasSelection(); + AddAssert("no drawable selection", getSelectedPanel, () => Is.Null); + + AddStep("add previous selection", () => BeatmapSets.Add(((BeatmapInfo)selection!).BeatmapSet!)); + + AddAssert("selection matches original carousel selection", () => selection, () => Is.EqualTo(Carousel.CurrentSelection)); + AddUntilStep("drawable selection restored", () => getSelectedPanel()?.Item?.Model, () => Is.EqualTo(selection)); + AddAssert("carousel item is visible", () => getSelectedPanel()?.Item?.IsVisible, () => Is.True); + + ClickVisiblePanel(0); + AddUntilStep("carousel item not visible", getSelectedPanel, () => Is.Null); + + ClickVisiblePanel(0); + AddUntilStep("carousel item is visible", () => getSelectedPanel()?.Item?.IsVisible, () => Is.True); + + BeatmapPanel? getSelectedPanel() => Carousel.ChildrenOfType().SingleOrDefault(p => p.Selected.Value); + } + + [Test] + public void TestKeyboardSelection() + { + AddBeatmaps(10, 3); + WaitForDrawablePanels(); + + SelectNextPanel(); + SelectNextPanel(); + SelectNextPanel(); + SelectNextPanel(); + CheckNoSelection(); + + // open first group + Select(); + CheckNoSelection(); + AddUntilStep("some beatmaps visible", () => Carousel.ChildrenOfType().Count(p => p.Alpha > 0), () => Is.GreaterThan(0)); + + SelectNextPanel(); + Select(); + WaitForGroupSelection(0, 0); + + SelectNextGroup(); + WaitForGroupSelection(0, 1); + + SelectNextGroup(); + WaitForGroupSelection(0, 2); + + SelectPrevGroup(); + WaitForGroupSelection(0, 1); + + SelectPrevGroup(); + WaitForGroupSelection(0, 0); + + SelectPrevGroup(); + WaitForGroupSelection(2, 9); + } + } +} diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2Selection.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2Selection.cs index 3c42969d8c..50395cf1ff 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2Selection.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2Selection.cs @@ -22,10 +22,10 @@ namespace osu.Game.Tests.Visual.SongSelect { AddBeatmaps(10); WaitForDrawablePanels(); - checkNoSelection(); + CheckNoSelection(); - select(); - checkNoSelection(); + Select(); + CheckNoSelection(); AddStep("press down arrow", () => InputManager.PressKey(Key.Down)); checkSelectionIterating(false); @@ -39,8 +39,8 @@ namespace osu.Game.Tests.Visual.SongSelect AddStep("release up arrow", () => InputManager.ReleaseKey(Key.Up)); checkSelectionIterating(false); - select(); - checkHasSelection(); + Select(); + CheckHasSelection(); } /// @@ -52,7 +52,7 @@ namespace osu.Game.Tests.Visual.SongSelect { AddBeatmaps(10); WaitForDrawablePanels(); - checkNoSelection(); + CheckNoSelection(); AddStep("press right arrow", () => InputManager.PressKey(Key.Right)); checkSelectionIterating(true); @@ -73,13 +73,13 @@ namespace osu.Game.Tests.Visual.SongSelect AddBeatmaps(10); WaitForDrawablePanels(); - selectNextGroup(); + SelectNextGroup(); object? selection = null; AddStep("store drawable selection", () => selection = getSelectedPanel()?.Item?.Model); - checkHasSelection(); + CheckHasSelection(); AddAssert("drawable selection non-null", () => selection, () => Is.Not.Null); AddAssert("drawable selection matches carousel selection", () => selection, () => Is.EqualTo(Carousel.CurrentSelection)); @@ -89,13 +89,14 @@ namespace osu.Game.Tests.Visual.SongSelect AddBeatmaps(10); WaitForDrawablePanels(); - checkHasSelection(); + CheckHasSelection(); AddAssert("no drawable selection", getSelectedPanel, () => Is.Null); AddStep("add previous selection", () => BeatmapSets.Add(((BeatmapInfo)selection!).BeatmapSet!)); + AddAssert("selection matches original carousel selection", () => selection, () => Is.EqualTo(Carousel.CurrentSelection)); AddUntilStep("drawable selection restored", () => getSelectedPanel()?.Item?.Model, () => Is.EqualTo(selection)); - AddAssert("drawable selection matches carousel selection", () => selection, () => Is.EqualTo(Carousel.CurrentSelection)); + AddAssert("carousel item is visible", () => getSelectedPanel()?.Item?.IsVisible, () => Is.True); BeatmapPanel? getSelectedPanel() => Carousel.ChildrenOfType().SingleOrDefault(p => p.Selected.Value); } @@ -108,10 +109,10 @@ namespace osu.Game.Tests.Visual.SongSelect AddBeatmaps(total_set_count); WaitForDrawablePanels(); - selectNextGroup(); - waitForSelection(0, 0); - selectPrevGroup(); - waitForSelection(total_set_count - 1, 0); + SelectNextGroup(); + WaitForSelection(0, 0); + SelectPrevGroup(); + WaitForSelection(total_set_count - 1, 0); } [Test] @@ -122,10 +123,10 @@ namespace osu.Game.Tests.Visual.SongSelect AddBeatmaps(total_set_count); WaitForDrawablePanels(); - selectPrevGroup(); - waitForSelection(total_set_count - 1, 0); - selectNextGroup(); - waitForSelection(0, 0); + SelectPrevGroup(); + WaitForSelection(total_set_count - 1, 0); + SelectNextGroup(); + WaitForSelection(0, 0); } [Test] @@ -134,71 +135,50 @@ namespace osu.Game.Tests.Visual.SongSelect AddBeatmaps(10, 3); WaitForDrawablePanels(); - selectNextPanel(); - selectNextPanel(); - selectNextPanel(); - selectNextPanel(); - checkNoSelection(); + SelectNextPanel(); + SelectNextPanel(); + SelectNextPanel(); + SelectNextPanel(); + CheckNoSelection(); - select(); - waitForSelection(3, 0); + Select(); + WaitForSelection(3, 0); - selectNextPanel(); - waitForSelection(3, 0); + SelectNextPanel(); + WaitForSelection(3, 0); - select(); - waitForSelection(3, 1); + Select(); + WaitForSelection(3, 1); - selectNextPanel(); - waitForSelection(3, 1); + SelectNextPanel(); + WaitForSelection(3, 1); - select(); - waitForSelection(3, 2); + Select(); + WaitForSelection(3, 2); - selectNextPanel(); - waitForSelection(3, 2); + SelectNextPanel(); + WaitForSelection(3, 2); - select(); - waitForSelection(4, 0); + Select(); + WaitForSelection(4, 0); } [Test] public void TestEmptyTraversal() { - selectNextPanel(); - checkNoSelection(); + SelectNextPanel(); + CheckNoSelection(); - selectNextGroup(); - checkNoSelection(); + SelectNextGroup(); + CheckNoSelection(); - selectPrevPanel(); - checkNoSelection(); + SelectPrevPanel(); + CheckNoSelection(); - selectPrevGroup(); - checkNoSelection(); + SelectPrevGroup(); + CheckNoSelection(); } - private void waitForSelection(int set, int? diff = null) - { - AddUntilStep($"selected is set{set}{(diff.HasValue ? $" diff{diff.Value}" : "")}", () => - { - if (diff != null) - return ReferenceEquals(Carousel.CurrentSelection, BeatmapSets[set].Beatmaps[diff.Value]); - - return BeatmapSets[set].Beatmaps.Contains(Carousel.CurrentSelection); - }); - } - - private void selectNextPanel() => AddStep("select next panel", () => InputManager.Key(Key.Down)); - private void selectPrevPanel() => AddStep("select prev panel", () => InputManager.Key(Key.Up)); - private void selectNextGroup() => AddStep("select next group", () => InputManager.Key(Key.Right)); - private void selectPrevGroup() => AddStep("select prev group", () => InputManager.Key(Key.Left)); - - private void select() => AddStep("select", () => InputManager.Key(Key.Enter)); - - private void checkNoSelection() => AddAssert("has no selection", () => Carousel.CurrentSelection, () => Is.Null); - private void checkHasSelection() => AddAssert("has selection", () => Carousel.CurrentSelection, () => Is.Not.Null); - private void checkSelectionIterating(bool isIterating) { object? selection = null; diff --git a/osu.Game/Screens/SelectV2/Carousel.cs b/osu.Game/Screens/SelectV2/Carousel.cs index 312dbc1bd9..0da9cb5c19 100644 --- a/osu.Game/Screens/SelectV2/Carousel.cs +++ b/osu.Game/Screens/SelectV2/Carousel.cs @@ -131,7 +131,7 @@ namespace osu.Game.Screens.SelectV2 /// /// A filter may add, mutate or remove items. /// - protected IEnumerable Filters { get; init; } = Enumerable.Empty(); + public IEnumerable Filters { get; init; } = Enumerable.Empty(); /// /// All items which are to be considered for display in this carousel. From 6a18d18feb0ada227cb85fdb9144439196b3cef7 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sun, 2 Feb 2025 13:28:31 +0900 Subject: [PATCH 0830/1275] Fix null handling when no items are populated but a selection is made --- osu.Game/Screens/SelectV2/BeatmapCarousel.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/SelectV2/BeatmapCarousel.cs b/osu.Game/Screens/SelectV2/BeatmapCarousel.cs index 10bc069cfc..858888c517 100644 --- a/osu.Game/Screens/SelectV2/BeatmapCarousel.cs +++ b/osu.Game/Screens/SelectV2/BeatmapCarousel.cs @@ -124,9 +124,10 @@ namespace osu.Game.Screens.SelectV2 if (Criteria.SplitOutDifficulties) { // Find the containing group. There should never be too many groups so iterating is efficient enough. - GroupDefinition group = grouping.GroupItems.Single(kvp => kvp.Value.Any(i => ReferenceEquals(i.Model, beatmapInfo))).Key; + GroupDefinition? group = grouping.GroupItems.SingleOrDefault(kvp => kvp.Value.Any(i => ReferenceEquals(i.Model, beatmapInfo))).Key; - setVisibleGroup(group); + if (group != null) + setVisibleGroup(group); } else { From 48e30f4ee80af5fd9c0e6e39bfd28d48a5df6ccf Mon Sep 17 00:00:00 2001 From: Nathan Du Date: Mon, 3 Feb 2025 09:49:37 +0800 Subject: [PATCH 0831/1275] remove skinning section swell delay test Replaced by TestHitSwellThenHitHit in TestSceneSwellJudgements. --- .../TestSceneDrawableSwellExpireDelay.cs | 41 ------------------- 1 file changed, 41 deletions(-) delete mode 100644 osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneDrawableSwellExpireDelay.cs diff --git a/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneDrawableSwellExpireDelay.cs b/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneDrawableSwellExpireDelay.cs deleted file mode 100644 index ad78ed3b20..0000000000 --- a/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneDrawableSwellExpireDelay.cs +++ /dev/null @@ -1,41 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -using System.Collections.Generic; -using NUnit.Framework; -using osu.Game.Rulesets.Replays; -using osu.Game.Rulesets.Scoring; -using osu.Game.Rulesets.Taiko.Objects; -using osu.Game.Rulesets.Taiko.Replays; -using osu.Game.Rulesets.Taiko.Tests.Judgements; - -namespace osu.Game.Rulesets.Taiko.Tests.Skinning -{ - public partial class TestSceneDrawableSwellExpireDelay : JudgementTest - { - [Test] - public void TestExpireDelay() - { - const double swell_start = 1000; - const double swell_duration = 1000; - - Swell swell = new Swell - { - StartTime = swell_start, - Duration = swell_duration, - }; - - Hit hit = new Hit { StartTime = swell_start + swell_duration + 50 }; - - List frames = new List - { - new TaikoReplayFrame(0), - new TaikoReplayFrame(2100, TaikoAction.LeftCentre), - }; - - PerformTest(frames, CreateBeatmap(swell, hit)); - - AssertResult(0, HitResult.Ok); - } - } -} From 210fa14759313b8b8f0b1aadc7c5e0c84394a4ee Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Mon, 3 Feb 2025 14:15:43 +0900 Subject: [PATCH 0832/1275] Play sound via results screen instead --- .../Expanded/Accuracy/AccuracyCircle.cs | 49 +----- osu.Game/Screens/Ranking/ResultsScreen.cs | 166 ++++++++++++------ 2 files changed, 116 insertions(+), 99 deletions(-) diff --git a/osu.Game/Screens/Ranking/Expanded/Accuracy/AccuracyCircle.cs b/osu.Game/Screens/Ranking/Expanded/Accuracy/AccuracyCircle.cs index 319a87fdfc..4b960b05fb 100644 --- a/osu.Game/Screens/Ranking/Expanded/Accuracy/AccuracyCircle.cs +++ b/osu.Game/Screens/Ranking/Expanded/Accuracy/AccuracyCircle.cs @@ -2,8 +2,8 @@ // See the LICENCE file in the repository root for full licence text. using System; -using System.Collections.Generic; using System.Linq; +using System.Threading; using osu.Framework.Allocation; using osu.Framework.Audio; using osu.Framework.Bindables; @@ -91,6 +91,9 @@ namespace osu.Game.Screens.Ranking.Expanded.Accuracy private readonly ScoreInfo score; + [Resolved] + private ResultsScreen? resultsScreen { get; set; } + private CircularProgress accuracyCircle = null!; private GradedCircles gradedCircles = null!; private Container badges = null!; @@ -101,7 +104,6 @@ namespace osu.Game.Screens.Ranking.Expanded.Accuracy private PoolableSkinnableSample? badgeMaxSound; private PoolableSkinnableSample? swooshUpSound; private PoolableSkinnableSample? rankImpactSound; - private PoolableSkinnableSample? rankApplauseSound; private readonly Bindable tickPlaybackRate = new Bindable(); @@ -197,15 +199,9 @@ namespace osu.Game.Screens.Ranking.Expanded.Accuracy if (withFlair) { - var applauseSamples = new List { applauseSampleName }; - if (score.Rank >= ScoreRank.B) - // when rank is B or higher, play legacy applause sample on legacy skins. - applauseSamples.Insert(0, @"applause"); - AddRangeInternal(new Drawable[] { rankImpactSound = new PoolableSkinnableSample(new SampleInfo(impactSampleName)), - rankApplauseSound = new PoolableSkinnableSample(new SampleInfo(applauseSamples.ToArray())), scoreTickSound = new PoolableSkinnableSample(new SampleInfo(@"Results/score-tick")), badgeTickSound = new PoolableSkinnableSample(new SampleInfo(@"Results/badge-dink")), badgeMaxSound = new PoolableSkinnableSample(new SampleInfo(@"Results/badge-dink-max")), @@ -333,16 +329,9 @@ namespace osu.Game.Screens.Ranking.Expanded.Accuracy }); const double applause_pre_delay = 545f; - const double applause_volume = 0.8f; using (BeginDelayedSequence(applause_pre_delay)) - { - Schedule(() => - { - rankApplauseSound!.VolumeTo(applause_volume); - rankApplauseSound!.Play(); - }); - } + Schedule(() => resultsScreen?.PlayApplause(score.Rank)); } } @@ -384,34 +373,6 @@ namespace osu.Game.Screens.Ranking.Expanded.Accuracy } } - private string applauseSampleName - { - get - { - switch (score.Rank) - { - default: - case ScoreRank.D: - return @"Results/applause-d"; - - case ScoreRank.C: - return @"Results/applause-c"; - - case ScoreRank.B: - return @"Results/applause-b"; - - case ScoreRank.A: - return @"Results/applause-a"; - - case ScoreRank.S: - case ScoreRank.SH: - case ScoreRank.X: - case ScoreRank.XH: - return @"Results/applause-s"; - } - } - } - private string impactSampleName { get diff --git a/osu.Game/Screens/Ranking/ResultsScreen.cs b/osu.Game/Screens/Ranking/ResultsScreen.cs index 95dbfb2712..b10684b22e 100644 --- a/osu.Game/Screens/Ranking/ResultsScreen.cs +++ b/osu.Game/Screens/Ranking/ResultsScreen.cs @@ -17,6 +17,7 @@ using osu.Framework.Graphics.Shapes; using osu.Framework.Input.Bindings; using osu.Framework.Input.Events; using osu.Framework.Screens; +using osu.Game.Audio; using osu.Game.Graphics; using osu.Game.Graphics.Containers; using osu.Game.Graphics.UserInterface; @@ -29,10 +30,12 @@ using osu.Game.Scoring; using osu.Game.Screens.Play; using osu.Game.Screens.Ranking.Expanded.Accuracy; using osu.Game.Screens.Ranking.Statistics; +using osu.Game.Skinning; using osuTK; namespace osu.Game.Screens.Ranking { + [Cached] public abstract partial class ResultsScreen : ScreenWithBeatmapBackground, IKeyBindingHandler { protected const float BACKGROUND_BLUR = 20; @@ -64,7 +67,6 @@ namespace osu.Game.Screens.Ranking private Drawable bottomPanel = null!; private Container detachedPanelContainer = null!; - private AudioContainer audioContainer = null!; private bool lastFetchCompleted; @@ -101,80 +103,76 @@ namespace osu.Game.Screens.Ranking popInSample = audio.Samples.Get(@"UI/overlay-pop-in"); - InternalChild = audioContainer = new AudioContainer + InternalChild = new PopoverContainer { RelativeSizeAxes = Axes.Both, - Child = new PopoverContainer + Child = new GridContainer { RelativeSizeAxes = Axes.Both, - Child = new GridContainer + Content = new[] { - RelativeSizeAxes = Axes.Both, - Content = new[] + new Drawable[] { - new Drawable[] + VerticalScrollContent = new VerticalScrollContainer { - VerticalScrollContent = new VerticalScrollContainer + RelativeSizeAxes = Axes.Both, + ScrollbarVisible = false, + Child = new Container { RelativeSizeAxes = Axes.Both, - ScrollbarVisible = false, - Child = new Container - { - RelativeSizeAxes = Axes.Both, - Children = new Drawable[] - { - StatisticsPanel = createStatisticsPanel().With(panel => - { - panel.RelativeSizeAxes = Axes.Both; - panel.Score.BindTarget = SelectedScore; - }), - ScorePanelList = new ScorePanelList - { - RelativeSizeAxes = Axes.Both, - SelectedScore = { BindTarget = SelectedScore }, - PostExpandAction = () => StatisticsPanel.ToggleVisibility() - }, - detachedPanelContainer = new Container - { - RelativeSizeAxes = Axes.Both - }, - } - } - }, - }, - new[] - { - bottomPanel = new Container - { - Anchor = Anchor.BottomLeft, - Origin = Anchor.BottomLeft, - RelativeSizeAxes = Axes.X, - Height = TwoLayerButton.SIZE_EXTENDED.Y, - Alpha = 0, Children = new Drawable[] { - new Box + StatisticsPanel = createStatisticsPanel().With(panel => + { + panel.RelativeSizeAxes = Axes.Both; + panel.Score.BindTarget = SelectedScore; + }), + ScorePanelList = new ScorePanelList { RelativeSizeAxes = Axes.Both, - Colour = Color4Extensions.FromHex("#333") + SelectedScore = { BindTarget = SelectedScore }, + PostExpandAction = () => StatisticsPanel.ToggleVisibility() }, - buttons = new FillFlowContainer + detachedPanelContainer = new Container { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - AutoSizeAxes = Axes.Both, - Spacing = new Vector2(5), - Direction = FillDirection.Horizontal + RelativeSizeAxes = Axes.Both }, } } - } + }, }, - RowDimensions = new[] + new[] { - new Dimension(), - new Dimension(GridSizeMode.AutoSize) + bottomPanel = new Container + { + Anchor = Anchor.BottomLeft, + Origin = Anchor.BottomLeft, + RelativeSizeAxes = Axes.X, + Height = TwoLayerButton.SIZE_EXTENDED.Y, + Alpha = 0, + Children = new Drawable[] + { + new Box + { + RelativeSizeAxes = Axes.Both, + Colour = Color4Extensions.FromHex("#333") + }, + buttons = new FillFlowContainer + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + AutoSizeAxes = Axes.Both, + Spacing = new Vector2(5), + Direction = FillDirection.Horizontal + }, + } + } } + }, + RowDimensions = new[] + { + new Dimension(), + new Dimension(GridSizeMode.AutoSize) } } }; @@ -268,6 +266,64 @@ namespace osu.Game.Screens.Ranking } } + #region Applause + + private PoolableSkinnableSample? rankApplauseSound; + + public void PlayApplause(ScoreRank rank) + { + const double applause_volume = 0.8f; + + if (!this.IsCurrentScreen()) + return; + + rankApplauseSound?.Dispose(); + + var applauseSamples = new List(); + + if (rank >= ScoreRank.B) + // when rank is B or higher, play legacy applause sample on legacy skins. + applauseSamples.Insert(0, @"applause"); + + switch (rank) + { + default: + case ScoreRank.D: + applauseSamples.Add(@"Results/applause-d"); + break; + + case ScoreRank.C: + applauseSamples.Add(@"Results/applause-c"); + break; + + case ScoreRank.B: + applauseSamples.Add(@"Results/applause-b"); + break; + + case ScoreRank.A: + applauseSamples.Add(@"Results/applause-a"); + break; + + case ScoreRank.S: + case ScoreRank.SH: + case ScoreRank.X: + case ScoreRank.XH: + applauseSamples.Add(@"Results/applause-s"); + break; + } + + LoadComponentAsync(rankApplauseSound = new PoolableSkinnableSample(new SampleInfo(applauseSamples.ToArray())), s => + { + if (!this.IsCurrentScreen() || s != rankApplauseSound) + return; + + rankApplauseSound.VolumeTo(applause_volume); + rankApplauseSound.Play(); + }); + } + + #endregion + /// /// Performs a fetch/refresh of scores to be displayed. /// @@ -336,7 +392,7 @@ namespace osu.Game.Screens.Ranking if (!skipExitTransition) this.FadeOut(100); - audioContainer.Volume.Value = 0; + rankApplauseSound?.Stop(); return false; } From 9033a4d480ed78a69c5c57c10c31789b15b688fd Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Mon, 3 Feb 2025 14:20:56 +0900 Subject: [PATCH 0833/1275] Remove unused using --- osu.Game/Screens/Ranking/Expanded/Accuracy/AccuracyCircle.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/osu.Game/Screens/Ranking/Expanded/Accuracy/AccuracyCircle.cs b/osu.Game/Screens/Ranking/Expanded/Accuracy/AccuracyCircle.cs index 4b960b05fb..f6cf71d8a6 100644 --- a/osu.Game/Screens/Ranking/Expanded/Accuracy/AccuracyCircle.cs +++ b/osu.Game/Screens/Ranking/Expanded/Accuracy/AccuracyCircle.cs @@ -3,7 +3,6 @@ using System; using System.Linq; -using System.Threading; using osu.Framework.Allocation; using osu.Framework.Audio; using osu.Framework.Bindables; From a23de0b1885a3c5f62e4b9971b094167d8c5b1a7 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 3 Feb 2025 16:29:39 +0900 Subject: [PATCH 0834/1275] Avoid accessing `WorkingBeatmap.Beatmap` every update call Notice in passing. Comes with overheads that can be easily avoided. Left a note for a future (slightly more involved) optimisation. --- osu.Game/Beatmaps/WorkingBeatmap.cs | 2 ++ .../Play/MasterGameplayClockContainer.cs | 32 +++++++++---------- 2 files changed, 17 insertions(+), 17 deletions(-) diff --git a/osu.Game/Beatmaps/WorkingBeatmap.cs b/osu.Game/Beatmaps/WorkingBeatmap.cs index 890a969415..fd40097c4e 100644 --- a/osu.Game/Beatmaps/WorkingBeatmap.cs +++ b/osu.Game/Beatmaps/WorkingBeatmap.cs @@ -203,6 +203,8 @@ namespace osu.Game.Beatmaps { try { + // TODO: This is a touch expensive and can become an issue if being accessed every Update call. + // Optimally we would not involve the async flow if things are already loaded. return loadBeatmapAsync().GetResultSafely(); } catch (AggregateException ae) diff --git a/osu.Game/Screens/Play/MasterGameplayClockContainer.cs b/osu.Game/Screens/Play/MasterGameplayClockContainer.cs index c20d461526..747ea3090c 100644 --- a/osu.Game/Screens/Play/MasterGameplayClockContainer.cs +++ b/osu.Game/Screens/Play/MasterGameplayClockContainer.cs @@ -53,7 +53,7 @@ namespace osu.Game.Screens.Play private readonly Bindable playbackRateValid = new Bindable(true); - private readonly WorkingBeatmap beatmap; + private readonly IBeatmap beatmap; private Track track; @@ -63,20 +63,19 @@ namespace osu.Game.Screens.Play /// /// Create a new master gameplay clock container. /// - /// The beatmap to be used for time and metadata references. + /// The beatmap to be used for time and metadata references. /// The latest time which should be used when introducing gameplay. Will be used when skipping forward. - public MasterGameplayClockContainer(WorkingBeatmap beatmap, double gameplayStartTime) - : base(beatmap.Track, applyOffsets: true, requireDecoupling: true) + public MasterGameplayClockContainer(WorkingBeatmap working, double gameplayStartTime) + : base(working.Track, applyOffsets: true, requireDecoupling: true) { - this.beatmap = beatmap; - - track = beatmap.Track; + beatmap = working.Beatmap; + track = working.Track; GameplayStartTime = gameplayStartTime; - StartTime = findEarliestStartTime(gameplayStartTime, beatmap); + StartTime = findEarliestStartTime(gameplayStartTime, working); } - private static double findEarliestStartTime(double gameplayStartTime, WorkingBeatmap beatmap) + private static double findEarliestStartTime(double gameplayStartTime, WorkingBeatmap working) { // here we are trying to find the time to start playback from the "zero" point. // generally this is either zero, or some point earlier than zero in the case of storyboards, lead-ins etc. @@ -86,15 +85,15 @@ namespace osu.Game.Screens.Play // if a storyboard is present, it may dictate the appropriate start time by having events in negative time space. // this is commonly used to display an intro before the audio track start. - double? firstStoryboardEvent = beatmap.Storyboard.EarliestEventTime; + double? firstStoryboardEvent = working.Storyboard.EarliestEventTime; if (firstStoryboardEvent != null) time = Math.Min(time, firstStoryboardEvent.Value); // some beatmaps specify a current lead-in time which should be used instead of the ruleset-provided value when available. // this is not available as an option in the live editor but can still be applied via .osu editing. - double firstHitObjectTime = beatmap.Beatmap.HitObjects.First().StartTime; - if (beatmap.Beatmap.AudioLeadIn > 0) - time = Math.Min(time, firstHitObjectTime - beatmap.Beatmap.AudioLeadIn); + double firstHitObjectTime = working.Beatmap.HitObjects.First().StartTime; + if (working.Beatmap.AudioLeadIn > 0) + time = Math.Min(time, firstHitObjectTime - working.Beatmap.AudioLeadIn); return time; } @@ -136,7 +135,7 @@ namespace osu.Game.Screens.Play { removeAdjustmentsFromTrack(); - track = new TrackVirtual(beatmap.Track.Length); + track = new TrackVirtual(track.Length); track.Seek(CurrentTime); if (IsRunning) track.Start(); @@ -228,9 +227,8 @@ namespace osu.Game.Screens.Play removeAdjustmentsFromTrack(); } - ControlPointInfo IBeatSyncProvider.ControlPoints => beatmap.Beatmap.ControlPointInfo; + ControlPointInfo IBeatSyncProvider.ControlPoints => beatmap.ControlPointInfo; + ChannelAmplitudes IHasAmplitudes.CurrentAmplitudes => track.CurrentAmplitudes; IClock IBeatSyncProvider.Clock => this; - - ChannelAmplitudes IHasAmplitudes.CurrentAmplitudes => beatmap.TrackLoaded ? beatmap.Track.CurrentAmplitudes : ChannelAmplitudes.Empty; } } From c587958f387db1287218801292a1ed9480d8edef Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Sat, 1 Feb 2025 03:01:47 -0500 Subject: [PATCH 0835/1275] Apply depth ordering relative to selected item --- osu.Game/Screens/SelectV2/Carousel.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/SelectV2/Carousel.cs b/osu.Game/Screens/SelectV2/Carousel.cs index 648c2d090a..f41154b878 100644 --- a/osu.Game/Screens/SelectV2/Carousel.cs +++ b/osu.Game/Screens/SelectV2/Carousel.cs @@ -544,8 +544,8 @@ namespace osu.Game.Screens.SelectV2 if (c.Item == null) continue; - if (panel.Depth != c.DrawYPosition) - scroll.Panels.ChangeChildDepth(panel, (float)c.DrawYPosition); + double selectedYPos = currentSelection?.CarouselItem?.CarouselYPosition ?? 0; + scroll.Panels.ChangeChildDepth(panel, (float)Math.Abs(c.DrawYPosition - selectedYPos)); if (c.DrawYPosition != c.Item.CarouselYPosition) c.DrawYPosition = Interpolation.DampContinuously(c.DrawYPosition, c.Item.CarouselYPosition, 50, Time.Elapsed); From 26a8fb6984e66ef3d992db23beec6f86ca0b682d Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 3 Feb 2025 17:34:55 +0900 Subject: [PATCH 0836/1275] Make distance snap settings mutually exclusive --- osu.Game/Screens/Edit/Editor.cs | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/osu.Game/Screens/Edit/Editor.cs b/osu.Game/Screens/Edit/Editor.cs index d5ed54db81..6b18b05174 100644 --- a/osu.Game/Screens/Edit/Editor.cs +++ b/osu.Game/Screens/Edit/Editor.cs @@ -330,6 +330,18 @@ namespace osu.Game.Screens.Edit editorTimelineShowTicks = config.GetBindable(OsuSetting.EditorTimelineShowTicks); editorContractSidebars = config.GetBindable(OsuSetting.EditorContractSidebars); + // These two settings don't work together. Make them mutually exclusive to let the user know. + editorAutoSeekOnPlacement.BindValueChanged(enabled => + { + if (enabled.NewValue) + editorLimitedDistanceSnap.Value = false; + }); + editorLimitedDistanceSnap.BindValueChanged(enabled => + { + if (enabled.NewValue) + editorAutoSeekOnPlacement.Value = false; + }); + AddInternal(new OsuContextMenuContainer { RelativeSizeAxes = Axes.Both, From df51d345c5e1e49b98c43b898f38ccd0403b5abf Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 3 Feb 2025 17:36:47 +0900 Subject: [PATCH 0837/1275] Change menus to fade out with a slight delay so settings changes are visible Useful for cases like https://github.com/ppy/osu/pull/31778, where a change to one setting can affect another. --- osu.Game/Graphics/UserInterface/OsuMenu.cs | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/osu.Game/Graphics/UserInterface/OsuMenu.cs b/osu.Game/Graphics/UserInterface/OsuMenu.cs index 7cc1bab25f..9b099c0884 100644 --- a/osu.Game/Graphics/UserInterface/OsuMenu.cs +++ b/osu.Game/Graphics/UserInterface/OsuMenu.cs @@ -68,7 +68,9 @@ namespace osu.Game.Graphics.UserInterface if (!TopLevelMenu && wasOpened) menuSamples?.PlayCloseSample(); - this.FadeOut(300, Easing.OutQuint); + this.Delay(50) + .FadeOut(300, Easing.OutQuint); + wasOpened = false; } @@ -77,12 +79,21 @@ namespace osu.Game.Graphics.UserInterface if (Direction == Direction.Vertical) { Width = newSize.X; - this.ResizeHeightTo(newSize.Y, 300, Easing.OutQuint); + + if (newSize.Y > 0) + this.ResizeHeightTo(newSize.Y, 300, Easing.OutQuint); + else + // Delay until the fade out finishes from AnimateClose. + this.Delay(350).ResizeHeightTo(0); } else { Height = newSize.Y; - this.ResizeWidthTo(newSize.X, 300, Easing.OutQuint); + if (newSize.X > 0) + this.ResizeWidthTo(newSize.X, 300, Easing.OutQuint); + else + // Delay until the fade out finishes from AnimateClose. + this.Delay(350).ResizeWidthTo(0); } } From 55f46e3b668fbc16856f872044cc011f739e05b8 Mon Sep 17 00:00:00 2001 From: NecoDev <120387312+necocat0918@users.noreply.github.com> Date: Mon, 3 Feb 2025 16:06:27 +0800 Subject: [PATCH 0838/1275] Added warning --- osu.Game/Screens/Edit/BookmarkResetDialog.cs | 26 ++++++++++++++++++++ osu.Game/Screens/Edit/Editor.cs | 3 ++- 2 files changed, 28 insertions(+), 1 deletion(-) create mode 100644 osu.Game/Screens/Edit/BookmarkResetDialog.cs diff --git a/osu.Game/Screens/Edit/BookmarkResetDialog.cs b/osu.Game/Screens/Edit/BookmarkResetDialog.cs new file mode 100644 index 0000000000..48a0202c86 --- /dev/null +++ b/osu.Game/Screens/Edit/BookmarkResetDialog.cs @@ -0,0 +1,26 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Allocation; +using osu.Game.Overlays.Dialog; + +namespace osu.Game.Screens.Edit +{ + public partial class BookmarkResetDialog : DeletionDialog + { + private readonly EditorBeatmap editor; + + public BookmarkResetDialog(EditorBeatmap editorBeatmap) + { + editor = editorBeatmap; + BodyText = "All Bookmarks"; + } + + [BackgroundDependencyLoader] + private void load() + { + DangerousAction = () => editor.Bookmarks.Clear(); + } + } +} + diff --git a/osu.Game/Screens/Edit/Editor.cs b/osu.Game/Screens/Edit/Editor.cs index d5ed54db81..8cffab87ea 100644 --- a/osu.Game/Screens/Edit/Editor.cs +++ b/osu.Game/Screens/Edit/Editor.cs @@ -8,6 +8,7 @@ using System.Collections.Generic; using System.Linq; using System.Threading; using System.Threading.Tasks; +using DiffPlex.Model; using JetBrains.Annotations; using osu.Framework; using osu.Framework.Allocation; @@ -450,7 +451,7 @@ namespace osu.Game.Screens.Edit { Hotkey = new Hotkey(GlobalAction.EditorSeekToNextBookmark) }, - new EditorMenuItem(EditorStrings.ResetBookmarks, MenuItemType.Destructive, () => editorBeatmap.Bookmarks.Clear()) + new EditorMenuItem(EditorStrings.ResetBookmarks, MenuItemType.Destructive, () => dialogOverlay?.Push(new BookmarkResetDialog(editorBeatmap))) } } } From 444e0970d600d90e087e47af1816b63d7487a796 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 3 Feb 2025 18:54:18 +0900 Subject: [PATCH 0839/1275] Standardise naming to use "Freestyle" not "FreeStyle" --- osu.Game/Online/Rooms/MultiplayerPlaylistItem.cs | 2 +- osu.Game/Online/Rooms/PlaylistItem.cs | 8 ++++---- ...ButtonFreeStyle.cs => FooterButtonFreestyle.cs} | 4 ++-- .../OnlinePlay/Lounge/Components/DrawableRoom.cs | 2 +- ...eeStyleStatusPill.cs => FreestyleStatusPill.cs} | 12 ++++++------ osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs | 8 ++++---- .../Multiplayer/MultiplayerMatchSongSelect.cs | 2 +- .../Screens/OnlinePlay/OnlinePlaySongSelect.cs | 14 +++++++------- .../OnlinePlay/Playlists/PlaylistsSongSelect.cs | 2 +- 9 files changed, 27 insertions(+), 27 deletions(-) rename osu.Game/Screens/OnlinePlay/{FooterButtonFreeStyle.cs => FooterButtonFreestyle.cs} (96%) rename osu.Game/Screens/OnlinePlay/Lounge/Components/{FreeStyleStatusPill.cs => FreestyleStatusPill.cs} (84%) diff --git a/osu.Game/Online/Rooms/MultiplayerPlaylistItem.cs b/osu.Game/Online/Rooms/MultiplayerPlaylistItem.cs index 4dfb3b389d..b737cda4ba 100644 --- a/osu.Game/Online/Rooms/MultiplayerPlaylistItem.cs +++ b/osu.Game/Online/Rooms/MultiplayerPlaylistItem.cs @@ -60,7 +60,7 @@ namespace osu.Game.Online.Rooms /// Indicates whether participants in the room are able to pick their own choice of beatmap difficulty and ruleset. /// [Key(11)] - public bool FreeStyle { get; set; } + public bool Freestyle { get; set; } [SerializationConstructor] public MultiplayerPlaylistItem() diff --git a/osu.Game/Online/Rooms/PlaylistItem.cs b/osu.Game/Online/Rooms/PlaylistItem.cs index e8725b6792..817b42f503 100644 --- a/osu.Game/Online/Rooms/PlaylistItem.cs +++ b/osu.Game/Online/Rooms/PlaylistItem.cs @@ -71,7 +71,7 @@ namespace osu.Game.Online.Rooms /// Indicates whether participants in the room are able to pick their own choice of beatmap difficulty and ruleset. /// [JsonProperty("freestyle")] - public bool FreeStyle { get; set; } + public bool Freestyle { get; set; } /// /// A beatmap representing this playlist item. @@ -107,7 +107,7 @@ namespace osu.Game.Online.Rooms PlayedAt = item.PlayedAt; RequiredMods = item.RequiredMods.ToArray(); AllowedMods = item.AllowedMods.ToArray(); - FreeStyle = item.FreeStyle; + Freestyle = item.Freestyle; } public void MarkInvalid() => valid.Value = false; @@ -139,7 +139,7 @@ namespace osu.Game.Online.Rooms PlayedAt = PlayedAt, AllowedMods = AllowedMods, RequiredMods = RequiredMods, - FreeStyle = FreeStyle, + Freestyle = Freestyle, valid = { Value = Valid.Value }, }; } @@ -152,6 +152,6 @@ namespace osu.Game.Online.Rooms && PlaylistOrder == other.PlaylistOrder && AllowedMods.SequenceEqual(other.AllowedMods) && RequiredMods.SequenceEqual(other.RequiredMods) - && FreeStyle == other.FreeStyle; + && Freestyle == other.Freestyle; } } diff --git a/osu.Game/Screens/OnlinePlay/FooterButtonFreeStyle.cs b/osu.Game/Screens/OnlinePlay/FooterButtonFreestyle.cs similarity index 96% rename from osu.Game/Screens/OnlinePlay/FooterButtonFreeStyle.cs rename to osu.Game/Screens/OnlinePlay/FooterButtonFreestyle.cs index 0e22b3d3fb..157f90d078 100644 --- a/osu.Game/Screens/OnlinePlay/FooterButtonFreeStyle.cs +++ b/osu.Game/Screens/OnlinePlay/FooterButtonFreestyle.cs @@ -16,7 +16,7 @@ using osu.Game.Localisation; namespace osu.Game.Screens.OnlinePlay { - public partial class FooterButtonFreeStyle : FooterButton, IHasCurrentValue + public partial class FooterButtonFreestyle : FooterButton, IHasCurrentValue { private readonly BindableWithCurrent current = new BindableWithCurrent(); @@ -34,7 +34,7 @@ namespace osu.Game.Screens.OnlinePlay [Resolved] private OsuColour colours { get; set; } = null!; - public FooterButtonFreeStyle() + public FooterButtonFreestyle() { // Overwrite any external behaviour as we delegate the main toggle action to a sub-button. base.Action = () => current.Value = !current.Value; diff --git a/osu.Game/Screens/OnlinePlay/Lounge/Components/DrawableRoom.cs b/osu.Game/Screens/OnlinePlay/Lounge/Components/DrawableRoom.cs index 7bc0b612f1..a16267aa10 100644 --- a/osu.Game/Screens/OnlinePlay/Lounge/Components/DrawableRoom.cs +++ b/osu.Game/Screens/OnlinePlay/Lounge/Components/DrawableRoom.cs @@ -169,7 +169,7 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components Anchor = Anchor.CentreLeft, Origin = Anchor.CentreLeft }, - new FreeStyleStatusPill(Room) + new FreestyleStatusPill(Room) { Anchor = Anchor.CentreLeft, Origin = Anchor.CentreLeft diff --git a/osu.Game/Screens/OnlinePlay/Lounge/Components/FreeStyleStatusPill.cs b/osu.Game/Screens/OnlinePlay/Lounge/Components/FreestyleStatusPill.cs similarity index 84% rename from osu.Game/Screens/OnlinePlay/Lounge/Components/FreeStyleStatusPill.cs rename to osu.Game/Screens/OnlinePlay/Lounge/Components/FreestyleStatusPill.cs index 1c0135fb89..b306e27f84 100644 --- a/osu.Game/Screens/OnlinePlay/Lounge/Components/FreeStyleStatusPill.cs +++ b/osu.Game/Screens/OnlinePlay/Lounge/Components/FreestyleStatusPill.cs @@ -10,7 +10,7 @@ using osuTK.Graphics; namespace osu.Game.Screens.OnlinePlay.Lounge.Components { - public partial class FreeStyleStatusPill : OnlinePlayPill + public partial class FreestyleStatusPill : OnlinePlayPill { private readonly Room room; @@ -19,7 +19,7 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components protected override FontUsage Font => base.Font.With(weight: FontWeight.SemiBold); - public FreeStyleStatusPill(Room room) + public FreestyleStatusPill(Room room) { this.room = room; } @@ -35,7 +35,7 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components TextFlow.Colour = Color4.Black; room.PropertyChanged += onRoomPropertyChanged; - updateFreeStyleStatus(); + updateFreestyleStatus(); } private void onRoomPropertyChanged(object? sender, PropertyChangedEventArgs e) @@ -44,15 +44,15 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components { case nameof(Room.CurrentPlaylistItem): case nameof(Room.Playlist): - updateFreeStyleStatus(); + updateFreestyleStatus(); break; } } - private void updateFreeStyleStatus() + private void updateFreestyleStatus() { PlaylistItem? currentItem = room.Playlist.GetCurrentItem() ?? room.CurrentPlaylistItem; - Alpha = currentItem?.FreeStyle == true ? 1 : 0; + Alpha = currentItem?.Freestyle == true ? 1 : 0; } protected override void Dispose(bool isDisposing) diff --git a/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs b/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs index c9c9c3eca7..9f7e193131 100644 --- a/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs @@ -450,11 +450,11 @@ namespace osu.Game.Screens.OnlinePlay.Match Ruleset.Value = GetGameplayRuleset(); bool freeMod = item.AllowedMods.Any(); - bool freeStyle = item.FreeStyle; + bool freestyle = item.Freestyle; // For now, the game can never be in a state where freemod and freestyle are on at the same time. // This will change, but due to the current implementation if this was to occur drawables will overlap so let's assert. - Debug.Assert(!freeMod || !freeStyle); + Debug.Assert(!freeMod || !freestyle); if (freeMod) { @@ -468,7 +468,7 @@ namespace osu.Game.Screens.OnlinePlay.Match UserModsSelectOverlay.IsValidMod = _ => false; } - if (freeStyle) + if (freestyle) { UserStyleSection.Show(); @@ -481,7 +481,7 @@ namespace osu.Game.Screens.OnlinePlay.Match UserStyleDisplayContainer.Child = new DrawableRoomPlaylistItem(gameplayItem, true) { AllowReordering = false, - AllowEditing = freeStyle, + AllowEditing = freestyle, RequestEdit = _ => OpenStyleSelection() }; } diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSongSelect.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSongSelect.cs index 5754bcb963..b42a58787d 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSongSelect.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSongSelect.cs @@ -87,7 +87,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer RulesetID = item.RulesetID, RequiredMods = item.RequiredMods.ToArray(), AllowedMods = item.AllowedMods.ToArray(), - FreeStyle = item.FreeStyle + Freestyle = item.Freestyle }; Task task = itemToEdit != null ? client.EditPlaylistItem(multiplayerItem) : client.AddPlaylistItem(multiplayerItem); diff --git a/osu.Game/Screens/OnlinePlay/OnlinePlaySongSelect.cs b/osu.Game/Screens/OnlinePlay/OnlinePlaySongSelect.cs index f6403c010e..8d1e3c3cb1 100644 --- a/osu.Game/Screens/OnlinePlay/OnlinePlaySongSelect.cs +++ b/osu.Game/Screens/OnlinePlay/OnlinePlaySongSelect.cs @@ -42,7 +42,7 @@ namespace osu.Game.Screens.OnlinePlay protected override UserActivity InitialActivity => new UserActivity.InLobby(room); protected readonly Bindable> FreeMods = new Bindable>(Array.Empty()); - protected readonly Bindable FreeStyle = new Bindable(); + protected readonly Bindable Freestyle = new Bindable(); private readonly Room room; private readonly PlaylistItem? initialItem; @@ -112,17 +112,17 @@ namespace osu.Game.Screens.OnlinePlay FreeMods.Value = initialItem.AllowedMods.Select(m => m.ToMod(rulesetInstance)).ToArray(); } - FreeStyle.Value = initialItem.FreeStyle; + Freestyle.Value = initialItem.Freestyle; } Mods.BindValueChanged(onModsChanged); Ruleset.BindValueChanged(onRulesetChanged); - FreeStyle.BindValueChanged(onFreeStyleChanged, true); + Freestyle.BindValueChanged(onFreestyleChanged, true); freeModSelectOverlayRegistration = OverlayManager?.RegisterBlockingOverlay(freeModSelect); } - private void onFreeStyleChanged(ValueChangedEvent enabled) + private void onFreestyleChanged(ValueChangedEvent enabled) { if (enabled.NewValue) { @@ -162,7 +162,7 @@ namespace osu.Game.Screens.OnlinePlay RulesetID = Ruleset.Value.OnlineID, RequiredMods = Mods.Value.Select(m => new APIMod(m)).ToArray(), AllowedMods = FreeMods.Value.Select(m => new APIMod(m)).ToArray(), - FreeStyle = FreeStyle.Value + Freestyle = Freestyle.Value }; return SelectItem(item); @@ -204,12 +204,12 @@ namespace osu.Game.Screens.OnlinePlay baseButtons.Single(i => i.button is FooterButtonMods).button.TooltipText = MultiplayerMatchStrings.RequiredModsButtonTooltip; freeModsFooterButton = new FooterButtonFreeMods(freeModSelect) { Current = FreeMods }; - var freeStyleButton = new FooterButtonFreeStyle { Current = FreeStyle }; + var freestyleButton = new FooterButtonFreestyle { Current = Freestyle }; baseButtons.InsertRange(baseButtons.FindIndex(b => b.button is FooterButtonMods) + 1, new (FooterButton, OverlayContainer?)[] { (freeModsFooterButton, null), - (freeStyleButton, null) + (freestyleButton, null) }); return baseButtons; diff --git a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsSongSelect.cs b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsSongSelect.cs index abf80c0d44..84446ed0cf 100644 --- a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsSongSelect.cs +++ b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsSongSelect.cs @@ -40,7 +40,7 @@ namespace osu.Game.Screens.OnlinePlay.Playlists RulesetID = Ruleset.Value.OnlineID, RequiredMods = Mods.Value.Select(m => new APIMod(m)).ToArray(), AllowedMods = FreeMods.Value.Select(m => new APIMod(m)).ToArray(), - FreeStyle = FreeStyle.Value + Freestyle = Freestyle.Value }; } } From 37abb1a21bc24185b8d554fc38f2f0cef09284e5 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 3 Feb 2025 19:09:58 +0900 Subject: [PATCH 0840/1275] Tidy up button construction code --- osu.Game/Screens/OnlinePlay/OnlinePlaySongSelect.cs | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/OnlinePlaySongSelect.cs b/osu.Game/Screens/OnlinePlay/OnlinePlaySongSelect.cs index 8d1e3c3cb1..4ca6abbf7d 100644 --- a/osu.Game/Screens/OnlinePlay/OnlinePlaySongSelect.cs +++ b/osu.Game/Screens/OnlinePlay/OnlinePlaySongSelect.cs @@ -203,13 +203,10 @@ namespace osu.Game.Screens.OnlinePlay baseButtons.Single(i => i.button is FooterButtonMods).button.TooltipText = MultiplayerMatchStrings.RequiredModsButtonTooltip; - freeModsFooterButton = new FooterButtonFreeMods(freeModSelect) { Current = FreeMods }; - var freestyleButton = new FooterButtonFreestyle { Current = Freestyle }; - baseButtons.InsertRange(baseButtons.FindIndex(b => b.button is FooterButtonMods) + 1, new (FooterButton, OverlayContainer?)[] { - (freeModsFooterButton, null), - (freestyleButton, null) + (freeModsFooterButton = new FooterButtonFreeMods(freeModSelect) { Current = FreeMods }, null), + (new FooterButtonFreestyle { Current = Freestyle }, null) }); return baseButtons; From 8bb7bea04e56fab9247baa59ae879e16c8b4bd9b Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 3 Feb 2025 19:21:21 +0900 Subject: [PATCH 0841/1275] Rename freestyle select screen classes for better discoverability --- ...MatchStyleSelect.cs => MultiplayerMatchFreestyleSelect.cs} | 4 ++-- .../OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs | 2 +- ...{OnlinePlayStyleSelect.cs => OnlinePlayFreestyleSelect.cs} | 4 ++-- ...istsRoomStyleSelect.cs => PlaylistsRoomFreestyleSelect.cs} | 4 ++-- .../Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs | 2 +- 5 files changed, 8 insertions(+), 8 deletions(-) rename osu.Game/Screens/OnlinePlay/Multiplayer/{MultiplayerMatchStyleSelect.cs => MultiplayerMatchFreestyleSelect.cs} (94%) rename osu.Game/Screens/OnlinePlay/{OnlinePlayStyleSelect.cs => OnlinePlayFreestyleSelect.cs} (94%) rename osu.Game/Screens/OnlinePlay/Playlists/{PlaylistsRoomStyleSelect.cs => PlaylistsRoomFreestyleSelect.cs} (87%) diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchStyleSelect.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchFreestyleSelect.cs similarity index 94% rename from osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchStyleSelect.cs rename to osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchFreestyleSelect.cs index 3fe4926052..0c04c2712c 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchStyleSelect.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchFreestyleSelect.cs @@ -12,7 +12,7 @@ using osu.Game.Online.Rooms; namespace osu.Game.Screens.OnlinePlay.Multiplayer { - public partial class MultiplayerMatchStyleSelect : OnlinePlayStyleSelect + public partial class MultiplayerMatchFreestyleSelect : OnlinePlayFreestyleSelect { [Resolved] private MultiplayerClient client { get; set; } = null!; @@ -25,7 +25,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer private LoadingLayer loadingLayer = null!; private IDisposable? selectionOperation; - public MultiplayerMatchStyleSelect(Room room, PlaylistItem item) + public MultiplayerMatchFreestyleSelect(Room room, PlaylistItem item) : base(room, item) { } diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs index f882fb7f89..b803c5f28b 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs @@ -258,7 +258,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer if (!this.IsCurrentScreen() || SelectedItem.Value is not PlaylistItem item) return; - this.Push(new MultiplayerMatchStyleSelect(Room, item)); + this.Push(new MultiplayerMatchFreestyleSelect(Room, item)); } protected override Drawable CreateFooter() => new MultiplayerMatchFooter diff --git a/osu.Game/Screens/OnlinePlay/OnlinePlayStyleSelect.cs b/osu.Game/Screens/OnlinePlay/OnlinePlayFreestyleSelect.cs similarity index 94% rename from osu.Game/Screens/OnlinePlay/OnlinePlayStyleSelect.cs rename to osu.Game/Screens/OnlinePlay/OnlinePlayFreestyleSelect.cs index 4d34000d3c..4844d096ce 100644 --- a/osu.Game/Screens/OnlinePlay/OnlinePlayStyleSelect.cs +++ b/osu.Game/Screens/OnlinePlay/OnlinePlayFreestyleSelect.cs @@ -16,7 +16,7 @@ using osu.Game.Users; namespace osu.Game.Screens.OnlinePlay { - public abstract partial class OnlinePlayStyleSelect : SongSelect, IOnlinePlaySubScreen, IHandlePresentBeatmap + public abstract partial class OnlinePlayFreestyleSelect : SongSelect, IOnlinePlaySubScreen, IHandlePresentBeatmap { public string ShortTitle => "style selection"; @@ -29,7 +29,7 @@ namespace osu.Game.Screens.OnlinePlay private readonly Room room; private readonly PlaylistItem item; - protected OnlinePlayStyleSelect(Room room, PlaylistItem item) + protected OnlinePlayFreestyleSelect(Room room, PlaylistItem item) { this.room = room; this.item = item; diff --git a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomStyleSelect.cs b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomFreestyleSelect.cs similarity index 87% rename from osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomStyleSelect.cs rename to osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomFreestyleSelect.cs index 912496ba34..9c85088cc9 100644 --- a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomStyleSelect.cs +++ b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomFreestyleSelect.cs @@ -9,12 +9,12 @@ using osu.Game.Rulesets; namespace osu.Game.Screens.OnlinePlay.Playlists { - public partial class PlaylistsRoomStyleSelect : OnlinePlayStyleSelect + public partial class PlaylistsRoomFreestyleSelect : OnlinePlayFreestyleSelect { public new readonly Bindable Beatmap = new Bindable(); public new readonly Bindable Ruleset = new Bindable(); - public PlaylistsRoomStyleSelect(Room room, PlaylistItem item) + public PlaylistsRoomFreestyleSelect(Room room, PlaylistItem item) : base(room, item) { } diff --git a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs index 2c74767f42..2195ed4722 100644 --- a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs @@ -319,7 +319,7 @@ namespace osu.Game.Screens.OnlinePlay.Playlists if (!this.IsCurrentScreen() || SelectedItem.Value is not PlaylistItem item) return; - this.Push(new PlaylistsRoomStyleSelect(Room, item) + this.Push(new PlaylistsRoomFreestyleSelect(Room, item) { Beatmap = { BindTarget = userBeatmap }, Ruleset = { BindTarget = userRuleset } From 99192404f125b3f5f380b4a167f7a6be1d6646ae Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 3 Feb 2025 19:26:14 +0900 Subject: [PATCH 0842/1275] Tidy up `WorkingBeatmap` passing in `ctor` --- .../Screens/Play/MasterGameplayClockContainer.cs | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/osu.Game/Screens/Play/MasterGameplayClockContainer.cs b/osu.Game/Screens/Play/MasterGameplayClockContainer.cs index 747ea3090c..07ecb5a5fb 100644 --- a/osu.Game/Screens/Play/MasterGameplayClockContainer.cs +++ b/osu.Game/Screens/Play/MasterGameplayClockContainer.cs @@ -12,6 +12,7 @@ using osu.Framework.Timing; using osu.Game.Beatmaps; using osu.Game.Beatmaps.ControlPoints; using osu.Game.Overlays; +using osu.Game.Storyboards; namespace osu.Game.Screens.Play { @@ -72,10 +73,10 @@ namespace osu.Game.Screens.Play track = working.Track; GameplayStartTime = gameplayStartTime; - StartTime = findEarliestStartTime(gameplayStartTime, working); + StartTime = findEarliestStartTime(gameplayStartTime, beatmap, working.Storyboard); } - private static double findEarliestStartTime(double gameplayStartTime, WorkingBeatmap working) + private static double findEarliestStartTime(double gameplayStartTime, IBeatmap beatmap, Storyboard storyboard) { // here we are trying to find the time to start playback from the "zero" point. // generally this is either zero, or some point earlier than zero in the case of storyboards, lead-ins etc. @@ -85,15 +86,15 @@ namespace osu.Game.Screens.Play // if a storyboard is present, it may dictate the appropriate start time by having events in negative time space. // this is commonly used to display an intro before the audio track start. - double? firstStoryboardEvent = working.Storyboard.EarliestEventTime; + double? firstStoryboardEvent = storyboard.EarliestEventTime; if (firstStoryboardEvent != null) time = Math.Min(time, firstStoryboardEvent.Value); // some beatmaps specify a current lead-in time which should be used instead of the ruleset-provided value when available. // this is not available as an option in the live editor but can still be applied via .osu editing. - double firstHitObjectTime = working.Beatmap.HitObjects.First().StartTime; - if (working.Beatmap.AudioLeadIn > 0) - time = Math.Min(time, firstHitObjectTime - working.Beatmap.AudioLeadIn); + double firstHitObjectTime = beatmap.HitObjects.First().StartTime; + if (beatmap.AudioLeadIn > 0) + time = Math.Min(time, firstHitObjectTime - beatmap.AudioLeadIn); return time; } From c7780c9fdca97525d2f20920bc44951b652e4854 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 3 Feb 2025 19:53:46 +0900 Subject: [PATCH 0843/1275] Refactor how grouping is performed --- .../SongSelect/BeatmapCarouselV2TestScene.cs | 2 +- .../TestSceneBeatmapCarouselV2Basics.cs | 2 +- ...estSceneBeatmapCarouselV2GroupSelection.cs | 2 +- .../SelectV2/BeatmapCarouselFilterGrouping.cs | 133 +++++++++++------- 4 files changed, 85 insertions(+), 54 deletions(-) diff --git a/osu.Game.Tests/Visual/SongSelect/BeatmapCarouselV2TestScene.cs b/osu.Game.Tests/Visual/SongSelect/BeatmapCarouselV2TestScene.cs index 5143d681a6..0a9719423c 100644 --- a/osu.Game.Tests/Visual/SongSelect/BeatmapCarouselV2TestScene.cs +++ b/osu.Game.Tests/Visual/SongSelect/BeatmapCarouselV2TestScene.cs @@ -130,7 +130,7 @@ namespace osu.Game.Tests.Visual.SongSelect }); } - protected void SortBy(FilterCriteria criteria) => AddStep($"sort by {criteria.Sort}", () => Carousel.Filter(criteria)); + protected void SortBy(FilterCriteria criteria) => AddStep($"sort {criteria.Sort} group {criteria.Group}", () => Carousel.Filter(criteria)); protected void WaitForDrawablePanels() => AddUntilStep("drawable panels loaded", () => Carousel.ChildrenOfType().Count(), () => Is.GreaterThan(0)); protected void WaitForSorting() => AddUntilStep("sorting finished", () => Carousel.IsFiltering, () => Is.False); diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2Basics.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2Basics.cs index 0e72ee4f8c..8ffb51b995 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2Basics.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2Basics.cs @@ -50,7 +50,7 @@ namespace osu.Game.Tests.Visual.SongSelect public void TestSorting() { AddBeatmaps(10); - SortBy(new FilterCriteria { Sort = SortMode.Difficulty }); + SortBy(new FilterCriteria { Group = GroupMode.Difficulty, Sort = SortMode.Difficulty }); SortBy(new FilterCriteria { Sort = SortMode.Artist }); } diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2GroupSelection.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2GroupSelection.cs index bcb609500f..5728583507 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2GroupSelection.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2GroupSelection.cs @@ -20,7 +20,7 @@ namespace osu.Game.Tests.Visual.SongSelect CreateCarousel(); - SortBy(new FilterCriteria { Sort = SortMode.Difficulty }); + SortBy(new FilterCriteria { Group = GroupMode.Difficulty, Sort = SortMode.Difficulty }); } [Test] diff --git a/osu.Game/Screens/SelectV2/BeatmapCarouselFilterGrouping.cs b/osu.Game/Screens/SelectV2/BeatmapCarouselFilterGrouping.cs index 951b010564..34fbfdbaa6 100644 --- a/osu.Game/Screens/SelectV2/BeatmapCarouselFilterGrouping.cs +++ b/osu.Game/Screens/SelectV2/BeatmapCarouselFilterGrouping.cs @@ -8,6 +8,7 @@ using System.Threading; using System.Threading.Tasks; using osu.Game.Beatmaps; using osu.Game.Screens.Select; +using osu.Game.Screens.Select.Filter; namespace osu.Game.Screens.SelectV2 { @@ -35,70 +36,100 @@ namespace osu.Game.Screens.SelectV2 public async Task> Run(IEnumerable items, CancellationToken cancellationToken) => await Task.Run(() => { + bool groupSetsTogether; + setItems.Clear(); groupItems.Clear(); var criteria = getCriteria(); - - int starGroup = int.MinValue; - - if (criteria.SplitOutDifficulties) - { - var diffItems = new List(items.Count()); - - GroupDefinition? group = null; - - foreach (var item in items) - { - var b = (BeatmapInfo)item.Model; - - if (b.StarRating > starGroup) - { - starGroup = (int)Math.Floor(b.StarRating); - group = new GroupDefinition($"{starGroup} - {++starGroup} *"); - diffItems.Add(new CarouselItem(group) { DrawHeight = GroupPanel.HEIGHT }); - } - - if (!groupItems.TryGetValue(group!, out var related)) - groupItems[group!] = related = new HashSet(); - related.Add(item); - - diffItems.Add(item); - - item.IsVisible = false; - } - - return diffItems; - } - - CarouselItem? lastItem = null; - var newItems = new List(items.Count()); - foreach (var item in items) + // Add criteria groups. + switch (criteria.Group) + { + default: + groupSetsTogether = true; + newItems.AddRange(items); + break; + + case GroupMode.Difficulty: + groupSetsTogether = false; + int starGroup = int.MinValue; + + foreach (var item in items) + { + cancellationToken.ThrowIfCancellationRequested(); + + var b = (BeatmapInfo)item.Model; + + if (b.StarRating > starGroup) + { + starGroup = (int)Math.Floor(b.StarRating); + newItems.Add(new CarouselItem(new GroupDefinition($"{starGroup} - {++starGroup} *")) { DrawHeight = GroupPanel.HEIGHT }); + } + + newItems.Add(item); + } + + break; + } + + // Add set headers wherever required. + CarouselItem? lastItem = null; + + if (groupSetsTogether) + { + for (int i = 0; i < newItems.Count; i++) + { + cancellationToken.ThrowIfCancellationRequested(); + + var item = newItems[i]; + + if (item.Model is BeatmapInfo beatmap) + { + if (groupSetsTogether) + { + bool newBeatmapSet = lastItem == null || (lastItem.Model is BeatmapInfo lastBeatmap && lastBeatmap.BeatmapSet!.ID != beatmap.BeatmapSet!.ID); + + if (newBeatmapSet) + { + newItems.Insert(i, new CarouselItem(beatmap.BeatmapSet!) { DrawHeight = BeatmapSetPanel.HEIGHT }); + i++; + } + + if (!setItems.TryGetValue(beatmap.BeatmapSet!, out var related)) + setItems[beatmap.BeatmapSet!] = related = new HashSet(); + + related.Add(item); + item.IsVisible = false; + } + } + + lastItem = item; + } + } + + // Link group items to their headers. + GroupDefinition? lastGroup = null; + + foreach (var item in newItems) { cancellationToken.ThrowIfCancellationRequested(); - if (item.Model is BeatmapInfo b) + if (item.Model is GroupDefinition group) { - // Add set header - if (lastItem == null || (lastItem.Model is BeatmapInfo b2 && b2.BeatmapSet!.OnlineID != b.BeatmapSet!.OnlineID)) - { - newItems.Add(new CarouselItem(b.BeatmapSet!) - { - DrawHeight = BeatmapSetPanel.HEIGHT, - }); - } - - if (!setItems.TryGetValue(b.BeatmapSet!, out var related)) - setItems[b.BeatmapSet!] = related = new HashSet(); - related.Add(item); + lastGroup = group; + continue; } - newItems.Add(item); - lastItem = item; + if (lastGroup != null) + { + if (!groupItems.TryGetValue(lastGroup, out var groupRelated)) + groupItems[lastGroup] = groupRelated = new HashSet(); + groupRelated.Add(item); - item.IsVisible = false; + item.IsVisible = false; + } } return newItems; From a1185df2ebb833c0cfb9a4a93987a2a97e547453 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 22 Jan 2025 14:33:14 +0100 Subject: [PATCH 0844/1275] Refactor `IDistanceSnapProvider` to accept slider velocity objects as a reference Method signatures are also changed to be a lot more explicit as to what inputs they expect. --- .../Edit/CatchDistanceSnapProvider.cs | 2 +- .../Components/PathControlPointVisualiser.cs | 2 +- .../Sliders/SliderPlacementBlueprint.cs | 2 +- .../Sliders/SliderSelectionBlueprint.cs | 4 +- .../Edit/OsuDistanceSnapGrid.cs | 5 +- .../Edit/OsuDistanceSnapProvider.cs | 2 +- .../Edit/OsuHitObjectComposer.cs | 17 ++-- ...tSceneHitObjectComposerDistanceSnapping.cs | 31 +++---- .../Editing/TestSceneDistanceSnapGrid.cs | 14 +-- .../Edit/ComposerDistanceSnapProvider.cs | 46 +++------ .../Rulesets/Edit/IDistanceSnapProvider.cs | 93 ++++++++++++------- .../Rulesets/Objects/SliderPathExtensions.cs | 4 +- .../Components/CircularDistanceSnapGrid.cs | 30 +++--- .../Compose/Components/DistanceSnapGrid.cs | 20 ++-- 14 files changed, 138 insertions(+), 134 deletions(-) diff --git a/osu.Game.Rulesets.Catch/Edit/CatchDistanceSnapProvider.cs b/osu.Game.Rulesets.Catch/Edit/CatchDistanceSnapProvider.cs index ae4025aa2f..420a0eb34f 100644 --- a/osu.Game.Rulesets.Catch/Edit/CatchDistanceSnapProvider.cs +++ b/osu.Game.Rulesets.Catch/Edit/CatchDistanceSnapProvider.cs @@ -17,7 +17,7 @@ namespace osu.Game.Rulesets.Catch.Edit // // The implementation below is probably correct but should be checked if/when exposed via controls. - float expectedDistance = DurationToDistance(before, after.StartTime - before.GetEndTime()); + float expectedDistance = DurationToDistance(after.StartTime - before.GetEndTime(), before.StartTime); float previousEndX = (before as JuiceStream)?.EndX ?? ((CatchHitObject)before).EffectiveX; float actualDistance = Math.Abs(previousEndX - ((CatchHitObject)after).EffectiveX); diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs index b9938209ae..bc3d27fd68 100644 --- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs +++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs @@ -34,7 +34,7 @@ using osuTK.Input; namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components { public partial class PathControlPointVisualiser : CompositeDrawable, IKeyBindingHandler, IHasContextMenu - where T : OsuHitObject, IHasPath + where T : OsuHitObject, IHasPath, IHasSliderVelocity { public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => true; // allow context menu to appear outside the playfield. diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderPlacementBlueprint.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderPlacementBlueprint.cs index 21817045c4..a747d4fce8 100644 --- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderPlacementBlueprint.cs +++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderPlacementBlueprint.cs @@ -434,7 +434,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders if (state == SliderPlacementState.Drawing) HitObject.Path.ExpectedDistance.Value = (float)HitObject.Path.CalculatedDistance; else - HitObject.Path.ExpectedDistance.Value = distanceSnapProvider?.FindSnappedDistance(HitObject, (float)HitObject.Path.CalculatedDistance, DistanceSnapTarget.Start) ?? (float)HitObject.Path.CalculatedDistance; + HitObject.Path.ExpectedDistance.Value = distanceSnapProvider?.FindSnappedDistance((float)HitObject.Path.CalculatedDistance, HitObject.StartTime, HitObject) ?? (float)HitObject.Path.CalculatedDistance; bodyPiece.UpdateFrom(HitObject); headCirclePiece.UpdateFrom(HitObject.HeadCircle); diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs index 740862c9fd..f7c25b43dd 100644 --- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs +++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs @@ -274,9 +274,9 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders } else { - double minDistance = distanceSnapProvider?.GetBeatSnapDistanceAt(HitObject, false) * oldVelocityMultiplier ?? 1; + double minDistance = distanceSnapProvider?.GetBeatSnapDistance() * oldVelocityMultiplier ?? 1; // Add a small amount to the proposed distance to make it easier to snap to the full length of the slider. - proposedDistance = distanceSnapProvider?.FindSnappedDistance(HitObject, (float)proposedDistance + 1, DistanceSnapTarget.Start) ?? proposedDistance; + proposedDistance = distanceSnapProvider?.FindSnappedDistance((float)proposedDistance + 1, HitObject.StartTime, HitObject) ?? proposedDistance; proposedDistance = MathHelper.Clamp(proposedDistance, minDistance, HitObject.Path.CalculatedDistance); } diff --git a/osu.Game.Rulesets.Osu/Edit/OsuDistanceSnapGrid.cs b/osu.Game.Rulesets.Osu/Edit/OsuDistanceSnapGrid.cs index 848c994974..3323acce15 100644 --- a/osu.Game.Rulesets.Osu/Edit/OsuDistanceSnapGrid.cs +++ b/osu.Game.Rulesets.Osu/Edit/OsuDistanceSnapGrid.cs @@ -5,6 +5,7 @@ using JetBrains.Annotations; using osu.Game.Rulesets.Objects; +using osu.Game.Rulesets.Objects.Types; using osu.Game.Rulesets.Osu.Objects; using osu.Game.Screens.Edit.Compose.Components; @@ -12,8 +13,8 @@ namespace osu.Game.Rulesets.Osu.Edit { public partial class OsuDistanceSnapGrid : CircularDistanceSnapGrid { - public OsuDistanceSnapGrid(OsuHitObject hitObject, [CanBeNull] OsuHitObject nextHitObject = null) - : base(hitObject, hitObject.StackedEndPosition, hitObject.GetEndTime(), nextHitObject?.StartTime - 1) + public OsuDistanceSnapGrid(OsuHitObject hitObject, [CanBeNull] OsuHitObject nextHitObject = null, [CanBeNull] IHasSliderVelocity sliderVelocitySource = null) + : base(hitObject.StackedEndPosition, hitObject.GetEndTime(), nextHitObject?.StartTime - 1, sliderVelocitySource) { Masking = true; } diff --git a/osu.Game.Rulesets.Osu/Edit/OsuDistanceSnapProvider.cs b/osu.Game.Rulesets.Osu/Edit/OsuDistanceSnapProvider.cs index 4042cfa0e2..3c0889d027 100644 --- a/osu.Game.Rulesets.Osu/Edit/OsuDistanceSnapProvider.cs +++ b/osu.Game.Rulesets.Osu/Edit/OsuDistanceSnapProvider.cs @@ -14,7 +14,7 @@ namespace osu.Game.Rulesets.Osu.Edit { public override double ReadCurrentDistanceSnap(HitObject before, HitObject after) { - float expectedDistance = DurationToDistance(before, after.StartTime - before.GetEndTime()); + float expectedDistance = DurationToDistance(after.StartTime - before.GetEndTime(), before.StartTime); float actualDistance = Vector2.Distance(((OsuHitObject)before).EndPosition, ((OsuHitObject)after).Position); return actualDistance / expectedDistance; diff --git a/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs b/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs index 563d0b1e3e..60c37cd4a4 100644 --- a/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs +++ b/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs @@ -23,6 +23,7 @@ using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Edit.Tools; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Objects; +using osu.Game.Rulesets.Objects.Types; using osu.Game.Rulesets.Osu.Objects; using osu.Game.Rulesets.Osu.UI; using osu.Game.Rulesets.UI; @@ -406,22 +407,26 @@ namespace osu.Game.Rulesets.Osu.Edit { ArgumentOutOfRangeException.ThrowIfNegativeOrZero(targetOffset); - int sourceIndex = -1; + int positionSourceObjectIndex = -1; + IHasSliderVelocity? sliderVelocitySource = null; for (int i = 0; i < EditorBeatmap.HitObjects.Count; i++) { if (!sourceSelector(EditorBeatmap.HitObjects[i])) break; - sourceIndex = i; + positionSourceObjectIndex = i; + + if (EditorBeatmap.HitObjects[i] is IHasSliderVelocity hasSliderVelocity) + sliderVelocitySource = hasSliderVelocity; } - if (sourceIndex == -1) + if (positionSourceObjectIndex == -1) return null; - HitObject sourceObject = EditorBeatmap.HitObjects[sourceIndex]; + HitObject sourceObject = EditorBeatmap.HitObjects[positionSourceObjectIndex]; - int targetIndex = sourceIndex + targetOffset; + int targetIndex = positionSourceObjectIndex + targetOffset; HitObject targetObject = null; // Keep advancing the target object while its start time falls before the end time of the source object @@ -442,7 +447,7 @@ namespace osu.Game.Rulesets.Osu.Edit if (sourceObject is Spinner) return null; - return new OsuDistanceSnapGrid((OsuHitObject)sourceObject, (OsuHitObject)targetObject); + return new OsuDistanceSnapGrid((OsuHitObject)sourceObject, (OsuHitObject)targetObject, sliderVelocitySource); } } } diff --git a/osu.Game.Tests/Editing/TestSceneHitObjectComposerDistanceSnapping.cs b/osu.Game.Tests/Editing/TestSceneHitObjectComposerDistanceSnapping.cs index 0f8583253b..af116ad334 100644 --- a/osu.Game.Tests/Editing/TestSceneHitObjectComposerDistanceSnapping.cs +++ b/osu.Game.Tests/Editing/TestSceneHitObjectComposerDistanceSnapping.cs @@ -12,6 +12,7 @@ using osu.Framework.Utils; using osu.Game.Beatmaps.ControlPoints; using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Objects; +using osu.Game.Rulesets.Objects.Types; using osu.Game.Rulesets.Osu; using osu.Game.Rulesets.Osu.Beatmaps; using osu.Game.Rulesets.Osu.Edit; @@ -67,17 +68,7 @@ namespace osu.Game.Tests.Editing { AddStep($"set slider multiplier = {multiplier}", () => composer.EditorBeatmap.Difficulty.SliderMultiplier = multiplier); - assertSnapDistance(100 * multiplier, null, true); - } - - [TestCase(1)] - [TestCase(2)] - public void TestSpeedMultiplierDoesNotChangeDistanceSnap(float multiplier) - { - assertSnapDistance(100, new Slider - { - SliderVelocityMultiplier = multiplier - }, false); + assertSnapDistance(100 * multiplier); } [TestCase(1)] @@ -87,7 +78,7 @@ namespace osu.Game.Tests.Editing assertSnapDistance(100 * multiplier, new Slider { SliderVelocityMultiplier = multiplier - }, true); + }); } [TestCase(1)] @@ -96,7 +87,7 @@ namespace osu.Game.Tests.Editing { AddStep($"set divisor = {divisor}", () => BeatDivisor.Value = divisor); - assertSnapDistance(100f / divisor, null, true); + assertSnapDistance(100f / divisor); } /// @@ -114,7 +105,7 @@ namespace osu.Game.Tests.Editing }; AddStep("add to beatmap", () => composer.EditorBeatmap.Add(referenceObject)); - assertSnapDistance(base_distance * slider_velocity, referenceObject, true); + assertSnapDistance(base_distance * slider_velocity, referenceObject); assertSnappedDistance(base_distance * slider_velocity + 10, base_distance * slider_velocity, referenceObject); assertSnappedDuration(base_distance * slider_velocity + 10, 1000, referenceObject); @@ -289,20 +280,20 @@ namespace osu.Game.Tests.Editing AddUntilStep("use current snap not available", () => getCurrentSnapButton().Enabled.Value, () => Is.False); } - private void assertSnapDistance(float expectedDistance, HitObject? referenceObject, bool includeSliderVelocity) - => AddAssert($"distance is {expectedDistance}", () => composer.DistanceSnapProvider.GetBeatSnapDistanceAt(referenceObject ?? new HitObject(), includeSliderVelocity), () => Is.EqualTo(expectedDistance).Within(Precision.FLOAT_EPSILON)); + private void assertSnapDistance(float expectedDistance, IHasSliderVelocity? hasSliderVelocity = null) + => AddAssert($"distance is {expectedDistance}", () => composer.DistanceSnapProvider.GetBeatSnapDistance(hasSliderVelocity), () => Is.EqualTo(expectedDistance).Within(Precision.FLOAT_EPSILON)); private void assertDurationToDistance(double duration, float expectedDistance, HitObject? referenceObject = null) - => AddAssert($"duration = {duration} -> distance = {expectedDistance}", () => composer.DistanceSnapProvider.DurationToDistance(referenceObject ?? new HitObject(), duration), () => Is.EqualTo(expectedDistance).Within(Precision.FLOAT_EPSILON)); + => AddAssert($"duration = {duration} -> distance = {expectedDistance}", () => composer.DistanceSnapProvider.DurationToDistance(duration, referenceObject?.StartTime ?? 0, referenceObject as IHasSliderVelocity), () => Is.EqualTo(expectedDistance).Within(Precision.FLOAT_EPSILON)); private void assertDistanceToDuration(float distance, double expectedDuration, HitObject? referenceObject = null) - => AddAssert($"distance = {distance} -> duration = {expectedDuration}", () => composer.DistanceSnapProvider.DistanceToDuration(referenceObject ?? new HitObject(), distance), () => Is.EqualTo(expectedDuration).Within(Precision.FLOAT_EPSILON)); + => AddAssert($"distance = {distance} -> duration = {expectedDuration}", () => composer.DistanceSnapProvider.DistanceToDuration(distance, referenceObject?.StartTime ?? 0, referenceObject as IHasSliderVelocity), () => Is.EqualTo(expectedDuration).Within(Precision.FLOAT_EPSILON)); private void assertSnappedDuration(float distance, double expectedDuration, HitObject? referenceObject = null) - => AddAssert($"distance = {distance} -> duration = {expectedDuration} (snapped)", () => composer.DistanceSnapProvider.FindSnappedDuration(referenceObject ?? new HitObject(), distance), () => Is.EqualTo(expectedDuration).Within(Precision.FLOAT_EPSILON)); + => AddAssert($"distance = {distance} -> duration = {expectedDuration} (snapped)", () => composer.DistanceSnapProvider.FindSnappedDuration(distance, referenceObject?.StartTime ?? 0, referenceObject as IHasSliderVelocity), () => Is.EqualTo(expectedDuration).Within(Precision.FLOAT_EPSILON)); private void assertSnappedDistance(float distance, float expectedDistance, HitObject? referenceObject = null) - => AddAssert($"distance = {distance} -> distance = {expectedDistance} (snapped)", () => composer.DistanceSnapProvider.FindSnappedDistance(referenceObject ?? new HitObject(), distance, DistanceSnapTarget.End), () => Is.EqualTo(expectedDistance).Within(Precision.FLOAT_EPSILON)); + => AddAssert($"distance = {distance} -> distance = {expectedDistance} (snapped)", () => composer.DistanceSnapProvider.FindSnappedDistance(distance, referenceObject?.GetEndTime() ?? 0, referenceObject as IHasSliderVelocity), () => Is.EqualTo(expectedDistance).Within(Precision.FLOAT_EPSILON)); private partial class TestHitObjectComposer : OsuHitObjectComposer { diff --git a/osu.Game.Tests/Visual/Editing/TestSceneDistanceSnapGrid.cs b/osu.Game.Tests/Visual/Editing/TestSceneDistanceSnapGrid.cs index 818862d958..51e4f526a1 100644 --- a/osu.Game.Tests/Visual/Editing/TestSceneDistanceSnapGrid.cs +++ b/osu.Game.Tests/Visual/Editing/TestSceneDistanceSnapGrid.cs @@ -10,7 +10,7 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Shapes; using osu.Game.Beatmaps.ControlPoints; using osu.Game.Rulesets.Edit; -using osu.Game.Rulesets.Objects; +using osu.Game.Rulesets.Objects.Types; using osu.Game.Rulesets.Osu; using osu.Game.Rulesets.Osu.Beatmaps; using osu.Game.Screens.Edit; @@ -115,7 +115,7 @@ namespace osu.Game.Tests.Visual.Editing public new int MaxIntervals => base.MaxIntervals; public TestDistanceSnapGrid(double? endTime = null) - : base(new HitObject(), grid_position, 0, endTime) + : base(grid_position, 0, endTime) { } @@ -191,15 +191,15 @@ namespace osu.Game.Tests.Visual.Editing Bindable IDistanceSnapProvider.DistanceSpacingMultiplier => DistanceSpacingMultiplier; - public float GetBeatSnapDistanceAt(HitObject referenceObject, bool useReferenceSliderVelocity = true) => beat_snap_distance; + public float GetBeatSnapDistance(IHasSliderVelocity withVelocity = null) => beat_snap_distance; - public float DurationToDistance(HitObject referenceObject, double duration) => (float)duration; + public float DurationToDistance(double duration, double timingReference, IHasSliderVelocity withVelocity = null) => (float)duration; - public double DistanceToDuration(HitObject referenceObject, float distance) => distance; + public double DistanceToDuration(float distance, double timingReference, IHasSliderVelocity withVelocity = null) => distance; - public double FindSnappedDuration(HitObject referenceObject, float distance) => 0; + public double FindSnappedDuration(float distance, double snapReferenceTime, IHasSliderVelocity withVelocity = null) => 0; - public float FindSnappedDistance(HitObject referenceObject, float distance, DistanceSnapTarget target) => 0; + public float FindSnappedDistance(float distance, double snapReferenceTime, IHasSliderVelocity? withVelocity = null) => 0; } } } diff --git a/osu.Game/Rulesets/Edit/ComposerDistanceSnapProvider.cs b/osu.Game/Rulesets/Edit/ComposerDistanceSnapProvider.cs index 0ca01ccee6..997d1f927b 100644 --- a/osu.Game/Rulesets/Edit/ComposerDistanceSnapProvider.cs +++ b/osu.Game/Rulesets/Edit/ComposerDistanceSnapProvider.cs @@ -265,57 +265,41 @@ namespace osu.Game.Rulesets.Edit #region IDistanceSnapProvider - public virtual float GetBeatSnapDistanceAt(HitObject referenceObject, bool useReferenceSliderVelocity = true) + public virtual float GetBeatSnapDistance(IHasSliderVelocity? withVelocity = null) { - return (float)(100 * (useReferenceSliderVelocity && referenceObject is IHasSliderVelocity hasSliderVelocity ? hasSliderVelocity.SliderVelocityMultiplier : 1) * editorBeatmap.Difficulty.SliderMultiplier * 1 + return (float)(100 * (withVelocity?.SliderVelocityMultiplier ?? 1) * editorBeatmap.Difficulty.SliderMultiplier * 1 / beatSnapProvider.BeatDivisor); } - public virtual float DurationToDistance(HitObject referenceObject, double duration) + public virtual float DurationToDistance(double duration, double timingReference, IHasSliderVelocity? withVelocity = null) { - double beatLength = beatSnapProvider.GetBeatLengthAtTime(referenceObject.StartTime); - return (float)(duration / beatLength * GetBeatSnapDistanceAt(referenceObject)); + double beatLength = beatSnapProvider.GetBeatLengthAtTime(timingReference); + return (float)(duration / beatLength * GetBeatSnapDistance(withVelocity)); } - public virtual double DistanceToDuration(HitObject referenceObject, float distance) + public virtual double DistanceToDuration(float distance, double timingReference, IHasSliderVelocity? withVelocity = null) { - double beatLength = beatSnapProvider.GetBeatLengthAtTime(referenceObject.StartTime); - return distance / GetBeatSnapDistanceAt(referenceObject) * beatLength; + double beatLength = beatSnapProvider.GetBeatLengthAtTime(timingReference); + return distance / GetBeatSnapDistance(withVelocity) * beatLength; } - public virtual double FindSnappedDuration(HitObject referenceObject, float distance) - => beatSnapProvider.SnapTime(referenceObject.StartTime + DistanceToDuration(referenceObject, distance), referenceObject.StartTime) - referenceObject.StartTime; + public virtual double FindSnappedDuration(float distance, double snapReferenceTime, IHasSliderVelocity? withVelocity = null) + => beatSnapProvider.SnapTime(snapReferenceTime + DistanceToDuration(distance, snapReferenceTime, withVelocity), snapReferenceTime) - snapReferenceTime; - public virtual float FindSnappedDistance(HitObject referenceObject, float distance, DistanceSnapTarget target) + public virtual float FindSnappedDistance(float distance, double snapReferenceTime, IHasSliderVelocity? withVelocity = null) { - double referenceTime; + double actualDuration = snapReferenceTime + DistanceToDuration(distance, snapReferenceTime, withVelocity); - switch (target) - { - case DistanceSnapTarget.Start: - referenceTime = referenceObject.StartTime; - break; + double snappedTime = beatSnapProvider.SnapTime(actualDuration, snapReferenceTime); - case DistanceSnapTarget.End: - referenceTime = referenceObject.GetEndTime(); - break; - - default: - throw new ArgumentOutOfRangeException(nameof(target), target, $"Unknown {nameof(DistanceSnapTarget)} value"); - } - - double actualDuration = referenceTime + DistanceToDuration(referenceObject, distance); - - double snappedTime = beatSnapProvider.SnapTime(actualDuration, referenceTime); - - double beatLength = beatSnapProvider.GetBeatLengthAtTime(referenceTime); + double beatLength = beatSnapProvider.GetBeatLengthAtTime(snapReferenceTime); // we don't want to exceed the actual duration and snap to a point in the future. // as we are snapping to beat length via SnapTime (which will round-to-nearest), check for snapping in the forward direction and reverse it. if (snappedTime > actualDuration + 1) snappedTime -= beatLength; - return DurationToDistance(referenceObject, snappedTime - referenceTime); + return DurationToDistance(snappedTime - snapReferenceTime, snapReferenceTime, withVelocity); } #endregion diff --git a/osu.Game/Rulesets/Edit/IDistanceSnapProvider.cs b/osu.Game/Rulesets/Edit/IDistanceSnapProvider.cs index 612e09d3ea..99a9083273 100644 --- a/osu.Game/Rulesets/Edit/IDistanceSnapProvider.cs +++ b/osu.Game/Rulesets/Edit/IDistanceSnapProvider.cs @@ -4,7 +4,7 @@ using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Game.Beatmaps; -using osu.Game.Rulesets.Objects; +using osu.Game.Rulesets.Objects.Types; namespace osu.Game.Rulesets.Edit { @@ -22,53 +22,74 @@ namespace osu.Game.Rulesets.Edit Bindable DistanceSpacingMultiplier { get; } /// - /// Retrieves the distance between two points within a timing point that are one beat length apart. + /// Returns the spatial distance between objects which are temporally one beat apart. + /// Depends on: + /// + /// the slider velocity taken from , + /// the beatmap's ,, + /// the current beat divisor. + /// + /// Note that the returned value does NOT depend on ; + /// consumers are expected to include that multiplier as they see fit. /// - /// An object to be used as a reference point for this operation. - /// Whether the 's slider velocity should be factored into the returned distance. - /// The distance between two points residing in the timing point that are one beat length apart. - float GetBeatSnapDistanceAt(HitObject referenceObject, bool useReferenceSliderVelocity = true); + float GetBeatSnapDistance(IHasSliderVelocity? withVelocity = null); /// - /// Converts a duration to a distance without applying any snapping. + /// Converts a temporal duration into a spatial distance. + /// Does not perform any snapping. + /// Depends on: + /// + /// the provided, + /// a used to retrieve the beat length of the beatmap at that time, + /// the slider velocity taken from , + /// the beatmap's ,, + /// the current beat divisor. + /// + /// Note that the returned value does NOT depend on ; + /// consumers are expected to include that multiplier as they see fit. /// - /// An object to be used as a reference point for this operation. - /// The duration to convert. - /// A value that represents as a distance in the timing point. - float DurationToDistance(HitObject referenceObject, double duration); + float DurationToDistance(double duration, double timingReference, IHasSliderVelocity? withVelocity = null); /// - /// Converts a distance to a duration without applying any snapping. + /// Converts a spatial distance into a temporal duration. + /// Does not perform any snapping. + /// Depends on: + /// + /// the provided, + /// a used to retrieve the beat length of the beatmap at that time, + /// the slider velocity taken from , + /// the beatmap's ,, + /// the current beat divisor. + /// + /// Note that the returned value does NOT depend on ; + /// consumers are expected to include that multiplier as they see fit. /// - /// An object to be used as a reference point for this operation. - /// The distance to convert. - /// A value that represents as a duration in the timing point. - double DistanceToDuration(HitObject referenceObject, float distance); + double DistanceToDuration(float distance, double timingReference, IHasSliderVelocity? withVelocity = null); /// - /// Given a distance from the provided hit object, find the valid snapped duration. + /// Converts a spatial distance into a temporal duration and then snaps said duration to the beat, relative to . + /// Depends on: + /// + /// the provided, + /// a used to retrieve the beat length of the beatmap at that time, + /// the slider velocity taken from , + /// the beatmap's ,, + /// the current beat divisor. + /// /// - /// An object to be used as a reference point for this operation. - /// The distance to convert. - /// A value that represents as a duration snapped to the closest beat of the timing point. - double FindSnappedDuration(HitObject referenceObject, float distance); + double FindSnappedDuration(float distance, double snapReferenceTime, IHasSliderVelocity? withVelocity = null); /// - /// Given a distance from the provided hit object, find the valid snapped distance. + /// Snaps a spatial distance to the beat, relative to . + /// Depends on: + /// + /// the provided, + /// a used to retrieve the beat length of the beatmap at that time, + /// the slider velocity taken from , + /// the beatmap's ,, + /// the current beat divisor. + /// /// - /// An object to be used as a reference point for this operation. - /// The distance to convert. - /// Whether the distance measured should be from the start or the end of . - /// - /// A value that represents snapped to the closest beat of the timing point. - /// The distance will always be less than or equal to the provided . - /// - float FindSnappedDistance(HitObject referenceObject, float distance, DistanceSnapTarget target); - } - - public enum DistanceSnapTarget - { - Start, - End, + float FindSnappedDistance(float distance, double snapReferenceTime, IHasSliderVelocity? withVelocity = null); } } diff --git a/osu.Game/Rulesets/Objects/SliderPathExtensions.cs b/osu.Game/Rulesets/Objects/SliderPathExtensions.cs index a631274f74..4ce8166421 100644 --- a/osu.Game/Rulesets/Objects/SliderPathExtensions.cs +++ b/osu.Game/Rulesets/Objects/SliderPathExtensions.cs @@ -15,9 +15,9 @@ namespace osu.Game.Rulesets.Objects /// Snaps the provided 's duration using the . /// public static void SnapTo(this THitObject hitObject, IDistanceSnapProvider? snapProvider) - where THitObject : HitObject, IHasPath + where THitObject : HitObject, IHasPath, IHasSliderVelocity { - hitObject.Path.ExpectedDistance.Value = snapProvider?.FindSnappedDistance(hitObject, (float)hitObject.Path.CalculatedDistance, DistanceSnapTarget.Start) ?? hitObject.Path.CalculatedDistance; + hitObject.Path.ExpectedDistance.Value = snapProvider?.FindSnappedDistance((float)hitObject.Path.CalculatedDistance, hitObject.StartTime, hitObject) ?? hitObject.Path.CalculatedDistance; } /// diff --git a/osu.Game/Screens/Edit/Compose/Components/CircularDistanceSnapGrid.cs b/osu.Game/Screens/Edit/Compose/Components/CircularDistanceSnapGrid.cs index e84c2ebc35..9ddf54b779 100644 --- a/osu.Game/Screens/Edit/Compose/Components/CircularDistanceSnapGrid.cs +++ b/osu.Game/Screens/Edit/Compose/Components/CircularDistanceSnapGrid.cs @@ -8,7 +8,7 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.UserInterface; using osu.Game.Rulesets.Edit; -using osu.Game.Rulesets.Objects; +using osu.Game.Rulesets.Objects.Types; using osuTK; using osuTK.Graphics; @@ -16,8 +16,8 @@ namespace osu.Game.Screens.Edit.Compose.Components { public abstract partial class CircularDistanceSnapGrid : DistanceSnapGrid { - protected CircularDistanceSnapGrid(HitObject referenceObject, Vector2 startPosition, double startTime, double? endTime = null) - : base(referenceObject, startPosition, startTime, endTime) + protected CircularDistanceSnapGrid(Vector2 startPosition, double startTime, double? endTime = null, IHasSliderVelocity? sliderVelocitySource = null) + : base(startPosition, startTime, endTime, sliderVelocitySource) { } @@ -56,14 +56,14 @@ namespace osu.Game.Screens.Edit.Compose.Components // Picture the scenario where the user has just placed an object on a 1/2 snap, then changes to // 1/3 snap and expects to be able to place the next object on a valid 1/3 snap, regardless of the // fact that the 1/2 snap reference object is not valid for 1/3 snapping. - float offset = SnapProvider.FindSnappedDistance(ReferenceObject, 0, DistanceSnapTarget.End); + float offset = SnapProvider.FindSnappedDistance(0, StartTime, SliderVelocitySource); for (int i = 0; i < requiredCircles; i++) { const float thickness = 4; float diameter = (offset + (i + 1) * DistanceBetweenTicks + thickness / 2) * 2; - AddInternal(new Ring(ReferenceObject, GetColourForIndexFromPlacement(i)) + AddInternal(new Ring(StartTime, GetColourForIndexFromPlacement(i)) { Position = StartPosition, Origin = Anchor.Centre, @@ -98,19 +98,19 @@ namespace osu.Game.Screens.Edit.Compose.Components travelLength = DistanceBetweenTicks; float snappedDistance = fixedTime != null - ? SnapProvider.DurationToDistance(ReferenceObject, fixedTime.Value - ReferenceObject.GetEndTime()) + ? SnapProvider.DurationToDistance(fixedTime.Value - StartTime, StartTime, SliderVelocitySource) // When interacting with the resolved snap provider, the distance spacing multiplier should first be removed // to allow for snapping at a non-multiplied ratio. - : SnapProvider.FindSnappedDistance(ReferenceObject, travelLength / distanceSpacingMultiplier, DistanceSnapTarget.End); + : SnapProvider.FindSnappedDistance(travelLength / distanceSpacingMultiplier, StartTime, SliderVelocitySource); - double snappedTime = StartTime + SnapProvider.DistanceToDuration(ReferenceObject, snappedDistance); + double snappedTime = StartTime + SnapProvider.DistanceToDuration(snappedDistance, StartTime, SliderVelocitySource); if (snappedTime > LatestEndTime) { double tickLength = Beatmap.GetBeatLengthAtTime(StartTime); - snappedDistance = SnapProvider.DurationToDistance(ReferenceObject, MaxIntervals * tickLength); - snappedTime = StartTime + SnapProvider.DistanceToDuration(ReferenceObject, snappedDistance); + snappedDistance = SnapProvider.DurationToDistance(MaxIntervals * tickLength, StartTime, SliderVelocitySource); + snappedTime = StartTime + SnapProvider.DistanceToDuration(snappedDistance, StartTime, SliderVelocitySource); } // The multiplier can then be reapplied to the final position. @@ -127,13 +127,13 @@ namespace osu.Game.Screens.Edit.Compose.Components [Resolved] private EditorClock? editorClock { get; set; } - private readonly HitObject referenceObject; + private readonly double startTime; private readonly Color4 baseColour; - public Ring(HitObject referenceObject, Color4 baseColour) + public Ring(double startTime, Color4 baseColour) { - this.referenceObject = referenceObject; + this.startTime = startTime; Colour = this.baseColour = baseColour; @@ -148,9 +148,9 @@ namespace osu.Game.Screens.Edit.Compose.Components return; float distanceSpacingMultiplier = (float)snapProvider.DistanceSpacingMultiplier.Value; - double timeFromReferencePoint = editorClock.CurrentTime - referenceObject.GetEndTime(); + double timeFromReferencePoint = editorClock.CurrentTime - startTime; - float distanceForCurrentTime = snapProvider.DurationToDistance(referenceObject, timeFromReferencePoint) + float distanceForCurrentTime = snapProvider.DurationToDistance(timeFromReferencePoint, startTime) * distanceSpacingMultiplier; float timeBasedAlpha = 1 - Math.Clamp(Math.Abs(distanceForCurrentTime - Size.X / 2) / 30, 0, 1); diff --git a/osu.Game/Screens/Edit/Compose/Components/DistanceSnapGrid.cs b/osu.Game/Screens/Edit/Compose/Components/DistanceSnapGrid.cs index aaf58e0f7a..dd1671cfdd 100644 --- a/osu.Game/Screens/Edit/Compose/Components/DistanceSnapGrid.cs +++ b/osu.Game/Screens/Edit/Compose/Components/DistanceSnapGrid.cs @@ -4,6 +4,7 @@ #nullable disable using System; +using JetBrains.Annotations; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Extensions.Color4Extensions; @@ -12,7 +13,7 @@ using osu.Framework.Graphics.Containers; using osu.Framework.Layout; using osu.Game.Graphics; using osu.Game.Rulesets.Edit; -using osu.Game.Rulesets.Objects; +using osu.Game.Rulesets.Objects.Types; using osuTK; using osuTK.Graphics; @@ -48,6 +49,9 @@ namespace osu.Game.Screens.Edit.Compose.Components protected readonly double? LatestEndTime; + [CanBeNull] + protected readonly IHasSliderVelocity SliderVelocitySource; + [Resolved] protected OsuColour Colours { get; private set; } @@ -62,19 +66,17 @@ namespace osu.Game.Screens.Edit.Compose.Components private readonly LayoutValue gridCache = new LayoutValue(Invalidation.RequiredParentSizeToFit); - protected readonly HitObject ReferenceObject; - /// /// Creates a new . /// - /// A reference object to gather relevant difficulty values from. /// The position at which the grid should start. The first tick is located one distance spacing length away from this point. /// The snapping time at . /// The time at which the snapping grid should end. If null, the grid will continue until the bounds of the screen are exceeded. - protected DistanceSnapGrid(HitObject referenceObject, Vector2 startPosition, double startTime, double? endTime = null) + /// The reference object with slider velocity to include in the calculations for distance snapping. + protected DistanceSnapGrid(Vector2 startPosition, double startTime, double? endTime = null, [CanBeNull] IHasSliderVelocity sliderVelocitySource = null) { - ReferenceObject = referenceObject; LatestEndTime = endTime; + SliderVelocitySource = sliderVelocitySource; StartPosition = startPosition; StartTime = startTime; @@ -97,14 +99,14 @@ namespace osu.Game.Screens.Edit.Compose.Components private void updateSpacing() { float distanceSpacingMultiplier = (float)DistanceSpacingMultiplier.Value; - float beatSnapDistance = SnapProvider.GetBeatSnapDistanceAt(ReferenceObject, false); + float beatSnapDistance = SnapProvider.GetBeatSnapDistance(SliderVelocitySource); DistanceBetweenTicks = beatSnapDistance * distanceSpacingMultiplier; if (LatestEndTime == null) MaxIntervals = int.MaxValue; else - MaxIntervals = (int)((LatestEndTime.Value - StartTime) / SnapProvider.DistanceToDuration(ReferenceObject, beatSnapDistance)); + MaxIntervals = (int)((LatestEndTime.Value - StartTime) / SnapProvider.DistanceToDuration(beatSnapDistance, StartTime, SliderVelocitySource)); gridCache.Invalidate(); } @@ -132,7 +134,7 @@ namespace osu.Game.Screens.Edit.Compose.Components /// The original position in coordinate space local to this . /// /// Whether the snap operation should be temporally constrained to a particular time instant, - /// thus fixing the possible positions to a set distance from the . + /// thus fixing the possible positions to a set distance relative from the . /// /// A tuple containing the snapped position in coordinate space local to this and the respective time value. public abstract (Vector2 position, double time) GetSnappedPosition(Vector2 position, double? fixedTime = null); From df37768ff4075ca4de2c4afee377967d745d5e49 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 3 Feb 2025 13:55:04 +0100 Subject: [PATCH 0845/1275] Remove unused method Only used in test code. --- ...tSceneHitObjectComposerDistanceSnapping.cs | 37 ------------------- .../Editing/TestSceneDistanceSnapGrid.cs | 2 - .../Edit/ComposerDistanceSnapProvider.cs | 3 -- .../Rulesets/Edit/IDistanceSnapProvider.cs | 13 ------- 4 files changed, 55 deletions(-) diff --git a/osu.Game.Tests/Editing/TestSceneHitObjectComposerDistanceSnapping.cs b/osu.Game.Tests/Editing/TestSceneHitObjectComposerDistanceSnapping.cs index af116ad334..408db39d54 100644 --- a/osu.Game.Tests/Editing/TestSceneHitObjectComposerDistanceSnapping.cs +++ b/osu.Game.Tests/Editing/TestSceneHitObjectComposerDistanceSnapping.cs @@ -107,7 +107,6 @@ namespace osu.Game.Tests.Editing assertSnapDistance(base_distance * slider_velocity, referenceObject); assertSnappedDistance(base_distance * slider_velocity + 10, base_distance * slider_velocity, referenceObject); - assertSnappedDuration(base_distance * slider_velocity + 10, 1000, referenceObject); assertDistanceToDuration(base_distance * slider_velocity, 1000, referenceObject); assertDurationToDistance(1000, base_distance * slider_velocity, referenceObject); @@ -155,39 +154,6 @@ namespace osu.Game.Tests.Editing assertDistanceToDuration(400, 1000); } - [Test] - public void TestGetSnappedDurationFromDistance() - { - assertSnappedDuration(0, 0); - assertSnappedDuration(50, 1000); - assertSnappedDuration(100, 1000); - assertSnappedDuration(150, 2000); - assertSnappedDuration(200, 2000); - assertSnappedDuration(250, 3000); - - AddStep("set slider multiplier = 2", () => composer.EditorBeatmap.Difficulty.SliderMultiplier = 2); - - assertSnappedDuration(0, 0); - assertSnappedDuration(50, 0); - assertSnappedDuration(100, 1000); - assertSnappedDuration(150, 1000); - assertSnappedDuration(200, 1000); - assertSnappedDuration(250, 1000); - - AddStep("set beat length = 500", () => - { - composer.EditorBeatmap.ControlPointInfo.Clear(); - composer.EditorBeatmap.ControlPointInfo.Add(0, new TimingControlPoint { BeatLength = 500 }); - }); - - assertSnappedDuration(50, 0); - assertSnappedDuration(100, 500); - assertSnappedDuration(150, 500); - assertSnappedDuration(200, 500); - assertSnappedDuration(250, 500); - assertSnappedDuration(400, 1000); - } - [Test] public void GetSnappedDistanceFromDistance() { @@ -289,9 +255,6 @@ namespace osu.Game.Tests.Editing private void assertDistanceToDuration(float distance, double expectedDuration, HitObject? referenceObject = null) => AddAssert($"distance = {distance} -> duration = {expectedDuration}", () => composer.DistanceSnapProvider.DistanceToDuration(distance, referenceObject?.StartTime ?? 0, referenceObject as IHasSliderVelocity), () => Is.EqualTo(expectedDuration).Within(Precision.FLOAT_EPSILON)); - private void assertSnappedDuration(float distance, double expectedDuration, HitObject? referenceObject = null) - => AddAssert($"distance = {distance} -> duration = {expectedDuration} (snapped)", () => composer.DistanceSnapProvider.FindSnappedDuration(distance, referenceObject?.StartTime ?? 0, referenceObject as IHasSliderVelocity), () => Is.EqualTo(expectedDuration).Within(Precision.FLOAT_EPSILON)); - private void assertSnappedDistance(float distance, float expectedDistance, HitObject? referenceObject = null) => AddAssert($"distance = {distance} -> distance = {expectedDistance} (snapped)", () => composer.DistanceSnapProvider.FindSnappedDistance(distance, referenceObject?.GetEndTime() ?? 0, referenceObject as IHasSliderVelocity), () => Is.EqualTo(expectedDistance).Within(Precision.FLOAT_EPSILON)); diff --git a/osu.Game.Tests/Visual/Editing/TestSceneDistanceSnapGrid.cs b/osu.Game.Tests/Visual/Editing/TestSceneDistanceSnapGrid.cs index 51e4f526a1..af02333468 100644 --- a/osu.Game.Tests/Visual/Editing/TestSceneDistanceSnapGrid.cs +++ b/osu.Game.Tests/Visual/Editing/TestSceneDistanceSnapGrid.cs @@ -197,8 +197,6 @@ namespace osu.Game.Tests.Visual.Editing public double DistanceToDuration(float distance, double timingReference, IHasSliderVelocity withVelocity = null) => distance; - public double FindSnappedDuration(float distance, double snapReferenceTime, IHasSliderVelocity withVelocity = null) => 0; - public float FindSnappedDistance(float distance, double snapReferenceTime, IHasSliderVelocity? withVelocity = null) => 0; } } diff --git a/osu.Game/Rulesets/Edit/ComposerDistanceSnapProvider.cs b/osu.Game/Rulesets/Edit/ComposerDistanceSnapProvider.cs index 997d1f927b..d0b279f201 100644 --- a/osu.Game/Rulesets/Edit/ComposerDistanceSnapProvider.cs +++ b/osu.Game/Rulesets/Edit/ComposerDistanceSnapProvider.cs @@ -283,9 +283,6 @@ namespace osu.Game.Rulesets.Edit return distance / GetBeatSnapDistance(withVelocity) * beatLength; } - public virtual double FindSnappedDuration(float distance, double snapReferenceTime, IHasSliderVelocity? withVelocity = null) - => beatSnapProvider.SnapTime(snapReferenceTime + DistanceToDuration(distance, snapReferenceTime, withVelocity), snapReferenceTime) - snapReferenceTime; - public virtual float FindSnappedDistance(float distance, double snapReferenceTime, IHasSliderVelocity? withVelocity = null) { double actualDuration = snapReferenceTime + DistanceToDuration(distance, snapReferenceTime, withVelocity); diff --git a/osu.Game/Rulesets/Edit/IDistanceSnapProvider.cs b/osu.Game/Rulesets/Edit/IDistanceSnapProvider.cs index 99a9083273..8006db14a3 100644 --- a/osu.Game/Rulesets/Edit/IDistanceSnapProvider.cs +++ b/osu.Game/Rulesets/Edit/IDistanceSnapProvider.cs @@ -66,19 +66,6 @@ namespace osu.Game.Rulesets.Edit /// double DistanceToDuration(float distance, double timingReference, IHasSliderVelocity? withVelocity = null); - /// - /// Converts a spatial distance into a temporal duration and then snaps said duration to the beat, relative to . - /// Depends on: - /// - /// the provided, - /// a used to retrieve the beat length of the beatmap at that time, - /// the slider velocity taken from , - /// the beatmap's ,, - /// the current beat divisor. - /// - /// - double FindSnappedDuration(float distance, double snapReferenceTime, IHasSliderVelocity? withVelocity = null); - /// /// Snaps a spatial distance to the beat, relative to . /// Depends on: From 2d6f64e89185e71d755201c52c951236116a0fe3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 3 Feb 2025 15:17:32 +0100 Subject: [PATCH 0846/1275] Fix code quality --- osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs | 2 +- osu.Game.Tests/Visual/Editing/TestSceneDistanceSnapGrid.cs | 2 +- osu.Game/Rulesets/Edit/IDistanceSnapProvider.cs | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs b/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs index 60c37cd4a4..b3e23daa99 100644 --- a/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs +++ b/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs @@ -408,7 +408,7 @@ namespace osu.Game.Rulesets.Osu.Edit ArgumentOutOfRangeException.ThrowIfNegativeOrZero(targetOffset); int positionSourceObjectIndex = -1; - IHasSliderVelocity? sliderVelocitySource = null; + IHasSliderVelocity sliderVelocitySource = null; for (int i = 0; i < EditorBeatmap.HitObjects.Count; i++) { diff --git a/osu.Game.Tests/Visual/Editing/TestSceneDistanceSnapGrid.cs b/osu.Game.Tests/Visual/Editing/TestSceneDistanceSnapGrid.cs index af02333468..fb57422e66 100644 --- a/osu.Game.Tests/Visual/Editing/TestSceneDistanceSnapGrid.cs +++ b/osu.Game.Tests/Visual/Editing/TestSceneDistanceSnapGrid.cs @@ -197,7 +197,7 @@ namespace osu.Game.Tests.Visual.Editing public double DistanceToDuration(float distance, double timingReference, IHasSliderVelocity withVelocity = null) => distance; - public float FindSnappedDistance(float distance, double snapReferenceTime, IHasSliderVelocity? withVelocity = null) => 0; + public float FindSnappedDistance(float distance, double snapReferenceTime, IHasSliderVelocity withVelocity = null) => 0; } } } diff --git a/osu.Game/Rulesets/Edit/IDistanceSnapProvider.cs b/osu.Game/Rulesets/Edit/IDistanceSnapProvider.cs index 8006db14a3..195dbf0d46 100644 --- a/osu.Game/Rulesets/Edit/IDistanceSnapProvider.cs +++ b/osu.Game/Rulesets/Edit/IDistanceSnapProvider.cs @@ -67,7 +67,7 @@ namespace osu.Game.Rulesets.Edit double DistanceToDuration(float distance, double timingReference, IHasSliderVelocity? withVelocity = null); /// - /// Snaps a spatial distance to the beat, relative to . + /// Snaps a spatial distance to the beat, relative to . /// Depends on: /// /// the provided, From b433eef1389ae8a07627ee6a9597bebe336d61c6 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 4 Feb 2025 01:51:43 +0900 Subject: [PATCH 0847/1275] Remove redundant conditional check --- .../SelectV2/BeatmapCarouselFilterGrouping.cs | 25 ++++++++----------- 1 file changed, 11 insertions(+), 14 deletions(-) diff --git a/osu.Game/Screens/SelectV2/BeatmapCarouselFilterGrouping.cs b/osu.Game/Screens/SelectV2/BeatmapCarouselFilterGrouping.cs index 34fbfdbaa6..ea737d8b7f 100644 --- a/osu.Game/Screens/SelectV2/BeatmapCarouselFilterGrouping.cs +++ b/osu.Game/Screens/SelectV2/BeatmapCarouselFilterGrouping.cs @@ -87,22 +87,19 @@ namespace osu.Game.Screens.SelectV2 if (item.Model is BeatmapInfo beatmap) { - if (groupSetsTogether) + bool newBeatmapSet = lastItem == null || (lastItem.Model is BeatmapInfo lastBeatmap && lastBeatmap.BeatmapSet!.ID != beatmap.BeatmapSet!.ID); + + if (newBeatmapSet) { - bool newBeatmapSet = lastItem == null || (lastItem.Model is BeatmapInfo lastBeatmap && lastBeatmap.BeatmapSet!.ID != beatmap.BeatmapSet!.ID); - - if (newBeatmapSet) - { - newItems.Insert(i, new CarouselItem(beatmap.BeatmapSet!) { DrawHeight = BeatmapSetPanel.HEIGHT }); - i++; - } - - if (!setItems.TryGetValue(beatmap.BeatmapSet!, out var related)) - setItems[beatmap.BeatmapSet!] = related = new HashSet(); - - related.Add(item); - item.IsVisible = false; + newItems.Insert(i, new CarouselItem(beatmap.BeatmapSet!) { DrawHeight = BeatmapSetPanel.HEIGHT }); + i++; } + + if (!setItems.TryGetValue(beatmap.BeatmapSet!, out var related)) + setItems[beatmap.BeatmapSet!] = related = new HashSet(); + + related.Add(item); + item.IsVisible = false; } lastItem = item; From b5c4e3bc147e0c4f085de754ed8019dc18ead270 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 4 Feb 2025 02:41:56 +0900 Subject: [PATCH 0848/1275] Add failing tests for traversal on group headers --- .../TestSceneBeatmapCarouselV2GroupSelection.cs | 14 ++++++++++++++ .../TestSceneBeatmapCarouselV2Selection.cs | 15 +++++++++++++++ 2 files changed, 29 insertions(+) diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2GroupSelection.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2GroupSelection.cs index 5728583507..04ca0a9085 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2GroupSelection.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2GroupSelection.cs @@ -81,6 +81,20 @@ namespace osu.Game.Tests.Visual.SongSelect BeatmapPanel? getSelectedPanel() => Carousel.ChildrenOfType().SingleOrDefault(p => p.Selected.Value); } + [Test] + public void TestGroupSelectionOnHeader() + { + AddBeatmaps(10, 3); + WaitForDrawablePanels(); + + SelectNextGroup(); + WaitForGroupSelection(0, 0); + + SelectPrevPanel(); + SelectPrevGroup(); + WaitForGroupSelection(2, 9); + } + [Test] public void TestKeyboardSelection() { diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2Selection.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2Selection.cs index 50395cf1ff..b087c252e4 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2Selection.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2Selection.cs @@ -129,6 +129,21 @@ namespace osu.Game.Tests.Visual.SongSelect WaitForSelection(0, 0); } + [Test] + public void TestGroupSelectionOnHeader() + { + AddBeatmaps(10, 3); + WaitForDrawablePanels(); + + SelectNextGroup(); + SelectNextGroup(); + WaitForSelection(1, 0); + + SelectPrevPanel(); + SelectPrevGroup(); + WaitForSelection(0, 0); + } + [Test] public void TestKeyboardSelection() { From e454fa558cb5891ac6614dd9c626fa21834c168f Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 4 Feb 2025 02:55:57 +0900 Subject: [PATCH 0849/1275] Adjust group traversal logic to handle cases where keyboard selection redirects --- osu.Game/Screens/SelectV2/Carousel.cs | 35 +++++++++++++++------------ 1 file changed, 20 insertions(+), 15 deletions(-) diff --git a/osu.Game/Screens/SelectV2/Carousel.cs b/osu.Game/Screens/SelectV2/Carousel.cs index 0da9cb5c19..a13de0e26d 100644 --- a/osu.Game/Screens/SelectV2/Carousel.cs +++ b/osu.Game/Screens/SelectV2/Carousel.cs @@ -377,26 +377,31 @@ namespace osu.Game.Screens.SelectV2 if (currentSelection.CarouselItem != currentKeyboardSelection.CarouselItem) { TryActivateSelection(); - return; + + // There's a chance this couldn't resolve, at which point continue with standard traversal. + if (currentSelection.CarouselItem == currentKeyboardSelection.CarouselItem) + return; } int originalIndex; + int newIndex; - if (currentKeyboardSelection.Index != null) - originalIndex = currentKeyboardSelection.Index.Value; - else if (direction > 0) - originalIndex = carouselItems.Count - 1; - else - originalIndex = 0; - - int newIndex = originalIndex; - - // As a second special case, if we're group selecting backwards and the current selection isn't a group, - // make sure to go back to the group header this item belongs to, so that the block below doesn't find it and stop too early. - if (direction < 0) + if (currentSelection.Index == null) { - while (!CheckValidForGroupSelection(carouselItems[newIndex])) - newIndex--; + // If there's no current selection, start from either end of the full list. + newIndex = originalIndex = direction > 0 ? carouselItems.Count - 1 : 0; + } + else + { + newIndex = originalIndex = currentSelection.Index.Value; + + // As a second special case, if we're group selecting backwards and the current selection isn't a group, + // make sure to go back to the group header this item belongs to, so that the block below doesn't find it and stop too early. + if (direction < 0) + { + while (!CheckValidForGroupSelection(carouselItems[newIndex])) + newIndex--; + } } // Iterate over every item back to the current selection, finding the first valid item. From 38933039880b3b50eaef5557290a9c806dd79f51 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 17 Oct 2024 11:59:27 +0200 Subject: [PATCH 0850/1275] Implement "form button" control --- .../UserInterface/TestSceneFormControls.cs | 166 ++++++++------- .../Graphics/UserInterfaceV2/FormButton.cs | 189 ++++++++++++++++++ 2 files changed, 280 insertions(+), 75 deletions(-) create mode 100644 osu.Game/Graphics/UserInterfaceV2/FormButton.cs diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneFormControls.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneFormControls.cs index 118fbca97b..2003f5de83 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneFormControls.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneFormControls.cs @@ -6,6 +6,7 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Cursor; using osu.Game.Beatmaps; +using osu.Game.Graphics.Containers; using osu.Game.Graphics.Cursor; using osu.Game.Graphics.UserInterfaceV2; using osu.Game.Localisation; @@ -27,87 +28,102 @@ namespace osu.Game.Tests.Visual.UserInterface Child = new PopoverContainer { RelativeSizeAxes = Axes.Both, - Child = new FillFlowContainer + Child = new OsuScrollContainer { - RelativeSizeAxes = Axes.Y, - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Width = 400, - Direction = FillDirection.Vertical, - Spacing = new Vector2(5), - Padding = new MarginPadding(10), - Children = new Drawable[] + RelativeSizeAxes = Axes.Both, + Child = new FillFlowContainer { - new FormTextBox + AutoSizeAxes = Axes.Y, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Width = 400, + Direction = FillDirection.Vertical, + Spacing = new Vector2(5), + Padding = new MarginPadding(10), + Children = new Drawable[] { - Caption = "Artist", - HintText = "Poot artist here!", - PlaceholderText = "Here is an artist", - TabbableContentContainer = this, - }, - new FormTextBox - { - Caption = "Artist", - HintText = "Poot artist here!", - PlaceholderText = "Here is an artist", - Current = { Disabled = true }, - TabbableContentContainer = this, - }, - new FormNumberBox(allowDecimals: true) - { - Caption = "Number", - HintText = "Insert your favourite number", - PlaceholderText = "Mine is 42!", - TabbableContentContainer = this, - }, - new FormCheckBox - { - Caption = EditorSetupStrings.LetterboxDuringBreaks, - HintText = EditorSetupStrings.LetterboxDuringBreaksDescription, - }, - new FormCheckBox - { - Caption = EditorSetupStrings.LetterboxDuringBreaks, - HintText = EditorSetupStrings.LetterboxDuringBreaksDescription, - Current = { Disabled = true }, - }, - new FormSliderBar - { - Caption = "Slider", - Current = new BindableFloat + new FormTextBox { - MinValue = 0, - MaxValue = 10, - Value = 5, - Precision = 0.1f, + Caption = "Artist", + HintText = "Poot artist here!", + PlaceholderText = "Here is an artist", + TabbableContentContainer = this, }, - TabbableContentContainer = this, - }, - new FormEnumDropdown - { - Caption = EditorSetupStrings.EnableCountdown, - HintText = EditorSetupStrings.CountdownDescription, - }, - new FormFileSelector - { - Caption = "File selector", - PlaceholderText = "Select a file", - }, - new FormBeatmapFileSelector(true) - { - Caption = "File selector with intermediate choice dialog", - PlaceholderText = "Select a file", - }, - new FormColourPalette - { - Caption = "Combo colours", - Colours = + new FormTextBox { - Colour4.Red, - Colour4.Green, - Colour4.Blue, - Colour4.Yellow, - } + Caption = "Artist", + HintText = "Poot artist here!", + PlaceholderText = "Here is an artist", + Current = { Disabled = true }, + TabbableContentContainer = this, + }, + new FormNumberBox(allowDecimals: true) + { + Caption = "Number", + HintText = "Insert your favourite number", + PlaceholderText = "Mine is 42!", + TabbableContentContainer = this, + }, + new FormCheckBox + { + Caption = EditorSetupStrings.LetterboxDuringBreaks, + HintText = EditorSetupStrings.LetterboxDuringBreaksDescription, + }, + new FormCheckBox + { + Caption = EditorSetupStrings.LetterboxDuringBreaks, + HintText = EditorSetupStrings.LetterboxDuringBreaksDescription, + Current = { Disabled = true }, + }, + new FormSliderBar + { + Caption = "Slider", + Current = new BindableFloat + { + MinValue = 0, + MaxValue = 10, + Value = 5, + Precision = 0.1f, + }, + TabbableContentContainer = this, + }, + new FormEnumDropdown + { + Caption = EditorSetupStrings.EnableCountdown, + HintText = EditorSetupStrings.CountdownDescription, + }, + new FormFileSelector + { + Caption = "File selector", + PlaceholderText = "Select a file", + }, + new FormBeatmapFileSelector(true) + { + Caption = "File selector with intermediate choice dialog", + PlaceholderText = "Select a file", + }, + new FormColourPalette + { + Caption = "Combo colours", + Colours = + { + Colour4.Red, + Colour4.Green, + Colour4.Blue, + Colour4.Yellow, + } + }, + new FormButton + { + Caption = "No text in button", + Action = () => { }, + }, + new FormButton + { + Caption = "Text in button which is pretty long and is very likely to wrap", + ButtonText = "Foo the bar", + Action = () => { }, + }, }, }, }, diff --git a/osu.Game/Graphics/UserInterfaceV2/FormButton.cs b/osu.Game/Graphics/UserInterfaceV2/FormButton.cs new file mode 100644 index 0000000000..fec855153b --- /dev/null +++ b/osu.Game/Graphics/UserInterfaceV2/FormButton.cs @@ -0,0 +1,189 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using System.Diagnostics; +using osu.Framework.Allocation; +using osu.Framework.Extensions.Color4Extensions; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Colour; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; +using osu.Framework.Graphics.Sprites; +using osu.Framework.Input.Events; +using osu.Framework.Localisation; +using osu.Game.Graphics.Backgrounds; +using osu.Game.Graphics.Containers; +using osu.Game.Graphics.UserInterface; +using osu.Game.Overlays; +using osuTK; +using osuTK.Graphics; + +namespace osu.Game.Graphics.UserInterfaceV2 +{ + public partial class FormButton : CompositeDrawable + { + /// + /// Caption describing this button, displayed on the left of it. + /// + public LocalisableString Caption { get; init; } + + public LocalisableString ButtonText { get; init; } + + public Action? Action { get; init; } + + [Resolved] + private OverlayColourProvider colourProvider { get; set; } = null!; + + [BackgroundDependencyLoader] + private void load() + { + RelativeSizeAxes = Axes.X; + Height = 50; + + Masking = true; + CornerRadius = 5; + CornerExponent = 2.5f; + + InternalChildren = new Drawable[] + { + new Box + { + RelativeSizeAxes = Axes.Both, + Colour = colourProvider.Background5, + }, + new Container + { + RelativeSizeAxes = Axes.Both, + Padding = new MarginPadding + { + Left = 9, + Right = 5, + Vertical = 5, + }, + Children = new Drawable[] + { + new OsuTextFlowContainer + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Width = 0.45f, + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + Text = Caption, + }, + new Button + { + Action = Action, + Text = ButtonText, + RelativeSizeAxes = ButtonText == default ? Axes.None : Axes.X, + Width = ButtonText == default ? 90 : 0.45f, + Anchor = Anchor.CentreRight, + Origin = Anchor.CentreRight, + } + }, + }, + }; + } + + protected override bool OnHover(HoverEvent e) + { + updateState(); + return true; + } + + protected override void OnHoverLost(HoverLostEvent e) + { + base.OnHoverLost(e); + updateState(); + } + + private void updateState() + { + BorderThickness = IsHovered ? 2 : 0; + + if (IsHovered) + BorderColour = colourProvider.Light4; + } + + public partial class Button : OsuButton + { + private TrianglesV2? triangles { get; set; } + + protected override float HoverLayerFinalAlpha => 0; + + private Color4? triangleGradientSecondColour; + + public override Color4 BackgroundColour + { + get => base.BackgroundColour; + set + { + base.BackgroundColour = value; + triangleGradientSecondColour = BackgroundColour.Lighten(0.2f); + updateColours(); + } + } + + [BackgroundDependencyLoader] + private void load(OverlayColourProvider overlayColourProvider) + { + DefaultBackgroundColour = overlayColourProvider.Colour3; + triangleGradientSecondColour ??= overlayColourProvider.Colour1; + + if (Text == default) + { + Add(new SpriteIcon + { + Icon = FontAwesome.Solid.ChevronRight, + Size = new Vector2(16), + Shadow = true, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + }); + } + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + Content.CornerRadius = 2; + + Add(triangles = new TrianglesV2 + { + Thickness = 0.02f, + SpawnRatio = 0.6f, + RelativeSizeAxes = Axes.Both, + Depth = float.MaxValue, + }); + + updateColours(); + } + + private void updateColours() + { + if (triangles == null) + return; + + Debug.Assert(triangleGradientSecondColour != null); + + triangles.Colour = ColourInfo.GradientVertical(triangleGradientSecondColour.Value, BackgroundColour); + } + + protected override bool OnHover(HoverEvent e) + { + Debug.Assert(triangleGradientSecondColour != null); + + Background.FadeColour(triangleGradientSecondColour.Value, 300, Easing.OutQuint); + return base.OnHover(e); + } + + protected override void OnHoverLost(HoverLostEvent e) + { + Background.FadeColour(BackgroundColour, 300, Easing.OutQuint); + base.OnHoverLost(e); + } + } + } +} From 4dd4e52e6dc43c1a4f4fe55c4262650b3e4e6919 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 4 Feb 2025 08:58:37 +0100 Subject: [PATCH 0851/1275] Implement visual appearance of beatmap submission wizard --- .../TestSceneBeatmapSubmissionOverlay.cs | 42 ++++++ osu.Game/Configuration/OsuConfigManager.cs | 5 + .../Localisation/BeatmapSubmissionStrings.cs | 124 ++++++++++++++++++ .../Overlays/FirstRunSetup/ScreenWelcome.cs | 2 + osu.Game/Overlays/WizardOverlay.cs | 13 +- osu.Game/Overlays/WizardScreen.cs | 3 + .../Submission/BeatmapSubmissionOverlay.cs | 28 ++++ .../Submission/ScreenContentPermissions.cs | 44 +++++++ .../ScreenFrequentlyAskedQuestions.cs | 62 +++++++++ .../Submission/ScreenSubmissionSettings.cs | 73 +++++++++++ 10 files changed, 389 insertions(+), 7 deletions(-) create mode 100644 osu.Game.Tests/Visual/Editing/TestSceneBeatmapSubmissionOverlay.cs create mode 100644 osu.Game/Localisation/BeatmapSubmissionStrings.cs create mode 100644 osu.Game/Screens/Edit/Submission/BeatmapSubmissionOverlay.cs create mode 100644 osu.Game/Screens/Edit/Submission/ScreenContentPermissions.cs create mode 100644 osu.Game/Screens/Edit/Submission/ScreenFrequentlyAskedQuestions.cs create mode 100644 osu.Game/Screens/Edit/Submission/ScreenSubmissionSettings.cs diff --git a/osu.Game.Tests/Visual/Editing/TestSceneBeatmapSubmissionOverlay.cs b/osu.Game.Tests/Visual/Editing/TestSceneBeatmapSubmissionOverlay.cs new file mode 100644 index 0000000000..07a794b7eb --- /dev/null +++ b/osu.Game.Tests/Visual/Editing/TestSceneBeatmapSubmissionOverlay.cs @@ -0,0 +1,42 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Testing; +using osu.Game.Screens.Edit.Submission; +using osu.Game.Screens.Footer; + +namespace osu.Game.Tests.Visual.Editing +{ + public partial class TestSceneBeatmapSubmissionOverlay : OsuTestScene + { + private ScreenFooter footer = null!; + private BeatmapSubmissionOverlay overlay = null!; + + [SetUpSteps] + public void SetUpSteps() + { + AddStep("add overlay", () => + { + var receptor = new ScreenFooter.BackReceptor(); + footer = new ScreenFooter(receptor); + + Child = new DependencyProvidingContainer + { + RelativeSizeAxes = Axes.Both, + CachedDependencies = new[] { (typeof(ScreenFooter), (object)footer) }, + Children = new Drawable[] + { + receptor, + overlay = new BeatmapSubmissionOverlay() + { + State = { Value = Visibility.Visible, }, + }, + footer, + } + }; + }); + } + } +} diff --git a/osu.Game/Configuration/OsuConfigManager.cs b/osu.Game/Configuration/OsuConfigManager.cs index 908f434655..1244dd8cfc 100644 --- a/osu.Game/Configuration/OsuConfigManager.cs +++ b/osu.Game/Configuration/OsuConfigManager.cs @@ -220,6 +220,9 @@ namespace osu.Game.Configuration SetDefault(OsuSetting.AlwaysShowHoldForMenuButton, false); SetDefault(OsuSetting.AlwaysRequireHoldingForPause, false); SetDefault(OsuSetting.EditorShowStoryboard, true); + + SetDefault(OsuSetting.EditorSubmissionNotifyOnDiscussionReplies, true); + SetDefault(OsuSetting.EditorSubmissionLoadInBrowserAfterSubmission, true); } protected override bool CheckLookupContainsPrivateInformation(OsuSetting lookup) @@ -461,5 +464,7 @@ namespace osu.Game.Configuration BeatmapListingFeaturedArtistFilter, ShowMobileDisclaimer, EditorShowStoryboard, + EditorSubmissionNotifyOnDiscussionReplies, + EditorSubmissionLoadInBrowserAfterSubmission, } } diff --git a/osu.Game/Localisation/BeatmapSubmissionStrings.cs b/osu.Game/Localisation/BeatmapSubmissionStrings.cs new file mode 100644 index 0000000000..85fe922703 --- /dev/null +++ b/osu.Game/Localisation/BeatmapSubmissionStrings.cs @@ -0,0 +1,124 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Localisation; + +namespace osu.Game.Localisation +{ + public static class BeatmapSubmissionStrings + { + private const string prefix = @"osu.Game.Resources.Localisation.BeatmapSubmission"; + + /// + /// "Beatmap submission" + /// + public static LocalisableString BeatmapSubmissionTitle => new TranslatableString(getKey(@"beatmap_submission_title"), @"Beatmap submission"); + + /// + /// "Share your beatmap with the world!" + /// + public static LocalisableString BeatmapSubmissionDescription => new TranslatableString(getKey(@"beatmap_submission_description"), @"Share your beatmap with the world!"); + + /// + /// "Content permissions" + /// + public static LocalisableString ContentPermissions => new TranslatableString(getKey(@"content_permissions"), @"Content permissions"); + + /// + /// "I understand" + /// + public static LocalisableString ContentPermissionsAcknowledgement => new TranslatableString(getKey(@"content_permissions_acknowledgement"), @"I understand"); + + /// + /// "Frequently asked questions" + /// + public static LocalisableString FrequentlyAskedQuestions => new TranslatableString(getKey(@"frequently_asked_questions"), @"Frequently asked questions"); + + /// + /// "Submission settings" + /// + public static LocalisableString SubmissionSettings => new TranslatableString(getKey(@"submission_settings"), @"Submission settings"); + + /// + /// "Before you continue, we ask you to check whether the content you are uploading has been cleared for upload. Please understand that you are responsible for the content you upload to the platform and if in doubt, should ask permission from the creators before uploading!" + /// + public static LocalisableString ContentPermissionsDisclaimer => new TranslatableString(getKey(@"content_permissions_disclaimer"), @"Before you continue, we ask you to check whether the content you are uploading has been cleared for upload. Please understand that you are responsible for the content you upload to the platform and if in doubt, should ask permission from the creators before uploading!"); + + /// + /// "Check the content usage guidelines for more information" + /// + public static LocalisableString CheckContentUsageGuidelines => new TranslatableString(getKey(@"check_content_usage_guidelines"), @"Check the content usage guidelines for more information"); + + /// + /// "Beatmap ranking criteria" + /// + public static LocalisableString BeatmapRankingCriteria => new TranslatableString(getKey(@"beatmap_ranking_criteria"), @"Beatmap ranking criteria"); + + /// + /// "Not sure you meet the guidelines? Check the list and speed up the ranking process!" + /// + public static LocalisableString BeatmapRankingCriteriaDescription => new TranslatableString(getKey(@"beatmap_ranking_criteria_description"), @"Not sure you meet the guidelines? Check the list and speed up the ranking process!"); + + /// + /// "Submission process" + /// + public static LocalisableString SubmissionProcess => new TranslatableString(getKey(@"submission_process"), @"Submission process"); + + /// + /// "Unsure about the submission process? Check out the wiki entry!" + /// + public static LocalisableString SubmissionProcessDescription => new TranslatableString(getKey(@"submission_process_description"), @"Unsure about the submission process? Check out the wiki entry!"); + + /// + /// "Mapping help forum" + /// + public static LocalisableString MappingHelpForum => new TranslatableString(getKey(@"mapping_help_forum"), @"Mapping help forum"); + + /// + /// "Got some questions about mapping and submission? Ask them in the forums!" + /// + public static LocalisableString MappingHelpForumDescription => new TranslatableString(getKey(@"mapping_help_forum_description"), @"Got some questions about mapping and submission? Ask them in the forums!"); + + /// + /// "Modding queues forum" + /// + public static LocalisableString ModdingQueuesForum => new TranslatableString(getKey(@"modding_queues_forum"), @"Modding queues forum"); + + /// + /// "Having trouble getting feedback? Why not ask in a mod queue!" + /// + public static LocalisableString ModdingQueuesForumDescription => new TranslatableString(getKey(@"modding_queues_forum_description"), @"Having trouble getting feedback? Why not ask in a mod queue!"); + + /// + /// "Where would you like to post your map?" + /// + public static LocalisableString BeatmapSubmissionTargetCaption => new TranslatableString(getKey(@"beatmap_submission_target_caption"), @"Where would you like to post your map?"); + + /// + /// "Works in Progress / Help (incomplete, not ready for ranking)" + /// + public static LocalisableString BeatmapSubmissionTargetWIP => new TranslatableString(getKey(@"beatmap_submission_target_wip"), @"Works in Progress / Help (incomplete, not ready for ranking)"); + + /// + /// "Pending (complete, ready for ranking)" + /// + public static LocalisableString BeatmapSubmissionTargetPending => new TranslatableString(getKey(@"beatmap_submission_target_pending"), @"Pending (complete, ready for ranking)"); + + /// + /// "Receive notifications for discussion replies" + /// + public static LocalisableString NotifyOnDiscussionReplies => new TranslatableString(getKey(@"notify_for_discussion_replies"), @"Receive notifications for discussion replies"); + + /// + /// "Load in browser after submission" + /// + public static LocalisableString LoadInBrowserAfterSubmission => new TranslatableString(getKey(@"load_in_browser_after_submission"), @"Load in browser after submission"); + + /// + /// "Note: In order to make it possible for users of all osu! versions to enjoy your beatmap, it will be exported in a backwards-compatible format. While we have made efforts to ensure that that process keeps the beatmap playable in its intended form, some data related to features that previous versions of osu! do not support may be lost." + /// + public static LocalisableString LegacyExportDisclaimer => new TranslatableString(getKey(@"legacy_export_disclaimer"), @"Note: In order to make it possible for users of all osu! versions to enjoy your beatmap, it will be exported in a backwards-compatible format. While we have made efforts to ensure that that process keeps the beatmap playable in its intended form, some data related to features that previous versions of osu! do not support may be lost."); + + private static string getKey(string key) => $@"{prefix}:{key}"; + } +} diff --git a/osu.Game/Overlays/FirstRunSetup/ScreenWelcome.cs b/osu.Game/Overlays/FirstRunSetup/ScreenWelcome.cs index 93cf555bc9..e03a08dd46 100644 --- a/osu.Game/Overlays/FirstRunSetup/ScreenWelcome.cs +++ b/osu.Game/Overlays/FirstRunSetup/ScreenWelcome.cs @@ -65,6 +65,8 @@ namespace osu.Game.Overlays.FirstRunSetup }; } + public override LocalisableString? NextStepText => FirstRunSetupOverlayStrings.GetStarted; + private partial class LanguageSelectionFlow : FillFlowContainer { private Bindable language = null!; diff --git a/osu.Game/Overlays/WizardOverlay.cs b/osu.Game/Overlays/WizardOverlay.cs index 38701efc96..34ffa7bd77 100644 --- a/osu.Game/Overlays/WizardOverlay.cs +++ b/osu.Game/Overlays/WizardOverlay.cs @@ -227,7 +227,7 @@ namespace osu.Game.Overlays updateButtons(); } - private void updateButtons() => DisplayedFooterContent?.UpdateButtons(CurrentStepIndex, steps); + private void updateButtons() => DisplayedFooterContent?.UpdateButtons(CurrentStepIndex, CurrentScreen, steps); public partial class WizardFooterContent : VisibilityContainer { @@ -248,24 +248,23 @@ namespace osu.Game.Overlays RelativeSizeAxes = Axes.X, Width = 1, Text = FirstRunSetupOverlayStrings.GetStarted, - DarkerColour = colourProvider.Colour2, - LighterColour = colourProvider.Colour1, + DarkerColour = colourProvider.Colour3, + LighterColour = colourProvider.Colour2, Action = () => ShowNextStep?.Invoke(), }; } - public void UpdateButtons(int? currentStep, IReadOnlyList steps) + public void UpdateButtons(int? currentStep, WizardScreen? currentScreen, IReadOnlyList steps) { NextButton.Enabled.Value = currentStep != null; if (currentStep == null) return; - bool isFirstStep = currentStep == 0; bool isLastStep = currentStep == steps.Count - 1; - if (isFirstStep) - NextButton.Text = FirstRunSetupOverlayStrings.GetStarted; + if (currentScreen?.NextStepText != null) + NextButton.Text = currentScreen.NextStepText.Value; else { NextButton.Text = isLastStep diff --git a/osu.Game/Overlays/WizardScreen.cs b/osu.Game/Overlays/WizardScreen.cs index 7f3b1fe7f4..5112efaa61 100644 --- a/osu.Game/Overlays/WizardScreen.cs +++ b/osu.Game/Overlays/WizardScreen.cs @@ -7,6 +7,7 @@ using osu.Framework.Allocation; using osu.Framework.Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; +using osu.Framework.Localisation; using osu.Framework.Screens; using osu.Game.Graphics; using osu.Game.Graphics.Containers; @@ -102,5 +103,7 @@ namespace osu.Game.Overlays base.OnSuspending(e); } + + public virtual LocalisableString? NextStepText => null; } } diff --git a/osu.Game/Screens/Edit/Submission/BeatmapSubmissionOverlay.cs b/osu.Game/Screens/Edit/Submission/BeatmapSubmissionOverlay.cs new file mode 100644 index 0000000000..da2abd8c23 --- /dev/null +++ b/osu.Game/Screens/Edit/Submission/BeatmapSubmissionOverlay.cs @@ -0,0 +1,28 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Allocation; +using osu.Game.Overlays; +using osu.Game.Localisation; + +namespace osu.Game.Screens.Edit.Submission +{ + public partial class BeatmapSubmissionOverlay : WizardOverlay + { + public BeatmapSubmissionOverlay() + : base(OverlayColourScheme.Aquamarine) + { + } + + [BackgroundDependencyLoader] + private void load() + { + AddStep(); + AddStep(); + AddStep(); + + Header.Title = BeatmapSubmissionStrings.BeatmapSubmissionTitle; + Header.Description = BeatmapSubmissionStrings.BeatmapSubmissionDescription; + } + } +} diff --git a/osu.Game/Screens/Edit/Submission/ScreenContentPermissions.cs b/osu.Game/Screens/Edit/Submission/ScreenContentPermissions.cs new file mode 100644 index 0000000000..92a4ac4e4e --- /dev/null +++ b/osu.Game/Screens/Edit/Submission/ScreenContentPermissions.cs @@ -0,0 +1,44 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Allocation; +using osu.Framework.Graphics; +using osu.Framework.Localisation; +using osu.Game.Graphics; +using osu.Game.Graphics.Containers; +using osu.Game.Graphics.UserInterfaceV2; +using osu.Game.Localisation; +using osu.Game.Overlays; + +namespace osu.Game.Screens.Edit.Submission +{ + [LocalisableDescription(typeof(BeatmapSubmissionStrings), nameof(BeatmapSubmissionStrings.ContentPermissions))] + public partial class ScreenContentPermissions : WizardScreen + { + [BackgroundDependencyLoader] + private void load(OsuGame? game) + { + Content.AddRange(new Drawable[] + { + new OsuTextFlowContainer(cp => cp.Font = OsuFont.Default.With(size: CONTENT_FONT_SIZE)) + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre, + Text = BeatmapSubmissionStrings.ContentPermissionsDisclaimer, + }, + new RoundedButton + { + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre, + Width = 450, + Text = BeatmapSubmissionStrings.CheckContentUsageGuidelines, + Action = () => game?.ShowWiki(@"Rules/Content_usage_permissions"), + }, + }); + } + + public override LocalisableString? NextStepText => BeatmapSubmissionStrings.ContentPermissionsAcknowledgement; + } +} diff --git a/osu.Game/Screens/Edit/Submission/ScreenFrequentlyAskedQuestions.cs b/osu.Game/Screens/Edit/Submission/ScreenFrequentlyAskedQuestions.cs new file mode 100644 index 0000000000..c8d226bbcb --- /dev/null +++ b/osu.Game/Screens/Edit/Submission/ScreenFrequentlyAskedQuestions.cs @@ -0,0 +1,62 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Allocation; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Localisation; +using osu.Game.Graphics.UserInterfaceV2; +using osu.Game.Localisation; +using osu.Game.Online.API; +using osu.Game.Overlays; +using osuTK; + +namespace osu.Game.Screens.Edit.Submission +{ + [LocalisableDescription(typeof(BeatmapSubmissionStrings), nameof(BeatmapSubmissionStrings.FrequentlyAskedQuestions))] + public partial class ScreenFrequentlyAskedQuestions : WizardScreen + { + [BackgroundDependencyLoader] + private void load(OsuGame? game, IAPIProvider api) + { + Content.Add(new FillFlowContainer + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Direction = FillDirection.Vertical, + Spacing = new Vector2(5), + Children = new Drawable[] + { + new FormButton + { + RelativeSizeAxes = Axes.X, + Caption = BeatmapSubmissionStrings.BeatmapRankingCriteriaDescription, + ButtonText = BeatmapSubmissionStrings.BeatmapRankingCriteria, + Action = () => game?.ShowWiki(@"Ranking_Criteria"), + }, + new FormButton + { + RelativeSizeAxes = Axes.X, + Caption = BeatmapSubmissionStrings.SubmissionProcessDescription, + ButtonText = BeatmapSubmissionStrings.SubmissionProcess, + Action = () => game?.ShowWiki(@"Beatmap_ranking_procedure"), + }, + new FormButton + { + RelativeSizeAxes = Axes.X, + Caption = BeatmapSubmissionStrings.MappingHelpForumDescription, + ButtonText = BeatmapSubmissionStrings.MappingHelpForum, + Action = () => game?.OpenUrlExternally($@"{api.WebsiteRootUrl}/community/forums/56"), + }, + new FormButton + { + RelativeSizeAxes = Axes.X, + Caption = BeatmapSubmissionStrings.ModdingQueuesForumDescription, + ButtonText = BeatmapSubmissionStrings.ModdingQueuesForum, + Action = () => game?.OpenUrlExternally($@"{api.WebsiteRootUrl}/community/forums/60"), + }, + }, + }); + } + } +} diff --git a/osu.Game/Screens/Edit/Submission/ScreenSubmissionSettings.cs b/osu.Game/Screens/Edit/Submission/ScreenSubmissionSettings.cs new file mode 100644 index 0000000000..72da94afa1 --- /dev/null +++ b/osu.Game/Screens/Edit/Submission/ScreenSubmissionSettings.cs @@ -0,0 +1,73 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Allocation; +using osu.Framework.Bindables; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Localisation; +using osu.Game.Configuration; +using osu.Game.Graphics; +using osu.Game.Graphics.Containers; +using osu.Game.Graphics.UserInterfaceV2; +using osu.Game.Localisation; +using osu.Game.Overlays; +using osuTK; + +namespace osu.Game.Screens.Edit.Submission +{ + [LocalisableDescription(typeof(BeatmapSubmissionStrings), nameof(BeatmapSubmissionStrings.SubmissionSettings))] + public partial class ScreenSubmissionSettings : WizardScreen + { + private readonly BindableBool notifyOnDiscussionReplies = new BindableBool(); + private readonly BindableBool loadInBrowserAfterSubmission = new BindableBool(); + + [BackgroundDependencyLoader] + private void load(OsuConfigManager configManager, OsuColour colours) + { + configManager.BindWith(OsuSetting.EditorSubmissionNotifyOnDiscussionReplies, notifyOnDiscussionReplies); + configManager.BindWith(OsuSetting.EditorSubmissionLoadInBrowserAfterSubmission, loadInBrowserAfterSubmission); + + Content.Add(new FillFlowContainer + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Spacing = new Vector2(5), + Children = new Drawable[] + { + new FormEnumDropdown + { + RelativeSizeAxes = Axes.X, + Caption = BeatmapSubmissionStrings.BeatmapSubmissionTargetCaption, + }, + new FormCheckBox + { + Caption = BeatmapSubmissionStrings.NotifyOnDiscussionReplies, + Current = notifyOnDiscussionReplies, + }, + new FormCheckBox + { + Caption = BeatmapSubmissionStrings.LoadInBrowserAfterSubmission, + Current = loadInBrowserAfterSubmission, + }, + new OsuTextFlowContainer(cp => cp.Font = OsuFont.Default.With(size: CONTENT_FONT_SIZE, weight: FontWeight.Bold)) + { + RelativeSizeAxes = Axes.X, + Colour = colours.Orange1, + Text = BeatmapSubmissionStrings.LegacyExportDisclaimer, + Padding = new MarginPadding { Top = 20 } + }, + } + }); + } + + private enum BeatmapSubmissionTarget + { + [LocalisableDescription(typeof(BeatmapSubmissionStrings), nameof(BeatmapSubmissionStrings.BeatmapSubmissionTargetWIP))] + WIP, + + [LocalisableDescription(typeof(BeatmapSubmissionStrings), nameof(BeatmapSubmissionStrings.BeatmapSubmissionTargetPending))] + Pending, + } + } +} From 2f2dc158e0353aa5ba27108980a1bed1466a2f36 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 4 Feb 2025 17:44:59 +0900 Subject: [PATCH 0852/1275] Ensure test step doesn't consider pooled instances of drawables --- osu.Game.Tests/Visual/SongSelect/BeatmapCarouselV2TestScene.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/osu.Game.Tests/Visual/SongSelect/BeatmapCarouselV2TestScene.cs b/osu.Game.Tests/Visual/SongSelect/BeatmapCarouselV2TestScene.cs index 0a9719423c..2e67e625f9 100644 --- a/osu.Game.Tests/Visual/SongSelect/BeatmapCarouselV2TestScene.cs +++ b/osu.Game.Tests/Visual/SongSelect/BeatmapCarouselV2TestScene.cs @@ -175,7 +175,8 @@ namespace osu.Game.Tests.Visual.SongSelect { AddStep($"click panel at index {index}", () => { - Carousel.ChildrenOfType() + Carousel.ChildrenOfType().Single() + .ChildrenOfType() .Where(p => ((ICarouselPanel)p).Item?.IsVisible == true) .Reverse() .ElementAt(index) From ccdb6e4c4870ef64b3a2e549716c4bf7b412b646 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 4 Feb 2025 17:50:14 +0900 Subject: [PATCH 0853/1275] Fix carousel tests failing due to dependency on depth ordering --- osu.Game.Tests/Visual/SongSelect/BeatmapCarouselV2TestScene.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Tests/Visual/SongSelect/BeatmapCarouselV2TestScene.cs b/osu.Game.Tests/Visual/SongSelect/BeatmapCarouselV2TestScene.cs index 2e67e625f9..f7be5f12e8 100644 --- a/osu.Game.Tests/Visual/SongSelect/BeatmapCarouselV2TestScene.cs +++ b/osu.Game.Tests/Visual/SongSelect/BeatmapCarouselV2TestScene.cs @@ -178,7 +178,7 @@ namespace osu.Game.Tests.Visual.SongSelect Carousel.ChildrenOfType().Single() .ChildrenOfType() .Where(p => ((ICarouselPanel)p).Item?.IsVisible == true) - .Reverse() + .OrderBy(p => p.Y) .ElementAt(index) .TriggerClick(); }); From 58560f8acfe0259795358e969ddee6ca0600d2ad Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 4 Feb 2025 17:11:09 +0900 Subject: [PATCH 0854/1275] Add tracking of expansion states for groups and sets --- .../SongSelect/BeatmapCarouselV2TestScene.cs | 3 +- osu.Game/Screens/SelectV2/BeatmapCarousel.cs | 38 ++++++++++++------- .../SelectV2/BeatmapCarouselFilterGrouping.cs | 20 +++++----- osu.Game/Screens/SelectV2/CarouselItem.cs | 7 +++- 4 files changed, 42 insertions(+), 26 deletions(-) diff --git a/osu.Game.Tests/Visual/SongSelect/BeatmapCarouselV2TestScene.cs b/osu.Game.Tests/Visual/SongSelect/BeatmapCarouselV2TestScene.cs index f7be5f12e8..72c9611fdb 100644 --- a/osu.Game.Tests/Visual/SongSelect/BeatmapCarouselV2TestScene.cs +++ b/osu.Game.Tests/Visual/SongSelect/BeatmapCarouselV2TestScene.cs @@ -153,7 +153,8 @@ namespace osu.Game.Tests.Visual.SongSelect var groupingFilter = Carousel.Filters.OfType().Single(); GroupDefinition g = groupingFilter.GroupItems.Keys.ElementAt(group); - CarouselItem item = groupingFilter.GroupItems[g].ElementAt(panel); + // offset by one because the group itself is included in the items list. + CarouselItem item = groupingFilter.GroupItems[g].ElementAt(panel + 1); return ReferenceEquals(Carousel.CurrentSelection, item.Model); }); diff --git a/osu.Game/Screens/SelectV2/BeatmapCarousel.cs b/osu.Game/Screens/SelectV2/BeatmapCarousel.cs index 858888c517..9f62780dda 100644 --- a/osu.Game/Screens/SelectV2/BeatmapCarousel.cs +++ b/osu.Game/Screens/SelectV2/BeatmapCarousel.cs @@ -105,12 +105,12 @@ namespace osu.Game.Screens.SelectV2 // Special case – collapsing an open group. if (lastSelectedGroup == group) { - setVisibilityOfGroupItems(lastSelectedGroup, false); + setExpansionStateOfGroup(lastSelectedGroup, false); lastSelectedGroup = null; return false; } - setVisibleGroup(group); + setExpandedGroup(group); return false; case BeatmapSetInfo setInfo: @@ -127,11 +127,11 @@ namespace osu.Game.Screens.SelectV2 GroupDefinition? group = grouping.GroupItems.SingleOrDefault(kvp => kvp.Value.Any(i => ReferenceEquals(i.Model, beatmapInfo))).Key; if (group != null) - setVisibleGroup(group); + setExpandedGroup(group); } else { - setVisibleSet(beatmapInfo); + setExpandedSet(beatmapInfo); } return true; @@ -158,37 +158,47 @@ namespace osu.Game.Screens.SelectV2 } } - private void setVisibleGroup(GroupDefinition group) + private void setExpandedGroup(GroupDefinition group) { if (lastSelectedGroup != null) - setVisibilityOfGroupItems(lastSelectedGroup, false); + setExpansionStateOfGroup(lastSelectedGroup, false); lastSelectedGroup = group; - setVisibilityOfGroupItems(group, true); + setExpansionStateOfGroup(group, true); } - private void setVisibilityOfGroupItems(GroupDefinition group, bool visible) + private void setExpansionStateOfGroup(GroupDefinition group, bool expanded) { if (grouping.GroupItems.TryGetValue(group, out var items)) { foreach (var i in items) - i.IsVisible = visible; + { + if (i.Model is GroupDefinition) + i.IsExpanded = expanded; + else + i.IsVisible = expanded; + } } } - private void setVisibleSet(BeatmapInfo beatmapInfo) + private void setExpandedSet(BeatmapInfo beatmapInfo) { if (lastSelectedBeatmap != null) - setVisibilityOfSetItems(lastSelectedBeatmap.BeatmapSet!, false); + setExpansionStateOfSetItems(lastSelectedBeatmap.BeatmapSet!, false); lastSelectedBeatmap = beatmapInfo; - setVisibilityOfSetItems(beatmapInfo.BeatmapSet!, true); + setExpansionStateOfSetItems(beatmapInfo.BeatmapSet!, true); } - private void setVisibilityOfSetItems(BeatmapSetInfo set, bool visible) + private void setExpansionStateOfSetItems(BeatmapSetInfo set, bool expanded) { if (grouping.SetItems.TryGetValue(set, out var items)) { foreach (var i in items) - i.IsVisible = visible; + { + if (i.Model is BeatmapSetInfo) + i.IsExpanded = expanded; + else + i.IsVisible = expanded; + } } } diff --git a/osu.Game/Screens/SelectV2/BeatmapCarouselFilterGrouping.cs b/osu.Game/Screens/SelectV2/BeatmapCarouselFilterGrouping.cs index ea737d8b7f..e4160cc0fa 100644 --- a/osu.Game/Screens/SelectV2/BeatmapCarouselFilterGrouping.cs +++ b/osu.Game/Screens/SelectV2/BeatmapCarouselFilterGrouping.cs @@ -65,7 +65,11 @@ namespace osu.Game.Screens.SelectV2 if (b.StarRating > starGroup) { starGroup = (int)Math.Floor(b.StarRating); - newItems.Add(new CarouselItem(new GroupDefinition($"{starGroup} - {++starGroup} *")) { DrawHeight = GroupPanel.HEIGHT }); + var groupDefinition = new GroupDefinition($"{starGroup} - {++starGroup} *"); + var groupItem = new CarouselItem(groupDefinition) { DrawHeight = GroupPanel.HEIGHT }; + + newItems.Add(groupItem); + groupItems[groupDefinition] = new HashSet { groupItem }; } newItems.Add(item); @@ -91,14 +95,13 @@ namespace osu.Game.Screens.SelectV2 if (newBeatmapSet) { - newItems.Insert(i, new CarouselItem(beatmap.BeatmapSet!) { DrawHeight = BeatmapSetPanel.HEIGHT }); + var setItem = new CarouselItem(beatmap.BeatmapSet!) { DrawHeight = BeatmapSetPanel.HEIGHT }; + setItems[beatmap.BeatmapSet!] = new HashSet { setItem }; + newItems.Insert(i, setItem); i++; } - if (!setItems.TryGetValue(beatmap.BeatmapSet!, out var related)) - setItems[beatmap.BeatmapSet!] = related = new HashSet(); - - related.Add(item); + setItems[beatmap.BeatmapSet!].Add(item); item.IsVisible = false; } @@ -121,10 +124,7 @@ namespace osu.Game.Screens.SelectV2 if (lastGroup != null) { - if (!groupItems.TryGetValue(lastGroup, out var groupRelated)) - groupItems[lastGroup] = groupRelated = new HashSet(); - groupRelated.Add(item); - + groupItems[lastGroup].Add(item); item.IsVisible = false; } } diff --git a/osu.Game/Screens/SelectV2/CarouselItem.cs b/osu.Game/Screens/SelectV2/CarouselItem.cs index 13d5c840cf..32be33e99a 100644 --- a/osu.Game/Screens/SelectV2/CarouselItem.cs +++ b/osu.Game/Screens/SelectV2/CarouselItem.cs @@ -30,10 +30,15 @@ namespace osu.Game.Screens.SelectV2 public float DrawHeight { get; set; } = DEFAULT_HEIGHT; /// - /// Whether this item is visible or collapsed (hidden). + /// Whether this item is visible or hidden. /// public bool IsVisible { get; set; } = true; + /// + /// Whether this item is expanded or not. Should only be used for headers of groups. + /// + public bool IsExpanded { get; set; } + public CarouselItem(object model) { Model = model; From 61419ec9c840fe55886c338f0eb53a8dd919be89 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Tue, 4 Feb 2025 17:54:03 +0900 Subject: [PATCH 0855/1275] Refactor user presence watching to be tokenised --- .../Online/TestSceneMetadataClient.cs | 52 +++++++++++++++ .../Online/TestSceneCurrentlyOnlineDisplay.cs | 12 ++-- osu.Game/Online/Metadata/MetadataClient.cs | 59 ++++++++++++++--- .../Online/Metadata/OnlineMetadataClient.cs | 63 +++++++++---------- osu.Game/Overlays/DashboardOverlay.cs | 9 ++- .../Visual/Metadata/TestMetadataClient.cs | 20 +++--- 6 files changed, 156 insertions(+), 59 deletions(-) create mode 100644 osu.Game.Tests/Online/TestSceneMetadataClient.cs diff --git a/osu.Game.Tests/Online/TestSceneMetadataClient.cs b/osu.Game.Tests/Online/TestSceneMetadataClient.cs new file mode 100644 index 0000000000..8c738eeca6 --- /dev/null +++ b/osu.Game.Tests/Online/TestSceneMetadataClient.cs @@ -0,0 +1,52 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using NUnit.Framework; +using osu.Framework.Testing; +using osu.Game.Tests.Visual; +using osu.Game.Tests.Visual.Metadata; + +namespace osu.Game.Tests.Online +{ + [TestFixture] + [HeadlessTest] + public class TestSceneMetadataClient : OsuTestScene + { + private TestMetadataClient client = null!; + + [SetUp] + public void Setup() => Schedule(() => + { + Child = client = new TestMetadataClient(); + }); + + [Test] + public void TestWatchingMultipleTimesInvokesServerMethodsOnce() + { + int countBegin = 0; + int countEnd = 0; + + IDisposable token1 = null!; + IDisposable token2 = null!; + + AddStep("setup", () => + { + client.OnBeginWatchingUserPresence += () => countBegin++; + client.OnEndWatchingUserPresence += () => countEnd++; + }); + + AddStep("begin watching presence (1)", () => token1 = client.BeginWatchingUserPresence()); + AddAssert("server method invoked once", () => countBegin, () => Is.EqualTo(1)); + + AddStep("begin watching presence (2)", () => token2 = client.BeginWatchingUserPresence()); + AddAssert("server method not invoked a second time", () => countBegin, () => Is.EqualTo(1)); + + AddStep("end watching presence (1)", () => token1.Dispose()); + AddAssert("server method not invoked", () => countEnd, () => Is.EqualTo(0)); + + AddStep("end watching presence (2)", () => token2.Dispose()); + AddAssert("server method invoked once", () => countEnd, () => Is.EqualTo(1)); + } + } +} diff --git a/osu.Game.Tests/Visual/Online/TestSceneCurrentlyOnlineDisplay.cs b/osu.Game.Tests/Visual/Online/TestSceneCurrentlyOnlineDisplay.cs index b696c5d8ca..2e53ec2ba4 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneCurrentlyOnlineDisplay.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneCurrentlyOnlineDisplay.cs @@ -65,7 +65,9 @@ namespace osu.Game.Tests.Visual.Online [Test] public void TestBasicDisplay() { - AddStep("Begin watching user presence", () => metadataClient.BeginWatchingUserPresence()); + IDisposable token = null!; + + AddStep("Begin watching user presence", () => token = metadataClient.BeginWatchingUserPresence()); AddStep("Add online user", () => metadataClient.UserPresenceUpdated(streamingUser.Id, new UserPresence { Status = UserStatus.Online, Activity = new UserActivity.ChoosingBeatmap() })); AddUntilStep("Panel loaded", () => currentlyOnline.ChildrenOfType().FirstOrDefault()?.User.Id == 2); AddAssert("Spectate button disabled", () => currentlyOnline.ChildrenOfType().First().Enabled.Value, () => Is.False); @@ -78,14 +80,16 @@ namespace osu.Game.Tests.Visual.Online AddStep("Remove playing user", () => metadataClient.UserPresenceUpdated(streamingUser.Id, null)); AddUntilStep("Panel no longer present", () => !currentlyOnline.ChildrenOfType().Any()); - AddStep("End watching user presence", () => metadataClient.EndWatchingUserPresence()); + AddStep("End watching user presence", () => token.Dispose()); } [Test] public void TestUserWasPlayingBeforeWatchingUserPresence() { + IDisposable token = null!; + AddStep("User began playing", () => spectatorClient.SendStartPlay(streamingUser.Id, 0)); - AddStep("Begin watching user presence", () => metadataClient.BeginWatchingUserPresence()); + AddStep("Begin watching user presence", () => token = metadataClient.BeginWatchingUserPresence()); AddStep("Add online user", () => metadataClient.UserPresenceUpdated(streamingUser.Id, new UserPresence { Status = UserStatus.Online, Activity = new UserActivity.ChoosingBeatmap() })); AddUntilStep("Panel loaded", () => currentlyOnline.ChildrenOfType().FirstOrDefault()?.User.Id == 2); AddAssert("Spectate button enabled", () => currentlyOnline.ChildrenOfType().First().Enabled.Value, () => Is.True); @@ -93,7 +97,7 @@ namespace osu.Game.Tests.Visual.Online AddStep("User finished playing", () => spectatorClient.SendEndPlay(streamingUser.Id)); AddAssert("Spectate button disabled", () => currentlyOnline.ChildrenOfType().First().Enabled.Value, () => Is.False); AddStep("Remove playing user", () => metadataClient.UserPresenceUpdated(streamingUser.Id, null)); - AddStep("End watching user presence", () => metadataClient.EndWatchingUserPresence()); + AddStep("End watching user presence", () => token.Dispose()); } internal partial class TestUserLookupCache : UserLookupCache diff --git a/osu.Game/Online/Metadata/MetadataClient.cs b/osu.Game/Online/Metadata/MetadataClient.cs index 356c50bcc0..1da245e80d 100644 --- a/osu.Game/Online/Metadata/MetadataClient.cs +++ b/osu.Game/Online/Metadata/MetadataClient.cs @@ -3,11 +3,13 @@ using System; using System.Linq; +using System.Threading; using System.Threading.Tasks; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Game.Online.API; +using osu.Game.Online.Multiplayer; using osu.Game.Users; namespace osu.Game.Online.Metadata @@ -37,11 +39,6 @@ namespace osu.Game.Online.Metadata #region User presence updates - /// - /// Whether the client is currently receiving user presence updates from the server. - /// - public abstract IBindable IsWatchingUserPresence { get; } - /// /// The information about the current user. /// @@ -82,11 +79,36 @@ namespace osu.Game.Online.Metadata /// public abstract Task UpdateStatus(UserStatus? status); - /// - public abstract Task BeginWatchingUserPresence(); + private int userPresenceWatchCount; + + protected bool IsWatchingUserPresence + => Interlocked.CompareExchange(ref userPresenceWatchCount, userPresenceWatchCount, userPresenceWatchCount) > 0; + + /// + public IDisposable BeginWatchingUserPresence() + => new UserPresenceWatchToken(this); /// - public abstract Task EndWatchingUserPresence(); + Task IMetadataServer.BeginWatchingUserPresence() + { + if (Interlocked.Increment(ref userPresenceWatchCount) == 1) + return BeginWatchingUserPresenceInternal(); + + return Task.CompletedTask; + } + + /// + Task IMetadataServer.EndWatchingUserPresence() + { + if (Interlocked.Decrement(ref userPresenceWatchCount) == 0) + return EndWatchingUserPresenceInternal(); + + return Task.CompletedTask; + } + + protected abstract Task BeginWatchingUserPresenceInternal(); + + protected abstract Task EndWatchingUserPresenceInternal(); /// public abstract Task UserPresenceUpdated(int userId, UserPresence? presence); @@ -94,6 +116,27 @@ namespace osu.Game.Online.Metadata /// public abstract Task FriendPresenceUpdated(int userId, UserPresence? presence); + private class UserPresenceWatchToken : IDisposable + { + private readonly IMetadataServer server; + private bool isDisposed; + + public UserPresenceWatchToken(IMetadataServer server) + { + this.server = server; + server.BeginWatchingUserPresence().FireAndForget(); + } + + public void Dispose() + { + if (isDisposed) + return; + + server.EndWatchingUserPresence().FireAndForget(); + isDisposed = true; + } + } + #endregion #region Daily Challenge diff --git a/osu.Game/Online/Metadata/OnlineMetadataClient.cs b/osu.Game/Online/Metadata/OnlineMetadataClient.cs index 5aeeb04d11..c7c7dfc58b 100644 --- a/osu.Game/Online/Metadata/OnlineMetadataClient.cs +++ b/osu.Game/Online/Metadata/OnlineMetadataClient.cs @@ -20,9 +20,6 @@ namespace osu.Game.Online.Metadata { public override IBindable IsConnected { get; } = new Bindable(); - public override IBindable IsWatchingUserPresence => isWatchingUserPresence; - private readonly BindableBool isWatchingUserPresence = new BindableBool(); - public override UserPresence LocalUserPresence => localUserPresence; private UserPresence localUserPresence; @@ -109,15 +106,18 @@ namespace osu.Game.Online.Metadata { Schedule(() => { - isWatchingUserPresence.Value = false; userPresences.Clear(); friendPresences.Clear(); dailyChallengeInfo.Value = null; localUserPresence = default; }); + return; } + if (IsWatchingUserPresence) + BeginWatchingUserPresenceInternal(); + if (localUser.Value is not GuestUser) { UpdateActivity(userActivity.Value); @@ -201,6 +201,31 @@ namespace osu.Game.Online.Metadata return connection.InvokeAsync(nameof(IMetadataServer.UpdateStatus), status); } + protected override Task BeginWatchingUserPresenceInternal() + { + if (connector?.IsConnected.Value != true) + return Task.CompletedTask; + + Logger.Log($@"{nameof(OnlineMetadataClient)} began watching user presence", LoggingTarget.Network); + + Debug.Assert(connection != null); + return connection.InvokeAsync(nameof(IMetadataServer.BeginWatchingUserPresence)); + } + + protected override Task EndWatchingUserPresenceInternal() + { + if (connector?.IsConnected.Value != true) + return Task.CompletedTask; + + Logger.Log($@"{nameof(OnlineMetadataClient)} stopped watching user presence", LoggingTarget.Network); + + // must be scheduled before any remote calls to avoid mis-ordering. + Schedule(() => userPresences.Clear()); + + Debug.Assert(connection != null); + return connection.InvokeAsync(nameof(IMetadataServer.EndWatchingUserPresence)); + } + public override Task UserPresenceUpdated(int userId, UserPresence? presence) { Schedule(() => @@ -237,36 +262,6 @@ namespace osu.Game.Online.Metadata return Task.CompletedTask; } - public override async Task BeginWatchingUserPresence() - { - if (connector?.IsConnected.Value != true) - throw new OperationCanceledException(); - - Debug.Assert(connection != null); - await connection.InvokeAsync(nameof(IMetadataServer.BeginWatchingUserPresence)).ConfigureAwait(false); - Schedule(() => isWatchingUserPresence.Value = true); - Logger.Log($@"{nameof(OnlineMetadataClient)} began watching user presence", LoggingTarget.Network); - } - - public override async Task EndWatchingUserPresence() - { - try - { - if (connector?.IsConnected.Value != true) - throw new OperationCanceledException(); - - // must be scheduled before any remote calls to avoid mis-ordering. - Schedule(() => userPresences.Clear()); - Debug.Assert(connection != null); - await connection.InvokeAsync(nameof(IMetadataServer.EndWatchingUserPresence)).ConfigureAwait(false); - Logger.Log($@"{nameof(OnlineMetadataClient)} stopped watching user presence", LoggingTarget.Network); - } - finally - { - Schedule(() => isWatchingUserPresence.Value = false); - } - } - public override Task DailyChallengeUpdated(DailyChallengeInfo? info) { Schedule(() => dailyChallengeInfo.Value = info); diff --git a/osu.Game/Overlays/DashboardOverlay.cs b/osu.Game/Overlays/DashboardOverlay.cs index 1861f892bd..1912736135 100644 --- a/osu.Game/Overlays/DashboardOverlay.cs +++ b/osu.Game/Overlays/DashboardOverlay.cs @@ -6,7 +6,6 @@ using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics.Containers; using osu.Game.Online.Metadata; -using osu.Game.Online.Multiplayer; using osu.Game.Overlays.Dashboard; using osu.Game.Overlays.Dashboard.Friends; @@ -18,6 +17,7 @@ namespace osu.Game.Overlays private MetadataClient metadataClient { get; set; } = null!; private IBindable metadataConnected = null!; + private IDisposable? userPresenceWatchToken; public DashboardOverlay() : base(OverlayColourScheme.Purple) @@ -61,9 +61,12 @@ namespace osu.Game.Overlays return; if (State.Value == Visibility.Visible) - metadataClient.BeginWatchingUserPresence().FireAndForget(); + userPresenceWatchToken ??= metadataClient.BeginWatchingUserPresence(); else - metadataClient.EndWatchingUserPresence().FireAndForget(); + { + userPresenceWatchToken?.Dispose(); + userPresenceWatchToken = null; + } } } } diff --git a/osu.Game/Tests/Visual/Metadata/TestMetadataClient.cs b/osu.Game/Tests/Visual/Metadata/TestMetadataClient.cs index d14cbd7743..dca1b0e468 100644 --- a/osu.Game/Tests/Visual/Metadata/TestMetadataClient.cs +++ b/osu.Game/Tests/Visual/Metadata/TestMetadataClient.cs @@ -16,9 +16,6 @@ namespace osu.Game.Tests.Visual.Metadata public override IBindable IsConnected => isConnected; private readonly BindableBool isConnected = new BindableBool(true); - public override IBindable IsWatchingUserPresence => isWatchingUserPresence; - private readonly BindableBool isWatchingUserPresence = new BindableBool(); - public override UserPresence LocalUserPresence => localUserPresence; private UserPresence localUserPresence; @@ -34,15 +31,18 @@ namespace osu.Game.Tests.Visual.Metadata [Resolved] private IAPIProvider api { get; set; } = null!; - public override Task BeginWatchingUserPresence() + public event Action? OnBeginWatchingUserPresence; + public event Action? OnEndWatchingUserPresence; + + protected override Task BeginWatchingUserPresenceInternal() { - isWatchingUserPresence.Value = true; + OnBeginWatchingUserPresence?.Invoke(); return Task.CompletedTask; } - public override Task EndWatchingUserPresence() + protected override Task EndWatchingUserPresenceInternal() { - isWatchingUserPresence.Value = false; + OnEndWatchingUserPresence?.Invoke(); return Task.CompletedTask; } @@ -50,7 +50,7 @@ namespace osu.Game.Tests.Visual.Metadata { localUserPresence = localUserPresence with { Activity = activity }; - if (isWatchingUserPresence.Value) + if (IsWatchingUserPresence) { if (userPresences.ContainsKey(api.LocalUser.Value.Id)) userPresences[api.LocalUser.Value.Id] = localUserPresence; @@ -63,7 +63,7 @@ namespace osu.Game.Tests.Visual.Metadata { localUserPresence = localUserPresence with { Status = status }; - if (isWatchingUserPresence.Value) + if (IsWatchingUserPresence) { if (userPresences.ContainsKey(api.LocalUser.Value.Id)) userPresences[api.LocalUser.Value.Id] = localUserPresence; @@ -74,7 +74,7 @@ namespace osu.Game.Tests.Visual.Metadata public override Task UserPresenceUpdated(int userId, UserPresence? presence) { - if (isWatchingUserPresence.Value) + if (IsWatchingUserPresence) { if (presence?.Status != null) { From 2f90bb4d6793475835d1d51bef92b2c40f69112c Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Tue, 4 Feb 2025 17:55:50 +0900 Subject: [PATCH 0856/1275] Watch global user presence while in spectator screen --- osu.Game/Screens/Spectate/SpectatorScreen.cs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/osu.Game/Screens/Spectate/SpectatorScreen.cs b/osu.Game/Screens/Spectate/SpectatorScreen.cs index ddc638b7c5..84b5889751 100644 --- a/osu.Game/Screens/Spectate/SpectatorScreen.cs +++ b/osu.Game/Screens/Spectate/SpectatorScreen.cs @@ -12,6 +12,7 @@ using osu.Framework.Extensions.ObjectExtensions; using osu.Game.Beatmaps; using osu.Game.Database; using osu.Game.Online.API.Requests.Responses; +using osu.Game.Online.Metadata; using osu.Game.Online.Spectator; using osu.Game.Replays; using osu.Game.Rulesets; @@ -38,6 +39,9 @@ namespace osu.Game.Screens.Spectate [Resolved] private SpectatorClient spectatorClient { get; set; } = null!; + [Resolved] + private MetadataClient metadataClient { get; set; } = null!; + [Resolved] private UserLookupCache userLookupCache { get; set; } = null!; @@ -50,6 +54,7 @@ namespace osu.Game.Screens.Spectate private readonly Dictionary gameplayStates = new Dictionary(); private IDisposable? realmSubscription; + private IDisposable? userWatchToken; /// /// Creates a new . @@ -64,6 +69,8 @@ namespace osu.Game.Screens.Spectate { base.LoadComplete(); + userWatchToken = metadataClient.BeginWatchingUserPresence(); + userLookupCache.GetUsersAsync(users.ToArray()).ContinueWith(task => Schedule(() => { var foundUsers = task.GetResultSafely(); @@ -282,6 +289,7 @@ namespace osu.Game.Screens.Spectate } realmSubscription?.Dispose(); + userWatchToken?.Dispose(); } } } From 599b59cb1447467048bda41105956bd0c532863e Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 4 Feb 2025 17:16:36 +0900 Subject: [PATCH 0857/1275] Add expanded state to sample drawable representations --- ...estSceneBeatmapCarouselV2GroupSelection.cs | 25 ++++++++++++++++++- osu.Game/Screens/SelectV2/BeatmapPanel.cs | 1 + osu.Game/Screens/SelectV2/BeatmapSetPanel.cs | 9 ++++++- osu.Game/Screens/SelectV2/Carousel.cs | 2 ++ osu.Game/Screens/SelectV2/GroupPanel.cs | 10 +++++++- osu.Game/Screens/SelectV2/ICarouselPanel.cs | 7 +++++- 6 files changed, 50 insertions(+), 4 deletions(-) diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2GroupSelection.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2GroupSelection.cs index 04ca0a9085..f4d97be5a5 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2GroupSelection.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2GroupSelection.cs @@ -24,7 +24,7 @@ namespace osu.Game.Tests.Visual.SongSelect } [Test] - public void TestOpenCloseGroupWithNoSelection() + public void TestOpenCloseGroupWithNoSelectionMouse() { AddBeatmaps(10, 5); WaitForDrawablePanels(); @@ -41,6 +41,29 @@ namespace osu.Game.Tests.Visual.SongSelect CheckNoSelection(); } + [Test] + public void TestOpenCloseGroupWithNoSelectionKeyboard() + { + AddBeatmaps(10, 5); + WaitForDrawablePanels(); + + AddAssert("no beatmaps visible", () => Carousel.ChildrenOfType().Count(p => p.Alpha > 0), () => Is.Zero); + CheckNoSelection(); + + SelectNextPanel(); + Select(); + AddUntilStep("some beatmaps visible", () => Carousel.ChildrenOfType().Count(p => p.Alpha > 0), () => Is.GreaterThan(0)); + AddAssert("keyboard selected is expanded", () => getKeyboardSelectedPanel()?.Expanded.Value, () => Is.True); + CheckNoSelection(); + + Select(); + AddUntilStep("no beatmaps visible", () => Carousel.ChildrenOfType().Count(p => p.Alpha > 0), () => Is.Zero); + AddAssert("keyboard selected is collapsed", () => getKeyboardSelectedPanel()?.Expanded.Value, () => Is.False); + CheckNoSelection(); + + GroupPanel? getKeyboardSelectedPanel() => Carousel.ChildrenOfType().SingleOrDefault(p => p.KeyboardSelected.Value); + } + [Test] public void TestCarouselRemembersSelection() { diff --git a/osu.Game/Screens/SelectV2/BeatmapPanel.cs b/osu.Game/Screens/SelectV2/BeatmapPanel.cs index 4a9e406def..3edfd4203b 100644 --- a/osu.Game/Screens/SelectV2/BeatmapPanel.cs +++ b/osu.Game/Screens/SelectV2/BeatmapPanel.cs @@ -100,6 +100,7 @@ namespace osu.Game.Screens.SelectV2 public CarouselItem? Item { get; set; } public BindableBool Selected { get; } = new BindableBool(); + public BindableBool Expanded { get; } = new BindableBool(); public BindableBool KeyboardSelected { get; } = new BindableBool(); public double DrawYPosition { get; set; } diff --git a/osu.Game/Screens/SelectV2/BeatmapSetPanel.cs b/osu.Game/Screens/SelectV2/BeatmapSetPanel.cs index 06e3ad3426..79ffe0f68a 100644 --- a/osu.Game/Screens/SelectV2/BeatmapSetPanel.cs +++ b/osu.Game/Screens/SelectV2/BeatmapSetPanel.cs @@ -25,6 +25,7 @@ namespace osu.Game.Screens.SelectV2 private BeatmapCarousel carousel { get; set; } = null!; private OsuSpriteText text = null!; + private Box box = null!; [BackgroundDependencyLoader] private void load() @@ -34,7 +35,7 @@ namespace osu.Game.Screens.SelectV2 InternalChildren = new Drawable[] { - new Box + box = new Box { Colour = Color4.Yellow.Darken(5), Alpha = 0.8f, @@ -48,6 +49,11 @@ namespace osu.Game.Screens.SelectV2 } }; + Expanded.BindValueChanged(value => + { + box.FadeColour(value.NewValue ? Color4.Yellow.Darken(2) : Color4.Yellow.Darken(5), 500, Easing.OutQuint); + }); + KeyboardSelected.BindValueChanged(value => { if (value.NewValue) @@ -85,6 +91,7 @@ namespace osu.Game.Screens.SelectV2 public CarouselItem? Item { get; set; } public BindableBool Selected { get; } = new BindableBool(); + public BindableBool Expanded { get; } = new BindableBool(); public BindableBool KeyboardSelected { get; } = new BindableBool(); public double DrawYPosition { get; set; } diff --git a/osu.Game/Screens/SelectV2/Carousel.cs b/osu.Game/Screens/SelectV2/Carousel.cs index a1bafac620..608ef207d9 100644 --- a/osu.Game/Screens/SelectV2/Carousel.cs +++ b/osu.Game/Screens/SelectV2/Carousel.cs @@ -571,6 +571,7 @@ namespace osu.Game.Screens.SelectV2 c.Selected.Value = c.Item == currentSelection?.CarouselItem; c.KeyboardSelected.Value = c.Item == currentKeyboardSelection?.CarouselItem; + c.Expanded.Value = c.Item.IsExpanded; } } @@ -674,6 +675,7 @@ namespace osu.Game.Screens.SelectV2 carouselPanel.Item = null; carouselPanel.Selected.Value = false; carouselPanel.KeyboardSelected.Value = false; + carouselPanel.Expanded.Value = false; } #endregion diff --git a/osu.Game/Screens/SelectV2/GroupPanel.cs b/osu.Game/Screens/SelectV2/GroupPanel.cs index 882d77cb8d..7ed256ca6a 100644 --- a/osu.Game/Screens/SelectV2/GroupPanel.cs +++ b/osu.Game/Screens/SelectV2/GroupPanel.cs @@ -26,6 +26,8 @@ namespace osu.Game.Screens.SelectV2 private Box activationFlash = null!; private OsuSpriteText text = null!; + private Box box = null!; + [BackgroundDependencyLoader] private void load() { @@ -34,7 +36,7 @@ namespace osu.Game.Screens.SelectV2 InternalChildren = new Drawable[] { - new Box + box = new Box { Colour = Color4.DarkBlue.Darken(5), Alpha = 0.8f, @@ -60,6 +62,11 @@ namespace osu.Game.Screens.SelectV2 activationFlash.FadeTo(value.NewValue ? 0.2f : 0, 500, Easing.OutQuint); }); + Expanded.BindValueChanged(value => + { + box.FadeColour(value.NewValue ? Color4.SkyBlue : Color4.DarkBlue.Darken(5), 500, Easing.OutQuint); + }); + KeyboardSelected.BindValueChanged(value => { if (value.NewValue) @@ -97,6 +104,7 @@ namespace osu.Game.Screens.SelectV2 public CarouselItem? Item { get; set; } public BindableBool Selected { get; } = new BindableBool(); + public BindableBool Expanded { get; } = new BindableBool(); public BindableBool KeyboardSelected { get; } = new BindableBool(); public double DrawYPosition { get; set; } diff --git a/osu.Game/Screens/SelectV2/ICarouselPanel.cs b/osu.Game/Screens/SelectV2/ICarouselPanel.cs index a956bb22a3..4fba0d2827 100644 --- a/osu.Game/Screens/SelectV2/ICarouselPanel.cs +++ b/osu.Game/Screens/SelectV2/ICarouselPanel.cs @@ -14,10 +14,15 @@ namespace osu.Game.Screens.SelectV2 public interface ICarouselPanel { /// - /// Whether this item has selection. Should be read from to update the visual state. + /// Whether this item has selection (see ). Should be read from to update the visual state. /// BindableBool Selected { get; } + /// + /// Whether this item is expanded (see ). Should be read from to update the visual state. + /// + BindableBool Expanded { get; } + /// /// Whether this item has keyboard selection. Should be read from to update the visual state. /// From 0ad97c1fad6c85b2e95864ba007ba670de00b3ba Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Tue, 4 Feb 2025 18:24:57 +0900 Subject: [PATCH 0858/1275] Fix inspection --- osu.Game.Tests/Online/TestSceneMetadataClient.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Tests/Online/TestSceneMetadataClient.cs b/osu.Game.Tests/Online/TestSceneMetadataClient.cs index 8c738eeca6..04e1d91edf 100644 --- a/osu.Game.Tests/Online/TestSceneMetadataClient.cs +++ b/osu.Game.Tests/Online/TestSceneMetadataClient.cs @@ -11,7 +11,7 @@ namespace osu.Game.Tests.Online { [TestFixture] [HeadlessTest] - public class TestSceneMetadataClient : OsuTestScene + public partial class TestSceneMetadataClient : OsuTestScene { private TestMetadataClient client = null!; From 6c6063464aed10ca52237ac764386fd1877a64a9 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 4 Feb 2025 18:41:26 +0900 Subject: [PATCH 0859/1275] Remove `Scheduler.AddOnce` from `updateSpecifics` To keep things simple, let's not bother debouncing this. The debouncing was causing spectating handling to fail because of two interdependent components binding to `BeatmapAvailability`: Binding to update the screen's `Beatmap` after a download completes: https://github.com/ppy/osu/blob/58747061171c4ebe70201dfe4d3329ed7f4343f5/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs#L266-L267 Binding to attempt a load request: https://github.com/ppy/osu/blob/8bb7bea04e56fab9247baa59ae879e16c8b4bd9b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs#L67 The first must update the beatmap before the second runs, else gameplay will not load due to `Beatmap.IsDefault`. --- osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs b/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs index 9f7e193131..f4d50b5170 100644 --- a/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs @@ -427,7 +427,7 @@ namespace osu.Game.Screens.OnlinePlay.Match /// The screen to enter. protected abstract Screen CreateGameplayScreen(PlaylistItem selectedItem); - private void updateSpecifics() => Scheduler.AddOnce(() => + private void updateSpecifics() { if (!this.IsCurrentScreen() || SelectedItem.Value is not PlaylistItem item) return; @@ -487,7 +487,7 @@ namespace osu.Game.Screens.OnlinePlay.Match } else UserStyleSection.Hide(); - }); + } protected virtual APIMod[] GetGameplayMods() => UserMods.Value.Select(m => new APIMod(m)).Concat(SelectedItem.Value!.RequiredMods).ToArray(); From ccc446a8ca8d004ff74cba2b11bb0d438861f3ed Mon Sep 17 00:00:00 2001 From: Nathan Du Date: Tue, 4 Feb 2025 17:48:44 +0800 Subject: [PATCH 0860/1275] code cleanup --- .../Objects/Drawables/DrawableSwell.cs | 2 +- .../Argon/TaikoArgonSkinTransformer.cs | 2 +- .../Skinning/Legacy/LegacySwellCirclePiece.cs | 23 ------------------- .../Legacy/TaikoLegacySkinTransformer.cs | 2 +- .../TaikoSkinComponents.cs | 2 +- 5 files changed, 4 insertions(+), 27 deletions(-) delete mode 100644 osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacySwellCirclePiece.cs diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableSwell.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableSwell.cs index d75fdbc40a..1dde4b6f9c 100644 --- a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableSwell.cs +++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableSwell.cs @@ -53,7 +53,7 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables AddInternal(ticks = new Container { RelativeSizeAxes = Axes.Both }); } - protected override SkinnableDrawable CreateMainPiece() => new SkinnableDrawable(new TaikoSkinComponentLookup(TaikoSkinComponents.SwellBody), + protected override SkinnableDrawable CreateMainPiece() => new SkinnableDrawable(new TaikoSkinComponentLookup(TaikoSkinComponents.Swell), _ => new DefaultSwell { Anchor = Anchor.Centre, diff --git a/osu.Game.Rulesets.Taiko/Skinning/Argon/TaikoArgonSkinTransformer.cs b/osu.Game.Rulesets.Taiko/Skinning/Argon/TaikoArgonSkinTransformer.cs index b588a22d12..26bb1900b9 100644 --- a/osu.Game.Rulesets.Taiko/Skinning/Argon/TaikoArgonSkinTransformer.cs +++ b/osu.Game.Rulesets.Taiko/Skinning/Argon/TaikoArgonSkinTransformer.cs @@ -68,7 +68,7 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Argon case TaikoSkinComponents.TaikoExplosionOk: return new ArgonHitExplosion(taikoComponent.Component); - case TaikoSkinComponents.SwellBody: + case TaikoSkinComponents.Swell: return new ArgonSwell(); } diff --git a/osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacySwellCirclePiece.cs b/osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacySwellCirclePiece.cs deleted file mode 100644 index 40501d1d40..0000000000 --- a/osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacySwellCirclePiece.cs +++ /dev/null @@ -1,23 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -using osu.Framework.Allocation; -using osu.Framework.Graphics; -using osu.Framework.Graphics.Sprites; -using osu.Game.Skinning; -using osuTK; - -namespace osu.Game.Rulesets.Taiko.Skinning.Legacy -{ - internal partial class LegacySwellCirclePiece : Sprite - { - [BackgroundDependencyLoader] - private void load(ISkinSource skin, SkinManager skinManager) - { - Texture = skin.GetTexture("spinner-warning") ?? skinManager.DefaultClassicSkin.GetTexture("spinner-circle"); - Anchor = Anchor.Centre; - Origin = Anchor.Centre; - Scale = skin.GetTexture("spinner-warning") != null ? Vector2.One : new Vector2(0.18f); - } - } -} diff --git a/osu.Game.Rulesets.Taiko/Skinning/Legacy/TaikoLegacySkinTransformer.cs b/osu.Game.Rulesets.Taiko/Skinning/Legacy/TaikoLegacySkinTransformer.cs index 8fa4551fd4..c6221e0589 100644 --- a/osu.Game.Rulesets.Taiko/Skinning/Legacy/TaikoLegacySkinTransformer.cs +++ b/osu.Game.Rulesets.Taiko/Skinning/Legacy/TaikoLegacySkinTransformer.cs @@ -65,7 +65,7 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy case TaikoSkinComponents.DrumRollTick: return this.GetAnimation("sliderscorepoint", false, false); - case TaikoSkinComponents.SwellBody: + case TaikoSkinComponents.Swell: if (GetTexture("spinner-circle") != null) return new LegacySwell(); diff --git a/osu.Game.Rulesets.Taiko/TaikoSkinComponents.cs b/osu.Game.Rulesets.Taiko/TaikoSkinComponents.cs index 05c6316a05..28133ffcb2 100644 --- a/osu.Game.Rulesets.Taiko/TaikoSkinComponents.cs +++ b/osu.Game.Rulesets.Taiko/TaikoSkinComponents.cs @@ -10,7 +10,7 @@ namespace osu.Game.Rulesets.Taiko RimHit, DrumRollBody, DrumRollTick, - SwellBody, + Swell, HitTarget, PlayfieldBackgroundLeft, PlayfieldBackgroundRight, From 731f100aaf656ae8273412dc8bdb3134415d4889 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 4 Feb 2025 11:45:15 +0100 Subject: [PATCH 0861/1275] Fix incorrect snapping behaviour when previous object is not snapped to beat --- osu.Game/Rulesets/Edit/IDistanceSnapProvider.cs | 2 ++ .../Screens/Edit/Compose/Components/CircularDistanceSnapGrid.cs | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/osu.Game/Rulesets/Edit/IDistanceSnapProvider.cs b/osu.Game/Rulesets/Edit/IDistanceSnapProvider.cs index 195dbf0d46..bb0a0dbd7f 100644 --- a/osu.Game/Rulesets/Edit/IDistanceSnapProvider.cs +++ b/osu.Game/Rulesets/Edit/IDistanceSnapProvider.cs @@ -76,6 +76,8 @@ namespace osu.Game.Rulesets.Edit /// the beatmap's ,, /// the current beat divisor. /// + /// Note that the returned value does NOT depend on ; + /// consumers are expected to include that multiplier as they see fit. /// float FindSnappedDistance(float distance, double snapReferenceTime, IHasSliderVelocity? withVelocity = null); } diff --git a/osu.Game/Screens/Edit/Compose/Components/CircularDistanceSnapGrid.cs b/osu.Game/Screens/Edit/Compose/Components/CircularDistanceSnapGrid.cs index 9ddf54b779..164a209958 100644 --- a/osu.Game/Screens/Edit/Compose/Components/CircularDistanceSnapGrid.cs +++ b/osu.Game/Screens/Edit/Compose/Components/CircularDistanceSnapGrid.cs @@ -56,7 +56,7 @@ namespace osu.Game.Screens.Edit.Compose.Components // Picture the scenario where the user has just placed an object on a 1/2 snap, then changes to // 1/3 snap and expects to be able to place the next object on a valid 1/3 snap, regardless of the // fact that the 1/2 snap reference object is not valid for 1/3 snapping. - float offset = SnapProvider.FindSnappedDistance(0, StartTime, SliderVelocitySource); + float offset = (float)(SnapProvider.FindSnappedDistance(0, StartTime, SliderVelocitySource) * DistanceSpacingMultiplier.Value); for (int i = 0; i < requiredCircles; i++) { From d28ea7bfbf5a81e2d4a97966c3b04fc1e37729bc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 4 Feb 2025 12:30:36 +0100 Subject: [PATCH 0862/1275] Fix code quality --- .../Visual/Editing/TestSceneBeatmapSubmissionOverlay.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/osu.Game.Tests/Visual/Editing/TestSceneBeatmapSubmissionOverlay.cs b/osu.Game.Tests/Visual/Editing/TestSceneBeatmapSubmissionOverlay.cs index 07a794b7eb..e3e8c0de39 100644 --- a/osu.Game.Tests/Visual/Editing/TestSceneBeatmapSubmissionOverlay.cs +++ b/osu.Game.Tests/Visual/Editing/TestSceneBeatmapSubmissionOverlay.cs @@ -12,7 +12,6 @@ namespace osu.Game.Tests.Visual.Editing public partial class TestSceneBeatmapSubmissionOverlay : OsuTestScene { private ScreenFooter footer = null!; - private BeatmapSubmissionOverlay overlay = null!; [SetUpSteps] public void SetUpSteps() @@ -29,7 +28,7 @@ namespace osu.Game.Tests.Visual.Editing Children = new Drawable[] { receptor, - overlay = new BeatmapSubmissionOverlay() + new BeatmapSubmissionOverlay { State = { Value = Visibility.Visible, }, }, From a0b6610054d3385cf39ea43e6e4051e64b52eb3a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 24 Jan 2025 15:05:22 +0100 Subject: [PATCH 0863/1275] Always select the closest control point group regardless of whether it has a timing point --- osu.Game/Screens/Edit/Timing/ControlPointList.cs | 15 +++------------ osu.Game/Screens/Edit/Timing/TimingScreen.cs | 15 ++++++++++++--- 2 files changed, 15 insertions(+), 15 deletions(-) diff --git a/osu.Game/Screens/Edit/Timing/ControlPointList.cs b/osu.Game/Screens/Edit/Timing/ControlPointList.cs index 12c6390812..86d8ac681f 100644 --- a/osu.Game/Screens/Edit/Timing/ControlPointList.cs +++ b/osu.Game/Screens/Edit/Timing/ControlPointList.cs @@ -20,6 +20,8 @@ namespace osu.Game.Screens.Edit.Timing { public partial class ControlPointList : CompositeDrawable { + public Action? SelectClosestTimingPoint { get; init; } + private ControlPointTable table = null!; private Container controls = null!; private OsuButton deleteButton = null!; @@ -75,7 +77,7 @@ namespace osu.Game.Screens.Edit.Timing new RoundedButton { Text = "Select closest to current time", - Action = goToCurrentGroup, + Action = SelectClosestTimingPoint, Size = new Vector2(220, 30), Anchor = Anchor.CentreLeft, Origin = Anchor.CentreLeft, @@ -146,17 +148,6 @@ namespace osu.Game.Screens.Edit.Timing table.Padding = new MarginPadding { Bottom = controls.DrawHeight }; } - private void goToCurrentGroup() - { - double accurateTime = clock.CurrentTimeAccurate; - - var activeTimingPoint = Beatmap.ControlPointInfo.TimingPointAt(accurateTime); - var activeEffectPoint = Beatmap.ControlPointInfo.EffectPointAt(accurateTime); - - double latestActiveTime = Math.Max(activeTimingPoint.Time, activeEffectPoint.Time); - selectedGroup.Value = Beatmap.ControlPointInfo.GroupAt(latestActiveTime); - } - private void delete() { if (selectedGroup.Value == null) diff --git a/osu.Game/Screens/Edit/Timing/TimingScreen.cs b/osu.Game/Screens/Edit/Timing/TimingScreen.cs index cddde34aca..e2ef356808 100644 --- a/osu.Game/Screens/Edit/Timing/TimingScreen.cs +++ b/osu.Game/Screens/Edit/Timing/TimingScreen.cs @@ -1,6 +1,7 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; @@ -37,7 +38,10 @@ namespace osu.Game.Screens.Edit.Timing { new Drawable[] { - new ControlPointList(), + new ControlPointList + { + SelectClosestTimingPoint = selectClosestTimingPoint, + }, new ControlPointSettings(), }, } @@ -70,8 +74,13 @@ namespace osu.Game.Screens.Edit.Timing if (editorClock == null) return; - var nearestTimingPoint = EditorBeatmap.ControlPointInfo.TimingPointAt(editorClock.CurrentTime); - SelectedGroup.Value = EditorBeatmap.ControlPointInfo.GroupAt(nearestTimingPoint.Time); + double accurateTime = editorClock.CurrentTimeAccurate; + + var activeTimingPoint = EditorBeatmap.ControlPointInfo.TimingPointAt(accurateTime); + var activeEffectPoint = EditorBeatmap.ControlPointInfo.EffectPointAt(accurateTime); + + double latestActiveTime = Math.Max(activeTimingPoint.Time, activeEffectPoint.Time); + SelectedGroup.Value = EditorBeatmap.ControlPointInfo.GroupAt(latestActiveTime); } protected override void ConfigureTimeline(TimelineArea timelineArea) From 2dbf30a0965767f0c8be93d918abe59322910a6b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 4 Feb 2025 12:44:05 +0100 Subject: [PATCH 0864/1275] Select timing point on enter if no effect point is active at the time Noticed during testing. --- osu.Game/Screens/Edit/Timing/TimingScreen.cs | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/Edit/Timing/TimingScreen.cs b/osu.Game/Screens/Edit/Timing/TimingScreen.cs index e2ef356808..e7bf798298 100644 --- a/osu.Game/Screens/Edit/Timing/TimingScreen.cs +++ b/osu.Game/Screens/Edit/Timing/TimingScreen.cs @@ -79,8 +79,13 @@ namespace osu.Game.Screens.Edit.Timing var activeTimingPoint = EditorBeatmap.ControlPointInfo.TimingPointAt(accurateTime); var activeEffectPoint = EditorBeatmap.ControlPointInfo.EffectPointAt(accurateTime); - double latestActiveTime = Math.Max(activeTimingPoint.Time, activeEffectPoint.Time); - SelectedGroup.Value = EditorBeatmap.ControlPointInfo.GroupAt(latestActiveTime); + if (activeEffectPoint.Equals(EffectControlPoint.DEFAULT)) + SelectedGroup.Value = EditorBeatmap.ControlPointInfo.GroupAt(activeTimingPoint.Time); + else + { + double latestActiveTime = Math.Max(activeTimingPoint.Time, activeEffectPoint.Time); + SelectedGroup.Value = EditorBeatmap.ControlPointInfo.GroupAt(latestActiveTime); + } } protected override void ConfigureTimeline(TimelineArea timelineArea) From 386fb553923f37976bd2e8f53ce169cabfa0e170 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 4 Feb 2025 21:48:45 +0900 Subject: [PATCH 0865/1275] Update framework --- osu.Android.props | 2 +- osu.Game/osu.Game.csproj | 2 +- osu.iOS.props | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Android.props b/osu.Android.props index d2682fc024..6bbd432ee7 100644 --- a/osu.Android.props +++ b/osu.Android.props @@ -10,7 +10,7 @@ true - + diff --git a/osu.iOS.props b/osu.iOS.props index 309a9dcc87..ca2604858c 100644 --- a/osu.iOS.props +++ b/osu.iOS.props @@ -17,6 +17,6 @@ -all - + From 099ce3953127e075f73ee5b11d0f1307e012fe07 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 4 Feb 2025 23:21:41 +0900 Subject: [PATCH 0866/1275] Use same delay in context menus --- osu.Game/Graphics/UserInterface/OsuContextMenu.cs | 7 +++---- osu.Game/Graphics/UserInterface/OsuMenu.cs | 7 +++++-- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/osu.Game/Graphics/UserInterface/OsuContextMenu.cs b/osu.Game/Graphics/UserInterface/OsuContextMenu.cs index 433d37834f..e81d77ce43 100644 --- a/osu.Game/Graphics/UserInterface/OsuContextMenu.cs +++ b/osu.Game/Graphics/UserInterface/OsuContextMenu.cs @@ -12,8 +12,6 @@ namespace osu.Game.Graphics.UserInterface { public partial class OsuContextMenu : OsuMenu { - private const int fade_duration = 250; - [Resolved] private OsuMenuSamples menuSamples { get; set; } = null!; @@ -48,7 +46,7 @@ namespace osu.Game.Graphics.UserInterface protected override void AnimateOpen() { wasOpened = true; - this.FadeIn(fade_duration, Easing.OutQuint); + this.FadeIn(FADE_DURATION, Easing.OutQuint); if (!playClickSample) return; @@ -59,7 +57,8 @@ namespace osu.Game.Graphics.UserInterface protected override void AnimateClose() { - this.FadeOut(fade_duration, Easing.OutQuint); + this.Delay(DELAY_BEFORE_FADE_OUT) + .FadeOut(FADE_DURATION, Easing.OutQuint); if (wasOpened) menuSamples.PlayCloseSample(); diff --git a/osu.Game/Graphics/UserInterface/OsuMenu.cs b/osu.Game/Graphics/UserInterface/OsuMenu.cs index 9b099c0884..a75769b16b 100644 --- a/osu.Game/Graphics/UserInterface/OsuMenu.cs +++ b/osu.Game/Graphics/UserInterface/OsuMenu.cs @@ -18,6 +18,9 @@ namespace osu.Game.Graphics.UserInterface { public partial class OsuMenu : Menu { + protected const double DELAY_BEFORE_FADE_OUT = 50; + protected const double FADE_DURATION = 280; + // todo: this shouldn't be required after https://github.com/ppy/osu-framework/issues/4519 is fixed. private bool wasOpened; @@ -68,8 +71,8 @@ namespace osu.Game.Graphics.UserInterface if (!TopLevelMenu && wasOpened) menuSamples?.PlayCloseSample(); - this.Delay(50) - .FadeOut(300, Easing.OutQuint); + this.Delay(DELAY_BEFORE_FADE_OUT) + .FadeOut(FADE_DURATION, Easing.OutQuint); wasOpened = false; } From db7b665f4dc73d6250183285078734007f728e49 Mon Sep 17 00:00:00 2001 From: NecoDev <120387312+necocat0918@users.noreply.github.com> Date: Wed, 5 Feb 2025 13:31:57 +0800 Subject: [PATCH 0867/1275] Removed unused using For https://github.com/ppy/osu/pull/31780 --- osu.Game/Screens/Edit/Editor.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/osu.Game/Screens/Edit/Editor.cs b/osu.Game/Screens/Edit/Editor.cs index 8cffab87ea..1914aae13c 100644 --- a/osu.Game/Screens/Edit/Editor.cs +++ b/osu.Game/Screens/Edit/Editor.cs @@ -8,7 +8,6 @@ using System.Collections.Generic; using System.Linq; using System.Threading; using System.Threading.Tasks; -using DiffPlex.Model; using JetBrains.Annotations; using osu.Framework; using osu.Framework.Allocation; From fa844b0ebc783222beadd1e6889dada450823219 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 5 Feb 2025 15:01:59 +0900 Subject: [PATCH 0868/1275] Rename `Colour` / `Rhythm` related fields and classes --- .../Difficulty/Evaluators/ColourEvaluator.cs | 18 +++++----- .../Difficulty/Evaluators/RhythmEvaluator.cs | 12 +++---- .../Difficulty/Evaluators/StaminaEvaluator.cs | 4 +-- ...yHitObjectColour.cs => TaikoColourData.cs} | 2 +- .../TaikoColourDifficultyPreprocessor.cs | 10 +++--- ...yHitObjectRhythm.cs => TaikoRhythmData.cs} | 35 +++++++++---------- .../TaikoRhythmDifficultyPreprocessor.cs | 4 +-- .../Preprocessing/TaikoDifficultyHitObject.cs | 8 ++--- .../Difficulty/Skills/Reading.cs | 2 +- .../Difficulty/Skills/Stamina.cs | 2 +- 10 files changed, 48 insertions(+), 49 deletions(-) rename osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Colour/{TaikoDifficultyHitObjectColour.cs => TaikoColourData.cs} (96%) rename osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/{TaikoDifficultyHitObjectRhythm.cs => TaikoRhythmData.cs} (75%) diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Evaluators/ColourEvaluator.cs b/osu.Game.Rulesets.Taiko/Difficulty/Evaluators/ColourEvaluator.cs index 166c01f507..b715dfc37a 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Evaluators/ColourEvaluator.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Evaluators/ColourEvaluator.cs @@ -34,8 +34,8 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Evaluators var previousHitObject = (TaikoDifficultyHitObject)current.Previous(1); - double currentRatio = current.Rhythm.Ratio; - double previousRatio = previousHitObject.Rhythm.Ratio; + double currentRatio = current.RhythmData.Ratio; + double previousRatio = previousHitObject.RhythmData.Ratio; // A consistent interval is defined as the percentage difference between the two rhythmic ratios with the margin of error. if (Math.Abs(1 - currentRatio / previousRatio) <= threshold) @@ -61,17 +61,17 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Evaluators public static double EvaluateDifficultyOf(DifficultyHitObject hitObject) { var taikoObject = (TaikoDifficultyHitObject)hitObject; - TaikoDifficultyHitObjectColour colour = taikoObject.Colour; + TaikoColourData colourData = taikoObject.ColourData; double difficulty = 0.0d; - if (colour.MonoStreak?.FirstHitObject == hitObject) // Difficulty for MonoStreak - difficulty += evaluateMonoStreakDifficulty(colour.MonoStreak); + if (colourData.MonoStreak?.FirstHitObject == hitObject) // Difficulty for MonoStreak + difficulty += evaluateMonoStreakDifficulty(colourData.MonoStreak); - if (colour.AlternatingMonoPattern?.FirstHitObject == hitObject) // Difficulty for AlternatingMonoPattern - difficulty += evaluateAlternatingMonoPatternDifficulty(colour.AlternatingMonoPattern); + if (colourData.AlternatingMonoPattern?.FirstHitObject == hitObject) // Difficulty for AlternatingMonoPattern + difficulty += evaluateAlternatingMonoPatternDifficulty(colourData.AlternatingMonoPattern); - if (colour.RepeatingHitPattern?.FirstHitObject == hitObject) // Difficulty for RepeatingHitPattern - difficulty += evaluateRepeatingHitPatternsDifficulty(colour.RepeatingHitPattern); + if (colourData.RepeatingHitPattern?.FirstHitObject == hitObject) // Difficulty for RepeatingHitPattern + difficulty += evaluateRepeatingHitPatternsDifficulty(colourData.RepeatingHitPattern); double consistencyPenalty = consistentRatioPenalty(taikoObject); difficulty *= consistencyPenalty; diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Evaluators/RhythmEvaluator.cs b/osu.Game.Rulesets.Taiko/Difficulty/Evaluators/RhythmEvaluator.cs index f4686f2fe3..3b3aea07f3 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Evaluators/RhythmEvaluator.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Evaluators/RhythmEvaluator.cs @@ -18,21 +18,21 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Evaluators /// public static double EvaluateDifficultyOf(DifficultyHitObject hitObject, double hitWindow) { - TaikoDifficultyHitObjectRhythm rhythm = ((TaikoDifficultyHitObject)hitObject).Rhythm; + TaikoRhythmData rhythmData = ((TaikoDifficultyHitObject)hitObject).RhythmData; double difficulty = 0.0d; double sameRhythm = 0; double samePattern = 0; double intervalPenalty = 0; - if (rhythm.SameRhythmGroupedHitObjects?.FirstHitObject == hitObject) // Difficulty for SameRhythmGroupedHitObjects + if (rhythmData.SameRhythmGroupedHitObjects?.FirstHitObject == hitObject) // Difficulty for SameRhythmGroupedHitObjects { - sameRhythm += 10.0 * evaluateDifficultyOf(rhythm.SameRhythmGroupedHitObjects, hitWindow); - intervalPenalty = repeatedIntervalPenalty(rhythm.SameRhythmGroupedHitObjects, hitWindow); + sameRhythm += 10.0 * evaluateDifficultyOf(rhythmData.SameRhythmGroupedHitObjects, hitWindow); + intervalPenalty = repeatedIntervalPenalty(rhythmData.SameRhythmGroupedHitObjects, hitWindow); } - if (rhythm.SamePatternsGroupedHitObjects?.FirstHitObject == hitObject) // Difficulty for SamePatternsGroupedHitObjects - samePattern += 1.15 * ratioDifficulty(rhythm.SamePatternsGroupedHitObjects.IntervalRatio); + if (rhythmData.SamePatternsGroupedHitObjects?.FirstHitObject == hitObject) // Difficulty for SamePatternsGroupedHitObjects + samePattern += 1.15 * ratioDifficulty(rhythmData.SamePatternsGroupedHitObjects.IntervalRatio); difficulty += Math.Max(sameRhythm, samePattern) * intervalPenalty; diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Evaluators/StaminaEvaluator.cs b/osu.Game.Rulesets.Taiko/Difficulty/Evaluators/StaminaEvaluator.cs index a9884b2328..32ed8ec189 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Evaluators/StaminaEvaluator.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Evaluators/StaminaEvaluator.cs @@ -55,8 +55,8 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Evaluators /// private static int availableFingersFor(TaikoDifficultyHitObject hitObject) { - DifficultyHitObject? previousColourChange = hitObject.Colour.PreviousColourChange; - DifficultyHitObject? nextColourChange = hitObject.Colour.NextColourChange; + DifficultyHitObject? previousColourChange = hitObject.ColourData.PreviousColourChange; + DifficultyHitObject? nextColourChange = hitObject.ColourData.NextColourChange; if (previousColourChange != null && hitObject.StartTime - previousColourChange.StartTime < 300) { diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Colour/TaikoDifficultyHitObjectColour.cs b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Colour/TaikoColourData.cs similarity index 96% rename from osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Colour/TaikoDifficultyHitObjectColour.cs rename to osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Colour/TaikoColourData.cs index abf6fb3672..81201b6584 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Colour/TaikoDifficultyHitObjectColour.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Colour/TaikoColourData.cs @@ -8,7 +8,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Colour /// /// Stores colour compression information for a . /// - public class TaikoDifficultyHitObjectColour + public class TaikoColourData { /// /// The that encodes this note. diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Colour/TaikoColourDifficultyPreprocessor.cs b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Colour/TaikoColourDifficultyPreprocessor.cs index 18a299ae92..3c6ef7c53c 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Colour/TaikoColourDifficultyPreprocessor.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Colour/TaikoColourDifficultyPreprocessor.cs @@ -14,8 +14,8 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Colour public static class TaikoColourDifficultyPreprocessor { /// - /// Processes and encodes a list of s into a list of s, - /// assigning the appropriate s to each . + /// Processes and encodes a list of s into a list of s, + /// assigning the appropriate s to each . /// public static void ProcessAndAssign(List hitObjects) { @@ -41,9 +41,9 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Colour foreach (var hitObject in monoStreak.HitObjects) { - hitObject.Colour.RepeatingHitPattern = repeatingHitPattern; - hitObject.Colour.AlternatingMonoPattern = monoPattern; - hitObject.Colour.MonoStreak = monoStreak; + hitObject.ColourData.RepeatingHitPattern = repeatingHitPattern; + hitObject.ColourData.AlternatingMonoPattern = monoPattern; + hitObject.ColourData.MonoStreak = monoStreak; } } } diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/TaikoDifficultyHitObjectRhythm.cs b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/TaikoRhythmData.cs similarity index 75% rename from osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/TaikoDifficultyHitObjectRhythm.cs rename to osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/TaikoRhythmData.cs index 3503a836fa..d895dcfc55 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/TaikoDifficultyHitObjectRhythm.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/TaikoRhythmData.cs @@ -10,7 +10,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Rhythm /// /// Stores rhythm data for a . /// - public class TaikoDifficultyHitObjectRhythm + public class TaikoRhythmData { /// /// The group of hit objects with consistent rhythm that this object belongs to. @@ -39,25 +39,25 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Rhythm /// speeding up is generally harder than slowing down (with exceptions of rhythm changes requiring a hand switch). /// /// - private static readonly TaikoDifficultyHitObjectRhythm[] common_rhythms = + private static readonly TaikoRhythmData[] common_rhythms = { - new TaikoDifficultyHitObjectRhythm(1, 1), - new TaikoDifficultyHitObjectRhythm(2, 1), - new TaikoDifficultyHitObjectRhythm(1, 2), - new TaikoDifficultyHitObjectRhythm(3, 1), - new TaikoDifficultyHitObjectRhythm(1, 3), - new TaikoDifficultyHitObjectRhythm(3, 2), - new TaikoDifficultyHitObjectRhythm(2, 3), - new TaikoDifficultyHitObjectRhythm(5, 4), - new TaikoDifficultyHitObjectRhythm(4, 5) + new TaikoRhythmData(1, 1), + new TaikoRhythmData(2, 1), + new TaikoRhythmData(1, 2), + new TaikoRhythmData(3, 1), + new TaikoRhythmData(1, 3), + new TaikoRhythmData(3, 2), + new TaikoRhythmData(2, 3), + new TaikoRhythmData(5, 4), + new TaikoRhythmData(4, 5) }; /// - /// Initialises a new instance of s, + /// Initialises a new instance of s, /// calculating the closest rhythm change and its associated difficulty for the current hit object. /// /// The current being processed. - public TaikoDifficultyHitObjectRhythm(TaikoDifficultyHitObject current) + public TaikoRhythmData(TaikoDifficultyHitObject current) { var previous = current.Previous(0); @@ -67,8 +67,8 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Rhythm return; } - TaikoDifficultyHitObjectRhythm closestRhythm = getClosestRhythm(current.DeltaTime, previous.DeltaTime); - Ratio = closestRhythm.Ratio; + TaikoRhythmData closestRhythmData = getClosestRhythm(current.DeltaTime, previous.DeltaTime); + Ratio = closestRhythmData.Ratio; } /// @@ -76,7 +76,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Rhythm /// /// The numerator for . /// The denominator for - private TaikoDifficultyHitObjectRhythm(int numerator, int denominator) + private TaikoRhythmData(int numerator, int denominator) { Ratio = numerator / (double)denominator; } @@ -88,11 +88,10 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Rhythm /// The time difference between the current hit object and the previous one. /// The time difference between the previous hit object and the one before it. /// The closest matching rhythm from . - private TaikoDifficultyHitObjectRhythm getClosestRhythm(double currentDeltaTime, double previousDeltaTime) + private TaikoRhythmData getClosestRhythm(double currentDeltaTime, double previousDeltaTime) { double ratio = currentDeltaTime / previousDeltaTime; return common_rhythms.OrderBy(x => Math.Abs(x.Ratio - ratio)).First(); } } } - diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/TaikoRhythmDifficultyPreprocessor.cs b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/TaikoRhythmDifficultyPreprocessor.cs index 3ebc0c25b7..45cc29c99e 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/TaikoRhythmDifficultyPreprocessor.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/TaikoRhythmDifficultyPreprocessor.cs @@ -17,7 +17,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Rhythm { foreach (var hitObject in rhythmGroup.HitObjects) { - hitObject.Rhythm.SameRhythmGroupedHitObjects = rhythmGroup; + hitObject.RhythmData.SameRhythmGroupedHitObjects = rhythmGroup; hitObject.HitObjectInterval = rhythmGroup.HitObjectInterval; } } @@ -28,7 +28,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Rhythm { foreach (var hitObject in patternGroup.AllHitObjects) { - hitObject.Rhythm.SamePatternsGroupedHitObjects = patternGroup; + hitObject.RhythmData.SamePatternsGroupedHitObjects = patternGroup; } } } diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/TaikoDifficultyHitObject.cs b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/TaikoDifficultyHitObject.cs index d6a2d5874e..5c5503c25d 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/TaikoDifficultyHitObject.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/TaikoDifficultyHitObject.cs @@ -41,7 +41,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing /// /// The rhythm required to hit this hit object. /// - public readonly TaikoDifficultyHitObjectRhythm Rhythm; + public readonly TaikoRhythmData RhythmData; /// /// The interval between this hit object and the surrounding hit objects in its rhythm group. @@ -52,7 +52,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing /// Colour data for this hit object. This is used by colour evaluator to calculate colour difficulty, but can be used /// by other skills in the future. /// - public readonly TaikoDifficultyHitObjectColour Colour; + public readonly TaikoColourData ColourData; /// /// The adjusted BPM of this hit object, based on its slider velocity and scroll speed. @@ -92,10 +92,10 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing noteDifficultyHitObjects = noteObjects; // Create the Colour object, its properties should be filled in by TaikoDifficultyPreprocessor - Colour = new TaikoDifficultyHitObjectColour(); + ColourData = new TaikoColourData(); // Create a Rhythm object, its properties are filled in by TaikoDifficultyHitObjectRhythm - Rhythm = new TaikoDifficultyHitObjectRhythm(this); + RhythmData = new TaikoRhythmData(this); switch ((hitObject as Hit)?.Type) { diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Skills/Reading.cs b/osu.Game.Rulesets.Taiko/Difficulty/Skills/Reading.cs index 885131404a..7be1107b70 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Skills/Reading.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Skills/Reading.cs @@ -35,7 +35,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Skills } var taikoObject = (TaikoDifficultyHitObject)current; - int index = taikoObject.Colour.MonoStreak?.HitObjects.IndexOf(taikoObject) ?? 0; + int index = taikoObject.ColourData.MonoStreak?.HitObjects.IndexOf(taikoObject) ?? 0; currentStrain *= DifficultyCalculationUtils.Logistic(index, 4, -1 / 25.0, 0.5) + 0.5; diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Skills/Stamina.cs b/osu.Game.Rulesets.Taiko/Difficulty/Skills/Stamina.cs index 12e1396dd7..0e1f3d41cf 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Skills/Stamina.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Skills/Stamina.cs @@ -46,7 +46,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Skills // Safely prevents previous strains from shifting as new notes are added. var currentObject = current as TaikoDifficultyHitObject; - int index = currentObject?.Colour.MonoStreak?.HitObjects.IndexOf(currentObject) ?? 0; + int index = currentObject?.ColourData.MonoStreak?.HitObjects.IndexOf(currentObject) ?? 0; double monolengthBonus = isConvert ? 1 : 1 + Math.Min(Math.Max((index - 5) / 50.0, 0), 0.30); From 709ad02a517606b07b6a4aaf3f55e611a94219c9 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 5 Feb 2025 15:09:51 +0900 Subject: [PATCH 0869/1275] Simplify `TaikoRhythmData`'s ratio computation --- .../Preprocessing/Rhythm/TaikoRhythmData.cs | 68 +++++++------------ 1 file changed, 25 insertions(+), 43 deletions(-) diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/TaikoRhythmData.cs b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/TaikoRhythmData.cs index d895dcfc55..6c4a332624 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/TaikoRhythmData.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/TaikoRhythmData.cs @@ -27,30 +27,10 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Rhythm /// to previous for the rhythm change. /// A above 1 indicates a slow-down; a below 1 indicates a speed-up. /// - public readonly double Ratio; - - /// - /// List of most common rhythm changes in taiko maps. Based on how each object's interval compares to the previous object. - /// /// - /// The general guidelines for the values are: - /// - /// rhythm changes with ratio closer to 1 (that are not 1) are harder to play, - /// speeding up is generally harder than slowing down (with exceptions of rhythm changes requiring a hand switch). - /// + /// This is snapped to the closest matching . /// - private static readonly TaikoRhythmData[] common_rhythms = - { - new TaikoRhythmData(1, 1), - new TaikoRhythmData(2, 1), - new TaikoRhythmData(1, 2), - new TaikoRhythmData(3, 1), - new TaikoRhythmData(1, 3), - new TaikoRhythmData(3, 2), - new TaikoRhythmData(2, 3), - new TaikoRhythmData(5, 4), - new TaikoRhythmData(4, 5) - }; + public readonly double Ratio; /// /// Initialises a new instance of s, @@ -67,31 +47,33 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Rhythm return; } - TaikoRhythmData closestRhythmData = getClosestRhythm(current.DeltaTime, previous.DeltaTime); - Ratio = closestRhythmData.Ratio; + double actualRatio = current.DeltaTime / previous.DeltaTime; + double closestRatio = common_ratios.OrderBy(r => Math.Abs(r - actualRatio)).First(); + + Ratio = closestRatio; } /// - /// Creates an object representing a rhythm change. + /// List of most common rhythm changes in taiko maps. Based on how each object's interval compares to the previous object. /// - /// The numerator for . - /// The denominator for - private TaikoRhythmData(int numerator, int denominator) + /// + /// The general guidelines for the values are: + /// + /// rhythm changes with ratio closer to 1 (that are not 1) are harder to play, + /// speeding up is generally harder than slowing down (with exceptions of rhythm changes requiring a hand switch). + /// + /// + private static readonly double[] common_ratios = new[] { - Ratio = numerator / (double)denominator; - } - - /// - /// Determines the closest rhythm change from that matches the timing ratio - /// between the current and previous intervals. - /// - /// The time difference between the current hit object and the previous one. - /// The time difference between the previous hit object and the one before it. - /// The closest matching rhythm from . - private TaikoRhythmData getClosestRhythm(double currentDeltaTime, double previousDeltaTime) - { - double ratio = currentDeltaTime / previousDeltaTime; - return common_rhythms.OrderBy(x => Math.Abs(x.Ratio - ratio)).First(); - } + 1.0 / 1, + 2.0 / 1, + 1.0 / 2, + 3.0 / 1, + 1.0 / 3, + 3.0 / 2, + 2.0 / 3, + 5.0 / 4, + 4.0 / 5 + }; } } From fc933902844ce21ffa6961920dd96bbe47d94fa1 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 5 Feb 2025 15:10:15 +0900 Subject: [PATCH 0870/1275] Remove unused `HitObjectInterval` --- .../Rhythm/TaikoRhythmDifficultyPreprocessor.cs | 5 ----- .../Difficulty/Preprocessing/TaikoDifficultyHitObject.cs | 5 ----- 2 files changed, 10 deletions(-) diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/TaikoRhythmDifficultyPreprocessor.cs b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/TaikoRhythmDifficultyPreprocessor.cs index 45cc29c99e..8b126f85ce 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/TaikoRhythmDifficultyPreprocessor.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/TaikoRhythmDifficultyPreprocessor.cs @@ -16,10 +16,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Rhythm foreach (var rhythmGroup in rhythmGroups) { foreach (var hitObject in rhythmGroup.HitObjects) - { hitObject.RhythmData.SameRhythmGroupedHitObjects = rhythmGroup; - hitObject.HitObjectInterval = rhythmGroup.HitObjectInterval; - } } var patternGroups = createSamePatternGroupedHitObjects(rhythmGroups); @@ -27,9 +24,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Rhythm foreach (var patternGroup in patternGroups) { foreach (var hitObject in patternGroup.AllHitObjects) - { hitObject.RhythmData.SamePatternsGroupedHitObjects = patternGroup; - } } } diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/TaikoDifficultyHitObject.cs b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/TaikoDifficultyHitObject.cs index 5c5503c25d..489b36b259 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/TaikoDifficultyHitObject.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/TaikoDifficultyHitObject.cs @@ -43,11 +43,6 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing /// public readonly TaikoRhythmData RhythmData; - /// - /// The interval between this hit object and the surrounding hit objects in its rhythm group. - /// - public double? HitObjectInterval { get; set; } - /// /// Colour data for this hit object. This is used by colour evaluator to calculate colour difficulty, but can be used /// by other skills in the future. From 325483192a26f41d7019c4cf28c22fe91da1f1e3 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 5 Feb 2025 15:13:04 +0900 Subject: [PATCH 0871/1275] Tidy up xmldoc and remove another unused field --- .../Preprocessing/TaikoDifficultyHitObject.cs | 52 ++++++++----------- .../Difficulty/TaikoDifficultyCalculator.cs | 1 - .../Difficulty/Utils/IHasInterval.cs | 2 +- 3 files changed, 23 insertions(+), 32 deletions(-) diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/TaikoDifficultyHitObject.cs b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/TaikoDifficultyHitObject.cs index 489b36b259..f407e13ff1 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/TaikoDifficultyHitObject.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/TaikoDifficultyHitObject.cs @@ -6,6 +6,7 @@ using System.Linq; using osu.Game.Beatmaps.ControlPoints; using osu.Game.Rulesets.Difficulty.Preprocessing; using osu.Game.Rulesets.Objects; +using osu.Game.Rulesets.Taiko.Difficulty.Evaluators; using osu.Game.Rulesets.Taiko.Objects; using osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Colour; using osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Rhythm; @@ -39,13 +40,14 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing public readonly int NoteIndex; /// - /// The rhythm required to hit this hit object. + /// Rhythm data used by . + /// This is populated via . /// public readonly TaikoRhythmData RhythmData; /// - /// Colour data for this hit object. This is used by colour evaluator to calculate colour difficulty, but can be used - /// by other skills in the future. + /// Colour data used by and . + /// This is populated via . /// public readonly TaikoColourData ColourData; @@ -54,19 +56,11 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing /// public double EffectiveBPM; - /// - /// The current slider velocity of this hit object. - /// - public double CurrentSliderVelocity; - - public double Interval => DeltaTime; - /// /// Creates a new difficulty hit object. /// /// The gameplay associated with this difficulty object. /// The gameplay preceding . - /// The gameplay preceding . /// The rate of the gameplay clock. Modified by speed-changing mods. /// The list of all s in the current beatmap. /// The list of centre (don) s in the current beatmap. @@ -75,7 +69,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing /// The position of this in the list. /// The control point info of the beatmap. /// The global slider velocity of the beatmap. - public TaikoDifficultyHitObject(HitObject hitObject, HitObject lastObject, HitObject lastLastObject, double clockRate, + public TaikoDifficultyHitObject(HitObject hitObject, HitObject lastObject, double clockRate, List objects, List centreHitObjects, List rimHitObjects, @@ -86,29 +80,26 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing { noteDifficultyHitObjects = noteObjects; - // Create the Colour object, its properties should be filled in by TaikoDifficultyPreprocessor ColourData = new TaikoColourData(); - - // Create a Rhythm object, its properties are filled in by TaikoDifficultyHitObjectRhythm RhythmData = new TaikoRhythmData(this); - switch ((hitObject as Hit)?.Type) + if (hitObject is Hit hit) { - case HitType.Centre: - MonoIndex = centreHitObjects.Count; - centreHitObjects.Add(this); - monoDifficultyHitObjects = centreHitObjects; - break; + switch (hit.Type) + { + case HitType.Centre: + MonoIndex = centreHitObjects.Count; + centreHitObjects.Add(this); + monoDifficultyHitObjects = centreHitObjects; + break; - case HitType.Rim: - MonoIndex = rimHitObjects.Count; - rimHitObjects.Add(this); - monoDifficultyHitObjects = rimHitObjects; - break; - } + case HitType.Rim: + MonoIndex = rimHitObjects.Count; + rimHitObjects.Add(this); + monoDifficultyHitObjects = rimHitObjects; + break; + } - if (hitObject is Hit) - { NoteIndex = noteObjects.Count; noteObjects.Add(this); } @@ -121,7 +112,6 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing // Calculate the slider velocity at the note's start time. double currentSliderVelocity = calculateSliderVelocity(controlPointInfo, globalSliderVelocity, normalisedStartTime, clockRate); - CurrentSliderVelocity = currentSliderVelocity; EffectiveBPM = currentControlPoint.BPM * currentSliderVelocity; } @@ -142,5 +132,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing public TaikoDifficultyHitObject? PreviousNote(int backwardsIndex) => noteDifficultyHitObjects.ElementAtOrDefault(NoteIndex - (backwardsIndex + 1)); public TaikoDifficultyHitObject? NextNote(int forwardsIndex) => noteDifficultyHitObjects.ElementAtOrDefault(NoteIndex + (forwardsIndex + 1)); + + public double Interval => DeltaTime; } } diff --git a/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs b/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs index acd654f9b8..6b9986bd68 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs @@ -78,7 +78,6 @@ namespace osu.Game.Rulesets.Taiko.Difficulty difficultyHitObjects.Add(new TaikoDifficultyHitObject( beatmap.HitObjects[i], beatmap.HitObjects[i - 1], - beatmap.HitObjects[i - 2], clockRate, difficultyHitObjects, centreObjects, diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Utils/IHasInterval.cs b/osu.Game.Rulesets.Taiko/Difficulty/Utils/IHasInterval.cs index 8f80bb6079..a42940180c 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Utils/IHasInterval.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Utils/IHasInterval.cs @@ -9,7 +9,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Utils public interface IHasInterval { /// - /// The interval between 2 objects start times. + /// The interval – ie delta time – between this object and a known previous object. /// double Interval { get; } } From 8447679db9f038b5ddfefbe7337d87ea38000c22 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 5 Feb 2025 15:41:31 +0900 Subject: [PATCH 0872/1275] Initial tidy-up pass on `IntervalGroupingUtils` --- .../TaikoRhythmDifficultyPreprocessor.cs | 17 +++----- .../Difficulty/Utils/IntervalGroupingUtils.cs | 41 ++++++++----------- 2 files changed, 23 insertions(+), 35 deletions(-) diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/TaikoRhythmDifficultyPreprocessor.cs b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/TaikoRhythmDifficultyPreprocessor.cs index 8b126f85ce..5bc0fdbc03 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/TaikoRhythmDifficultyPreprocessor.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/TaikoRhythmDifficultyPreprocessor.cs @@ -2,6 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using System.Collections.Generic; +using System.Linq; using osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Rhythm.Data; using osu.Game.Rulesets.Taiko.Difficulty.Utils; @@ -31,13 +32,9 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Rhythm private static List createSameRhythmGroupedHitObjects(List hitObjects) { var rhythmGroups = new List(); - var groups = IntervalGroupingUtils.GroupByInterval(hitObjects); - foreach (var group in groups) - { - var previous = rhythmGroups.Count > 0 ? rhythmGroups[^1] : null; - rhythmGroups.Add(new SameRhythmHitObjectGrouping(previous, group)); - } + foreach (var grouped in IntervalGroupingUtils.GroupByInterval(hitObjects)) + rhythmGroups.Add(new SameRhythmHitObjectGrouping(rhythmGroups.LastOrDefault(), grouped)); return rhythmGroups; } @@ -45,13 +42,9 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Rhythm private static List createSamePatternGroupedHitObjects(List rhythmGroups) { var patternGroups = new List(); - var groups = IntervalGroupingUtils.GroupByInterval(rhythmGroups); - foreach (var group in groups) - { - var previous = patternGroups.Count > 0 ? patternGroups[^1] : null; - patternGroups.Add(new SamePatternsGroupedHitObjects(previous, group)); - } + foreach (var grouped in IntervalGroupingUtils.GroupByInterval(rhythmGroups)) + patternGroups.Add(new SamePatternsGroupedHitObjects(patternGroups.LastOrDefault(), grouped)); return patternGroups; } diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Utils/IntervalGroupingUtils.cs b/osu.Game.Rulesets.Taiko/Difficulty/Utils/IntervalGroupingUtils.cs index 3b6f5406b4..f04dec1c08 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Utils/IntervalGroupingUtils.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Utils/IntervalGroupingUtils.cs @@ -8,56 +8,51 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Utils { public static class IntervalGroupingUtils { - public static List> GroupByInterval(IReadOnlyList data, double marginOfError = 5) where T : IHasInterval + public static List> GroupByInterval(IReadOnlyList objects) where T : IHasInterval { var groups = new List>(); - if (data.Count == 0) - return groups; int i = 0; - - while (i < data.Count) - { - var group = createGroup(data, ref i, marginOfError); - groups.Add(group); - } + while (i < objects.Count) + groups.Add(createNextGroup(objects, ref i)); return groups; } - private static List createGroup(IReadOnlyList data, ref int i, double marginOfError) where T : IHasInterval + private static List createNextGroup(IReadOnlyList objects, ref int i) where T : IHasInterval { - var children = new List { data[i] }; + const double margin_of_error = 5; + + var groupedObjects = new List { objects[i] }; i++; - for (; i < data.Count - 1; i++) + for (; i < objects.Count - 1; i++) { - // An interval change occured, add the current data if the next interval is larger. - if (!Precision.AlmostEquals(data[i].Interval, data[i + 1].Interval, marginOfError)) + // An interval change occured, add the current object if the next interval is larger. + if (!Precision.AlmostEquals(objects[i].Interval, objects[i + 1].Interval, margin_of_error)) { - if (data[i + 1].Interval > data[i].Interval + marginOfError) + if (objects[i + 1].Interval > objects[i].Interval + margin_of_error) { - children.Add(data[i]); + groupedObjects.Add(objects[i]); i++; } - return children; + return groupedObjects; } // No interval change occurred - children.Add(data[i]); + groupedObjects.Add(objects[i]); } - // Check if the last two objects in the data form a "flat" rhythm pattern within the specified margin of error. + // Check if the last two objects in the object form a "flat" rhythm pattern within the specified margin of error. // If true, add the current object to the group and increment the index to process the next object. - if (data.Count > 2 && i < data.Count && - Precision.AlmostEquals(data[^1].Interval, data[^2].Interval, marginOfError)) + if (objects.Count > 2 && i < objects.Count && Precision.AlmostEquals(objects[^1].Interval, objects[^2].Interval, margin_of_error)) { - children.Add(data[i]); + groupedObjects.Add(objects[i]); i++; } - return children; + return groupedObjects; } } } From 09d26fbf5ed006339da279ca449d9f87dd5ba961 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 5 Feb 2025 15:58:34 +0900 Subject: [PATCH 0873/1275] Minor adjustments --- osu.Game/Graphics/UserInterfaceV2/FormButton.cs | 2 +- osu.Game/Localisation/BeatmapSubmissionStrings.cs | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Game/Graphics/UserInterfaceV2/FormButton.cs b/osu.Game/Graphics/UserInterfaceV2/FormButton.cs index fec855153b..1c5d4b5d80 100644 --- a/osu.Game/Graphics/UserInterfaceV2/FormButton.cs +++ b/osu.Game/Graphics/UserInterfaceV2/FormButton.cs @@ -148,7 +148,7 @@ namespace osu.Game.Graphics.UserInterfaceV2 { base.LoadComplete(); - Content.CornerRadius = 2; + Content.CornerRadius = 4; Add(triangles = new TrianglesV2 { diff --git a/osu.Game/Localisation/BeatmapSubmissionStrings.cs b/osu.Game/Localisation/BeatmapSubmissionStrings.cs index 85fe922703..a4c2b36894 100644 --- a/osu.Game/Localisation/BeatmapSubmissionStrings.cs +++ b/osu.Game/Localisation/BeatmapSubmissionStrings.cs @@ -90,9 +90,9 @@ namespace osu.Game.Localisation public static LocalisableString ModdingQueuesForumDescription => new TranslatableString(getKey(@"modding_queues_forum_description"), @"Having trouble getting feedback? Why not ask in a mod queue!"); /// - /// "Where would you like to post your map?" + /// "Where would you like to post your beatmap?" /// - public static LocalisableString BeatmapSubmissionTargetCaption => new TranslatableString(getKey(@"beatmap_submission_target_caption"), @"Where would you like to post your map?"); + public static LocalisableString BeatmapSubmissionTargetCaption => new TranslatableString(getKey(@"beatmap_submission_target_caption"), @"Where would you like to post your beatmap?"); /// /// "Works in Progress / Help (incomplete, not ready for ranking)" From 2356d3e2d0c02c8e12fb27b8ef1b3b5766d9a5e0 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 5 Feb 2025 16:24:13 +0900 Subject: [PATCH 0874/1275] Refactor `OsuContextMenu` to avoid code duplication --- .../Graphics/UserInterface/OsuContextMenu.cs | 33 ++++--------------- osu.Game/Graphics/UserInterface/OsuMenu.cs | 26 ++++++++++----- 2 files changed, 23 insertions(+), 36 deletions(-) diff --git a/osu.Game/Graphics/UserInterface/OsuContextMenu.cs b/osu.Game/Graphics/UserInterface/OsuContextMenu.cs index e81d77ce43..72ffde3574 100644 --- a/osu.Game/Graphics/UserInterface/OsuContextMenu.cs +++ b/osu.Game/Graphics/UserInterface/OsuContextMenu.cs @@ -15,12 +15,8 @@ namespace osu.Game.Graphics.UserInterface [Resolved] private OsuMenuSamples menuSamples { get; set; } = null!; - // todo: this shouldn't be required after https://github.com/ppy/osu-framework/issues/4519 is fixed. - private bool wasOpened; - private readonly bool playClickSample; - - public OsuContextMenu(bool playClickSample = false) - : base(Direction.Vertical) + public OsuContextMenu(bool playSamples) + : base(Direction.Vertical, topLevelMenu: false, playSamples) { MaskingContainer.CornerRadius = 5; MaskingContainer.EdgeEffect = new EdgeEffectParameters @@ -33,8 +29,6 @@ namespace osu.Game.Graphics.UserInterface ItemsContainer.Padding = new MarginPadding { Vertical = DrawableOsuMenuItem.MARGIN_VERTICAL }; MaxHeight = 250; - - this.playClickSample = playClickSample; } [BackgroundDependencyLoader] @@ -45,27 +39,12 @@ namespace osu.Game.Graphics.UserInterface protected override void AnimateOpen() { - wasOpened = true; - this.FadeIn(FADE_DURATION, Easing.OutQuint); + if (PlaySamples && !WasOpened) + menuSamples.PlayClickSample(); - if (!playClickSample) - return; - - menuSamples.PlayClickSample(); - menuSamples.PlayOpenSample(); + base.AnimateOpen(); } - protected override void AnimateClose() - { - this.Delay(DELAY_BEFORE_FADE_OUT) - .FadeOut(FADE_DURATION, Easing.OutQuint); - - if (wasOpened) - menuSamples.PlayCloseSample(); - - wasOpened = false; - } - - protected override Menu CreateSubMenu() => new OsuContextMenu(); + protected override Menu CreateSubMenu() => new OsuContextMenu(false); // sub menu samples are handled by OsuMenu.OnSubmenuOpen. } } diff --git a/osu.Game/Graphics/UserInterface/OsuMenu.cs b/osu.Game/Graphics/UserInterface/OsuMenu.cs index a75769b16b..11d9000dfa 100644 --- a/osu.Game/Graphics/UserInterface/OsuMenu.cs +++ b/osu.Game/Graphics/UserInterface/OsuMenu.cs @@ -22,20 +22,28 @@ namespace osu.Game.Graphics.UserInterface protected const double FADE_DURATION = 280; // todo: this shouldn't be required after https://github.com/ppy/osu-framework/issues/4519 is fixed. - private bool wasOpened; + protected bool WasOpened { get; private set; } + + public bool PlaySamples { get; } [Resolved] private OsuMenuSamples menuSamples { get; set; } = null!; public OsuMenu(Direction direction, bool topLevelMenu = false) + : this(direction, topLevelMenu, playSamples: !topLevelMenu) + { + } + + protected OsuMenu(Direction direction, bool topLevelMenu, bool playSamples) : base(direction, topLevelMenu) { + PlaySamples = playSamples; BackgroundColour = Color4.Black.Opacity(0.5f); MaskingContainer.CornerRadius = 4; ItemsContainer.Padding = new MarginPadding(5); - OnSubmenuOpen += _ => { menuSamples?.PlaySubOpenSample(); }; + OnSubmenuOpen += _ => menuSamples?.PlaySubOpenSample(); } protected override void Update() @@ -59,22 +67,22 @@ namespace osu.Game.Graphics.UserInterface protected override void AnimateOpen() { - if (!TopLevelMenu && !wasOpened) + if (PlaySamples && !WasOpened) menuSamples?.PlayOpenSample(); - this.FadeIn(300, Easing.OutQuint); - wasOpened = true; + WasOpened = true; + this.FadeIn(FADE_DURATION, Easing.OutQuint); } protected override void AnimateClose() { - if (!TopLevelMenu && wasOpened) + if (PlaySamples && WasOpened) menuSamples?.PlayCloseSample(); this.Delay(DELAY_BEFORE_FADE_OUT) .FadeOut(FADE_DURATION, Easing.OutQuint); - wasOpened = false; + WasOpened = false; } protected override void UpdateSize(Vector2 newSize) @@ -87,7 +95,7 @@ namespace osu.Game.Graphics.UserInterface this.ResizeHeightTo(newSize.Y, 300, Easing.OutQuint); else // Delay until the fade out finishes from AnimateClose. - this.Delay(350).ResizeHeightTo(0); + this.Delay(DELAY_BEFORE_FADE_OUT + FADE_DURATION).ResizeHeightTo(0); } else { @@ -96,7 +104,7 @@ namespace osu.Game.Graphics.UserInterface this.ResizeWidthTo(newSize.X, 300, Easing.OutQuint); else // Delay until the fade out finishes from AnimateClose. - this.Delay(350).ResizeWidthTo(0); + this.Delay(DELAY_BEFORE_FADE_OUT + FADE_DURATION).ResizeWidthTo(0); } } From 14273824dcce96bf5c0e59a344a10305fc2bf253 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 4 Feb 2025 19:37:00 +0900 Subject: [PATCH 0875/1275] Fix `Carousel.FilterAsync` not working when called from a non-update thread I was trying to be smart about things and make use of our `SynchronisationContext` setup, but it turns out to not work in all cases due to the context being missing depending on where you are calling the method from. For now let's prefer the "works everywhere" method of scheduling the final work back to update. --- .../SongSelect/BeatmapCarouselV2TestScene.cs | 2 +- osu.Game/Screens/SelectV2/Carousel.cs | 19 ++++++++++--------- 2 files changed, 11 insertions(+), 10 deletions(-) diff --git a/osu.Game.Tests/Visual/SongSelect/BeatmapCarouselV2TestScene.cs b/osu.Game.Tests/Visual/SongSelect/BeatmapCarouselV2TestScene.cs index 72c9611fdb..b29394c55d 100644 --- a/osu.Game.Tests/Visual/SongSelect/BeatmapCarouselV2TestScene.cs +++ b/osu.Game.Tests/Visual/SongSelect/BeatmapCarouselV2TestScene.cs @@ -130,7 +130,7 @@ namespace osu.Game.Tests.Visual.SongSelect }); } - protected void SortBy(FilterCriteria criteria) => AddStep($"sort {criteria.Sort} group {criteria.Group}", () => Carousel.Filter(criteria)); + protected void SortBy(FilterCriteria criteria) => AddStep($"sort:{criteria.Sort} group:{criteria.Group}", () => Carousel.Filter(criteria)); protected void WaitForDrawablePanels() => AddUntilStep("drawable panels loaded", () => Carousel.ChildrenOfType().Count(), () => Is.GreaterThan(0)); protected void WaitForSorting() => AddUntilStep("sorting finished", () => Carousel.IsFiltering, () => Is.False); diff --git a/osu.Game/Screens/SelectV2/Carousel.cs b/osu.Game/Screens/SelectV2/Carousel.cs index 608ef207d9..78c2c99d99 100644 --- a/osu.Game/Screens/SelectV2/Carousel.cs +++ b/osu.Game/Screens/SelectV2/Carousel.cs @@ -228,8 +228,6 @@ namespace osu.Game.Screens.SelectV2 private async Task performFilter() { - Debug.Assert(SynchronizationContext.Current != null); - Stopwatch stopwatch = Stopwatch.StartNew(); var cts = new CancellationTokenSource(); @@ -266,19 +264,22 @@ namespace osu.Game.Screens.SelectV2 { log("Cancelled due to newer request arriving"); } - }, cts.Token).ConfigureAwait(true); + }, cts.Token).ConfigureAwait(false); if (cts.Token.IsCancellationRequested) return; - log("Items ready for display"); - carouselItems = items.ToList(); - displayedRange = null; + Schedule(() => + { + log("Items ready for display"); + carouselItems = items.ToList(); + displayedRange = null; - // Need to call this to ensure correct post-selection logic is handled on the new items list. - HandleItemSelected(currentSelection.Model); + // Need to call this to ensure correct post-selection logic is handled on the new items list. + HandleItemSelected(currentSelection.Model); - refreshAfterSelection(); + refreshAfterSelection(); + }); void log(string text) => Logger.Log($"Carousel[op {cts.GetHashCode().ToString()}] {stopwatch.ElapsedMilliseconds} ms: {text}"); } From 7f8f528ae20da7ac8e0a0cb9a91e64e633b80c87 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Wed, 5 Feb 2025 16:26:21 +0900 Subject: [PATCH 0876/1275] Add helper for testing mod/freemod validity --- osu.Game.Tests/Mods/ModUtilsTest.cs | 35 ++++++++++++++++ .../Multiplayer/MultiplayerMatchSongSelect.cs | 5 --- .../OnlinePlay/OnlinePlaySongSelect.cs | 20 ++++----- osu.Game/Utils/ModUtils.cs | 41 +++++++++++++++++++ 4 files changed, 86 insertions(+), 15 deletions(-) diff --git a/osu.Game.Tests/Mods/ModUtilsTest.cs b/osu.Game.Tests/Mods/ModUtilsTest.cs index decb0a31ac..2964ca9396 100644 --- a/osu.Game.Tests/Mods/ModUtilsTest.cs +++ b/osu.Game.Tests/Mods/ModUtilsTest.cs @@ -6,6 +6,7 @@ using System.Linq; using Moq; using NUnit.Framework; using osu.Framework.Localisation; +using osu.Game.Online.Rooms; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Osu; using osu.Game.Rulesets.Osu.Mods; @@ -342,6 +343,40 @@ namespace osu.Game.Tests.Mods Assert.AreEqual(ModUtils.FormatScoreMultiplier(1.055).ToString(), "1.06x"); } + [Test] + public void TestRoomModValidity() + { + Assert.IsTrue(ModUtils.IsValidModForMatchType(new OsuModHardRock(), MatchType.Playlists)); + Assert.IsTrue(ModUtils.IsValidModForMatchType(new OsuModDoubleTime(), MatchType.Playlists)); + Assert.IsTrue(ModUtils.IsValidModForMatchType(new ModAdaptiveSpeed(), MatchType.Playlists)); + Assert.IsFalse(ModUtils.IsValidModForMatchType(new OsuModAutoplay(), MatchType.Playlists)); + Assert.IsFalse(ModUtils.IsValidModForMatchType(new OsuModTouchDevice(), MatchType.Playlists)); + + Assert.IsTrue(ModUtils.IsValidModForMatchType(new OsuModHardRock(), MatchType.HeadToHead)); + Assert.IsTrue(ModUtils.IsValidModForMatchType(new OsuModDoubleTime(), MatchType.HeadToHead)); + // For now, adaptive speed isn't allowed in multiplayer because it's a per-user rate adjustment. + Assert.IsFalse(ModUtils.IsValidModForMatchType(new ModAdaptiveSpeed(), MatchType.HeadToHead)); + Assert.IsFalse(ModUtils.IsValidModForMatchType(new OsuModAutoplay(), MatchType.HeadToHead)); + Assert.IsFalse(ModUtils.IsValidModForMatchType(new OsuModTouchDevice(), MatchType.HeadToHead)); + } + + [Test] + public void TestRoomFreeModValidity() + { + Assert.IsTrue(ModUtils.IsValidFreeModForMatchType(new OsuModHardRock(), MatchType.Playlists)); + Assert.IsTrue(ModUtils.IsValidFreeModForMatchType(new OsuModDoubleTime(), MatchType.Playlists)); + Assert.IsTrue(ModUtils.IsValidFreeModForMatchType(new ModAdaptiveSpeed(), MatchType.Playlists)); + Assert.IsFalse(ModUtils.IsValidFreeModForMatchType(new OsuModAutoplay(), MatchType.Playlists)); + Assert.IsFalse(ModUtils.IsValidFreeModForMatchType(new OsuModTouchDevice(), MatchType.Playlists)); + + Assert.IsTrue(ModUtils.IsValidFreeModForMatchType(new OsuModHardRock(), MatchType.HeadToHead)); + // For now, all rate adjustment mods aren't allowed as free mods in multiplayer. + Assert.IsFalse(ModUtils.IsValidFreeModForMatchType(new OsuModDoubleTime(), MatchType.HeadToHead)); + Assert.IsFalse(ModUtils.IsValidFreeModForMatchType(new ModAdaptiveSpeed(), MatchType.HeadToHead)); + Assert.IsFalse(ModUtils.IsValidFreeModForMatchType(new OsuModAutoplay(), MatchType.HeadToHead)); + Assert.IsFalse(ModUtils.IsValidFreeModForMatchType(new OsuModTouchDevice(), MatchType.HeadToHead)); + } + public abstract class CustomMod1 : Mod, IModCompatibilitySpecification { } diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSongSelect.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSongSelect.cs index b42a58787d..7328e01026 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSongSelect.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSongSelect.cs @@ -11,7 +11,6 @@ using osu.Framework.Screens; using osu.Game.Graphics.UserInterface; using osu.Game.Online.Multiplayer; using osu.Game.Online.Rooms; -using osu.Game.Rulesets.Mods; using osu.Game.Screens.Select; namespace osu.Game.Screens.OnlinePlay.Multiplayer @@ -122,9 +121,5 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer } protected override BeatmapDetailArea CreateBeatmapDetailArea() => new PlayBeatmapDetailArea(); - - protected override bool IsValidMod(Mod mod) => base.IsValidMod(mod) && mod.ValidForMultiplayer; - - protected override bool IsValidFreeMod(Mod mod) => base.IsValidFreeMod(mod) && mod.ValidForMultiplayerAsFreeMod; } } diff --git a/osu.Game/Screens/OnlinePlay/OnlinePlaySongSelect.cs b/osu.Game/Screens/OnlinePlay/OnlinePlaySongSelect.cs index 4ca6abbf7d..1164c4c0fc 100644 --- a/osu.Game/Screens/OnlinePlay/OnlinePlaySongSelect.cs +++ b/osu.Game/Screens/OnlinePlay/OnlinePlaySongSelect.cs @@ -67,7 +67,7 @@ namespace osu.Game.Screens.OnlinePlay freeModSelect = new FreeModSelectOverlay { SelectedMods = { BindTarget = FreeMods }, - IsValidMod = IsValidFreeMod, + IsValidMod = isValidFreeMod, }; } @@ -144,10 +144,10 @@ namespace osu.Game.Screens.OnlinePlay private void onModsChanged(ValueChangedEvent> mods) { - FreeMods.Value = FreeMods.Value.Where(checkCompatibleFreeMod).ToList(); + FreeMods.Value = FreeMods.Value.Where(isValidFreeMod).ToList(); // Reset the validity delegate to update the overlay's display. - freeModSelect.IsValidMod = IsValidFreeMod; + freeModSelect.IsValidMod = isValidFreeMod; } private void onRulesetChanged(ValueChangedEvent ruleset) @@ -194,7 +194,7 @@ namespace osu.Game.Screens.OnlinePlay protected override ModSelectOverlay CreateModSelectOverlay() => new UserModSelectOverlay(OverlayColourScheme.Plum) { - IsValidMod = IsValidMod + IsValidMod = isValidMod }; protected override IEnumerable<(FooterButton button, OverlayContainer? overlay)> CreateSongSelectFooterButtons() @@ -217,18 +217,18 @@ namespace osu.Game.Screens.OnlinePlay /// /// The to check. /// Whether is a valid mod for online play. - protected virtual bool IsValidMod(Mod mod) => mod.HasImplementation && ModUtils.FlattenMod(mod).All(m => m.UserPlayable); + private bool isValidMod(Mod mod) => ModUtils.IsValidModForMatchType(mod, room.Type); /// /// Checks whether a given is valid for per-player free-mod selection. /// /// The to check. /// Whether is a selectable free-mod. - protected virtual bool IsValidFreeMod(Mod mod) => IsValidMod(mod) && checkCompatibleFreeMod(mod); - - private bool checkCompatibleFreeMod(Mod mod) - => Mods.Value.All(m => m.Acronym != mod.Acronym) // Mod must not be contained in the required mods. - && ModUtils.CheckCompatibleSet(Mods.Value.Append(mod).ToArray()); // Mod must be compatible with all the required mods. + private bool isValidFreeMod(Mod mod) => ModUtils.IsValidFreeModForMatchType(mod, room.Type) + // Mod must not be contained in the required mods. + && Mods.Value.All(m => m.Acronym != mod.Acronym) + // Mod must be compatible with all the required mods. + && ModUtils.CheckCompatibleSet(Mods.Value.Append(mod).ToArray()); protected override void Dispose(bool isDisposing) { diff --git a/osu.Game/Utils/ModUtils.cs b/osu.Game/Utils/ModUtils.cs index 15fc34b468..ac24bf2130 100644 --- a/osu.Game/Utils/ModUtils.cs +++ b/osu.Game/Utils/ModUtils.cs @@ -8,6 +8,7 @@ using System.Linq; using osu.Framework.Extensions.LocalisationExtensions; using osu.Framework.Localisation; using osu.Game.Online.API; +using osu.Game.Online.Rooms; using osu.Game.Rulesets; using osu.Game.Rulesets.Mods; @@ -292,5 +293,45 @@ namespace osu.Game.Utils return rate; } + + /// + /// Determines whether a mod can be applied to playlist items in the given match type. + /// + /// The mod to test. + /// The match type. + public static bool IsValidModForMatchType(Mod mod, MatchType type) + { + if (mod.Type == ModType.System || !mod.UserPlayable || !mod.HasImplementation) + return false; + + switch (type) + { + case MatchType.Playlists: + return true; + + default: + return mod.ValidForMultiplayer; + } + } + + /// + /// Determines whether a mod can be applied as a free mod to playlist items in the given match type. + /// + /// The mod to test. + /// The match type. + public static bool IsValidFreeModForMatchType(Mod mod, MatchType type) + { + if (mod.Type == ModType.System || !mod.UserPlayable || !mod.HasImplementation) + return false; + + switch (type) + { + case MatchType.Playlists: + return true; + + default: + return mod.ValidForMultiplayerAsFreeMod; + } + } } } From 5c9e84caf0350760c1f7d78cbe80024aed7661de Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 5 Feb 2025 17:31:54 +0900 Subject: [PATCH 0877/1275] Add lock object --- osu.Game/Screens/SelectV2/Carousel.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/osu.Game/Screens/SelectV2/Carousel.cs b/osu.Game/Screens/SelectV2/Carousel.cs index 78c2c99d99..681da84390 100644 --- a/osu.Game/Screens/SelectV2/Carousel.cs +++ b/osu.Game/Screens/SelectV2/Carousel.cs @@ -226,12 +226,14 @@ namespace osu.Game.Screens.SelectV2 private Task filterTask = Task.CompletedTask; private CancellationTokenSource cancellationSource = new CancellationTokenSource(); + private readonly object cancellationLock = new object(); + private async Task performFilter() { Stopwatch stopwatch = Stopwatch.StartNew(); var cts = new CancellationTokenSource(); - lock (this) + lock (cancellationLock) { cancellationSource.Cancel(); cancellationSource = cts; From b7aa71c9759dc7d69249948591ebb60de34e2750 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 5 Feb 2025 18:24:07 +0900 Subject: [PATCH 0878/1275] Adjust xmldoc slightly to convey the disposal pattern --- osu.Game/Online/Metadata/MetadataClient.cs | 15 +++++---------- 1 file changed, 5 insertions(+), 10 deletions(-) diff --git a/osu.Game/Online/Metadata/MetadataClient.cs b/osu.Game/Online/Metadata/MetadataClient.cs index 1da245e80d..9885419b65 100644 --- a/osu.Game/Online/Metadata/MetadataClient.cs +++ b/osu.Game/Online/Metadata/MetadataClient.cs @@ -73,10 +73,8 @@ namespace osu.Game.Online.Metadata return null; } - /// public abstract Task UpdateActivity(UserActivity? activity); - /// public abstract Task UpdateStatus(UserStatus? status); private int userPresenceWatchCount; @@ -84,11 +82,12 @@ namespace osu.Game.Online.Metadata protected bool IsWatchingUserPresence => Interlocked.CompareExchange(ref userPresenceWatchCount, userPresenceWatchCount, userPresenceWatchCount) > 0; - /// - public IDisposable BeginWatchingUserPresence() - => new UserPresenceWatchToken(this); + /// + /// Signals to the server that we want to begin receiving status updates for all users. + /// + /// An which will end the session when disposed. + public IDisposable BeginWatchingUserPresence() => new UserPresenceWatchToken(this); - /// Task IMetadataServer.BeginWatchingUserPresence() { if (Interlocked.Increment(ref userPresenceWatchCount) == 1) @@ -97,7 +96,6 @@ namespace osu.Game.Online.Metadata return Task.CompletedTask; } - /// Task IMetadataServer.EndWatchingUserPresence() { if (Interlocked.Decrement(ref userPresenceWatchCount) == 0) @@ -110,10 +108,8 @@ namespace osu.Game.Online.Metadata protected abstract Task EndWatchingUserPresenceInternal(); - /// public abstract Task UserPresenceUpdated(int userId, UserPresence? presence); - /// public abstract Task FriendPresenceUpdated(int userId, UserPresence? presence); private class UserPresenceWatchToken : IDisposable @@ -143,7 +139,6 @@ namespace osu.Game.Online.Metadata public abstract IBindable DailyChallengeInfo { get; } - /// public abstract Task DailyChallengeUpdated(DailyChallengeInfo? info); #endregion From c5deb9f36b067f03bd9d597967ac17f8502ade27 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 5 Feb 2025 10:28:09 +0100 Subject: [PATCH 0879/1275] Use alternative lockless solution for atomic cancellation token recreation --- osu.Game/Screens/SelectV2/Carousel.cs | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/osu.Game/Screens/SelectV2/Carousel.cs b/osu.Game/Screens/SelectV2/Carousel.cs index 681da84390..0b706b4bb8 100644 --- a/osu.Game/Screens/SelectV2/Carousel.cs +++ b/osu.Game/Screens/SelectV2/Carousel.cs @@ -226,18 +226,13 @@ namespace osu.Game.Screens.SelectV2 private Task filterTask = Task.CompletedTask; private CancellationTokenSource cancellationSource = new CancellationTokenSource(); - private readonly object cancellationLock = new object(); - private async Task performFilter() { Stopwatch stopwatch = Stopwatch.StartNew(); var cts = new CancellationTokenSource(); - lock (cancellationLock) - { - cancellationSource.Cancel(); - cancellationSource = cts; - } + var previousCancellationSource = Interlocked.Exchange(ref cancellationSource, cts); + await previousCancellationSource.CancelAsync().ConfigureAwait(false); if (DebounceDelay > 0) { From e5943e460d657cb12545078d10b89ca58f6456f8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 5 Feb 2025 10:28:23 +0100 Subject: [PATCH 0880/1275] Unify `ConfigureAwait()` calls across method --- osu.Game/Screens/SelectV2/Carousel.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/SelectV2/Carousel.cs b/osu.Game/Screens/SelectV2/Carousel.cs index 0b706b4bb8..3371e45453 100644 --- a/osu.Game/Screens/SelectV2/Carousel.cs +++ b/osu.Game/Screens/SelectV2/Carousel.cs @@ -237,7 +237,7 @@ namespace osu.Game.Screens.SelectV2 if (DebounceDelay > 0) { log($"Filter operation queued, waiting for {DebounceDelay} ms debounce"); - await Task.Delay(DebounceDelay, cts.Token).ConfigureAwait(true); + await Task.Delay(DebounceDelay, cts.Token).ConfigureAwait(false); } // Copy must be performed on update thread for now (see ConfigureAwait above). From 40ea7ff2383248c4e3cdbd2c042cf692792f7bd8 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 5 Feb 2025 18:48:48 +0900 Subject: [PATCH 0881/1275] Add better documentation for interval change code --- .../Difficulty/Utils/IntervalGroupingUtils.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Utils/IntervalGroupingUtils.cs b/osu.Game.Rulesets.Taiko/Difficulty/Utils/IntervalGroupingUtils.cs index f04dec1c08..7bd7aa7677 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Utils/IntervalGroupingUtils.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Utils/IntervalGroupingUtils.cs @@ -28,9 +28,10 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Utils for (; i < objects.Count - 1; i++) { - // An interval change occured, add the current object if the next interval is larger. if (!Precision.AlmostEquals(objects[i].Interval, objects[i + 1].Interval, margin_of_error)) { + // When an interval change occurs, include the object with the differing interval in the case it increased + // See https://github.com/ppy/osu/pull/31636#discussion_r1942368372 for rationale. if (objects[i + 1].Interval > objects[i].Interval + margin_of_error) { groupedObjects.Add(objects[i]); From fc5832ce67d7af1b32f88109b705788c4bf07e07 Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Wed, 5 Feb 2025 04:44:06 -0500 Subject: [PATCH 0882/1275] Support variable spacing between carousel items --- osu.Game/Screens/SelectV2/BeatmapCarousel.cs | 11 +++++++ osu.Game/Screens/SelectV2/Carousel.cs | 33 +++++++++++++------- 2 files changed, 32 insertions(+), 12 deletions(-) diff --git a/osu.Game/Screens/SelectV2/BeatmapCarousel.cs b/osu.Game/Screens/SelectV2/BeatmapCarousel.cs index 9f62780dda..12660d8642 100644 --- a/osu.Game/Screens/SelectV2/BeatmapCarousel.cs +++ b/osu.Game/Screens/SelectV2/BeatmapCarousel.cs @@ -20,12 +20,23 @@ namespace osu.Game.Screens.SelectV2 [Cached] public partial class BeatmapCarousel : Carousel { + public const float SPACING = 5f; + private IBindableList detachedBeatmaps = null!; private readonly LoadingLayer loading; private readonly BeatmapCarouselFilterGrouping grouping; + protected override float GetSpacingBetweenPanels(CarouselItem top, CarouselItem bottom) + { + if (top.Model is BeatmapInfo || bottom.Model is BeatmapInfo) + // Beatmap difficulty panels do not overlap with themselves or any other panel. + return SPACING; + + return -SPACING; + } + public BeatmapCarousel() { DebounceDelay = 100; diff --git a/osu.Game/Screens/SelectV2/Carousel.cs b/osu.Game/Screens/SelectV2/Carousel.cs index 608ef207d9..d7b6f251c3 100644 --- a/osu.Game/Screens/SelectV2/Carousel.cs +++ b/osu.Game/Screens/SelectV2/Carousel.cs @@ -50,11 +50,6 @@ namespace osu.Game.Screens.SelectV2 /// public float DistanceOffscreenToPreload { get; set; } - /// - /// Vertical space between panel layout. Negative value can be used to create an overlapping effect. - /// - protected float SpacingBetweenPanels { get; set; } = -5; - /// /// When a new request arrives to change filtering, the number of milliseconds to wait before performing the filter. /// Regardless of any external debouncing, this is a safety measure to avoid triggering too many threaded operations. @@ -116,6 +111,11 @@ namespace osu.Game.Screens.SelectV2 } } + /// + /// Returns the vertical spacing between two given carousel items. Negative value can be used to create an overlapping effect. + /// + protected virtual float GetSpacingBetweenPanels(CarouselItem top, CarouselItem bottom) => 0f; + #endregion #region Properties and methods concerning implementations @@ -260,7 +260,7 @@ namespace osu.Game.Screens.SelectV2 } log("Updating Y positions"); - updateYPositions(items, visibleHalfHeight, SpacingBetweenPanels); + updateYPositions(items, visibleHalfHeight); } catch (OperationCanceledException) { @@ -283,17 +283,26 @@ namespace osu.Game.Screens.SelectV2 void log(string text) => Logger.Log($"Carousel[op {cts.GetHashCode().ToString()}] {stopwatch.ElapsedMilliseconds} ms: {text}"); } - private static void updateYPositions(IEnumerable carouselItems, float offset, float spacing) + private void updateYPositions(IEnumerable carouselItems, float offset) { + CarouselItem? previousVisible = null; + foreach (var item in carouselItems) - updateItemYPosition(item, ref offset, spacing); + updateItemYPosition(item, ref previousVisible, ref offset); } - private static void updateItemYPosition(CarouselItem item, ref float offset, float spacing) + private void updateItemYPosition(CarouselItem item, ref CarouselItem? previousVisible, ref float offset) { + float spacing = previousVisible == null || !item.IsVisible ? 0 : GetSpacingBetweenPanels(previousVisible, item); + + offset += spacing; item.CarouselYPosition = offset; + if (item.IsVisible) - offset += item.DrawHeight + spacing; + { + offset += item.DrawHeight; + previousVisible = item; + } } #endregion @@ -470,7 +479,7 @@ namespace osu.Game.Screens.SelectV2 return; } - float spacing = SpacingBetweenPanels; + CarouselItem? lastVisible = null; int count = carouselItems.Count; Selection prevKeyboard = currentKeyboardSelection; @@ -482,7 +491,7 @@ namespace osu.Game.Screens.SelectV2 { var item = carouselItems[i]; - updateItemYPosition(item, ref yPos, spacing); + updateItemYPosition(item, ref lastVisible, ref yPos); if (ReferenceEquals(item.Model, currentKeyboardSelection.Model)) currentKeyboardSelection = new Selection(item.Model, item, item.CarouselYPosition, i); From c389dbc711cc90aa2bd7c942d479f9c5336b377f Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Wed, 5 Feb 2025 04:45:32 -0500 Subject: [PATCH 0883/1275] Extend panel input area to cover gaps --- osu.Game/Screens/SelectV2/BeatmapPanel.cs | 10 ++++++++++ osu.Game/Screens/SelectV2/BeatmapSetPanel.cs | 10 ++++++++++ osu.Game/Screens/SelectV2/GroupPanel.cs | 10 ++++++++++ 3 files changed, 30 insertions(+) diff --git a/osu.Game/Screens/SelectV2/BeatmapPanel.cs b/osu.Game/Screens/SelectV2/BeatmapPanel.cs index 3edfd4203b..2fe509402b 100644 --- a/osu.Game/Screens/SelectV2/BeatmapPanel.cs +++ b/osu.Game/Screens/SelectV2/BeatmapPanel.cs @@ -24,6 +24,16 @@ namespace osu.Game.Screens.SelectV2 private Box activationFlash = null!; private OsuSpriteText text = null!; + public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) + { + var inputRectangle = DrawRectangle; + + // Cover the gaps introduced by the spacing between BeatmapPanels. + inputRectangle = inputRectangle.Inflate(new MarginPadding { Vertical = BeatmapCarousel.SPACING / 2f }); + + return inputRectangle.Contains(ToLocalSpace(screenSpacePos)); + } + [BackgroundDependencyLoader] private void load() { diff --git a/osu.Game/Screens/SelectV2/BeatmapSetPanel.cs b/osu.Game/Screens/SelectV2/BeatmapSetPanel.cs index 79ffe0f68a..85d5cc097d 100644 --- a/osu.Game/Screens/SelectV2/BeatmapSetPanel.cs +++ b/osu.Game/Screens/SelectV2/BeatmapSetPanel.cs @@ -27,6 +27,16 @@ namespace osu.Game.Screens.SelectV2 private OsuSpriteText text = null!; private Box box = null!; + public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) + { + var inputRectangle = DrawRectangle; + + // Cover a gap introduced by the spacing between a BeatmapSetPanel and a BeatmapPanel either below/above it. + inputRectangle = inputRectangle.Inflate(new MarginPadding { Vertical = BeatmapCarousel.SPACING / 2f }); + + return inputRectangle.Contains(ToLocalSpace(screenSpacePos)); + } + [BackgroundDependencyLoader] private void load() { diff --git a/osu.Game/Screens/SelectV2/GroupPanel.cs b/osu.Game/Screens/SelectV2/GroupPanel.cs index 7ed256ca6a..df930a3111 100644 --- a/osu.Game/Screens/SelectV2/GroupPanel.cs +++ b/osu.Game/Screens/SelectV2/GroupPanel.cs @@ -28,6 +28,16 @@ namespace osu.Game.Screens.SelectV2 private Box box = null!; + public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) + { + var inputRectangle = DrawRectangle; + + // Cover a gap introduced by the spacing between a GroupPanel and a BeatmapPanel either below/above it. + inputRectangle = inputRectangle.Inflate(new MarginPadding { Vertical = BeatmapCarousel.SPACING / 2f }); + + return inputRectangle.Contains(ToLocalSpace(screenSpacePos)); + } + [BackgroundDependencyLoader] private void load() { From 6037d5d8ce256fe70d6a7b22a723d1e26fb6ea42 Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Wed, 5 Feb 2025 04:46:05 -0500 Subject: [PATCH 0884/1275] Add test coverage --- ...estSceneBeatmapCarouselV2GroupSelection.cs | 62 ++++++++++++++++ .../TestSceneBeatmapCarouselV2Selection.cs | 70 ++++++++++++++----- 2 files changed, 115 insertions(+), 17 deletions(-) diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2GroupSelection.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2GroupSelection.cs index f4d97be5a5..ebdc54864e 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2GroupSelection.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2GroupSelection.cs @@ -1,6 +1,7 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System; using System.Linq; using NUnit.Framework; using osu.Framework.Testing; @@ -8,6 +9,8 @@ using osu.Game.Beatmaps; using osu.Game.Screens.Select; using osu.Game.Screens.Select.Filter; using osu.Game.Screens.SelectV2; +using osuTK; +using osuTK.Input; namespace osu.Game.Tests.Visual.SongSelect { @@ -154,5 +157,64 @@ namespace osu.Game.Tests.Visual.SongSelect SelectPrevGroup(); WaitForGroupSelection(2, 9); } + + [Test] + public void TestInputHandlingWithinGaps() + { + AddBeatmaps(5, 2); + WaitForDrawablePanels(); + SelectNextGroup(); + + clickOnPanel(0, 1, p => p.LayoutRectangle.TopLeft + new Vector2(20f, -1f)); + WaitForGroupSelection(0, 1); + + clickOnPanel(0, 0, p => p.LayoutRectangle.BottomLeft + new Vector2(20f, 1f)); + WaitForGroupSelection(0, 0); + + SelectNextPanel(); + Select(); + WaitForGroupSelection(0, 1); + + clickOnGroup(0, p => p.LayoutRectangle.BottomLeft + new Vector2(20f, 1f)); + AddAssert("group 0 collapsed", () => this.ChildrenOfType().OrderBy(g => g.Y).ElementAt(0).Expanded.Value, () => Is.False); + clickOnGroup(0, p => p.LayoutRectangle.Centre); + AddAssert("group 0 expanded", () => this.ChildrenOfType().OrderBy(g => g.Y).ElementAt(0).Expanded.Value, () => Is.True); + + AddStep("scroll to end", () => Scroll.ScrollToEnd(false)); + clickOnPanel(0, 4, p => p.LayoutRectangle.BottomLeft + new Vector2(20f, 1f)); + WaitForGroupSelection(0, 4); + + clickOnGroup(1, p => p.LayoutRectangle.TopLeft + new Vector2(20f, -1f)); + AddAssert("group 1 expanded", () => this.ChildrenOfType().OrderBy(g => g.Y).ElementAt(1).Expanded.Value, () => Is.True); + } + + private void clickOnGroup(int group, Func pos) + { + AddStep($"click on group{group}", () => + { + var groupingFilter = Carousel.Filters.OfType().Single(); + var model = groupingFilter.GroupItems.Keys.ElementAt(group); + + var panel = this.ChildrenOfType().Single(b => ReferenceEquals(b.Item!.Model, model)); + InputManager.MoveMouseTo(panel.ToScreenSpace(pos(panel))); + InputManager.Click(MouseButton.Left); + }); + } + + private void clickOnPanel(int group, int panel, Func pos) + { + AddStep($"click on group{group} panel{panel}", () => + { + var groupingFilter = Carousel.Filters.OfType().Single(); + + var g = groupingFilter.GroupItems.Keys.ElementAt(group); + // offset by one because the group itself is included in the items list. + object model = groupingFilter.GroupItems[g].ElementAt(panel + 1).Model; + + var p = this.ChildrenOfType().Single(b => ReferenceEquals(b.Item!.Model, model)); + InputManager.MoveMouseTo(p.ToScreenSpace(pos(p))); + InputManager.Click(MouseButton.Left); + }); + } } } diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2Selection.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2Selection.cs index b087c252e4..5541e217cf 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2Selection.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2Selection.cs @@ -1,11 +1,13 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System; using System.Linq; using NUnit.Framework; using osu.Framework.Testing; using osu.Game.Beatmaps; using osu.Game.Screens.SelectV2; +using osuTK; using osuTK.Input; namespace osu.Game.Tests.Visual.SongSelect @@ -94,9 +96,8 @@ namespace osu.Game.Tests.Visual.SongSelect AddStep("add previous selection", () => BeatmapSets.Add(((BeatmapInfo)selection!).BeatmapSet!)); - AddAssert("selection matches original carousel selection", () => selection, () => Is.EqualTo(Carousel.CurrentSelection)); AddUntilStep("drawable selection restored", () => getSelectedPanel()?.Item?.Model, () => Is.EqualTo(selection)); - AddAssert("carousel item is visible", () => getSelectedPanel()?.Item?.IsVisible, () => Is.True); + AddAssert("drawable selection matches carousel selection", () => selection, () => Is.EqualTo(Carousel.CurrentSelection)); BeatmapPanel? getSelectedPanel() => Carousel.ChildrenOfType().SingleOrDefault(p => p.Selected.Value); } @@ -129,21 +130,6 @@ namespace osu.Game.Tests.Visual.SongSelect WaitForSelection(0, 0); } - [Test] - public void TestGroupSelectionOnHeader() - { - AddBeatmaps(10, 3); - WaitForDrawablePanels(); - - SelectNextGroup(); - SelectNextGroup(); - WaitForSelection(1, 0); - - SelectPrevPanel(); - SelectPrevGroup(); - WaitForSelection(0, 0); - } - [Test] public void TestKeyboardSelection() { @@ -194,6 +180,34 @@ namespace osu.Game.Tests.Visual.SongSelect CheckNoSelection(); } + [Test] + public void TestInputHandlingWithinGaps() + { + AddBeatmaps(2, 5); + WaitForDrawablePanels(); + SelectNextGroup(); + + clickOnDifficulty(0, 1, p => p.LayoutRectangle.TopLeft + new Vector2(20f, -1f)); + WaitForSelection(0, 1); + + clickOnDifficulty(0, 0, p => p.LayoutRectangle.BottomLeft + new Vector2(20f, 1f)); + WaitForSelection(0, 0); + + SelectNextPanel(); + Select(); + WaitForSelection(0, 1); + + clickOnSet(0, p => p.LayoutRectangle.BottomLeft + new Vector2(20f, 1f)); + WaitForSelection(0, 0); + + AddStep("scroll to end", () => Scroll.ScrollToEnd(false)); + clickOnDifficulty(0, 4, p => p.LayoutRectangle.BottomLeft + new Vector2(20f, 1f)); + WaitForSelection(0, 4); + + clickOnSet(1, p => p.LayoutRectangle.TopLeft + new Vector2(20f, -1f)); + WaitForSelection(1, 0); + } + private void checkSelectionIterating(bool isIterating) { object? selection = null; @@ -207,5 +221,27 @@ namespace osu.Game.Tests.Visual.SongSelect AddUntilStep("selection not changed", () => Carousel.CurrentSelection == selection); } } + + private void clickOnSet(int set, Func pos) + { + AddStep($"click on set{set}", () => + { + var model = BeatmapSets[set]; + var panel = this.ChildrenOfType().Single(b => ReferenceEquals(b.Item!.Model, model)); + InputManager.MoveMouseTo(panel.ToScreenSpace(pos(panel))); + InputManager.Click(MouseButton.Left); + }); + } + + private void clickOnDifficulty(int set, int diff, Func pos) + { + AddStep($"click on set{set} diff{diff}", () => + { + var model = BeatmapSets[set].Beatmaps[diff]; + var panel = this.ChildrenOfType().Single(b => ReferenceEquals(b.Item!.Model, model)); + InputManager.MoveMouseTo(panel.ToScreenSpace(pos(panel))); + InputManager.Click(MouseButton.Left); + }); + } } } From c370c75fe2793dd379b4f4b8983fd3a35da17511 Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Wed, 5 Feb 2025 05:47:34 -0500 Subject: [PATCH 0885/1275] Allow ordering certain carousel panels behind others --- .../SelectV2/BeatmapCarouselFilterGrouping.cs | 14 ++++++++++++-- osu.Game/Screens/SelectV2/Carousel.cs | 7 +++++-- osu.Game/Screens/SelectV2/CarouselItem.cs | 6 ++++++ 3 files changed, 23 insertions(+), 4 deletions(-) diff --git a/osu.Game/Screens/SelectV2/BeatmapCarouselFilterGrouping.cs b/osu.Game/Screens/SelectV2/BeatmapCarouselFilterGrouping.cs index e4160cc0fa..55cb5fa5f9 100644 --- a/osu.Game/Screens/SelectV2/BeatmapCarouselFilterGrouping.cs +++ b/osu.Game/Screens/SelectV2/BeatmapCarouselFilterGrouping.cs @@ -66,7 +66,12 @@ namespace osu.Game.Screens.SelectV2 { starGroup = (int)Math.Floor(b.StarRating); var groupDefinition = new GroupDefinition($"{starGroup} - {++starGroup} *"); - var groupItem = new CarouselItem(groupDefinition) { DrawHeight = GroupPanel.HEIGHT }; + + var groupItem = new CarouselItem(groupDefinition) + { + DrawHeight = GroupPanel.HEIGHT, + DepthLayer = -2 + }; newItems.Add(groupItem); groupItems[groupDefinition] = new HashSet { groupItem }; @@ -95,7 +100,12 @@ namespace osu.Game.Screens.SelectV2 if (newBeatmapSet) { - var setItem = new CarouselItem(beatmap.BeatmapSet!) { DrawHeight = BeatmapSetPanel.HEIGHT }; + var setItem = new CarouselItem(beatmap.BeatmapSet!) + { + DrawHeight = BeatmapSetPanel.HEIGHT, + DepthLayer = -1 + }; + setItems[beatmap.BeatmapSet!] = new HashSet { setItem }; newItems.Insert(i, setItem); i++; diff --git a/osu.Game/Screens/SelectV2/Carousel.cs b/osu.Game/Screens/SelectV2/Carousel.cs index 608ef207d9..5dc8d80476 100644 --- a/osu.Game/Screens/SelectV2/Carousel.cs +++ b/osu.Game/Screens/SelectV2/Carousel.cs @@ -550,6 +550,9 @@ namespace osu.Game.Screens.SelectV2 updateDisplayedRange(range); } + double selectedYPos = currentSelection?.CarouselItem?.CarouselYPosition ?? 0; + double maximumDistanceFromSelection = scroll.Panels.Select(p => Math.Abs(((ICarouselPanel)p).DrawYPosition - selectedYPos)).DefaultIfEmpty().Max(); + foreach (var panel in scroll.Panels) { var c = (ICarouselPanel)panel; @@ -558,8 +561,8 @@ namespace osu.Game.Screens.SelectV2 if (c.Item == null) continue; - double selectedYPos = currentSelection?.CarouselItem?.CarouselYPosition ?? 0; - scroll.Panels.ChangeChildDepth(panel, (float)Math.Abs(c.DrawYPosition - selectedYPos)); + float normalisedDepth = (float)(Math.Abs(selectedYPos - c.DrawYPosition) / maximumDistanceFromSelection); + scroll.Panels.ChangeChildDepth(panel, normalisedDepth + c.Item.DepthLayer); if (c.DrawYPosition != c.Item.CarouselYPosition) c.DrawYPosition = Interpolation.DampContinuously(c.DrawYPosition, c.Item.CarouselYPosition, 50, Time.Elapsed); diff --git a/osu.Game/Screens/SelectV2/CarouselItem.cs b/osu.Game/Screens/SelectV2/CarouselItem.cs index 32be33e99a..e497c3890c 100644 --- a/osu.Game/Screens/SelectV2/CarouselItem.cs +++ b/osu.Game/Screens/SelectV2/CarouselItem.cs @@ -29,6 +29,12 @@ namespace osu.Game.Screens.SelectV2 /// public float DrawHeight { get; set; } = DEFAULT_HEIGHT; + /// + /// A number that defines the layer which this should be placed on depth-wise. + /// The higher the number, the farther the panel associated with this item is taken to the background. + /// + public int DepthLayer { get; set; } = 0; + /// /// Whether this item is visible or hidden. /// From 11de4296210a9727b8f8dbad928409611b669e93 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 5 Feb 2025 18:59:29 +0900 Subject: [PATCH 0886/1275] Add support for grouping by artist --- osu.Game.Tests/Resources/TestResources.cs | 29 ++++++++++++++----- .../SongSelect/BeatmapCarouselV2TestScene.cs | 2 +- .../TestSceneBeatmapCarouselV2Basics.cs | 1 + osu.Game/Screens/SelectV2/BeatmapCarousel.cs | 29 ++++++++++--------- .../SelectV2/BeatmapCarouselFilterGrouping.cs | 29 ++++++++++++++++++- 5 files changed, 68 insertions(+), 22 deletions(-) diff --git a/osu.Game.Tests/Resources/TestResources.cs b/osu.Game.Tests/Resources/TestResources.cs index e0572e604c..bf08097ffd 100644 --- a/osu.Game.Tests/Resources/TestResources.cs +++ b/osu.Game.Tests/Resources/TestResources.cs @@ -85,7 +85,8 @@ namespace osu.Game.Tests.Resources /// /// Number of difficulties. If null, a random number between 1 and 20 will be used. /// Rulesets to cycle through when creating difficulties. If null, osu! ruleset will be used. - public static BeatmapSetInfo CreateTestBeatmapSetInfo(int? difficultyCount = null, RulesetInfo[] rulesets = null) + /// Whether to randomise metadata to create a better distribution. + public static BeatmapSetInfo CreateTestBeatmapSetInfo(int? difficultyCount = null, RulesetInfo[] rulesets = null, bool randomiseMetadata = false) { int j = 0; @@ -95,13 +96,27 @@ namespace osu.Game.Tests.Resources int setId = GetNextTestID(); - var metadata = new BeatmapMetadata + char getRandomCharacter() { - // Create random metadata, then we can check if sorting works based on these - Artist = "Some Artist " + RNG.Next(0, 9), - Title = $"Some Song (set id {setId:000}) {Guid.NewGuid()}", - Author = { Username = "Some Guy " + RNG.Next(0, 9) }, - }; + const string chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz*"; + return chars[RNG.Next(chars.Length)]; + } + + var metadata = randomiseMetadata + ? new BeatmapMetadata + { + // Create random metadata, then we can check if sorting works based on these + Artist = $"{getRandomCharacter()}ome Artist " + RNG.Next(0, 9), + Title = $"{getRandomCharacter()}ome Song (set id {setId:000}) {Guid.NewGuid()}", + Author = { Username = $"{getRandomCharacter()}ome Guy " + RNG.Next(0, 9) }, + } + : new BeatmapMetadata + { + // Create random metadata, then we can check if sorting works based on these + Artist = "Some Artist " + RNG.Next(0, 9), + Title = $"Some Song (set id {setId:000}) {Guid.NewGuid()}", + Author = { Username = "Some Guy " + RNG.Next(0, 9) }, + }; Logger.Log($"🛠️ Generating beatmap set \"{metadata}\" for test consumption."); diff --git a/osu.Game.Tests/Visual/SongSelect/BeatmapCarouselV2TestScene.cs b/osu.Game.Tests/Visual/SongSelect/BeatmapCarouselV2TestScene.cs index 72c9611fdb..f5ea959c51 100644 --- a/osu.Game.Tests/Visual/SongSelect/BeatmapCarouselV2TestScene.cs +++ b/osu.Game.Tests/Visual/SongSelect/BeatmapCarouselV2TestScene.cs @@ -193,7 +193,7 @@ namespace osu.Game.Tests.Visual.SongSelect protected void AddBeatmaps(int count, int? fixedDifficultiesPerSet = null) => AddStep($"add {count} beatmaps", () => { for (int i = 0; i < count; i++) - BeatmapSets.Add(TestResources.CreateTestBeatmapSetInfo(fixedDifficultiesPerSet ?? RNG.Next(1, 4))); + BeatmapSets.Add(TestResources.CreateTestBeatmapSetInfo(fixedDifficultiesPerSet ?? RNG.Next(1, 4), randomiseMetadata: true)); }); protected void RemoveAllBeatmaps() => AddStep("clear all beatmaps", () => BeatmapSets.Clear()); diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2Basics.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2Basics.cs index 8ffb51b995..a173920dc6 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2Basics.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2Basics.cs @@ -51,6 +51,7 @@ namespace osu.Game.Tests.Visual.SongSelect { AddBeatmaps(10); SortBy(new FilterCriteria { Group = GroupMode.Difficulty, Sort = SortMode.Difficulty }); + SortBy(new FilterCriteria { Group = GroupMode.Artist, Sort = SortMode.Artist }); SortBy(new FilterCriteria { Sort = SortMode.Artist }); } diff --git a/osu.Game/Screens/SelectV2/BeatmapCarousel.cs b/osu.Game/Screens/SelectV2/BeatmapCarousel.cs index 9f62780dda..e7311fbfbc 100644 --- a/osu.Game/Screens/SelectV2/BeatmapCarousel.cs +++ b/osu.Game/Screens/SelectV2/BeatmapCarousel.cs @@ -119,20 +119,12 @@ namespace osu.Game.Screens.SelectV2 return false; case BeatmapInfo beatmapInfo: + // Find any containing group. There should never be too many groups so iterating is efficient enough. + GroupDefinition? containingGroup = grouping.GroupItems.SingleOrDefault(kvp => kvp.Value.Any(i => ReferenceEquals(i.Model, beatmapInfo))).Key; - // If we have groups, we need to account for them. - if (Criteria.SplitOutDifficulties) - { - // Find the containing group. There should never be too many groups so iterating is efficient enough. - GroupDefinition? group = grouping.GroupItems.SingleOrDefault(kvp => kvp.Value.Any(i => ReferenceEquals(i.Model, beatmapInfo))).Key; - - if (group != null) - setExpandedGroup(group); - } - else - { - setExpandedSet(beatmapInfo); - } + if (containingGroup != null) + setExpandedGroup(containingGroup); + setExpandedSet(beatmapInfo); return true; } @@ -170,6 +162,7 @@ namespace osu.Game.Screens.SelectV2 { if (grouping.GroupItems.TryGetValue(group, out var items)) { + // First pass ignoring set groupings. foreach (var i in items) { if (i.Model is GroupDefinition) @@ -177,6 +170,16 @@ namespace osu.Game.Screens.SelectV2 else i.IsVisible = expanded; } + + // Second pass to hide set children when not meant to be displayed. + if (expanded) + { + foreach (var i in items) + { + if (i.Model is BeatmapSetInfo set) + setExpansionStateOfSetItems(set, i.IsExpanded); + } + } } } diff --git a/osu.Game/Screens/SelectV2/BeatmapCarouselFilterGrouping.cs b/osu.Game/Screens/SelectV2/BeatmapCarouselFilterGrouping.cs index e4160cc0fa..d4e0a166ab 100644 --- a/osu.Game/Screens/SelectV2/BeatmapCarouselFilterGrouping.cs +++ b/osu.Game/Screens/SelectV2/BeatmapCarouselFilterGrouping.cs @@ -52,6 +52,33 @@ namespace osu.Game.Screens.SelectV2 newItems.AddRange(items); break; + case GroupMode.Artist: + groupSetsTogether = true; + char groupChar = (char)0; + + foreach (var item in items) + { + cancellationToken.ThrowIfCancellationRequested(); + + var b = (BeatmapInfo)item.Model; + + char beatmapFirstChar = char.ToUpperInvariant(b.Metadata.Artist[0]); + + if (beatmapFirstChar > groupChar) + { + groupChar = beatmapFirstChar; + var groupDefinition = new GroupDefinition($"{groupChar}"); + var groupItem = new CarouselItem(groupDefinition) { DrawHeight = GroupPanel.HEIGHT }; + + newItems.Add(groupItem); + groupItems[groupDefinition] = new HashSet { groupItem }; + } + + newItems.Add(item); + } + + break; + case GroupMode.Difficulty: groupSetsTogether = false; int starGroup = int.MinValue; @@ -91,7 +118,7 @@ namespace osu.Game.Screens.SelectV2 if (item.Model is BeatmapInfo beatmap) { - bool newBeatmapSet = lastItem == null || (lastItem.Model is BeatmapInfo lastBeatmap && lastBeatmap.BeatmapSet!.ID != beatmap.BeatmapSet!.ID); + bool newBeatmapSet = lastItem?.Model is not BeatmapInfo lastBeatmap || lastBeatmap.BeatmapSet!.ID != beatmap.BeatmapSet!.ID; if (newBeatmapSet) { From 2d75030e36c2304d86a8f617d320cc468c31a73d Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Wed, 5 Feb 2025 07:17:25 -0500 Subject: [PATCH 0887/1275] Change default carousel item header to 50px --- osu.Game/Screens/SelectV2/CarouselItem.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/SelectV2/CarouselItem.cs b/osu.Game/Screens/SelectV2/CarouselItem.cs index 32be33e99a..65b62be6ba 100644 --- a/osu.Game/Screens/SelectV2/CarouselItem.cs +++ b/osu.Game/Screens/SelectV2/CarouselItem.cs @@ -11,7 +11,7 @@ namespace osu.Game.Screens.SelectV2 /// public sealed class CarouselItem : IComparable { - public const float DEFAULT_HEIGHT = 40; + public const float DEFAULT_HEIGHT = 50; /// /// The model this item is representing. From f2d259cd95f405cdf835fd228c18b4eebc11fbf3 Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Wed, 5 Feb 2025 07:17:49 -0500 Subject: [PATCH 0888/1275] Cache overlay colour provider to carousel tests --- osu.Game.Tests/Visual/SongSelect/BeatmapCarouselV2TestScene.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/osu.Game.Tests/Visual/SongSelect/BeatmapCarouselV2TestScene.cs b/osu.Game.Tests/Visual/SongSelect/BeatmapCarouselV2TestScene.cs index 72c9611fdb..3a83ff68c6 100644 --- a/osu.Game.Tests/Visual/SongSelect/BeatmapCarouselV2TestScene.cs +++ b/osu.Game.Tests/Visual/SongSelect/BeatmapCarouselV2TestScene.cs @@ -37,6 +37,9 @@ namespace osu.Game.Tests.Visual.SongSelect [Cached(typeof(BeatmapStore))] private BeatmapStore store; + [Cached] + private readonly OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Aquamarine); + private OsuTextFlowContainer stats = null!; private int beatmapCount; From a5fa04e4d6b8cd4852c5c172488d571ebc121809 Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Wed, 5 Feb 2025 07:18:55 -0500 Subject: [PATCH 0889/1275] Extend beatmap carousel width in tests --- osu.Game.Tests/Visual/SongSelect/BeatmapCarouselV2TestScene.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/osu.Game.Tests/Visual/SongSelect/BeatmapCarouselV2TestScene.cs b/osu.Game.Tests/Visual/SongSelect/BeatmapCarouselV2TestScene.cs index 3a83ff68c6..a3f6eaf152 100644 --- a/osu.Game.Tests/Visual/SongSelect/BeatmapCarouselV2TestScene.cs +++ b/osu.Game.Tests/Visual/SongSelect/BeatmapCarouselV2TestScene.cs @@ -15,6 +15,7 @@ using osu.Game.Beatmaps; using osu.Game.Database; using osu.Game.Graphics; using osu.Game.Graphics.Containers; +using osu.Game.Overlays; using osu.Game.Screens.Select; using osu.Game.Screens.Select.Filter; using osu.Game.Screens.SelectV2; @@ -105,7 +106,7 @@ namespace osu.Game.Tests.Visual.SongSelect { Anchor = Anchor.Centre, Origin = Anchor.Centre, - Width = 500, + Width = 800, RelativeSizeAxes = Axes.Y, }, }, From 092b953dca56991df3e3c69cafd6a430aac5a115 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 5 Feb 2025 11:31:18 +0100 Subject: [PATCH 0890/1275] Implement visual component for displaying submission progress --- .../TestSceneSubmissionStageProgress.cs | 47 ++++ .../Submission/SubmissionStageProgress.cs | 212 ++++++++++++++++++ 2 files changed, 259 insertions(+) create mode 100644 osu.Game.Tests/Visual/Editing/TestSceneSubmissionStageProgress.cs create mode 100644 osu.Game/Screens/Edit/Submission/SubmissionStageProgress.cs diff --git a/osu.Game.Tests/Visual/Editing/TestSceneSubmissionStageProgress.cs b/osu.Game.Tests/Visual/Editing/TestSceneSubmissionStageProgress.cs new file mode 100644 index 0000000000..47414bb24e --- /dev/null +++ b/osu.Game.Tests/Visual/Editing/TestSceneSubmissionStageProgress.cs @@ -0,0 +1,47 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using NUnit.Framework; +using osu.Framework.Allocation; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Game.Overlays; +using osu.Game.Screens.Edit.Submission; +using osuTK; + +namespace osu.Game.Tests.Visual.Editing +{ + public partial class TestSceneSubmissionStageProgress : OsuTestScene + { + [Cached] + private OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Aquamarine); + + [Test] + public void TestAppearance() + { + SubmissionStageProgress progress = null!; + + AddStep("create content", () => Child = new Container + { + RelativeSizeAxes = Axes.Both, + Size = new Vector2(0.8f), + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Child = progress = new SubmissionStageProgress + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + StageDescription = "Frobnicating the foobarator...", + } + }); + AddStep("not started", () => progress.SetNotStarted()); + AddStep("indeterminate progress", () => progress.SetInProgress()); + AddStep("30% progress", () => progress.SetInProgress(0.3f)); + AddStep("70% progress", () => progress.SetInProgress(0.7f)); + AddStep("completed", () => progress.SetCompleted()); + AddStep("failed", () => progress.SetFailed("the foobarator has defrobnicated")); + AddStep("failed with long message", () => progress.SetFailed("this is a very very very very VERY VEEEEEEEEEEEEEEEEEEEEEEEEERY long error message like you would never believe")); + AddStep("canceled", () => progress.SetCanceled()); + } + } +} diff --git a/osu.Game/Screens/Edit/Submission/SubmissionStageProgress.cs b/osu.Game/Screens/Edit/Submission/SubmissionStageProgress.cs new file mode 100644 index 0000000000..101313c627 --- /dev/null +++ b/osu.Game/Screens/Edit/Submission/SubmissionStageProgress.cs @@ -0,0 +1,212 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Allocation; +using osu.Framework.Bindables; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; +using osu.Framework.Graphics.Sprites; +using osu.Framework.Localisation; +using osu.Game.Graphics; +using osu.Game.Graphics.Containers; +using osu.Game.Graphics.Sprites; +using osu.Game.Graphics.UserInterface; +using osu.Game.Overlays; +using osuTK; + +namespace osu.Game.Screens.Edit.Submission +{ + public partial class SubmissionStageProgress : CompositeDrawable + { + public LocalisableString StageDescription { get; init; } + + private Bindable status { get; } = new Bindable(); + + private Bindable progress { get; } = new Bindable(); + + private Container progressBarContainer = null!; + private Box progressBar = null!; + private Container iconContainer = null!; + private OsuTextFlowContainer errorMessage = null!; + + [Resolved] + private OsuColour colours { get; set; } = null!; + + [BackgroundDependencyLoader] + private void load(OverlayColourProvider colourProvider) + { + RelativeSizeAxes = Axes.X; + AutoSizeAxes = Axes.Y; + + InternalChildren = new Drawable[] + { + new OsuSpriteText + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + Text = StageDescription, + }, + new FillFlowContainer + { + AutoSizeAxes = Axes.Both, + Anchor = Anchor.CentreRight, + Origin = Anchor.CentreRight, + Direction = FillDirection.Horizontal, + Spacing = new Vector2(5), + Children = new Drawable[] + { + iconContainer = new Container + { + AutoSizeAxes = Axes.Both, + Anchor = Anchor.CentreRight, + Origin = Anchor.CentreRight, + }, + new Container + { + AutoSizeAxes = Axes.Both, + Anchor = Anchor.CentreRight, + Origin = Anchor.CentreRight, + Children = + [ + progressBarContainer = new Container + { + Anchor = Anchor.CentreRight, + Origin = Anchor.CentreRight, + Width = 150, + Height = 10, + CornerRadius = 5, + Masking = true, + Children = new[] + { + new Box + { + RelativeSizeAxes = Axes.Both, + Colour = colourProvider.Background6, + }, + progressBar = new Box + { + RelativeSizeAxes = Axes.Both, + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + Width = 0, + Colour = colourProvider.Highlight1, + } + } + }, + errorMessage = new OsuTextFlowContainer + { + Anchor = Anchor.CentreRight, + Origin = Anchor.CentreRight, + // should really be `CentreRight` too, but that's broken due to a framework bug + // (https://github.com/ppy/osu-framework/issues/5084) + TextAnchor = Anchor.BottomRight, + Width = 450, + AutoSizeAxes = Axes.Y, + Alpha = 0, + Colour = colours.Red1, + } + ] + } + } + } + }; + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + status.BindValueChanged(_ => Scheduler.AddOnce(updateStatus), true); + progress.BindValueChanged(_ => Scheduler.AddOnce(updateProgress), true); + } + + public void SetNotStarted() => status.Value = StageStatusType.NotStarted; + + public void SetInProgress(float? progress = null) + { + this.progress.Value = progress; + status.Value = StageStatusType.InProgress; + } + + public void SetCompleted() => status.Value = StageStatusType.Completed; + + public void SetFailed(string reason) + { + status.Value = StageStatusType.Failed; + errorMessage.Text = reason; + } + + public void SetCanceled() => status.Value = StageStatusType.Canceled; + + private const float transition_duration = 200; + + private void updateProgress() + { + if (progress.Value != null) + progressBar.ResizeWidthTo(progress.Value.Value, transition_duration, Easing.OutQuint); + + progressBarContainer.FadeTo(status.Value == StageStatusType.InProgress && progress.Value != null ? 1 : 0, transition_duration, Easing.OutQuint); + } + + private void updateStatus() + { + progressBarContainer.FadeTo(status.Value == StageStatusType.InProgress && progress.Value != null ? 1 : 0, transition_duration, Easing.OutQuint); + errorMessage.FadeTo(status.Value == StageStatusType.Failed ? 1 : 0, transition_duration, Easing.OutQuint); + + iconContainer.Clear(); + iconContainer.ClearTransforms(); + + switch (status.Value) + { + case StageStatusType.InProgress: + iconContainer.Child = new LoadingSpinner + { + Size = new Vector2(16), + State = { Value = Visibility.Visible, }, + }; + iconContainer.Colour = colours.Orange1; + break; + + case StageStatusType.Completed: + iconContainer.Child = new SpriteIcon + { + Icon = FontAwesome.Solid.CheckCircle, + Size = new Vector2(16), + }; + iconContainer.Colour = colours.Green1; + iconContainer.FlashColour(Colour4.White, 1000, Easing.OutQuint); + break; + + case StageStatusType.Failed: + iconContainer.Child = new SpriteIcon + { + Icon = FontAwesome.Solid.ExclamationCircle, + Size = new Vector2(16), + }; + iconContainer.Colour = colours.Red1; + iconContainer.FlashColour(Colour4.White, 1000, Easing.OutQuint); + break; + + case StageStatusType.Canceled: + iconContainer.Child = new SpriteIcon + { + Icon = FontAwesome.Solid.Ban, + Size = new Vector2(16), + }; + iconContainer.Colour = colours.Gray8; + iconContainer.FlashColour(Colour4.White, 1000, Easing.OutQuint); + break; + } + } + + public enum StageStatusType + { + NotStarted, + InProgress, + Completed, + Failed, + Canceled, + } + } +} From 7d299bb2ad5221df7a81f8aa80c644b70af447b5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 5 Feb 2025 11:46:33 +0100 Subject: [PATCH 0891/1275] Expose `EndpointConfiguration` directly in `IAPIAccess` --- osu.Desktop/DiscordRichPresence.cs | 2 +- osu.Game.Tests/Visual/Menus/TestSceneMainMenu.cs | 2 +- .../Visual/Online/TestSceneWikiMarkdownContainer.cs | 10 +++++----- osu.Game/Beatmaps/BeatmapInfoExtensions.cs | 2 +- osu.Game/Beatmaps/BeatmapSetInfoExtensions.cs | 4 ++-- osu.Game/Online/API/APIAccess.cs | 13 +++++-------- osu.Game/Online/API/APIRequest.cs | 2 +- osu.Game/Online/API/DummyAPIAccess.cs | 8 +++++--- osu.Game/Online/API/IAPIProvider.cs | 9 ++------- osu.Game/Online/Chat/ExternalLinkOpener.cs | 4 ++-- osu.Game/Online/Chat/NowPlayingCommand.cs | 2 +- osu.Game/Online/EndpointConfiguration.cs | 4 ++-- osu.Game/Online/Leaderboards/LeaderboardScore.cs | 2 +- osu.Game/Overlays/Comments/DrawableComment.cs | 2 +- osu.Game/Overlays/Login/LoginForm.cs | 2 +- osu.Game/Overlays/Login/SecondFactorAuthForm.cs | 2 +- .../Profile/Header/BottomHeaderContainer.cs | 4 ++-- .../Header/Components/DrawableTournamentBanner.cs | 2 +- .../Overlays/Profile/Header/TopHeaderContainer.cs | 2 +- .../Sections/Recent/DrawableRecentActivity.cs | 2 +- osu.Game/Overlays/Wiki/WikiPanelContainer.cs | 2 +- osu.Game/Overlays/WikiOverlay.cs | 4 ++-- .../Submission/ScreenFrequentlyAskedQuestions.cs | 4 ++-- .../OnlinePlay/Lounge/Components/DrawableRoom.cs | 2 +- .../SelectV2/Leaderboards/LeaderboardScoreV2.cs | 2 +- 25 files changed, 44 insertions(+), 50 deletions(-) diff --git a/osu.Desktop/DiscordRichPresence.cs b/osu.Desktop/DiscordRichPresence.cs index 6afb3e319d..cf56fe6115 100644 --- a/osu.Desktop/DiscordRichPresence.cs +++ b/osu.Desktop/DiscordRichPresence.cs @@ -173,7 +173,7 @@ namespace osu.Desktop new Button { Label = "View beatmap", - Url = $@"{api.WebsiteRootUrl}/beatmaps/{beatmapId}?mode={ruleset.Value.ShortName}" + Url = $@"{api.EndpointConfiguration.WebsiteRootUrl}/beatmaps/{beatmapId}?mode={ruleset.Value.ShortName}" } }; } diff --git a/osu.Game.Tests/Visual/Menus/TestSceneMainMenu.cs b/osu.Game.Tests/Visual/Menus/TestSceneMainMenu.cs index f3ea20c1aa..e2d5bc2917 100644 --- a/osu.Game.Tests/Visual/Menus/TestSceneMainMenu.cs +++ b/osu.Game.Tests/Visual/Menus/TestSceneMainMenu.cs @@ -80,7 +80,7 @@ namespace osu.Game.Tests.Visual.Menus new APIMenuImage { Image = @"https://assets.ppy.sh/main-menu/project-loved-2@2x.png", - Url = $@"{API.WebsiteRootUrl}/home/news/2023-12-21-project-loved-december-2023", + Url = $@"{API.EndpointConfiguration.WebsiteRootUrl}/home/news/2023-12-21-project-loved-december-2023", } } }); diff --git a/osu.Game.Tests/Visual/Online/TestSceneWikiMarkdownContainer.cs b/osu.Game.Tests/Visual/Online/TestSceneWikiMarkdownContainer.cs index 8909305602..cee3f37aea 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneWikiMarkdownContainer.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneWikiMarkdownContainer.cs @@ -67,19 +67,19 @@ namespace osu.Game.Tests.Visual.Online [Test] public void TestLink() { - AddStep("set current path", () => markdownContainer.CurrentPath = $"{API.WebsiteRootUrl}/wiki/Article_styling_criteria/"); + AddStep("set current path", () => markdownContainer.CurrentPath = $"{API.EndpointConfiguration.WebsiteRootUrl}/wiki/Article_styling_criteria/"); AddStep("set '/wiki/Main_page''", () => markdownContainer.Text = "[wiki main page](/wiki/Main_page)"); - AddAssert("check url", () => markdownContainer.Link.Url == $"{API.WebsiteRootUrl}/wiki/Main_page"); + AddAssert("check url", () => markdownContainer.Link.Url == $"{API.EndpointConfiguration.WebsiteRootUrl}/wiki/Main_page"); AddStep("set '../FAQ''", () => markdownContainer.Text = "[FAQ](../FAQ)"); - AddAssert("check url", () => markdownContainer.Link.Url == $"{API.WebsiteRootUrl}/wiki/FAQ"); + AddAssert("check url", () => markdownContainer.Link.Url == $"{API.EndpointConfiguration.WebsiteRootUrl}/wiki/FAQ"); AddStep("set './Writing''", () => markdownContainer.Text = "[wiki writing guidline](./Writing)"); - AddAssert("check url", () => markdownContainer.Link.Url == $"{API.WebsiteRootUrl}/wiki/Article_styling_criteria/Writing"); + AddAssert("check url", () => markdownContainer.Link.Url == $"{API.EndpointConfiguration.WebsiteRootUrl}/wiki/Article_styling_criteria/Writing"); AddStep("set 'Formatting''", () => markdownContainer.Text = "[wiki formatting guidline](Formatting)"); - AddAssert("check url", () => markdownContainer.Link.Url == $"{API.WebsiteRootUrl}/wiki/Article_styling_criteria/Formatting"); + AddAssert("check url", () => markdownContainer.Link.Url == $"{API.EndpointConfiguration.WebsiteRootUrl}/wiki/Article_styling_criteria/Formatting"); } [Test] diff --git a/osu.Game/Beatmaps/BeatmapInfoExtensions.cs b/osu.Game/Beatmaps/BeatmapInfoExtensions.cs index a82a288239..d0625c64e3 100644 --- a/osu.Game/Beatmaps/BeatmapInfoExtensions.cs +++ b/osu.Game/Beatmaps/BeatmapInfoExtensions.cs @@ -59,7 +59,7 @@ namespace osu.Game.Beatmaps if (beatmapInfo.OnlineID <= 0 || beatmapInfo.BeatmapSet == null) return null; - return $@"{api.WebsiteRootUrl}/beatmapsets/{beatmapInfo.BeatmapSet.OnlineID}#{ruleset?.ShortName ?? beatmapInfo.Ruleset.ShortName}/{beatmapInfo.OnlineID}"; + return $@"{api.EndpointConfiguration.WebsiteRootUrl}/beatmapsets/{beatmapInfo.BeatmapSet.OnlineID}#{ruleset?.ShortName ?? beatmapInfo.Ruleset.ShortName}/{beatmapInfo.OnlineID}"; } } } diff --git a/osu.Game/Beatmaps/BeatmapSetInfoExtensions.cs b/osu.Game/Beatmaps/BeatmapSetInfoExtensions.cs index 8a107ed486..ac191d36a9 100644 --- a/osu.Game/Beatmaps/BeatmapSetInfoExtensions.cs +++ b/osu.Game/Beatmaps/BeatmapSetInfoExtensions.cs @@ -41,9 +41,9 @@ namespace osu.Game.Beatmaps return null; if (ruleset != null) - return $@"{api.WebsiteRootUrl}/beatmapsets/{beatmapSetInfo.OnlineID}#{ruleset.ShortName}"; + return $@"{api.EndpointConfiguration.WebsiteRootUrl}/beatmapsets/{beatmapSetInfo.OnlineID}#{ruleset.ShortName}"; - return $@"{api.WebsiteRootUrl}/beatmapsets/{beatmapSetInfo.OnlineID}"; + return $@"{api.EndpointConfiguration.WebsiteRootUrl}/beatmapsets/{beatmapSetInfo.OnlineID}"; } } } diff --git a/osu.Game/Online/API/APIAccess.cs b/osu.Game/Online/API/APIAccess.cs index f7fbacf76c..ef7b49868c 100644 --- a/osu.Game/Online/API/APIAccess.cs +++ b/osu.Game/Online/API/APIAccess.cs @@ -40,9 +40,7 @@ namespace osu.Game.Online.API private readonly Queue queue = new Queue(); - public string APIEndpointUrl { get; } - - public string WebsiteRootUrl { get; } + public EndpointConfiguration EndpointConfiguration { get; } /// /// The API response version. @@ -89,14 +87,13 @@ namespace osu.Game.Online.API APIVersion = now.Year * 10000 + now.Month * 100 + now.Day; } - APIEndpointUrl = endpointConfiguration.APIEndpointUrl; - WebsiteRootUrl = endpointConfiguration.WebsiteRootUrl; + EndpointConfiguration = endpointConfiguration; NotificationsClient = setUpNotificationsClient(); - authentication = new OAuth(endpointConfiguration.APIClientID, endpointConfiguration.APIClientSecret, APIEndpointUrl); + authentication = new OAuth(endpointConfiguration.APIClientID, endpointConfiguration.APIClientSecret, EndpointConfiguration.APIEndpointUrl); log = Logger.GetLogger(LoggingTarget.Network); - log.Add($@"API endpoint root: {APIEndpointUrl}"); + log.Add($@"API endpoint root: {EndpointConfiguration.APIEndpointUrl}"); log.Add($@"API request version: {APIVersion}"); ProvidedUsername = config.Get(OsuSetting.Username); @@ -408,7 +405,7 @@ namespace osu.Game.Online.API var req = new RegistrationRequest { - Url = $@"{APIEndpointUrl}/users", + Url = $@"{EndpointConfiguration.APIEndpointUrl}/users", Method = HttpMethod.Post, Username = username, Email = email, diff --git a/osu.Game/Online/API/APIRequest.cs b/osu.Game/Online/API/APIRequest.cs index 5cbe9040ba..575e6f8a10 100644 --- a/osu.Game/Online/API/APIRequest.cs +++ b/osu.Game/Online/API/APIRequest.cs @@ -71,7 +71,7 @@ namespace osu.Game.Online.API protected virtual WebRequest CreateWebRequest() => new OsuWebRequest(Uri); - protected virtual string Uri => $@"{API!.APIEndpointUrl}/api/v2/{Target}"; + protected virtual string Uri => $@"{API!.EndpointConfiguration.APIEndpointUrl}/api/v2/{Target}"; protected IAPIProvider? API; diff --git a/osu.Game/Online/API/DummyAPIAccess.cs b/osu.Game/Online/API/DummyAPIAccess.cs index 48c08afb8c..7b3a8f357b 100644 --- a/osu.Game/Online/API/DummyAPIAccess.cs +++ b/osu.Game/Online/API/DummyAPIAccess.cs @@ -41,9 +41,11 @@ namespace osu.Game.Online.API public string ProvidedUsername => LocalUser.Value.Username; - public string APIEndpointUrl => "http://localhost"; - - public string WebsiteRootUrl => "http://localhost"; + public EndpointConfiguration EndpointConfiguration { get; } = new EndpointConfiguration + { + APIEndpointUrl = "http://localhost", + WebsiteRootUrl = "http://localhost", + }; public int APIVersion => int.Parse(DateTime.Now.ToString("yyyyMMdd")); diff --git a/osu.Game/Online/API/IAPIProvider.cs b/osu.Game/Online/API/IAPIProvider.cs index 3b6763d736..048193def7 100644 --- a/osu.Game/Online/API/IAPIProvider.cs +++ b/osu.Game/Online/API/IAPIProvider.cs @@ -51,14 +51,9 @@ namespace osu.Game.Online.API string ProvidedUsername { get; } /// - /// The URL endpoint for this API. Does not include a trailing slash. + /// Holds configuration for online endpoints. /// - string APIEndpointUrl { get; } - - /// - /// The root URL of the website, excluding the trailing slash. - /// - string WebsiteRootUrl { get; } + EndpointConfiguration EndpointConfiguration { get; } /// /// The version of the API. diff --git a/osu.Game/Online/Chat/ExternalLinkOpener.cs b/osu.Game/Online/Chat/ExternalLinkOpener.cs index f76d42c96d..1615b72033 100644 --- a/osu.Game/Online/Chat/ExternalLinkOpener.cs +++ b/osu.Game/Online/Chat/ExternalLinkOpener.cs @@ -49,12 +49,12 @@ namespace osu.Game.Online.Chat if (url.StartsWith('/')) { - url = $"{api.WebsiteRootUrl}{url}"; + url = $"{api.EndpointConfiguration.WebsiteRootUrl}{url}"; isTrustedDomain = true; } else { - isTrustedDomain = url.StartsWith(api.WebsiteRootUrl, StringComparison.Ordinal); + isTrustedDomain = url.StartsWith(api.EndpointConfiguration.WebsiteRootUrl, StringComparison.Ordinal); } if (!url.CheckIsValidUrl()) diff --git a/osu.Game/Online/Chat/NowPlayingCommand.cs b/osu.Game/Online/Chat/NowPlayingCommand.cs index db44017a1b..5e71980a55 100644 --- a/osu.Game/Online/Chat/NowPlayingCommand.cs +++ b/osu.Game/Online/Chat/NowPlayingCommand.cs @@ -95,7 +95,7 @@ namespace osu.Game.Online.Chat string getBeatmapPart() { - return beatmapOnlineID > 0 ? $"[{api.WebsiteRootUrl}/b/{beatmapOnlineID} {beatmapDisplayTitle}]" : beatmapDisplayTitle; + return beatmapOnlineID > 0 ? $"[{api.EndpointConfiguration.WebsiteRootUrl}/b/{beatmapOnlineID} {beatmapDisplayTitle}]" : beatmapDisplayTitle; } string getRulesetPart() diff --git a/osu.Game/Online/EndpointConfiguration.cs b/osu.Game/Online/EndpointConfiguration.cs index bd3c945124..8f76da41fd 100644 --- a/osu.Game/Online/EndpointConfiguration.cs +++ b/osu.Game/Online/EndpointConfiguration.cs @@ -9,12 +9,12 @@ namespace osu.Game.Online public class EndpointConfiguration { /// - /// The base URL for the website. + /// The base URL for the website. Does not include a trailing slash. /// public string WebsiteRootUrl { get; set; } = string.Empty; /// - /// The endpoint for the main (osu-web) API. + /// The endpoint for the main (osu-web) API. Does not include a trailing slash. /// public string APIEndpointUrl { get; set; } = string.Empty; diff --git a/osu.Game/Online/Leaderboards/LeaderboardScore.cs b/osu.Game/Online/Leaderboards/LeaderboardScore.cs index 6acf236bf3..f7efa08969 100644 --- a/osu.Game/Online/Leaderboards/LeaderboardScore.cs +++ b/osu.Game/Online/Leaderboards/LeaderboardScore.cs @@ -436,7 +436,7 @@ namespace osu.Game.Online.Leaderboards items.Add(new OsuMenuItem("Use these mods", MenuItemType.Highlighted, () => songSelect.Mods.Value = Score.Mods)); if (Score.OnlineID > 0) - items.Add(new OsuMenuItem(CommonStrings.CopyLink, MenuItemType.Standard, () => clipboard?.SetText($@"{api.WebsiteRootUrl}/scores/{Score.OnlineID}"))); + items.Add(new OsuMenuItem(CommonStrings.CopyLink, MenuItemType.Standard, () => clipboard?.SetText($@"{api.EndpointConfiguration.WebsiteRootUrl}/scores/{Score.OnlineID}"))); if (Score.Files.Count > 0) { diff --git a/osu.Game/Overlays/Comments/DrawableComment.cs b/osu.Game/Overlays/Comments/DrawableComment.cs index d664a44be9..b06be3e74a 100644 --- a/osu.Game/Overlays/Comments/DrawableComment.cs +++ b/osu.Game/Overlays/Comments/DrawableComment.cs @@ -419,7 +419,7 @@ namespace osu.Game.Overlays.Comments private void copyUrl() { - clipboard.SetText($@"{api.APIEndpointUrl}/comments/{Comment.Id}"); + clipboard.SetText($@"{api.EndpointConfiguration.APIEndpointUrl}/comments/{Comment.Id}"); onScreenDisplay?.Display(new CopyUrlToast()); } diff --git a/osu.Game/Overlays/Login/LoginForm.cs b/osu.Game/Overlays/Login/LoginForm.cs index 0ff30da2a1..2b6d523b95 100644 --- a/osu.Game/Overlays/Login/LoginForm.cs +++ b/osu.Game/Overlays/Login/LoginForm.cs @@ -129,7 +129,7 @@ namespace osu.Game.Overlays.Login } }; - forgottenPasswordLink.AddLink(LayoutStrings.PopupLoginLoginForgot, $"{api.WebsiteRootUrl}/home/password-reset"); + forgottenPasswordLink.AddLink(LayoutStrings.PopupLoginLoginForgot, $"{api.EndpointConfiguration.WebsiteRootUrl}/home/password-reset"); password.OnCommit += (_, _) => performLogin(); diff --git a/osu.Game/Overlays/Login/SecondFactorAuthForm.cs b/osu.Game/Overlays/Login/SecondFactorAuthForm.cs index 506cb70d09..e36d62f827 100644 --- a/osu.Game/Overlays/Login/SecondFactorAuthForm.cs +++ b/osu.Game/Overlays/Login/SecondFactorAuthForm.cs @@ -98,7 +98,7 @@ namespace osu.Game.Overlays.Login explainText.AddParagraph(UserVerificationStrings.BoxInfoCheckSpam); // We can't support localisable strings with nested links yet. Not sure if we even can (probably need to allow markdown link formatting or something). explainText.AddParagraph("If you can't access your email or have forgotten what you used, please follow the "); - explainText.AddLink(UserVerificationStrings.BoxInfoRecoverLink, $"{api.WebsiteRootUrl}/home/password-reset"); + explainText.AddLink(UserVerificationStrings.BoxInfoRecoverLink, $"{api.EndpointConfiguration.WebsiteRootUrl}/home/password-reset"); explainText.AddText(". You can also "); explainText.AddLink(UserVerificationStrings.BoxInfoReissueLink, () => { diff --git a/osu.Game/Overlays/Profile/Header/BottomHeaderContainer.cs b/osu.Game/Overlays/Profile/Header/BottomHeaderContainer.cs index d5b4d844b2..d9d23f16fd 100644 --- a/osu.Game/Overlays/Profile/Header/BottomHeaderContainer.cs +++ b/osu.Game/Overlays/Profile/Header/BottomHeaderContainer.cs @@ -124,12 +124,12 @@ namespace osu.Game.Overlays.Profile.Header } topLinkContainer.AddText("Contributed "); - topLinkContainer.AddLink("forum post".ToQuantity(user.PostCount, "#,##0"), $"{api.WebsiteRootUrl}/users/{user.Id}/posts", creationParameters: embolden); + topLinkContainer.AddLink("forum post".ToQuantity(user.PostCount, "#,##0"), $"{api.EndpointConfiguration.WebsiteRootUrl}/users/{user.Id}/posts", creationParameters: embolden); addSpacer(topLinkContainer); topLinkContainer.AddText("Posted "); - topLinkContainer.AddLink("comment".ToQuantity(user.CommentsCount, "#,##0"), $"{api.WebsiteRootUrl}/comments?user_id={user.Id}", creationParameters: embolden); + topLinkContainer.AddLink("comment".ToQuantity(user.CommentsCount, "#,##0"), $"{api.EndpointConfiguration.WebsiteRootUrl}/comments?user_id={user.Id}", creationParameters: embolden); string websiteWithoutProtocol = user.Website; diff --git a/osu.Game/Overlays/Profile/Header/Components/DrawableTournamentBanner.cs b/osu.Game/Overlays/Profile/Header/Components/DrawableTournamentBanner.cs index c099009ca4..a66a5c8fe9 100644 --- a/osu.Game/Overlays/Profile/Header/Components/DrawableTournamentBanner.cs +++ b/osu.Game/Overlays/Profile/Header/Components/DrawableTournamentBanner.cs @@ -33,7 +33,7 @@ namespace osu.Game.Overlays.Profile.Header.Components Texture = textures.Get(banner.Image), }; - Action = () => game?.OpenUrlExternally($@"{api.WebsiteRootUrl}/community/tournaments/{banner.TournamentId}"); + Action = () => game?.OpenUrlExternally($@"{api.EndpointConfiguration.WebsiteRootUrl}/community/tournaments/{banner.TournamentId}"); } protected override void LoadComplete() diff --git a/osu.Game/Overlays/Profile/Header/TopHeaderContainer.cs b/osu.Game/Overlays/Profile/Header/TopHeaderContainer.cs index 165a576c03..fb1bdca57c 100644 --- a/osu.Game/Overlays/Profile/Header/TopHeaderContainer.cs +++ b/osu.Game/Overlays/Profile/Header/TopHeaderContainer.cs @@ -213,7 +213,7 @@ namespace osu.Game.Overlays.Profile.Header cover.User = user; avatar.User = user; usernameText.Text = user?.Username ?? string.Empty; - openUserExternally.Link = $@"{api.WebsiteRootUrl}/users/{user?.Id ?? 0}"; + openUserExternally.Link = $@"{api.EndpointConfiguration.WebsiteRootUrl}/users/{user?.Id ?? 0}"; userFlag.CountryCode = user?.CountryCode ?? default; userCountryText.Text = (user?.CountryCode ?? default).GetDescription(); userCountryContainer.Action = () => rankingsOverlay?.ShowCountry(user?.CountryCode ?? default); diff --git a/osu.Game/Overlays/Profile/Sections/Recent/DrawableRecentActivity.cs b/osu.Game/Overlays/Profile/Sections/Recent/DrawableRecentActivity.cs index 8a0003b4ea..a0bcf2dc47 100644 --- a/osu.Game/Overlays/Profile/Sections/Recent/DrawableRecentActivity.cs +++ b/osu.Game/Overlays/Profile/Sections/Recent/DrawableRecentActivity.cs @@ -223,7 +223,7 @@ namespace osu.Game.Overlays.Profile.Sections.Recent private void addBeatmapsetLink() => content.AddLink(activity.Beatmapset.AsNonNull().Title, LinkAction.OpenBeatmapSet, getLinkArgument(activity.Beatmapset.AsNonNull().Url), creationParameters: t => t.Font = getLinkFont()); - private object getLinkArgument(string url) => MessageFormatter.GetLinkDetails($"{api.WebsiteRootUrl}{url}").Argument.AsNonNull(); + private object getLinkArgument(string url) => MessageFormatter.GetLinkDetails($"{api.EndpointConfiguration.WebsiteRootUrl}{url}").Argument.AsNonNull(); private FontUsage getLinkFont(FontWeight fontWeight = FontWeight.Regular) => OsuFont.GetFont(size: font_size, weight: fontWeight, italics: true); diff --git a/osu.Game/Overlays/Wiki/WikiPanelContainer.cs b/osu.Game/Overlays/Wiki/WikiPanelContainer.cs index 555dab852e..773dde6436 100644 --- a/osu.Game/Overlays/Wiki/WikiPanelContainer.cs +++ b/osu.Game/Overlays/Wiki/WikiPanelContainer.cs @@ -54,7 +54,7 @@ namespace osu.Game.Overlays.Wiki Padding = new MarginPadding(padding), Child = new WikiPanelMarkdownContainer(isFullWidth) { - CurrentPath = $@"{api.WebsiteRootUrl}/wiki/", + CurrentPath = $@"{api.EndpointConfiguration.WebsiteRootUrl}/wiki/", Text = text, RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y diff --git a/osu.Game/Overlays/WikiOverlay.cs b/osu.Game/Overlays/WikiOverlay.cs index ef258da82b..c360d1eb9e 100644 --- a/osu.Game/Overlays/WikiOverlay.cs +++ b/osu.Game/Overlays/WikiOverlay.cs @@ -167,7 +167,7 @@ namespace osu.Game.Overlays } else { - LoadDisplay(articlePage = new WikiArticlePage($@"{api.WebsiteRootUrl}/wiki/{path.Value}/", response.Markdown)); + LoadDisplay(articlePage = new WikiArticlePage($@"{api.EndpointConfiguration.WebsiteRootUrl}/wiki/{path.Value}/", response.Markdown)); } } @@ -176,7 +176,7 @@ namespace osu.Game.Overlays wikiData.Value = null; path.Value = "error"; - LoadDisplay(articlePage = new WikiArticlePage($@"{api.WebsiteRootUrl}/wiki/", + LoadDisplay(articlePage = new WikiArticlePage($@"{api.EndpointConfiguration.WebsiteRootUrl}/wiki/", $"Something went wrong when trying to fetch page \"{originalPath}\".\n\n[Return to the main page]({INDEX_PATH}).")); } diff --git a/osu.Game/Screens/Edit/Submission/ScreenFrequentlyAskedQuestions.cs b/osu.Game/Screens/Edit/Submission/ScreenFrequentlyAskedQuestions.cs index c8d226bbcb..ff9cb07e2d 100644 --- a/osu.Game/Screens/Edit/Submission/ScreenFrequentlyAskedQuestions.cs +++ b/osu.Game/Screens/Edit/Submission/ScreenFrequentlyAskedQuestions.cs @@ -46,14 +46,14 @@ namespace osu.Game.Screens.Edit.Submission RelativeSizeAxes = Axes.X, Caption = BeatmapSubmissionStrings.MappingHelpForumDescription, ButtonText = BeatmapSubmissionStrings.MappingHelpForum, - Action = () => game?.OpenUrlExternally($@"{api.WebsiteRootUrl}/community/forums/56"), + Action = () => game?.OpenUrlExternally($@"{api.EndpointConfiguration.WebsiteRootUrl}/community/forums/56"), }, new FormButton { RelativeSizeAxes = Axes.X, Caption = BeatmapSubmissionStrings.ModdingQueuesForumDescription, ButtonText = BeatmapSubmissionStrings.ModdingQueuesForum, - Action = () => game?.OpenUrlExternally($@"{api.WebsiteRootUrl}/community/forums/60"), + Action = () => game?.OpenUrlExternally($@"{api.EndpointConfiguration.WebsiteRootUrl}/community/forums/60"), }, }, }); diff --git a/osu.Game/Screens/OnlinePlay/Lounge/Components/DrawableRoom.cs b/osu.Game/Screens/OnlinePlay/Lounge/Components/DrawableRoom.cs index 377c840d25..7b2e2c02f7 100644 --- a/osu.Game/Screens/OnlinePlay/Lounge/Components/DrawableRoom.cs +++ b/osu.Game/Screens/OnlinePlay/Lounge/Components/DrawableRoom.cs @@ -361,7 +361,7 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components return items.ToArray(); - string formatRoomUrl(long id) => $@"{api.WebsiteRootUrl}/multiplayer/rooms/{id}"; + string formatRoomUrl(long id) => $@"{api.EndpointConfiguration.WebsiteRootUrl}/multiplayer/rooms/{id}"; } } diff --git a/osu.Game/Screens/SelectV2/Leaderboards/LeaderboardScoreV2.cs b/osu.Game/Screens/SelectV2/Leaderboards/LeaderboardScoreV2.cs index 732fb2cd8c..2460fbe6f8 100644 --- a/osu.Game/Screens/SelectV2/Leaderboards/LeaderboardScoreV2.cs +++ b/osu.Game/Screens/SelectV2/Leaderboards/LeaderboardScoreV2.cs @@ -778,7 +778,7 @@ namespace osu.Game.Screens.SelectV2.Leaderboards items.Add(new OsuMenuItem("Use these mods", MenuItemType.Highlighted, () => SelectedMods.Value = score.Mods.Where(m => IsValidMod.Invoke(m)).ToArray())); if (score.OnlineID > 0) - items.Add(new OsuMenuItem(CommonStrings.CopyLink, MenuItemType.Standard, () => clipboard?.SetText($@"{api.WebsiteRootUrl}/scores/{score.OnlineID}"))); + items.Add(new OsuMenuItem(CommonStrings.CopyLink, MenuItemType.Standard, () => clipboard?.SetText($@"{api.EndpointConfiguration.WebsiteRootUrl}/scores/{score.OnlineID}"))); if (score.Files.Count <= 0) return items.ToArray(); From aaffd72032042834bb5b982fd9524a7427aa0f7c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 5 Feb 2025 11:48:07 +0100 Subject: [PATCH 0892/1275] Add beatmap submission service URL to endpoint configuration --- osu.Game/Online/EndpointConfiguration.cs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/osu.Game/Online/EndpointConfiguration.cs b/osu.Game/Online/EndpointConfiguration.cs index 8f76da41fd..39dd72d41a 100644 --- a/osu.Game/Online/EndpointConfiguration.cs +++ b/osu.Game/Online/EndpointConfiguration.cs @@ -42,5 +42,10 @@ namespace osu.Game.Online /// The endpoint for the SignalR metadata server. /// public string MetadataEndpointUrl { get; set; } = string.Empty; + + /// + /// The root URL for the service handling beatmap submission. Does not include a trailing slash. + /// + public string? BeatmapSubmissionServiceUrl { get; set; } } } From 8940ee5d9cb3a41a974d30ba3d3efa0dea74c751 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 5 Feb 2025 11:52:43 +0100 Subject: [PATCH 0893/1275] Add API request & response structures for beatmap submission --- .../Online/API/Requests/APIUploadRequest.cs | 26 ++++++ .../Requests/PatchBeatmapPackageRequest.cs | 51 ++++++++++++ .../API/Requests/PutBeatmapSetRequest.cs | 82 +++++++++++++++++++ .../Requests/ReplaceBeatmapPackageRequest.cs | 45 ++++++++++ .../Responses/PutBeatmapSetResponse.cs | 30 +++++++ 5 files changed, 234 insertions(+) create mode 100644 osu.Game/Online/API/Requests/APIUploadRequest.cs create mode 100644 osu.Game/Online/API/Requests/PatchBeatmapPackageRequest.cs create mode 100644 osu.Game/Online/API/Requests/PutBeatmapSetRequest.cs create mode 100644 osu.Game/Online/API/Requests/ReplaceBeatmapPackageRequest.cs create mode 100644 osu.Game/Online/API/Requests/Responses/PutBeatmapSetResponse.cs diff --git a/osu.Game/Online/API/Requests/APIUploadRequest.cs b/osu.Game/Online/API/Requests/APIUploadRequest.cs new file mode 100644 index 0000000000..3503b4cebb --- /dev/null +++ b/osu.Game/Online/API/Requests/APIUploadRequest.cs @@ -0,0 +1,26 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Diagnostics; +using osu.Framework.IO.Network; + +namespace osu.Game.Online.API.Requests +{ + public abstract class APIUploadRequest : APIRequest + { + protected override WebRequest CreateWebRequest() + { + var request = base.CreateWebRequest(); + request.UploadProgress += onUploadProgress; + return request; + } + + private void onUploadProgress(long current, long total) + { + Debug.Assert(API != null); + API.Schedule(() => Progressed?.Invoke(current, total)); + } + + public event APIProgressHandler? Progressed; + } +} diff --git a/osu.Game/Online/API/Requests/PatchBeatmapPackageRequest.cs b/osu.Game/Online/API/Requests/PatchBeatmapPackageRequest.cs new file mode 100644 index 0000000000..85981448da --- /dev/null +++ b/osu.Game/Online/API/Requests/PatchBeatmapPackageRequest.cs @@ -0,0 +1,51 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using System.Collections.Generic; +using System.Net.Http; +using osu.Framework.IO.Network; + +namespace osu.Game.Online.API.Requests +{ + public class PatchBeatmapPackageRequest : APIUploadRequest + { + protected override string Uri + { + get + { + // can be removed once the service has been successfully deployed to production + if (API!.EndpointConfiguration.BeatmapSubmissionServiceUrl == null) + throw new NotSupportedException("Beatmap submission not supported in this configuration!"); + + return $@"{API!.EndpointConfiguration.BeatmapSubmissionServiceUrl!}/beatmapsets/{BeatmapSetID}"; + } + } + + protected override string Target => throw new NotSupportedException(); + + public uint BeatmapSetID { get; } + public Dictionary FilesChanged { get; } = new Dictionary(); + public HashSet FilesDeleted { get; } = new HashSet(); + + public PatchBeatmapPackageRequest(uint beatmapSetId) + { + BeatmapSetID = beatmapSetId; + } + + protected override WebRequest CreateWebRequest() + { + var request = base.CreateWebRequest(); + request.Method = HttpMethod.Patch; + + foreach ((string filename, byte[] content) in FilesChanged) + request.AddFile(@"filesChanged", content, filename); + + foreach (string filename in FilesDeleted) + request.AddParameter(@"filesDeleted", filename, RequestParameterType.Form); + + request.Timeout = 60_000; + return request; + } + } +} diff --git a/osu.Game/Online/API/Requests/PutBeatmapSetRequest.cs b/osu.Game/Online/API/Requests/PutBeatmapSetRequest.cs new file mode 100644 index 0000000000..03b8397681 --- /dev/null +++ b/osu.Game/Online/API/Requests/PutBeatmapSetRequest.cs @@ -0,0 +1,82 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net.Http; +using Newtonsoft.Json; +using Newtonsoft.Json.Converters; +using osu.Framework.IO.Network; +using osu.Framework.Localisation; +using osu.Game.Localisation; +using osu.Game.Online.API.Requests.Responses; + +namespace osu.Game.Online.API.Requests +{ + public class PutBeatmapSetRequest : APIRequest + { + protected override string Uri + { + get + { + // can be removed once the service has been successfully deployed to production + if (API!.EndpointConfiguration.BeatmapSubmissionServiceUrl == null) + throw new NotSupportedException("Beatmap submission not supported in this configuration!"); + + return $@"{API!.EndpointConfiguration.BeatmapSubmissionServiceUrl}/beatmapsets"; + } + } + + protected override string Target => throw new NotSupportedException(); + + [JsonProperty("beatmapset_id")] + public uint? BeatmapSetID { get; init; } + + [JsonProperty("beatmaps_to_create")] + public uint BeatmapsToCreate { get; init; } + + [JsonProperty("beatmaps_to_keep")] + public uint[] BeatmapsToKeep { get; init; } = []; + + [JsonProperty("target")] + public BeatmapSubmissionTarget SubmissionTarget { get; init; } + + private PutBeatmapSetRequest() + { + } + + public static PutBeatmapSetRequest CreateNew(uint beatmapCount, BeatmapSubmissionTarget target) => new PutBeatmapSetRequest + { + BeatmapsToCreate = beatmapCount, + SubmissionTarget = target, + }; + + public static PutBeatmapSetRequest UpdateExisting(uint beatmapSetId, IEnumerable beatmapsToKeep, uint beatmapsToCreate, BeatmapSubmissionTarget target) => new PutBeatmapSetRequest + { + BeatmapSetID = beatmapSetId, + BeatmapsToKeep = beatmapsToKeep.ToArray(), + BeatmapsToCreate = beatmapsToCreate, + SubmissionTarget = target, + }; + + protected override WebRequest CreateWebRequest() + { + var req = base.CreateWebRequest(); + req.Method = HttpMethod.Put; + req.ContentType = @"application/json"; + req.AddRaw(JsonConvert.SerializeObject(this)); + return req; + } + } + + [JsonConverter(typeof(StringEnumConverter))] + public enum BeatmapSubmissionTarget + { + [LocalisableDescription(typeof(BeatmapSubmissionStrings), nameof(BeatmapSubmissionStrings.BeatmapSubmissionTargetWIP))] + WIP, + + [LocalisableDescription(typeof(BeatmapSubmissionStrings), nameof(BeatmapSubmissionStrings.BeatmapSubmissionTargetPending))] + Pending, + } +} diff --git a/osu.Game/Online/API/Requests/ReplaceBeatmapPackageRequest.cs b/osu.Game/Online/API/Requests/ReplaceBeatmapPackageRequest.cs new file mode 100644 index 0000000000..c9dd12d61e --- /dev/null +++ b/osu.Game/Online/API/Requests/ReplaceBeatmapPackageRequest.cs @@ -0,0 +1,45 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using System.Net.Http; +using osu.Framework.IO.Network; + +namespace osu.Game.Online.API.Requests +{ + public class ReplaceBeatmapPackageRequest : APIUploadRequest + { + protected override string Uri + { + get + { + // can be removed once the service has been successfully deployed to production + if (API!.EndpointConfiguration.BeatmapSubmissionServiceUrl == null) + throw new NotSupportedException("Beatmap submission not supported in this configuration!"); + + return $@"{API!.EndpointConfiguration.BeatmapSubmissionServiceUrl}/beatmapsets/{BeatmapSetID}"; + } + } + + protected override string Target => throw new NotSupportedException(); + + public uint BeatmapSetID { get; } + + private readonly byte[] oszPackage; + + public ReplaceBeatmapPackageRequest(uint beatmapSetID, byte[] oszPackage) + { + this.oszPackage = oszPackage; + BeatmapSetID = beatmapSetID; + } + + protected override WebRequest CreateWebRequest() + { + var request = base.CreateWebRequest(); + request.AddFile(@"beatmapArchive", oszPackage); + request.Method = HttpMethod.Put; + request.Timeout = 60_000; + return request; + } + } +} diff --git a/osu.Game/Online/API/Requests/Responses/PutBeatmapSetResponse.cs b/osu.Game/Online/API/Requests/Responses/PutBeatmapSetResponse.cs new file mode 100644 index 0000000000..e3ec617039 --- /dev/null +++ b/osu.Game/Online/API/Requests/Responses/PutBeatmapSetResponse.cs @@ -0,0 +1,30 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using System.Collections.Generic; +using Newtonsoft.Json; + +namespace osu.Game.Online.API.Requests.Responses +{ + public class PutBeatmapSetResponse + { + [JsonProperty("beatmapset_id")] + public uint BeatmapSetId { get; set; } + + [JsonProperty("beatmap_ids")] + public ICollection BeatmapIds { get; set; } = Array.Empty(); + + [JsonProperty("files")] + public ICollection Files { get; set; } = Array.Empty(); + } + + public struct BeatmapSetFile + { + [JsonProperty("filename")] + public string Filename { get; set; } + + [JsonProperty("sha2_hash")] + public string SHA2Hash { get; set; } + } +} From b6731ff7738ede0985297fd69d5b32a82c66bdfe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 5 Feb 2025 11:34:13 +0100 Subject: [PATCH 0894/1275] Add completion flag to `WizardOverlay` --- osu.Game/Overlays/WizardOverlay.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/osu.Game/Overlays/WizardOverlay.cs b/osu.Game/Overlays/WizardOverlay.cs index 34ffa7bd77..2a881045fd 100644 --- a/osu.Game/Overlays/WizardOverlay.cs +++ b/osu.Game/Overlays/WizardOverlay.cs @@ -45,6 +45,8 @@ namespace osu.Game.Overlays private LoadingSpinner loading = null!; private ScheduledDelegate? loadingShowDelegate; + public bool Completed { get; private set; } + protected WizardOverlay(OverlayColourScheme scheme) : base(scheme) { @@ -221,6 +223,7 @@ namespace osu.Game.Overlays else { CurrentStepIndex = null; + Completed = true; Hide(); } From fff99a8b4008800ce5a870ac600618e84d8ffdc5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 5 Feb 2025 11:54:26 +0100 Subject: [PATCH 0895/1275] Implement special exporter intended specifically for submission flows --- osu.Game/Database/LegacyBeatmapExporter.cs | 23 +++++--- .../Submission/SubmissionBeatmapExporter.cs | 58 +++++++++++++++++++ 2 files changed, 72 insertions(+), 9 deletions(-) create mode 100644 osu.Game/Screens/Edit/Submission/SubmissionBeatmapExporter.cs diff --git a/osu.Game/Database/LegacyBeatmapExporter.cs b/osu.Game/Database/LegacyBeatmapExporter.cs index 8f94fc9e63..e7e5ddb4d2 100644 --- a/osu.Game/Database/LegacyBeatmapExporter.cs +++ b/osu.Game/Database/LegacyBeatmapExporter.cs @@ -61,6 +61,20 @@ namespace osu.Game.Database Configuration = new LegacySkinDecoder().Decode(skinStreamReader) }; + MutateBeatmap(model, playableBeatmap); + + // Encode to legacy format + var stream = new MemoryStream(); + using (var sw = new StreamWriter(stream, Encoding.UTF8, 1024, true)) + new LegacyBeatmapEncoder(playableBeatmap, beatmapSkin).Encode(sw); + + stream.Seek(0, SeekOrigin.Begin); + + return stream; + } + + protected virtual void MutateBeatmap(BeatmapSetInfo beatmapSet, IBeatmap playableBeatmap) + { // Convert beatmap elements to be compatible with legacy format // So we truncate time and position values to integers, and convert paths with multiple segments to Bézier curves @@ -145,15 +159,6 @@ namespace osu.Game.Database hasPath.Path.ControlPoints.Add(new PathControlPoint(position)); } } - - // Encode to legacy format - var stream = new MemoryStream(); - using (var sw = new StreamWriter(stream, Encoding.UTF8, 1024, true)) - new LegacyBeatmapEncoder(playableBeatmap, beatmapSkin).Encode(sw); - - stream.Seek(0, SeekOrigin.Begin); - - return stream; } protected override string FileExtension => @".osz"; diff --git a/osu.Game/Screens/Edit/Submission/SubmissionBeatmapExporter.cs b/osu.Game/Screens/Edit/Submission/SubmissionBeatmapExporter.cs new file mode 100644 index 0000000000..3c50a1bf80 --- /dev/null +++ b/osu.Game/Screens/Edit/Submission/SubmissionBeatmapExporter.cs @@ -0,0 +1,58 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using System.Collections.Generic; +using System.Linq; +using osu.Framework.Platform; +using osu.Game.Beatmaps; +using osu.Game.Database; +using osu.Game.Online.API.Requests.Responses; + +namespace osu.Game.Screens.Edit.Submission +{ + public class SubmissionBeatmapExporter : LegacyBeatmapExporter + { + private readonly uint? beatmapSetId; + private readonly HashSet? beatmapIds; + + public SubmissionBeatmapExporter(Storage storage) + : base(storage) + { + } + + public SubmissionBeatmapExporter(Storage storage, PutBeatmapSetResponse putBeatmapSetResponse) + : base(storage) + { + beatmapSetId = putBeatmapSetResponse.BeatmapSetId; + beatmapIds = putBeatmapSetResponse.BeatmapIds.Select(id => (int)id).ToHashSet(); + } + + protected override void MutateBeatmap(BeatmapSetInfo beatmapSet, IBeatmap playableBeatmap) + { + base.MutateBeatmap(beatmapSet, playableBeatmap); + + if (beatmapSetId != null && beatmapIds != null) + { + playableBeatmap.BeatmapInfo.BeatmapSet = beatmapSet; + playableBeatmap.BeatmapInfo.BeatmapSet!.OnlineID = (int)beatmapSetId; + + if (beatmapIds.Contains(playableBeatmap.BeatmapInfo.OnlineID)) + { + beatmapIds.Remove(playableBeatmap.BeatmapInfo.OnlineID); + return; + } + + if (playableBeatmap.BeatmapInfo.OnlineID > 0) + throw new InvalidOperationException(@"Encountered beatmap with ID that has not been assigned to it by the server!"); + + if (beatmapIds.Count == 0) + throw new InvalidOperationException(@"Ran out of new beatmap IDs to assign to unsubmitted beatmaps!"); + + int newId = beatmapIds.First(); + beatmapIds.Remove(newId); + playableBeatmap.BeatmapInfo.OnlineID = newId; + } + } + } +} From 78e85dc2c7f773ac8cbde2b226ec6ba9b8791672 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 5 Feb 2025 12:22:33 +0100 Subject: [PATCH 0896/1275] Add beatmap submission support --- .../Localisation/BeatmapSubmissionStrings.cs | 40 ++ osu.Game/Localisation/EditorStrings.cs | 10 + osu.Game/Screens/Edit/Editor.cs | 55 ++- .../Submission/BeatmapSubmissionScreen.cs | 422 ++++++++++++++++++ .../Submission/BeatmapSubmissionSettings.cs | 13 + .../Submission/ScreenSubmissionSettings.cs | 15 +- 6 files changed, 544 insertions(+), 11 deletions(-) create mode 100644 osu.Game/Screens/Edit/Submission/BeatmapSubmissionScreen.cs create mode 100644 osu.Game/Screens/Edit/Submission/BeatmapSubmissionSettings.cs diff --git a/osu.Game/Localisation/BeatmapSubmissionStrings.cs b/osu.Game/Localisation/BeatmapSubmissionStrings.cs index a4c2b36894..50b65ab572 100644 --- a/osu.Game/Localisation/BeatmapSubmissionStrings.cs +++ b/osu.Game/Localisation/BeatmapSubmissionStrings.cs @@ -39,6 +39,31 @@ namespace osu.Game.Localisation /// public static LocalisableString SubmissionSettings => new TranslatableString(getKey(@"submission_settings"), @"Submission settings"); + /// + /// "Submit beatmap!" + /// + public static LocalisableString ConfirmSubmission => new TranslatableString(getKey(@"confirm_submission"), @"Submit beatmap!"); + + /// + /// "Exporting beatmap set in compatibility mode..." + /// + public static LocalisableString ExportingBeatmapSet => new TranslatableString(getKey(@"exporting_beatmap_set"), @"Exporting beatmap set in compatibility mode..."); + + /// + /// "Preparing beatmap set online..." + /// + public static LocalisableString PreparingBeatmapSet => new TranslatableString(getKey(@"preparing_beatmap_set"), @"Preparing beatmap set online..."); + + /// + /// "Uploading beatmap set contents..." + /// + public static LocalisableString UploadingBeatmapSetContents => new TranslatableString(getKey(@"uploading_beatmap_set_contents"), @"Uploading beatmap set contents..."); + + /// + /// "Updating local beatmap with relevant changes..." + /// + public static LocalisableString UpdatingLocalBeatmap => new TranslatableString(getKey(@"updating_local_beatmap"), @"Updating local beatmap with relevant changes..."); + /// /// "Before you continue, we ask you to check whether the content you are uploading has been cleared for upload. Please understand that you are responsible for the content you upload to the platform and if in doubt, should ask permission from the creators before uploading!" /// @@ -119,6 +144,21 @@ namespace osu.Game.Localisation /// public static LocalisableString LegacyExportDisclaimer => new TranslatableString(getKey(@"legacy_export_disclaimer"), @"Note: In order to make it possible for users of all osu! versions to enjoy your beatmap, it will be exported in a backwards-compatible format. While we have made efforts to ensure that that process keeps the beatmap playable in its intended form, some data related to features that previous versions of osu! do not support may be lost."); + /// + /// "Empty beatmaps cannot be submitted." + /// + public static LocalisableString EmptyBeatmapsCannotBeSubmitted => new TranslatableString(getKey(@"empty_beatmaps_cannot_be_submitted"), @"Empty beatmaps cannot be submitted."); + + /// + /// "Update beatmap!" + /// + public static LocalisableString UpdateBeatmap => new TranslatableString(getKey(@"update_beatmap"), @"Update beatmap!"); + + /// + /// "Upload NEW beatmap!" + /// + public static LocalisableString UploadNewBeatmap => new TranslatableString(getKey(@"upload_new_beatmap"), @"Upload NEW beatmap!"); + private static string getKey(string key) => $@"{prefix}:{key}"; } } diff --git a/osu.Game/Localisation/EditorStrings.cs b/osu.Game/Localisation/EditorStrings.cs index 3b4026be11..2c834c38bb 100644 --- a/osu.Game/Localisation/EditorStrings.cs +++ b/osu.Game/Localisation/EditorStrings.cs @@ -69,6 +69,16 @@ namespace osu.Game.Localisation /// public static LocalisableString DeleteDifficulty => new TranslatableString(getKey(@"delete_difficulty"), @"Delete difficulty"); + /// + /// "Edit externally" + /// + public static LocalisableString EditExternally => new TranslatableString(getKey(@"edit_externally"), @"Edit externally"); + + /// + /// "Submit beatmap" + /// + public static LocalisableString SubmitBeatmap => new TranslatableString(getKey(@"submit_beatmap"), @"Submit beatmap"); + /// /// "setup" /// diff --git a/osu.Game/Screens/Edit/Editor.cs b/osu.Game/Screens/Edit/Editor.cs index 3302fafbb8..c2a7264243 100644 --- a/osu.Game/Screens/Edit/Editor.cs +++ b/osu.Game/Screens/Edit/Editor.cs @@ -32,6 +32,7 @@ using osu.Game.Beatmaps; using osu.Game.Beatmaps.ControlPoints; using osu.Game.Configuration; using osu.Game.Database; +using osu.Game.Extensions; using osu.Game.Graphics.Cursor; using osu.Game.Graphics.UserInterface; using osu.Game.Input.Bindings; @@ -52,6 +53,7 @@ using osu.Game.Screens.Edit.Compose.Components.Timeline; using osu.Game.Screens.Edit.Design; using osu.Game.Screens.Edit.GameplayTest; using osu.Game.Screens.Edit.Setup; +using osu.Game.Screens.Edit.Submission; using osu.Game.Screens.Edit.Timing; using osu.Game.Screens.Edit.Verify; using osu.Game.Screens.OnlinePlay; @@ -111,6 +113,10 @@ namespace osu.Game.Screens.Edit [Resolved(canBeNull: true)] private INotificationOverlay notifications { get; set; } + [Resolved(canBeNull: true)] + [CanBeNull] + private LoginOverlay loginOverlay { get; set; } + [Resolved] private RealmAccess realm { get; set; } @@ -1309,11 +1315,22 @@ namespace osu.Game.Screens.Edit if (RuntimeInfo.IsDesktop) { - var externalEdit = new EditorMenuItem("Edit externally", MenuItemType.Standard, editExternally); + var externalEdit = new EditorMenuItem(EditorStrings.EditExternally, MenuItemType.Standard, editExternally); saveRelatedMenuItems.Add(externalEdit); yield return externalEdit; } + bool isSetMadeOfLegacyRulesetBeatmaps = (isNewBeatmap && Ruleset.Value.IsLegacyRuleset()) + || (!isNewBeatmap && Beatmap.Value.BeatmapSetInfo.Beatmaps.All(b => b.Ruleset.IsLegacyRuleset())); + bool submissionAvailable = api.EndpointConfiguration.BeatmapSubmissionServiceUrl != null; + + if (isSetMadeOfLegacyRulesetBeatmaps && submissionAvailable) + { + var upload = new EditorMenuItem(EditorStrings.SubmitBeatmap, MenuItemType.Standard, submitBeatmap); + saveRelatedMenuItems.Add(upload); + yield return upload; + } + yield return new OsuMenuItemSpacer(); yield return new EditorMenuItem(CommonStrings.Exit, MenuItemType.Standard, this.Exit); } @@ -1353,6 +1370,42 @@ namespace osu.Game.Screens.Edit } } + private void submitBeatmap() + { + if (api.State.Value != APIState.Online) + { + loginOverlay?.Show(); + return; + } + + if (!editorBeatmap.HitObjects.Any()) + { + notifications?.Post(new SimpleNotification + { + Text = BeatmapSubmissionStrings.EmptyBeatmapsCannotBeSubmitted, + }); + return; + } + + if (HasUnsavedChanges) + { + dialogOverlay.Push(new SaveRequiredPopupDialog(() => attemptMutationOperation(() => + { + if (!Save()) + return false; + + startSubmission(); + return true; + }))); + } + else + { + startSubmission(); + } + + void startSubmission() => this.Push(new BeatmapSubmissionScreen()); + } + private void exportBeatmap(bool legacy) { if (HasUnsavedChanges) diff --git a/osu.Game/Screens/Edit/Submission/BeatmapSubmissionScreen.cs b/osu.Game/Screens/Edit/Submission/BeatmapSubmissionScreen.cs new file mode 100644 index 0000000000..796d975e4f --- /dev/null +++ b/osu.Game/Screens/Edit/Submission/BeatmapSubmissionScreen.cs @@ -0,0 +1,422 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Threading.Tasks; +using osu.Framework.Allocation; +using osu.Framework.Development; +using osu.Framework.Extensions; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; +using osu.Framework.Logging; +using osu.Framework.Platform; +using osu.Framework.Screens; +using osu.Game.Beatmaps; +using osu.Game.Beatmaps.Drawables.Cards; +using osu.Game.Configuration; +using osu.Game.Database; +using osu.Game.Extensions; +using osu.Game.Graphics.UserInterfaceV2; +using osu.Game.IO.Archives; +using osu.Game.Localisation; +using osu.Game.Online.API; +using osu.Game.Online.API.Requests; +using osu.Game.Online.API.Requests.Responses; +using osu.Game.Overlays; +using osu.Game.Overlays.Notifications; +using osu.Game.Screens.Menu; +using osuTK; + +namespace osu.Game.Screens.Edit.Submission +{ + public partial class BeatmapSubmissionScreen : OsuScreen + { + private BeatmapSubmissionOverlay overlay = null!; + + public override bool AllowUserExit => false; + + [Cached] + private readonly OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Aquamarine); + + [Resolved] + private RealmAccess realmAccess { get; set; } = null!; + + [Resolved] + private Storage storage { get; set; } = null!; + + [Resolved] + private IAPIProvider api { get; set; } = null!; + + [Resolved] + private OsuConfigManager configManager { get; set; } = null!; + + [Resolved] + private OsuGame? game { get; set; } + + [Resolved] + private BeatmapManager beatmaps { get; set; } = null!; + + [Cached] + private BeatmapSubmissionSettings settings { get; } = new BeatmapSubmissionSettings(); + + private Container submissionProgress = null!; + private SubmissionStageProgress exportStep = null!; + private SubmissionStageProgress createSetStep = null!; + private SubmissionStageProgress uploadStep = null!; + private SubmissionStageProgress updateStep = null!; + private Container successContainer = null!; + private Container flashLayer = null!; + private RoundedButton backButton = null!; + + private uint? beatmapSetId; + + private SubmissionBeatmapExporter legacyBeatmapExporter = null!; + private ProgressNotification? exportProgressNotification; + private MemoryStream beatmapPackageStream = null!; + private ProgressNotification? updateProgressNotification; + + [BackgroundDependencyLoader] + private void load() + { + AddRangeInternal(new Drawable[] + { + overlay = new BeatmapSubmissionOverlay(), + submissionProgress = new Container + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Alpha = 0, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Width = 0.6f, + Masking = true, + CornerRadius = 10, + Children = new Drawable[] + { + new Box + { + RelativeSizeAxes = Axes.Both, + Colour = colourProvider.Background5, + }, + new FillFlowContainer + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Direction = FillDirection.Vertical, + Padding = new MarginPadding(20), + Spacing = new Vector2(5), + Children = new Drawable[] + { + createSetStep = new SubmissionStageProgress + { + StageDescription = BeatmapSubmissionStrings.PreparingBeatmapSet, + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre, + }, + exportStep = new SubmissionStageProgress + { + StageDescription = BeatmapSubmissionStrings.ExportingBeatmapSet, + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre, + }, + uploadStep = new SubmissionStageProgress + { + StageDescription = BeatmapSubmissionStrings.UploadingBeatmapSetContents, + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre, + }, + updateStep = new SubmissionStageProgress + { + StageDescription = BeatmapSubmissionStrings.UpdatingLocalBeatmap, + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre, + }, + successContainer = new Container + { + Padding = new MarginPadding(20), + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre, + AutoSizeAxes = Axes.Both, + AutoSizeDuration = 500, + AutoSizeEasing = Easing.OutQuint, + Masking = true, + CornerRadius = BeatmapCard.CORNER_RADIUS, + Child = flashLayer = new Container + { + RelativeSizeAxes = Axes.Both, + Masking = true, + CornerRadius = BeatmapCard.CORNER_RADIUS, + Depth = float.MinValue, + Alpha = 0, + Child = new Box + { + RelativeSizeAxes = Axes.Both, + } + } + }, + backButton = new RoundedButton + { + Text = CommonStrings.Back, + Width = 150, + Action = this.Exit, + Enabled = { Value = false }, + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre, + } + } + } + } + } + }); + + overlay.State.BindValueChanged(_ => + { + if (overlay.State.Value == Visibility.Hidden) + { + if (!overlay.Completed) + this.Exit(); + else + { + submissionProgress.FadeIn(200, Easing.OutQuint); + createBeatmapSet(); + } + } + }); + beatmapPackageStream = new MemoryStream(); + } + + private void createBeatmapSet() + { + bool beatmapHasOnlineId = Beatmap.Value.BeatmapSetInfo.OnlineID > 0; + + var createRequest = beatmapHasOnlineId + ? PutBeatmapSetRequest.UpdateExisting( + (uint)Beatmap.Value.BeatmapSetInfo.OnlineID, + Beatmap.Value.BeatmapSetInfo.Beatmaps.Where(b => b.OnlineID > 0).Select(b => (uint)b.OnlineID).ToArray(), + (uint)Beatmap.Value.BeatmapSetInfo.Beatmaps.Count(b => b.OnlineID <= 0), + settings.Target.Value) + : PutBeatmapSetRequest.CreateNew((uint)Beatmap.Value.BeatmapSetInfo.Beatmaps.Count, settings.Target.Value); + + createRequest.Success += async response => + { + createSetStep.SetCompleted(); + beatmapSetId = response.BeatmapSetId; + + // at this point the set has an assigned online ID. + // it's important to proactively store it to the realm database, + // so that in the event in further failures in the process, the online ID is not lost. + // losing it can incur creation of redundant new sets server-side, or even cause online ID confusion. + if (!beatmapHasOnlineId) + { + await realmAccess.WriteAsync(r => + { + var refetchedSet = r.Find(Beatmap.Value.BeatmapSetInfo.ID); + refetchedSet!.OnlineID = (int)beatmapSetId.Value; + }).ConfigureAwait(true); + } + + legacyBeatmapExporter = new SubmissionBeatmapExporter(storage, response); + await createBeatmapPackage(response.Files).ConfigureAwait(true); + }; + createRequest.Failure += ex => + { + createSetStep.SetFailed(ex.Message); + backButton.Enabled.Value = true; + Logger.Log($"Beatmap set submission failed on creation: {ex}"); + }; + + createSetStep.SetInProgress(); + api.Queue(createRequest); + } + + private async Task createBeatmapPackage(ICollection onlineFiles) + { + Debug.Assert(ThreadSafety.IsUpdateThread); + exportStep.SetInProgress(); + + try + { + await legacyBeatmapExporter.ExportToStreamAsync(Beatmap.Value.BeatmapSetInfo.ToLive(realmAccess), beatmapPackageStream, exportProgressNotification = new ProgressNotification()) + .ConfigureAwait(true); + } + catch (Exception ex) + { + exportStep.SetFailed(ex.Message); + Logger.Log($"Beatmap set submission failed on export: {ex}"); + backButton.Enabled.Value = true; + exportProgressNotification = null; + } + + exportStep.SetCompleted(); + exportProgressNotification = null; + + if (onlineFiles.Count > 0) + await patchBeatmapSet(onlineFiles).ConfigureAwait(true); + else + replaceBeatmapSet(); + } + + private async Task patchBeatmapSet(ICollection onlineFiles) + { + Debug.Assert(beatmapSetId != null); + + var onlineFilesByFilename = onlineFiles.ToDictionary(f => f.Filename, f => f.SHA2Hash); + + // disposing the `ArchiveReader` makes the underlying stream no longer readable which we don't want. + // make a local copy to defend against it. + using var archiveReader = new ZipArchiveReader(new MemoryStream(beatmapPackageStream.ToArray())); + var filesToUpdate = new HashSet(); + + foreach (string filename in archiveReader.Filenames) + { + string localHash = archiveReader.GetStream(filename).ComputeSHA2Hash(); + + if (!onlineFilesByFilename.Remove(filename, out string? onlineHash)) + { + filesToUpdate.Add(filename); + continue; + } + + if (localHash != onlineHash) + filesToUpdate.Add(filename); + } + + var changedFiles = new Dictionary(); + + foreach (string file in filesToUpdate) + changedFiles.Add(file, await archiveReader.GetStream(file).ReadAllBytesToArrayAsync().ConfigureAwait(true)); + + var patchRequest = new PatchBeatmapPackageRequest(beatmapSetId.Value); + patchRequest.FilesChanged.AddRange(changedFiles); + patchRequest.FilesDeleted.AddRange(onlineFilesByFilename.Keys); + patchRequest.Success += async () => + { + uploadStep.SetCompleted(); + + if (configManager.Get(OsuSetting.EditorSubmissionLoadInBrowserAfterSubmission)) + game?.OpenUrlExternally($"{api.EndpointConfiguration.WebsiteRootUrl}/beatmapsets/{beatmapSetId}"); + + await updateLocalBeatmap().ConfigureAwait(true); + }; + patchRequest.Failure += ex => + { + uploadStep.SetFailed(ex.Message); + Logger.Log($"Beatmap submission failed on upload: {ex}"); + backButton.Enabled.Value = true; + }; + patchRequest.Progressed += (current, total) => uploadStep.SetInProgress((float)current / total); + + api.Queue(patchRequest); + uploadStep.SetInProgress(); + } + + private void replaceBeatmapSet() + { + Debug.Assert(beatmapSetId != null); + + var uploadRequest = new ReplaceBeatmapPackageRequest(beatmapSetId.Value, beatmapPackageStream.ToArray()); + + uploadRequest.Success += async () => + { + uploadStep.SetCompleted(); + + if (configManager.Get(OsuSetting.EditorSubmissionLoadInBrowserAfterSubmission)) + game?.OpenUrlExternally($"{api.EndpointConfiguration.WebsiteRootUrl}/beatmapsets/{beatmapSetId}"); + + await updateLocalBeatmap().ConfigureAwait(true); + }; + uploadRequest.Failure += ex => + { + uploadStep.SetFailed(ex.Message); + Logger.Log($"Beatmap submission failed on upload: {ex}"); + backButton.Enabled.Value = true; + }; + uploadRequest.Progressed += (current, total) => uploadStep.SetInProgress((float)current / Math.Max(total, 1)); + + api.Queue(uploadRequest); + uploadStep.SetInProgress(); + } + + private async Task updateLocalBeatmap() + { + Debug.Assert(beatmapSetId != null); + updateStep.SetInProgress(); + + Live? importedSet; + + try + { + importedSet = await beatmaps.ImportAsUpdate( + updateProgressNotification = new ProgressNotification(), + new ImportTask(beatmapPackageStream, $"{beatmapSetId}.osz"), + Beatmap.Value.BeatmapSetInfo).ConfigureAwait(true); + } + catch (Exception ex) + { + updateStep.SetFailed(ex.Message); + Logger.Log($"Beatmap submission failed on local update: {ex}"); + Schedule(() => backButton.Enabled.Value = true); + return; + } + + updateStep.SetCompleted(); + backButton.Enabled.Value = true; + backButton.Action = () => + { + game?.PerformFromScreen(s => + { + if (s is OsuScreen osuScreen) + { + Debug.Assert(importedSet != null); + var targetBeatmap = importedSet.Value.Beatmaps.FirstOrDefault(b => b.DifficultyName == Beatmap.Value.BeatmapInfo.DifficultyName) + ?? importedSet.Value.Beatmaps.First(); + osuScreen.Beatmap.Value = beatmaps.GetWorkingBeatmap(targetBeatmap); + } + + s.Push(new EditorLoader()); + }, [typeof(MainMenu)]); + }; + showBeatmapCard(); + } + + private void showBeatmapCard() + { + Debug.Assert(beatmapSetId != null); + + var getBeatmapSetRequest = new GetBeatmapSetRequest((int)beatmapSetId.Value); + getBeatmapSetRequest.Success += beatmapSet => + { + LoadComponentAsync(new BeatmapCardExtra(beatmapSet, false), loaded => + { + successContainer.Add(loaded); + flashLayer.FadeOutFromOne(2000, Easing.OutQuint); + }); + }; + + api.Queue(getBeatmapSetRequest); + } + + protected override void Update() + { + base.Update(); + + if (exportProgressNotification != null && exportProgressNotification.Ongoing) + exportStep.SetInProgress(exportProgressNotification.Progress); + + if (updateProgressNotification != null && updateProgressNotification.Ongoing) + updateStep.SetInProgress(updateProgressNotification.Progress); + } + + public override void OnEntering(ScreenTransitionEvent e) + { + base.OnEntering(e); + + overlay.Show(); + } + } +} diff --git a/osu.Game/Screens/Edit/Submission/BeatmapSubmissionSettings.cs b/osu.Game/Screens/Edit/Submission/BeatmapSubmissionSettings.cs new file mode 100644 index 0000000000..359dc11f39 --- /dev/null +++ b/osu.Game/Screens/Edit/Submission/BeatmapSubmissionSettings.cs @@ -0,0 +1,13 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Bindables; +using osu.Game.Online.API.Requests; + +namespace osu.Game.Screens.Edit.Submission +{ + public class BeatmapSubmissionSettings + { + public Bindable Target { get; } = new Bindable(); + } +} diff --git a/osu.Game/Screens/Edit/Submission/ScreenSubmissionSettings.cs b/osu.Game/Screens/Edit/Submission/ScreenSubmissionSettings.cs index 72da94afa1..08b4d9f712 100644 --- a/osu.Game/Screens/Edit/Submission/ScreenSubmissionSettings.cs +++ b/osu.Game/Screens/Edit/Submission/ScreenSubmissionSettings.cs @@ -11,6 +11,7 @@ using osu.Game.Graphics; using osu.Game.Graphics.Containers; using osu.Game.Graphics.UserInterfaceV2; using osu.Game.Localisation; +using osu.Game.Online.API.Requests; using osu.Game.Overlays; using osuTK; @@ -22,8 +23,10 @@ namespace osu.Game.Screens.Edit.Submission private readonly BindableBool notifyOnDiscussionReplies = new BindableBool(); private readonly BindableBool loadInBrowserAfterSubmission = new BindableBool(); + public override LocalisableString? NextStepText => BeatmapSubmissionStrings.ConfirmSubmission; + [BackgroundDependencyLoader] - private void load(OsuConfigManager configManager, OsuColour colours) + private void load(OsuConfigManager configManager, OsuColour colours, BeatmapSubmissionSettings settings) { configManager.BindWith(OsuSetting.EditorSubmissionNotifyOnDiscussionReplies, notifyOnDiscussionReplies); configManager.BindWith(OsuSetting.EditorSubmissionLoadInBrowserAfterSubmission, loadInBrowserAfterSubmission); @@ -39,6 +42,7 @@ namespace osu.Game.Screens.Edit.Submission { RelativeSizeAxes = Axes.X, Caption = BeatmapSubmissionStrings.BeatmapSubmissionTargetCaption, + Current = settings.Target, }, new FormCheckBox { @@ -60,14 +64,5 @@ namespace osu.Game.Screens.Edit.Submission } }); } - - private enum BeatmapSubmissionTarget - { - [LocalisableDescription(typeof(BeatmapSubmissionStrings), nameof(BeatmapSubmissionStrings.BeatmapSubmissionTargetWIP))] - WIP, - - [LocalisableDescription(typeof(BeatmapSubmissionStrings), nameof(BeatmapSubmissionStrings.BeatmapSubmissionTargetPending))] - Pending, - } } } From 206b5c93c0a8eb43c89d8fb8bc909f2e3aea9ab7 Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Wed, 5 Feb 2025 07:15:53 -0500 Subject: [PATCH 0897/1275] Implement beatmap set header design --- .../TestSceneBeatmapCarouselSetPanel.cs | 90 ++++++ .../TestSceneUpdateBeatmapSetButtonV2.cs | 62 ++++ .../Drawables/DifficultySpectrumDisplay.cs | 69 ++-- osu.Game/Screens/SelectV2/BeatmapSetPanel.cs | 297 +++++++++++++++--- .../SelectV2/BeatmapSetPanelBackground.cs | 108 +++++++ osu.Game/Screens/SelectV2/TopLocalRankV2.cs | 108 +++++++ .../SelectV2/UpdateBeatmapSetButtonV2.cs | 198 ++++++++++++ 7 files changed, 860 insertions(+), 72 deletions(-) create mode 100644 osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselSetPanel.cs create mode 100644 osu.Game.Tests/Visual/SongSelectV2/TestSceneUpdateBeatmapSetButtonV2.cs create mode 100644 osu.Game/Screens/SelectV2/BeatmapSetPanelBackground.cs create mode 100644 osu.Game/Screens/SelectV2/TopLocalRankV2.cs create mode 100644 osu.Game/Screens/SelectV2/UpdateBeatmapSetButtonV2.cs diff --git a/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselSetPanel.cs b/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselSetPanel.cs new file mode 100644 index 0000000000..6b981d7b33 --- /dev/null +++ b/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselSetPanel.cs @@ -0,0 +1,90 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Linq; +using NUnit.Framework; +using osu.Framework.Allocation; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Utils; +using osu.Game.Beatmaps; +using osu.Game.Overlays; +using osu.Game.Screens.SelectV2; +using osu.Game.Tests.Resources; +using osu.Game.Tests.Visual.UserInterface; +using osuTK; + +namespace osu.Game.Tests.Visual.SongSelectV2 +{ + public partial class TestSceneBeatmapCarouselSetPanel : ThemeComparisonTestScene + { + [Resolved] + private BeatmapManager beatmaps { get; set; } = null!; + + private BeatmapSetInfo beatmapSet = null!; + + public TestSceneBeatmapCarouselSetPanel() + : base(false) + { + } + + [Test] + public void TestDisplay() + { + AddStep("set beatmap", () => + { + beatmapSet = beatmaps.GetAllUsableBeatmapSets().FirstOrDefault(b => b.OnlineID == 241526) + ?? beatmaps.GetAllUsableBeatmapSets().FirstOrDefault(b => !b.Protected) + ?? TestResources.CreateTestBeatmapSetInfo(); + CreateThemedContent(OverlayColourScheme.Aquamarine); + }); + } + + [Test] + public void TestRandomBeatmap() + { + AddStep("random beatmap", () => + { + beatmapSet = beatmaps.GetAllUsableBeatmapSets().OrderBy(_ => RNG.Next()).First(); + CreateThemedContent(OverlayColourScheme.Aquamarine); + }); + } + + protected override Drawable CreateContent() + { + return new FillFlowContainer + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Width = 0.5f, + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Direction = FillDirection.Vertical, + Spacing = new Vector2(0f, 5f), + Children = new Drawable[] + { + new BeatmapSetPanel + { + Item = new CarouselItem(beatmapSet) + }, + new BeatmapSetPanel + { + Item = new CarouselItem(beatmapSet), + KeyboardSelected = { Value = true } + }, + new BeatmapSetPanel + { + Item = new CarouselItem(beatmapSet), + Expanded = { Value = true } + }, + new BeatmapSetPanel + { + Item = new CarouselItem(beatmapSet), + KeyboardSelected = { Value = true }, + Expanded = { Value = true } + }, + } + }; + } + } +} diff --git a/osu.Game.Tests/Visual/SongSelectV2/TestSceneUpdateBeatmapSetButtonV2.cs b/osu.Game.Tests/Visual/SongSelectV2/TestSceneUpdateBeatmapSetButtonV2.cs new file mode 100644 index 0000000000..6e5d731453 --- /dev/null +++ b/osu.Game.Tests/Visual/SongSelectV2/TestSceneUpdateBeatmapSetButtonV2.cs @@ -0,0 +1,62 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using NUnit.Framework; +using osu.Framework.Graphics; +using osu.Game.Beatmaps; +using osu.Game.Screens.SelectV2; + +namespace osu.Game.Tests.Visual.SongSelectV2 +{ + public partial class TestSceneUpdateBeatmapSetButtonV2 : OsuTestScene + { + private UpdateBeatmapSetButtonV2 button = null!; + + [SetUp] + public void SetUp() => Schedule(() => + { + Child = button = new UpdateBeatmapSetButtonV2 + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + }; + }); + + [Test] + public void TestNullBeatmap() + { + AddStep("null beatmap", () => button.BeatmapSet = null); + AddAssert("button invisible", () => button.Alpha == 0f); + } + + [Test] + public void TestUpdatedBeatmap() + { + AddStep("updated beatmap", () => button.BeatmapSet = new BeatmapSetInfo + { + Beatmaps = { new BeatmapInfo() } + }); + AddAssert("button invisible", () => button.Alpha == 0f); + } + + [Test] + public void TestNonUpdatedBeatmap() + { + AddStep("non-updated beatmap", () => button.BeatmapSet = new BeatmapSetInfo + { + Beatmaps = + { + new BeatmapInfo + { + MD5Hash = "test", + OnlineMD5Hash = "online", + LastOnlineUpdate = DateTimeOffset.Now, + } + } + }); + + AddAssert("button visible", () => button.Alpha == 1f); + } + } +} diff --git a/osu.Game/Beatmaps/Drawables/DifficultySpectrumDisplay.cs b/osu.Game/Beatmaps/Drawables/DifficultySpectrumDisplay.cs index 2fb3a8eee4..56f6c77ba8 100644 --- a/osu.Game/Beatmaps/Drawables/DifficultySpectrumDisplay.cs +++ b/osu.Game/Beatmaps/Drawables/DifficultySpectrumDisplay.cs @@ -28,7 +28,7 @@ namespace osu.Game.Beatmaps.Drawables dotSize = value; if (IsLoaded) - updateDotDimensions(); + updateDisplay(); } } @@ -42,13 +42,27 @@ namespace osu.Game.Beatmaps.Drawables dotSpacing = value; if (IsLoaded) - updateDotDimensions(); + updateDisplay(); + } + } + + private IBeatmapSetInfo? beatmapSet; + + public IBeatmapSetInfo? BeatmapSet + { + get => beatmapSet; + set + { + beatmapSet = value; + + if (IsLoaded) + updateDisplay(); } } private readonly FillFlowContainer flow; - public DifficultySpectrumDisplay(IBeatmapSetInfo beatmapSet) + public DifficultySpectrumDisplay(IBeatmapSetInfo? beatmapSet = null) { AutoSizeAxes = Axes.Both; @@ -59,25 +73,31 @@ namespace osu.Game.Beatmaps.Drawables Direction = FillDirection.Horizontal, }; - // matching web: https://github.com/ppy/osu-web/blob/d06d8c5e735eb1f48799b1654b528e9a7afb0a35/resources/assets/lib/beatmapset-panel.tsx#L127 - bool collapsed = beatmapSet.Beatmaps.Count() > 12; - - foreach (var rulesetGrouping in beatmapSet.Beatmaps.GroupBy(beatmap => beatmap.Ruleset).OrderBy(group => group.Key)) - flow.Add(new RulesetDifficultyGroup(rulesetGrouping.Key.OnlineID, rulesetGrouping, collapsed)); + BeatmapSet = beatmapSet; } protected override void LoadComplete() { base.LoadComplete(); - updateDotDimensions(); + updateDisplay(); } - private void updateDotDimensions() + private void updateDisplay() { - foreach (var group in flow) + flow.Clear(); + + if (beatmapSet == null) + return; + + // matching web: https://github.com/ppy/osu-web/blob/d06d8c5e735eb1f48799b1654b528e9a7afb0a35/resources/assets/lib/beatmapset-panel.tsx#L127 + bool collapsed = beatmapSet.Beatmaps.Count() > 12; + + foreach (var rulesetGrouping in beatmapSet.Beatmaps.GroupBy(beatmap => beatmap.Ruleset).OrderBy(group => group.Key)) { - group.DotSize = DotSize; - group.DotSpacing = DotSpacing; + flow.Add(new RulesetDifficultyGroup(rulesetGrouping.Key.OnlineID, rulesetGrouping, collapsed, dotSize) + { + Spacing = new Vector2(DotSpacing, 0f), + }); } } @@ -86,26 +106,14 @@ namespace osu.Game.Beatmaps.Drawables private readonly int rulesetId; private readonly IEnumerable beatmapInfos; private readonly bool collapsed; + private readonly Vector2 dotSize; - public RulesetDifficultyGroup(int rulesetId, IEnumerable beatmapInfos, bool collapsed) + public RulesetDifficultyGroup(int rulesetId, IEnumerable beatmapInfos, bool collapsed, Vector2 dotSize) { this.rulesetId = rulesetId; this.beatmapInfos = beatmapInfos; this.collapsed = collapsed; - } - - public Vector2 DotSize - { - set - { - foreach (var dot in Children.OfType()) - dot.Size = value; - } - } - - public float DotSpacing - { - set => Spacing = new Vector2(value, 0); + this.dotSize = dotSize; } [BackgroundDependencyLoader] @@ -125,7 +133,7 @@ namespace osu.Game.Beatmaps.Drawables if (!collapsed) { foreach (var beatmapInfo in beatmapInfos.OrderBy(bi => bi.StarRating)) - Add(new DifficultyDot(beatmapInfo.StarRating)); + Add(new DifficultyDot(beatmapInfo.StarRating, dotSize)); } else { @@ -145,9 +153,10 @@ namespace osu.Game.Beatmaps.Drawables { private readonly double starDifficulty; - public DifficultyDot(double starDifficulty) + public DifficultyDot(double starDifficulty, Vector2 dotSize) { this.starDifficulty = starDifficulty; + Size = dotSize; } [BackgroundDependencyLoader] diff --git a/osu.Game/Screens/SelectV2/BeatmapSetPanel.cs b/osu.Game/Screens/SelectV2/BeatmapSetPanel.cs index 85d5cc097d..4706ea487a 100644 --- a/osu.Game/Screens/SelectV2/BeatmapSetPanel.cs +++ b/osu.Game/Screens/SelectV2/BeatmapSetPanel.cs @@ -3,15 +3,24 @@ using System; 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.Pooling; using osu.Framework.Graphics.Shapes; +using osu.Framework.Graphics.Sprites; using osu.Framework.Input.Events; +using osu.Framework.Localisation; using osu.Game.Beatmaps; +using osu.Game.Beatmaps.Drawables; +using osu.Game.Graphics; using osu.Game.Graphics.Sprites; +using osu.Game.Graphics.UserInterface; +using osu.Game.Overlays; using osuTK; using osuTK.Graphics; @@ -19,63 +28,182 @@ namespace osu.Game.Screens.SelectV2 { public partial class BeatmapSetPanel : PoolableDrawable, ICarouselPanel { - public const float HEIGHT = CarouselItem.DEFAULT_HEIGHT * 2; + public const float HEIGHT = CarouselItem.DEFAULT_HEIGHT * 1.6f; + + private const float arrow_container_width = 20; + private const float corner_radius = 10; + + private const float glow_offset = 10f; // extra space for the edge effect to not be cutoff by the right edge of the carousel. + private const float set_x_offset = 20f; // constant X offset for beatmap set panels specifically. + private const float preselected_x_offset = 25f; + private const float expanded_x_offset = 50f; + + private const float duration = 500; [Resolved] - private BeatmapCarousel carousel { get; set; } = null!; + private BeatmapCarousel? carousel { get; set; } - private OsuSpriteText text = null!; - private Box box = null!; + [Resolved] + private OverlayColourProvider colourProvider { get; set; } = null!; - public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) - { - var inputRectangle = DrawRectangle; + [Resolved] + private BeatmapManager beatmaps { get; set; } = null!; - // Cover a gap introduced by the spacing between a BeatmapSetPanel and a BeatmapPanel either below/above it. - inputRectangle = inputRectangle.Inflate(new MarginPadding { Vertical = BeatmapCarousel.SPACING / 2f }); + [Resolved] + private OsuColour colours { get; set; } = null!; - return inputRectangle.Contains(ToLocalSpace(screenSpacePos)); - } + private Container panel = null!; + private Box backgroundBorder = null!; + private BeatmapSetPanelBackground background = null!; + private Container backgroundContainer = null!; + private FillFlowContainer mainFlowContainer = null!; + private SpriteIcon chevronIcon = null!; + private Box hoverLayer = null!; + + private OsuSpriteText titleText = null!; + private OsuSpriteText artistText = null!; + private UpdateBeatmapSetButtonV2 updateButton = null!; + private BeatmapSetOnlineStatusPill statusPill = null!; + private DifficultySpectrumDisplay difficultiesDisplay = null!; [BackgroundDependencyLoader] private void load() { - Size = new Vector2(500, HEIGHT); - Masking = true; + Anchor = Anchor.TopRight; + Origin = Anchor.TopRight; + RelativeSizeAxes = Axes.X; + Height = HEIGHT; - InternalChildren = new Drawable[] + InternalChild = panel = new Container { - box = new Box + Masking = true, + CornerRadius = corner_radius, + RelativeSizeAxes = Axes.Both, + EdgeEffect = new EdgeEffectParameters { - Colour = Color4.Yellow.Darken(5), - Alpha = 0.8f, - RelativeSizeAxes = Axes.Both, + Type = EdgeEffectType.Shadow, + Radius = 10, }, - text = new OsuSpriteText + Children = new Drawable[] { - Padding = new MarginPadding(5), - Anchor = Anchor.CentreLeft, - Origin = Anchor.CentreLeft, + new BufferedContainer + { + RelativeSizeAxes = Axes.Both, + Children = new Drawable[] + { + backgroundBorder = new Box + { + RelativeSizeAxes = Axes.Y, + Alpha = 0, + EdgeSmoothness = new Vector2(2, 0), + }, + backgroundContainer = new Container + { + Masking = true, + CornerRadius = corner_radius, + RelativeSizeAxes = Axes.X, + MaskingSmoothness = 2, + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + Children = new Drawable[] + { + background = new BeatmapSetPanelBackground + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + RelativeSizeAxes = Axes.Both, + }, + }, + }, + } + }, + chevronIcon = new SpriteIcon + { + X = arrow_container_width / 2, + Origin = Anchor.Centre, + Anchor = Anchor.CentreLeft, + Icon = FontAwesome.Solid.ChevronRight, + Size = new Vector2(12), + Colour = colourProvider.Background5, + }, + mainFlowContainer = new FillFlowContainer + { + AutoSizeAxes = Axes.Both, + Direction = FillDirection.Vertical, + Padding = new MarginPadding { Top = 7.5f, Left = 15, Bottom = 5 }, + Children = new Drawable[] + { + titleText = new OsuSpriteText + { + Font = OsuFont.GetFont(weight: FontWeight.Bold, size: 22, italics: true), + Shadow = true, + }, + artistText = new OsuSpriteText + { + Font = OsuFont.GetFont(weight: FontWeight.SemiBold, size: 17, italics: true), + Shadow = true, + }, + new FillFlowContainer + { + Direction = FillDirection.Horizontal, + AutoSizeAxes = Axes.Both, + Margin = new MarginPadding { Top = 5f }, + Children = new Drawable[] + { + updateButton = new UpdateBeatmapSetButtonV2 + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + Margin = new MarginPadding { Right = 5f, Top = -2f }, + }, + statusPill = new BeatmapSetOnlineStatusPill + { + AutoSizeAxes = Axes.Both, + Origin = Anchor.CentreLeft, + Anchor = Anchor.CentreLeft, + TextSize = 11, + TextPadding = new MarginPadding { Horizontal = 8, Vertical = 2 }, + Margin = new MarginPadding { Right = 5f }, + }, + difficultiesDisplay = new DifficultySpectrumDisplay + { + DotSize = new Vector2(5, 10), + DotSpacing = 2, + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + }, + }, + } + } + }, + hoverLayer = new Box + { + Colour = colours.Blue.Opacity(0.1f), + Alpha = 0, + Blending = BlendingParameters.Additive, + RelativeSizeAxes = Axes.Both, + }, + new HoverSounds(), } }; + } - Expanded.BindValueChanged(value => - { - box.FadeColour(value.NewValue ? Color4.Yellow.Darken(2) : Color4.Yellow.Darken(5), 500, Easing.OutQuint); - }); + public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) + { + var inputRectangle = panel.DrawRectangle; - KeyboardSelected.BindValueChanged(value => - { - if (value.NewValue) - { - BorderThickness = 5; - BorderColour = Color4.Pink; - } - else - { - BorderThickness = 0; - } - }); + // Cover a gap introduced by the spacing between a BeatmapSetPanel and a BeatmapPanel either above it or below it. + inputRectangle = inputRectangle.Inflate(new MarginPadding { Vertical = BeatmapCarousel.SPACING / 2f }); + + return inputRectangle.Contains(panel.ToLocalSpace(screenSpacePos)); + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + Expanded.BindValueChanged(_ => updateExpandedDisplay(), true); + KeyboardSelected.BindValueChanged(_ => updateKeyboardSelectedDisplay(), true); } protected override void PrepareForUse() @@ -84,16 +212,101 @@ namespace osu.Game.Screens.SelectV2 Debug.Assert(Item != null); - var beatmapSetInfo = (BeatmapSetInfo)Item.Model; + var beatmapSet = (BeatmapSetInfo)Item.Model; - text.Text = $"{beatmapSetInfo.Metadata}"; + // Choice of background image matches BSS implementation (always uses the lowest `beatmap_id` from the set). + background.Beatmap = beatmaps.GetWorkingBeatmap(beatmapSet.Beatmaps.MinBy(b => b.OnlineID)); - this.FadeInFromZero(500, Easing.OutQuint); + titleText.Text = new RomanisableString(beatmapSet.Metadata.TitleUnicode, beatmapSet.Metadata.Title); + artistText.Text = new RomanisableString(beatmapSet.Metadata.ArtistUnicode, beatmapSet.Metadata.Artist); + updateButton.BeatmapSet = beatmapSet; + statusPill.Status = beatmapSet.Status; + difficultiesDisplay.BeatmapSet = beatmapSet; + + updateExpandedDisplay(); + FinishTransforms(true); + + this.FadeInFromZero(duration, Easing.OutQuint); + } + + protected override void FreeAfterUse() + { + base.FreeAfterUse(); + + background.Beatmap = null; + updateButton.BeatmapSet = null; + difficultiesDisplay.BeatmapSet = null; + } + + private void updateExpandedDisplay() + { + if (Item == null) + return; + + updatePanelPosition(); + + backgroundBorder.RelativeSizeAxes = Expanded.Value ? Axes.Both : Axes.Y; + backgroundBorder.Width = Expanded.Value ? 1 : arrow_container_width + corner_radius; + backgroundBorder.FadeTo(Expanded.Value ? 1 : 0, duration, Easing.OutQuint); + chevronIcon.FadeTo(Expanded.Value ? 1 : 0, duration, Easing.OutQuint); + + backgroundContainer.ResizeHeightTo(Expanded.Value ? HEIGHT - 4 : HEIGHT, duration, Easing.OutQuint); + backgroundContainer.MoveToX(Expanded.Value ? arrow_container_width : 0, duration, Easing.OutQuint); + mainFlowContainer.MoveToX(Expanded.Value ? arrow_container_width : 0, duration, Easing.OutQuint); + + panel.EdgeEffect = panel.EdgeEffect with { Radius = Expanded.Value ? 15 : 10 }; + + panel.FadeEdgeEffectTo(Expanded.Value + ? Color4Extensions.FromHex(@"4EBFFF").Opacity(0.5f) + : Color4.Black.Opacity(0.4f), duration, Easing.OutQuint); + } + + private void updateKeyboardSelectedDisplay() + { + updatePanelPosition(); + updateHover(); + } + + private void updatePanelPosition() + { + float x = glow_offset + set_x_offset + expanded_x_offset + preselected_x_offset; + + if (Expanded.Value) + x -= expanded_x_offset; + + if (KeyboardSelected.Value) + x -= preselected_x_offset; + + this.TransformTo(nameof(Padding), new MarginPadding { Left = x }, duration, Easing.OutQuint); + } + + private void updateHover() + { + bool hovered = IsHovered || KeyboardSelected.Value; + + if (hovered) + hoverLayer.FadeIn(100, Easing.OutQuint); + else + hoverLayer.FadeOut(1000, Easing.OutQuint); + } + + protected override bool OnHover(HoverEvent e) + { + updateHover(); + return true; + } + + protected override void OnHoverLost(HoverLostEvent e) + { + updateHover(); + base.OnHoverLost(e); } protected override bool OnClick(ClickEvent e) { - carousel.CurrentSelection = Item!.Model; + if (carousel != null) + carousel.CurrentSelection = Item!.Model; + return true; } diff --git a/osu.Game/Screens/SelectV2/BeatmapSetPanelBackground.cs b/osu.Game/Screens/SelectV2/BeatmapSetPanelBackground.cs new file mode 100644 index 0000000000..435a0ad262 --- /dev/null +++ b/osu.Game/Screens/SelectV2/BeatmapSetPanelBackground.cs @@ -0,0 +1,108 @@ +// 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.Colour; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; +using osu.Framework.Graphics.Sprites; +using osu.Game.Beatmaps; +using osu.Game.Overlays; +using osuTK; +using osuTK.Graphics; + +namespace osu.Game.Screens.SelectV2 +{ + public partial class BeatmapSetPanelBackground : ModelBackedDrawable + { + protected override bool TransformImmediately => true; + + public WorkingBeatmap? Beatmap + { + get => Model; + set => Model = value; + } + + protected override Drawable CreateDrawable(WorkingBeatmap? model) => new BackgroundSprite(model); + + private partial class BackgroundSprite : CompositeDrawable + { + private readonly WorkingBeatmap? working; + + public BackgroundSprite(WorkingBeatmap? working) + { + this.working = working; + + RelativeSizeAxes = Axes.Both; + } + + [BackgroundDependencyLoader] + private void load(OverlayColourProvider colourProvider) + { + var texture = working?.GetPanelBackground(); + + if (texture != null) + { + InternalChildren = new Drawable[] + { + new Sprite + { + RelativeSizeAxes = Axes.Both, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + FillMode = FillMode.Fill, + Texture = texture, + }, + new FillFlowContainer + { + Depth = -1, + RelativeSizeAxes = Axes.Both, + Direction = FillDirection.Horizontal, + // This makes the gradient not be perfectly horizontal, but diagonal at a ~40° angle + Shear = new Vector2(0.8f, 0), + Alpha = 0.5f, + Children = new[] + { + // The left half with no gradient applied + new Box + { + RelativeSizeAxes = Axes.Both, + Colour = Color4.Black, + Width = 0.4f, + }, + // Piecewise-linear gradient with 3 segments to make it appear smoother + new Box + { + RelativeSizeAxes = Axes.Both, + Colour = ColourInfo.GradientHorizontal(Color4.Black, new Color4(0f, 0f, 0f, 0.9f)), + Width = 0.05f, + }, + new Box + { + RelativeSizeAxes = Axes.Both, + Colour = ColourInfo.GradientHorizontal(new Color4(0f, 0f, 0f, 0.9f), new Color4(0f, 0f, 0f, 0.1f)), + Width = 0.2f, + }, + new Box + { + RelativeSizeAxes = Axes.Both, + Colour = ColourInfo.GradientHorizontal(new Color4(0f, 0f, 0f, 0.1f), new Color4(0, 0, 0, 0)), + Width = 0.05f, + }, + } + }, + }; + } + else + { + InternalChild = new Box + { + RelativeSizeAxes = Axes.Both, + Colour = colourProvider.Background6, + }; + } + } + } + } +} diff --git a/osu.Game/Screens/SelectV2/TopLocalRankV2.cs b/osu.Game/Screens/SelectV2/TopLocalRankV2.cs new file mode 100644 index 0000000000..241e92a67d --- /dev/null +++ b/osu.Game/Screens/SelectV2/TopLocalRankV2.cs @@ -0,0 +1,108 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using System.Linq; +using osu.Framework.Allocation; +using osu.Framework.Bindables; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Game.Beatmaps; +using osu.Game.Database; +using osu.Game.Models; +using osu.Game.Online.API; +using osu.Game.Online.Leaderboards; +using osu.Game.Rulesets; +using osu.Game.Scoring; +using osuTK; +using Realms; + +namespace osu.Game.Screens.SelectV2 +{ + public partial class TopLocalRankV2 : CompositeDrawable + { + private BeatmapInfo? beatmap; + + public BeatmapInfo? Beatmap + { + get => beatmap; + set + { + beatmap = value; + + if (IsLoaded) + updateSubscription(); + } + } + + [Resolved] + private IBindable ruleset { get; set; } = null!; + + [Resolved] + private RealmAccess realm { get; set; } = null!; + + [Resolved] + private IAPIProvider api { get; set; } = null!; + + private IDisposable? scoreSubscription; + + private readonly UpdateableRank updateable; + + public ScoreRank? DisplayedRank => updateable.Rank; + + public TopLocalRankV2(BeatmapInfo? beatmap = null) + { + AutoSizeAxes = Axes.Both; + + InternalChild = updateable = new UpdateableRank + { + Size = new Vector2(40, 20), + Alpha = 0, + }; + + Beatmap = beatmap; + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + ruleset.BindValueChanged(_ => updateSubscription(), true); + } + + private void updateSubscription() + { + scoreSubscription?.Dispose(); + + if (beatmap == null) + return; + + scoreSubscription = realm.RegisterForNotifications(r => + r.All() + .Filter($"{nameof(ScoreInfo.User)}.{nameof(RealmUser.OnlineID)} == $0" + + $" && {nameof(ScoreInfo.BeatmapInfo)}.{nameof(BeatmapInfo.ID)} == $1" + + $" && {nameof(ScoreInfo.BeatmapInfo)}.{nameof(BeatmapInfo.Hash)} == {nameof(ScoreInfo.BeatmapHash)}" + + $" && {nameof(ScoreInfo.Ruleset)}.{nameof(RulesetInfo.ShortName)} == $2" + + $" && {nameof(ScoreInfo.DeletePending)} == false", api.LocalUser.Value.Id, beatmap.ID, ruleset.Value.ShortName), + localScoresChanged); + } + + private void localScoresChanged(IRealmCollection sender, ChangeSet? changes) + { + // This subscription may fire from changes to linked beatmaps, which we don't care about. + // It's currently not possible for a score to be modified after insertion, so we can safely ignore callbacks with only modifications. + if (changes?.HasCollectionChanges() == false) + return; + + ScoreInfo? topScore = sender.MaxBy(info => (info.TotalScore, -info.Date.UtcDateTime.Ticks)); + updateable.Rank = topScore?.Rank; + updateable.Alpha = topScore != null ? 1 : 0; + } + + protected override void Dispose(bool isDisposing) + { + base.Dispose(isDisposing); + scoreSubscription?.Dispose(); + } + } +} diff --git a/osu.Game/Screens/SelectV2/UpdateBeatmapSetButtonV2.cs b/osu.Game/Screens/SelectV2/UpdateBeatmapSetButtonV2.cs new file mode 100644 index 0000000000..2d1ce4ba48 --- /dev/null +++ b/osu.Game/Screens/SelectV2/UpdateBeatmapSetButtonV2.cs @@ -0,0 +1,198 @@ +// 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.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; +using osu.Framework.Graphics.Sprites; +using osu.Framework.Input.Events; +using osu.Game.Beatmaps; +using osu.Game.Configuration; +using osu.Game.Graphics; +using osu.Game.Graphics.Sprites; +using osu.Game.Graphics.UserInterface; +using osu.Game.Online.API; +using osu.Game.Overlays; +using osu.Game.Screens.Select.Carousel; +using osuTK; +using osuTK.Graphics; + +namespace osu.Game.Screens.SelectV2 +{ + public partial class UpdateBeatmapSetButtonV2 : OsuAnimatedButton + { + private BeatmapSetInfo? beatmapSet; + + public BeatmapSetInfo? BeatmapSet + { + get => beatmapSet; + set + { + beatmapSet = value; + + if (IsLoaded) + beatmapChanged(); + } + } + + private SpriteIcon icon = null!; + private Box progressFill = null!; + + [Resolved] + private BeatmapModelDownloader beatmapDownloader { get; set; } = null!; + + [Resolved] + private IAPIProvider api { get; set; } = null!; + + [Resolved] + private LoginOverlay? loginOverlay { get; set; } + + [Resolved] + private IDialogOverlay? dialogOverlay { get; set; } + + public UpdateBeatmapSetButtonV2() + { + Size = new Vector2(75f, 22f); + } + + private Bindable preferNoVideo = null!; + + [BackgroundDependencyLoader] + private void load(OsuConfigManager config) + { + const float icon_size = 14; + + preferNoVideo = config.GetBindable(OsuSetting.PreferNoVideo); + + Content.Anchor = Anchor.Centre; + Content.Origin = Anchor.Centre; + Content.Shear = new Vector2(OsuGame.SHEAR, 0); + + Content.AddRange(new Drawable[] + { + progressFill = new Box + { + Colour = Color4.White, + Alpha = 0.2f, + Blending = BlendingParameters.Additive, + RelativeSizeAxes = Axes.Both, + Width = 0, + }, + new FillFlowContainer + { + Padding = new MarginPadding { Horizontal = 5, Vertical = 3 }, + AutoSizeAxes = Axes.Both, + Direction = FillDirection.Horizontal, + Spacing = new Vector2(4), + Shear = new Vector2(-OsuGame.SHEAR, 0), + Children = new Drawable[] + { + new Container + { + Size = new Vector2(icon_size), + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + Children = new Drawable[] + { + icon = new SpriteIcon + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Icon = FontAwesome.Solid.SyncAlt, + Size = new Vector2(icon_size), + }, + } + }, + new OsuSpriteText + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + Font = OsuFont.Default.With(weight: FontWeight.Bold), + Text = "Update", + } + } + }, + }); + + Action = performUpdate; + } + + protected override void LoadComplete() + { + base.LoadComplete(); + beatmapChanged(); + } + + private void beatmapChanged() + { + Alpha = beatmapSet?.AllBeatmapsUpToDate == false ? 1 : 0; + icon.Spin(4000, RotationDirection.Clockwise); + } + + protected override bool OnHover(HoverEvent e) + { + icon.Spin(400, RotationDirection.Clockwise); + return base.OnHover(e); + } + + protected override void OnHoverLost(HoverLostEvent e) + { + icon.Spin(4000, RotationDirection.Clockwise); + base.OnHoverLost(e); + } + + private bool updateConfirmed; + + private void performUpdate() + { + Debug.Assert(beatmapSet != null); + + if (!api.IsLoggedIn) + { + loginOverlay?.Show(); + return; + } + + if (dialogOverlay != null && beatmapSet.Status == BeatmapOnlineStatus.LocallyModified && !updateConfirmed) + { + dialogOverlay.Push(new UpdateLocalConfirmationDialog(() => + { + updateConfirmed = true; + performUpdate(); + })); + + return; + } + + updateConfirmed = false; + + beatmapDownloader.DownloadAsUpdate(beatmapSet, preferNoVideo.Value); + attachExistingDownload(); + } + + private void attachExistingDownload() + { + Debug.Assert(beatmapSet != null); + var download = beatmapDownloader.GetExistingDownload(beatmapSet); + + if (download != null) + { + Enabled.Value = false; + TooltipText = string.Empty; + + download.DownloadProgressed += progress => progressFill.ResizeWidthTo(progress, 100, Easing.OutQuint); + download.Failure += _ => attachExistingDownload(); + } + else + { + Enabled.Value = true; + TooltipText = "Update beatmap with online changes"; + + progressFill.ResizeWidthTo(0, 100, Easing.OutQuint); + } + } + } +} From 04d8bafdcee3c5b0a6a33e0046ced17f611da53f Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Wed, 5 Feb 2025 07:16:10 -0500 Subject: [PATCH 0898/1275] Implement beatmap difficulty panel design --- ...TestSceneBeatmapCarouselDifficultyPanel.cs | 101 +++++ osu.Game/Screens/SelectV2/BeatmapPanel.cs | 410 ++++++++++++++++-- 2 files changed, 463 insertions(+), 48 deletions(-) create mode 100644 osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselDifficultyPanel.cs diff --git a/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselDifficultyPanel.cs b/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselDifficultyPanel.cs new file mode 100644 index 0000000000..c0ecb06085 --- /dev/null +++ b/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselDifficultyPanel.cs @@ -0,0 +1,101 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Linq; +using NUnit.Framework; +using osu.Framework.Allocation; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Utils; +using osu.Game.Beatmaps; +using osu.Game.Overlays; +using osu.Game.Rulesets.Mania; +using osu.Game.Rulesets.Osu; +using osu.Game.Screens.SelectV2; +using osu.Game.Tests.Resources; +using osu.Game.Tests.Visual.UserInterface; +using osuTK; + +namespace osu.Game.Tests.Visual.SongSelectV2 +{ + public partial class TestSceneBeatmapCarouselDifficultyPanel : ThemeComparisonTestScene + { + [Resolved] + private BeatmapManager beatmaps { get; set; } = null!; + + private BeatmapInfo beatmap = null!; + + public TestSceneBeatmapCarouselDifficultyPanel() + : base(false) + { + } + + [Test] + public void TestDisplay() + { + AddStep("set beatmap", () => + { + var beatmapSet = beatmaps.GetAllUsableBeatmapSets().FirstOrDefault(b => b.OnlineID == 241526) + ?? beatmaps.GetAllUsableBeatmapSets().FirstOrDefault(b => !b.Protected) + ?? TestResources.CreateTestBeatmapSetInfo(); + + beatmap = beatmapSet.Beatmaps.First(); + CreateThemedContent(OverlayColourScheme.Aquamarine); + }); + } + + [Test] + public void TestRandomBeatmap() + { + AddStep("random beatmap", () => + { + beatmap = beatmaps.GetAllUsableBeatmapSets().OrderBy(_ => RNG.Next()) + .First().Beatmaps.OrderBy(_ => RNG.Next()).First(); + CreateThemedContent(OverlayColourScheme.Aquamarine); + }); + } + + [Test] + public void TestManiaRuleset() + { + AddToggleStep("mania ruleset", v => Ruleset.Value = v ? new ManiaRuleset().RulesetInfo : new OsuRuleset().RulesetInfo); + } + + protected override Drawable CreateContent() + { + return new FillFlowContainer + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Width = 0.5f, + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Direction = FillDirection.Vertical, + Spacing = new Vector2(0f, 5f), + Children = new Drawable[] + { + new BeatmapPanel + { + Item = new CarouselItem(beatmap) + }, + new BeatmapPanel + { + Item = new CarouselItem(beatmap), + KeyboardSelected = { Value = true } + }, + new BeatmapPanel + { + Item = new CarouselItem(beatmap), + Selected = { Value = true } + }, + new BeatmapPanel + { + Item = new CarouselItem(beatmap), + KeyboardSelected = { Value = true }, + Selected = { Value = true } + }, + } + }; + } + } +} diff --git a/osu.Game/Screens/SelectV2/BeatmapPanel.cs b/osu.Game/Screens/SelectV2/BeatmapPanel.cs index 2fe509402b..180acffe80 100644 --- a/osu.Game/Screens/SelectV2/BeatmapPanel.cs +++ b/osu.Game/Screens/SelectV2/BeatmapPanel.cs @@ -1,16 +1,29 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System.Collections.Generic; using System.Diagnostics; +using System.Threading; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; +using osu.Framework.Graphics.Colour; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Effects; using osu.Framework.Graphics.Pooling; using osu.Framework.Graphics.Shapes; using osu.Framework.Input.Events; using osu.Game.Beatmaps; +using osu.Game.Beatmaps.Drawables; +using osu.Game.Graphics; +using osu.Game.Graphics.Containers; using osu.Game.Graphics.Sprites; +using osu.Game.Graphics.UserInterface; +using osu.Game.Overlays; +using osu.Game.Resources.Localisation.Web; +using osu.Game.Rulesets; +using osu.Game.Rulesets.Mods; using osuTK; using osuTK.Graphics; @@ -18,68 +31,234 @@ namespace osu.Game.Screens.SelectV2 { public partial class BeatmapPanel : PoolableDrawable, ICarouselPanel { - [Resolved] - private BeatmapCarousel carousel { get; set; } = null!; + public const float HEIGHT = CarouselItem.DEFAULT_HEIGHT; + private const float colour_box_width = 30; + private const float corner_radius = 10; + + private const float glow_offset = 10f; // extra space for the edge effect to not be cutoff by the right edge of the carousel. + private const float difficulty_x_offset = 50f; // constant X offset for beatmap difficulty panels specifically. + private const float preselected_x_offset = 25f; + private const float selected_x_offset = 50f; + + private const float duration = 500; + + [Resolved] + private BeatmapCarousel? carousel { get; set; } + + [Resolved] + private IBindable ruleset { get; set; } = null!; + + [Resolved] + private IBindable> mods { get; set; } = null!; + + private Container panel = null!; + private StarCounter starCounter = null!; + private ConstrainedIconContainer iconContainer = null!; + private Box hoverLayer = null!; private Box activationFlash = null!; - private OsuSpriteText text = null!; + + private Box backgroundBorder = null!; + + private StarRatingDisplay starRatingDisplay = null!; + + [Resolved] + private OverlayColourProvider colourProvider { get; set; } = null!; + + [Resolved] + private OsuColour colours { get; set; } = null!; + + [Resolved] + private BeatmapDifficultyCache difficultyCache { get; set; } = null!; + + private OsuSpriteText keyCountText = null!; + + private IBindable? starDifficultyBindable; + private CancellationTokenSource? starDifficultyCancellationSource; + + private Container rightContainer = null!; + private Box starRatingGradient = null!; + private TopLocalRankV2 difficultyRank = null!; + private OsuSpriteText difficultyText = null!; + private OsuSpriteText authorText = null!; + + [BackgroundDependencyLoader] + private void load(OverlayColourProvider colourProvider) + { + Anchor = Anchor.TopRight; + Origin = Anchor.TopRight; + + RelativeSizeAxes = Axes.X; + Width = 0.9f; + Height = HEIGHT; + + InternalChild = panel = new Container + { + Masking = true, + CornerRadius = corner_radius, + RelativeSizeAxes = Axes.Both, + EdgeEffect = new EdgeEffectParameters + { + Type = EdgeEffectType.Shadow, + Offset = new Vector2(1f), + Radius = 10, + }, + Children = new Drawable[] + { + new BufferedContainer + { + RelativeSizeAxes = Axes.Both, + Children = new Drawable[] + { + backgroundBorder = new Box + { + RelativeSizeAxes = Axes.Both, + Colour = colours.ForStarDifficulty(0), + EdgeSmoothness = new Vector2(2, 0), + }, + rightContainer = new Container + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + Masking = true, + CornerRadius = corner_radius, + RelativeSizeAxes = Axes.X, + Height = HEIGHT, + X = colour_box_width, + Children = new Drawable[] + { + new Box + { + RelativeSizeAxes = Axes.Both, + Colour = ColourInfo.GradientHorizontal(colourProvider.Background3, colourProvider.Background4), + }, + starRatingGradient = new Box + { + RelativeSizeAxes = Axes.Both, + Alpha = 0, + }, + }, + }, + } + }, + iconContainer = new ConstrainedIconContainer + { + X = colour_box_width / 2, + Origin = Anchor.Centre, + Anchor = Anchor.CentreLeft, + Size = new Vector2(20), + Colour = colourProvider.Background5, + }, + new FillFlowContainer + { + Padding = new MarginPadding { Top = 8, Left = colour_box_width + corner_radius }, + Direction = FillDirection.Vertical, + AutoSizeAxes = Axes.Both, + Children = new Drawable[] + { + new FillFlowContainer + { + Direction = FillDirection.Horizontal, + Spacing = new Vector2(3, 0), + AutoSizeAxes = Axes.Both, + Children = new Drawable[] + { + starRatingDisplay = new StarRatingDisplay(default, StarRatingDisplaySize.Small) + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + }, + difficultyRank = new TopLocalRankV2 + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + Scale = new Vector2(0.75f) + }, + starCounter = new StarCounter + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + Scale = new Vector2(0.4f) + } + } + }, + new FillFlowContainer + { + Direction = FillDirection.Horizontal, + AutoSizeAxes = Axes.Both, + Children = new[] + { + keyCountText = new OsuSpriteText + { + Font = OsuFont.GetFont(size: 18, weight: FontWeight.SemiBold), + Anchor = Anchor.BottomLeft, + Origin = Anchor.BottomLeft, + Alpha = 0, + }, + difficultyText = new OsuSpriteText + { + Font = OsuFont.GetFont(size: 18, weight: FontWeight.SemiBold), + Anchor = Anchor.BottomLeft, + Origin = Anchor.BottomLeft, + Margin = new MarginPadding { Right = 8f }, + }, + authorText = new OsuSpriteText + { + Colour = colourProvider.Content2, + Font = OsuFont.GetFont(weight: FontWeight.SemiBold), + Anchor = Anchor.BottomLeft, + Origin = Anchor.BottomLeft + } + } + } + } + }, + hoverLayer = new Box + { + Colour = colours.Blue.Opacity(0.1f), + Alpha = 0, + Blending = BlendingParameters.Additive, + RelativeSizeAxes = Axes.Both, + }, + activationFlash = new Box + { + Blending = BlendingParameters.Additive, + Alpha = 0f, + RelativeSizeAxes = Axes.Both, + }, + new HoverSounds(), + } + }; + } public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) { - var inputRectangle = DrawRectangle; + var inputRectangle = panel.DrawRectangle; // Cover the gaps introduced by the spacing between BeatmapPanels. inputRectangle = inputRectangle.Inflate(new MarginPadding { Vertical = BeatmapCarousel.SPACING / 2f }); - return inputRectangle.Contains(ToLocalSpace(screenSpacePos)); + return inputRectangle.Contains(panel.ToLocalSpace(screenSpacePos)); } - [BackgroundDependencyLoader] - private void load() + protected override void LoadComplete() { - Size = new Vector2(500, CarouselItem.DEFAULT_HEIGHT); - Masking = true; + base.LoadComplete(); - InternalChildren = new Drawable[] + ruleset.BindValueChanged(_ => { - new Box - { - Colour = Color4.Aqua.Darken(5), - Alpha = 0.8f, - RelativeSizeAxes = Axes.Both, - }, - activationFlash = new Box - { - Colour = Color4.White, - Blending = BlendingParameters.Additive, - Alpha = 0, - RelativeSizeAxes = Axes.Both, - }, - text = new OsuSpriteText - { - Padding = new MarginPadding(5), - Anchor = Anchor.CentreLeft, - Origin = Anchor.CentreLeft, - } - }; - - Selected.BindValueChanged(value => - { - activationFlash.FadeTo(value.NewValue ? 0.2f : 0, 500, Easing.OutQuint); + computeStarRating(); + updateKeyCount(); }); - KeyboardSelected.BindValueChanged(value => + mods.BindValueChanged(_ => { - if (value.NewValue) - { - BorderThickness = 5; - BorderColour = Color4.Pink; - } - else - { - BorderThickness = 0; - } - }); + computeStarRating(); + updateKeyCount(); + }, true); + + Selected.BindValueChanged(_ => updateSelectionDisplay(), true); + KeyboardSelected.BindValueChanged(_ => updateKeyboardSelectedDisplay(), true); } protected override void PrepareForUse() @@ -89,13 +268,145 @@ namespace osu.Game.Screens.SelectV2 Debug.Assert(Item != null); var beatmap = (BeatmapInfo)Item.Model; - text.Text = $"Difficulty: {beatmap.DifficultyName} ({beatmap.StarRating:N1}*)"; + iconContainer.Icon = beatmap.Ruleset.CreateInstance().CreateIcon(); - this.FadeInFromZero(500, Easing.OutQuint); + difficultyRank.Beatmap = beatmap; + difficultyText.Text = beatmap.DifficultyName; + authorText.Text = BeatmapsetsStrings.ShowDetailsMappedBy(beatmap.Metadata.Author.Username); + + starDifficultyBindable = null; + + computeStarRating(); + updateKeyCount(); + + updateSelectionDisplay(); + FinishTransforms(true); + + this.FadeInFromZero(duration, Easing.OutQuint); + + // todo: only do this when visible. + // starCounter.ReplayAnimation(); + } + + private void updateSelectionDisplay() + { + bool selected = Selected.Value; + + rightContainer.ResizeHeightTo(selected ? HEIGHT - 4 : HEIGHT, duration, Easing.OutQuint); + + updatePanelPosition(); + updateEdgeEffectColour(); + updateHover(); + } + + private void updateKeyboardSelectedDisplay() + { + updatePanelPosition(); + updateHover(); + } + + private void updatePanelPosition() + { + float x = glow_offset + difficulty_x_offset + selected_x_offset + preselected_x_offset; + + if (Selected.Value) + x -= selected_x_offset; + + if (KeyboardSelected.Value) + x -= preselected_x_offset; + + this.TransformTo(nameof(Padding), new MarginPadding { Left = x }, duration, Easing.OutQuint); + } + + private void updateHover() + { + bool hovered = IsHovered || (KeyboardSelected.Value && !Selected.Value); + + if (hovered) + hoverLayer.FadeIn(100, Easing.OutQuint); + else + hoverLayer.FadeOut(1000, Easing.OutQuint); + } + + private void computeStarRating() + { + starDifficultyCancellationSource?.Cancel(); + starDifficultyCancellationSource = new CancellationTokenSource(); + + if (Item == null) + return; + + var beatmap = (BeatmapInfo)Item.Model; + + starDifficultyBindable = difficultyCache.GetBindableDifficulty(beatmap, starDifficultyCancellationSource.Token); + starDifficultyBindable.BindValueChanged(d => + { + var value = d.NewValue ?? default; + + starRatingDisplay.Current.Value = value; + starCounter.Current = (float)value.Stars; + + iconContainer.FadeColour(value.Stars > 6.5f ? colours.Orange1 : colourProvider.Background5, duration, Easing.OutQuint); + + var starRatingColour = colours.ForStarDifficulty(value.Stars); + + backgroundBorder.FadeColour(starRatingColour, duration, Easing.OutQuint); + starCounter.FadeColour(starRatingColour, duration, Easing.OutQuint); + starRatingGradient.FadeColour(ColourInfo.GradientHorizontal(starRatingColour.Opacity(0.25f), starRatingColour.Opacity(0)), duration, Easing.OutQuint); + starRatingGradient.FadeIn(duration, Easing.OutQuint); + + // todo: this doesn't work for dark star rating colours, still not sure how to fix. + activationFlash.FadeColour(starRatingColour, duration, Easing.OutQuint); + + updateEdgeEffectColour(); + }, true); + } + + private void updateEdgeEffectColour() + { + panel.FadeEdgeEffectTo(Selected.Value + ? colours.ForStarDifficulty(starDifficultyBindable?.Value?.Stars ?? 0f).Opacity(0.5f) + : Color4.Black.Opacity(0.4f), duration, Easing.OutQuint); + } + + private void updateKeyCount() + { + if (Item == null) + return; + + var beatmap = (BeatmapInfo)Item.Model; + + if (ruleset.Value.OnlineID == 3) + { + // Account for mania differences locally for now. + // Eventually this should be handled in a more modular way, allowing rulesets to add more information to the panel. + ILegacyRuleset legacyRuleset = (ILegacyRuleset)ruleset.Value.CreateInstance(); + int keyCount = legacyRuleset.GetKeyCount(beatmap, mods.Value); + + keyCountText.Alpha = 1; + keyCountText.Text = $"[{keyCount}K] "; + } + else + keyCountText.Alpha = 0; + } + + protected override bool OnHover(HoverEvent e) + { + updateHover(); + return true; + } + + protected override void OnHoverLost(HoverLostEvent e) + { + updateHover(); + base.OnHoverLost(e); } protected override bool OnClick(ClickEvent e) { + if (carousel == null) + return true; + if (carousel.CurrentSelection != Item!.Model) { carousel.CurrentSelection = Item!.Model; @@ -115,7 +426,10 @@ namespace osu.Game.Screens.SelectV2 public double DrawYPosition { get; set; } - public void Activated() => activationFlash.FadeOutFromOne(500, Easing.OutQuint); + public void Activated() + { + activationFlash.FadeOutFromOne(500, Easing.OutQuint); + } #endregion } From 696366f8cb13c2dd1ee6f3b02c8c2d7f806d9126 Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Wed, 5 Feb 2025 07:17:13 -0500 Subject: [PATCH 0899/1275] Implement beatmap "standalone" panel design --- ...TestSceneBeatmapCarouselStandalonePanel.cs | 101 ++++ .../SelectV2/BeatmapStandalonePanel.cs | 460 ++++++++++++++++++ 2 files changed, 561 insertions(+) create mode 100644 osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselStandalonePanel.cs create mode 100644 osu.Game/Screens/SelectV2/BeatmapStandalonePanel.cs diff --git a/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselStandalonePanel.cs b/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselStandalonePanel.cs new file mode 100644 index 0000000000..76dcfc9507 --- /dev/null +++ b/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselStandalonePanel.cs @@ -0,0 +1,101 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Linq; +using NUnit.Framework; +using osu.Framework.Allocation; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Utils; +using osu.Game.Beatmaps; +using osu.Game.Overlays; +using osu.Game.Rulesets.Mania; +using osu.Game.Rulesets.Osu; +using osu.Game.Screens.SelectV2; +using osu.Game.Tests.Resources; +using osu.Game.Tests.Visual.UserInterface; +using osuTK; + +namespace osu.Game.Tests.Visual.SongSelectV2 +{ + public partial class TestSceneBeatmapCarouselStandalonePanel : ThemeComparisonTestScene + { + [Resolved] + private BeatmapManager beatmaps { get; set; } = null!; + + private BeatmapInfo beatmap = null!; + + public TestSceneBeatmapCarouselStandalonePanel() + : base(false) + { + } + + [Test] + public void TestDisplay() + { + AddStep("set beatmap", () => + { + var beatmapSet = beatmaps.GetAllUsableBeatmapSets().FirstOrDefault(b => b.OnlineID == 241526) + ?? beatmaps.GetAllUsableBeatmapSets().FirstOrDefault(b => !b.Protected) + ?? TestResources.CreateTestBeatmapSetInfo(); + + beatmap = beatmapSet.Beatmaps.First(); + CreateThemedContent(OverlayColourScheme.Aquamarine); + }); + } + + [Test] + public void TestRandomBeatmap() + { + AddStep("random beatmap", () => + { + beatmap = beatmaps.GetAllUsableBeatmapSets().OrderBy(_ => RNG.Next()) + .First().Beatmaps.OrderBy(_ => RNG.Next()).First(); + CreateThemedContent(OverlayColourScheme.Aquamarine); + }); + } + + [Test] + public void TestManiaRuleset() + { + AddToggleStep("mania ruleset", v => Ruleset.Value = v ? new ManiaRuleset().RulesetInfo : new OsuRuleset().RulesetInfo); + } + + protected override Drawable CreateContent() + { + return new FillFlowContainer + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Width = 0.5f, + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Direction = FillDirection.Vertical, + Spacing = new Vector2(0f, 5f), + Children = new Drawable[] + { + new BeatmapStandalonePanel + { + Item = new CarouselItem(beatmap) + }, + new BeatmapStandalonePanel + { + Item = new CarouselItem(beatmap), + KeyboardSelected = { Value = true } + }, + new BeatmapStandalonePanel + { + Item = new CarouselItem(beatmap), + Selected = { Value = true } + }, + new BeatmapStandalonePanel + { + Item = new CarouselItem(beatmap), + KeyboardSelected = { Value = true }, + Selected = { Value = true } + }, + } + }; + } + } +} diff --git a/osu.Game/Screens/SelectV2/BeatmapStandalonePanel.cs b/osu.Game/Screens/SelectV2/BeatmapStandalonePanel.cs new file mode 100644 index 0000000000..11fa22ab09 --- /dev/null +++ b/osu.Game/Screens/SelectV2/BeatmapStandalonePanel.cs @@ -0,0 +1,460 @@ +// 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 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.Pooling; +using osu.Framework.Graphics.Shapes; +using osu.Framework.Input.Events; +using osu.Framework.Localisation; +using osu.Game.Beatmaps; +using osu.Game.Beatmaps.Drawables; +using osu.Game.Graphics; +using osu.Game.Graphics.Containers; +using osu.Game.Graphics.Sprites; +using osu.Game.Graphics.UserInterface; +using osu.Game.Overlays; +using osu.Game.Resources.Localisation.Web; +using osu.Game.Rulesets; +using osu.Game.Rulesets.Mods; +using osuTK; +using osuTK.Graphics; + +namespace osu.Game.Screens.SelectV2 +{ + public partial class BeatmapStandalonePanel : PoolableDrawable, ICarouselPanel + { + public const float HEIGHT = CarouselItem.DEFAULT_HEIGHT * 1.6f; + + private const float difficulty_icon_container_width = 30; + private const float corner_radius = 10; + + private const float glow_offset = 10f; // extra space for the edge effect to not be cutoff by the right edge of the carousel. + private const float preselected_x_offset = 25f; + private const float selected_x_offset = 50f; + + private const float duration = 500; + + [Resolved] + private BeatmapCarousel? carousel { get; set; } + + [Resolved] + private IBindable ruleset { get; set; } = null!; + + [Resolved] + private IBindable> mods { get; set; } = null!; + + [Resolved] + private OverlayColourProvider colourProvider { get; set; } = null!; + + [Resolved] + private BeatmapManager beatmaps { get; set; } = null!; + + [Resolved] + private OsuColour colours { get; set; } = null!; + + [Resolved] + private BeatmapDifficultyCache difficultyCache { get; set; } = null!; + + private IBindable? starDifficultyBindable; + private CancellationTokenSource? starDifficultyCancellationSource; + + private Container panel = null!; + private Box backgroundBorder = null!; + private BeatmapSetPanelBackground background = null!; + private Container backgroundContainer = null!; + private FillFlowContainer mainFlowContainer = null!; + private Box hoverLayer = null!; + + private OsuSpriteText titleText = null!; + private OsuSpriteText artistText = null!; + private UpdateBeatmapSetButtonV2 updateButton = null!; + private BeatmapSetOnlineStatusPill statusPill = null!; + + private ConstrainedIconContainer difficultyIcon = null!; + private FillFlowContainer difficultyLine = null!; + private StarRatingDisplay difficultyStarRating = null!; + private TopLocalRankV2 difficultyRank = null!; + private OsuSpriteText difficultyKeyCountText = null!; + private OsuSpriteText difficultyName = null!; + private OsuSpriteText difficultyAuthor = null!; + + [BackgroundDependencyLoader] + private void load() + { + Anchor = Anchor.TopRight; + Origin = Anchor.TopRight; + RelativeSizeAxes = Axes.X; + Width = 1f; + Height = HEIGHT; + + InternalChild = panel = new Container + { + Masking = true, + CornerRadius = corner_radius, + RelativeSizeAxes = Axes.Both, + EdgeEffect = new EdgeEffectParameters + { + Type = EdgeEffectType.Shadow, + Radius = 10, + }, + Children = new Drawable[] + { + new BufferedContainer + { + RelativeSizeAxes = Axes.Both, + Children = new Drawable[] + { + backgroundBorder = new Box + { + RelativeSizeAxes = Axes.Y, + Alpha = 0, + EdgeSmoothness = new Vector2(2, 0), + }, + backgroundContainer = new Container + { + Masking = true, + CornerRadius = corner_radius, + RelativeSizeAxes = Axes.X, + MaskingSmoothness = 2, + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + Children = new Drawable[] + { + background = new BeatmapSetPanelBackground + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + RelativeSizeAxes = Axes.Both, + }, + }, + }, + } + }, + difficultyIcon = new ConstrainedIconContainer + { + X = difficulty_icon_container_width / 2, + Origin = Anchor.Centre, + Anchor = Anchor.CentreLeft, + Size = new Vector2(20), + }, + mainFlowContainer = new FillFlowContainer + { + AutoSizeAxes = Axes.Both, + Direction = FillDirection.Vertical, + Padding = new MarginPadding { Top = 7.5f, Left = 15, Bottom = 5 }, + Children = new Drawable[] + { + titleText = new OsuSpriteText + { + Font = OsuFont.GetFont(weight: FontWeight.Bold, size: 22, italics: true), + Shadow = true, + }, + artistText = new OsuSpriteText + { + Font = OsuFont.GetFont(weight: FontWeight.SemiBold, size: 17, italics: true), + Shadow = true, + }, + new FillFlowContainer + { + Direction = FillDirection.Horizontal, + AutoSizeAxes = Axes.Both, + Margin = new MarginPadding { Top = 5f }, + Children = new Drawable[] + { + updateButton = new UpdateBeatmapSetButtonV2 + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + Margin = new MarginPadding { Right = 5f, Top = -2f }, + }, + statusPill = new BeatmapSetOnlineStatusPill + { + AutoSizeAxes = Axes.Both, + Origin = Anchor.CentreLeft, + Anchor = Anchor.CentreLeft, + TextSize = 11, + TextPadding = new MarginPadding { Horizontal = 8, Vertical = 2 }, + Margin = new MarginPadding { Right = 5f }, + }, + difficultyLine = new FillFlowContainer + { + Direction = FillDirection.Horizontal, + AutoSizeAxes = Axes.Both, + Children = new Drawable[] + { + difficultyStarRating = new StarRatingDisplay(default, StarRatingDisplaySize.Small) + { + Origin = Anchor.CentreLeft, + Anchor = Anchor.CentreLeft, + Scale = new Vector2(8f / 9f), + Margin = new MarginPadding { Right = 5f }, + }, + difficultyRank = new TopLocalRankV2 + { + Scale = new Vector2(8f / 11), + Origin = Anchor.CentreLeft, + Anchor = Anchor.CentreLeft, + Margin = new MarginPadding { Right = 5f }, + }, + difficultyKeyCountText = new OsuSpriteText + { + Font = OsuFont.GetFont(size: 18, weight: FontWeight.SemiBold), + Anchor = Anchor.BottomLeft, + Origin = Anchor.BottomLeft, + Alpha = 0, + Margin = new MarginPadding { Bottom = 2f }, + }, + difficultyName = new OsuSpriteText + { + Font = OsuFont.GetFont(size: 18, weight: FontWeight.SemiBold), + Origin = Anchor.BottomLeft, + Anchor = Anchor.BottomLeft, + Margin = new MarginPadding { Right = 5f, Bottom = 2f }, + }, + difficultyAuthor = new OsuSpriteText + { + Colour = colourProvider.Content2, + Font = OsuFont.GetFont(weight: FontWeight.SemiBold), + Origin = Anchor.BottomLeft, + Anchor = Anchor.BottomLeft, + Margin = new MarginPadding { Right = 5f, Bottom = 2f }, + } + } + }, + }, + } + } + }, + hoverLayer = new Box + { + Colour = colours.Blue.Opacity(0.1f), + Alpha = 0, + Blending = BlendingParameters.Additive, + RelativeSizeAxes = Axes.Both, + }, + new HoverSounds(), + } + }; + } + + public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) + { + var inputRectangle = panel.DrawRectangle; + + // Cover a gap introduced by the spacing between a BeatmapSetPanel and a BeatmapPanel either above it or below it. + inputRectangle = inputRectangle.Inflate(new MarginPadding { Vertical = BeatmapCarousel.SPACING / 2f }); + + return inputRectangle.Contains(panel.ToLocalSpace(screenSpacePos)); + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + ruleset.BindValueChanged(_ => + { + computeStarRating(); + updateKeyCount(); + }); + + mods.BindValueChanged(_ => + { + computeStarRating(); + updateKeyCount(); + }, true); + + Selected.BindValueChanged(_ => updateSelectedDisplay(), true); + KeyboardSelected.BindValueChanged(_ => updateKeyboardSelectedDisplay(), true); + } + + protected override void PrepareForUse() + { + base.PrepareForUse(); + + Debug.Assert(Item != null); + + var beatmap = (BeatmapInfo)Item.Model; + var beatmapSet = beatmap.BeatmapSet!; + + // Choice of background image matches BSS implementation (always uses the lowest `beatmap_id` from the set). + background.Beatmap = beatmaps.GetWorkingBeatmap(beatmapSet.Beatmaps.MinBy(b => b.OnlineID)); + + titleText.Text = new RomanisableString(beatmapSet.Metadata.TitleUnicode, beatmapSet.Metadata.Title); + artistText.Text = new RomanisableString(beatmapSet.Metadata.ArtistUnicode, beatmapSet.Metadata.Artist); + updateButton.BeatmapSet = beatmapSet; + statusPill.Status = beatmapSet.Status; + + difficultyIcon.Icon = beatmap.Ruleset.CreateInstance().CreateIcon(); + difficultyIcon.Show(); + + difficultyRank.Beatmap = beatmap; + difficultyName.Text = beatmap.DifficultyName; + difficultyAuthor.Text = BeatmapsetsStrings.ShowDetailsMappedBy(beatmap.Metadata.Author.Username); + difficultyLine.Show(); + + computeStarRating(); + + updateSelectedDisplay(); + FinishTransforms(true); + + this.FadeInFromZero(duration, Easing.OutQuint); + } + + protected override void FreeAfterUse() + { + base.FreeAfterUse(); + + background.Beatmap = null; + updateButton.BeatmapSet = null; + difficultyRank.Beatmap = null; + starDifficultyBindable = null; + } + + private void updateSelectedDisplay() + { + if (Item == null) + return; + + updatePanelPosition(); + + backgroundBorder.RelativeSizeAxes = Selected.Value ? Axes.Both : Axes.Y; + backgroundBorder.Width = Selected.Value ? 1 : difficulty_icon_container_width + corner_radius; + backgroundBorder.FadeTo(Selected.Value ? 1 : 0, duration, Easing.OutQuint); + difficultyIcon.FadeTo(Selected.Value ? 1 : 0, duration, Easing.OutQuint); + + backgroundContainer.ResizeHeightTo(Selected.Value ? HEIGHT - 4 : HEIGHT, duration, Easing.OutQuint); + backgroundContainer.MoveToX(Selected.Value ? difficulty_icon_container_width : 0, duration, Easing.OutQuint); + mainFlowContainer.MoveToX(Selected.Value ? difficulty_icon_container_width : 0, duration, Easing.OutQuint); + + panel.EdgeEffect = panel.EdgeEffect with { Radius = Selected.Value ? 15 : 10 }; + updateEdgeEffectColour(); + } + + private void updateKeyboardSelectedDisplay() + { + updatePanelPosition(); + updateHover(); + } + + private void updatePanelPosition() + { + float x = glow_offset + selected_x_offset + preselected_x_offset; + + if (Selected.Value) + x -= selected_x_offset; + + if (KeyboardSelected.Value) + x -= preselected_x_offset; + + this.TransformTo(nameof(Padding), new MarginPadding { Left = x }, duration, Easing.OutQuint); + } + + private void updateHover() + { + bool hovered = IsHovered || KeyboardSelected.Value; + + if (hovered) + hoverLayer.FadeIn(100, Easing.OutQuint); + else + hoverLayer.FadeOut(1000, Easing.OutQuint); + } + + private void computeStarRating() + { + starDifficultyCancellationSource?.Cancel(); + starDifficultyCancellationSource = new CancellationTokenSource(); + + if (Item == null) + return; + + var beatmap = (BeatmapInfo)Item.Model; + + starDifficultyBindable = difficultyCache.GetBindableDifficulty(beatmap, starDifficultyCancellationSource.Token); + starDifficultyBindable.BindValueChanged(d => + { + var value = d.NewValue ?? default; + + backgroundBorder.FadeColour(colours.ForStarDifficulty(value.Stars), duration, Easing.OutQuint); + difficultyIcon.FadeColour(value.Stars > 6.5f ? colours.Orange1 : colourProvider.Background5, duration, Easing.OutQuint); + difficultyStarRating.Current.Value = value; + + updateEdgeEffectColour(); + }, true); + } + + private void updateEdgeEffectColour() + { + panel.FadeEdgeEffectTo(Selected.Value + ? colours.ForStarDifficulty(starDifficultyBindable?.Value?.Stars ?? 0f).Opacity(0.5f) + : Color4.Black.Opacity(0.4f), duration, Easing.OutQuint); + } + + private void updateKeyCount() + { + if (Item == null) + return; + + var beatmap = (BeatmapInfo)Item.Model; + + if (ruleset.Value.OnlineID == 3) + { + // Account for mania differences locally for now. + // Eventually this should be handled in a more modular way, allowing rulesets to add more information to the panel. + ILegacyRuleset legacyRuleset = (ILegacyRuleset)ruleset.Value.CreateInstance(); + int keyCount = legacyRuleset.GetKeyCount(beatmap, mods.Value); + + difficultyKeyCountText.Alpha = 1; + difficultyKeyCountText.Text = $"[{keyCount}K] "; + } + else + difficultyKeyCountText.Alpha = 0; + } + + protected override bool OnHover(HoverEvent e) + { + updateHover(); + return true; + } + + protected override void OnHoverLost(HoverLostEvent e) + { + updateHover(); + base.OnHoverLost(e); + } + + protected override bool OnClick(ClickEvent e) + { + if (carousel != null) + carousel.CurrentSelection = Item!.Model; + + return true; + } + + #region ICarouselPanel + + public CarouselItem? Item { get; set; } + public BindableBool Selected { get; } = new BindableBool(); + public BindableBool Expanded { get; } = new BindableBool(); + public BindableBool KeyboardSelected { get; } = new BindableBool(); + + public double DrawYPosition { get; set; } + + public void Activated() + { + // sets should never be activated. + throw new InvalidOperationException(); + } + + #endregion + } +} From c94d11b7fe08b7d2284615049dd0c0f9de14c5d7 Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Wed, 5 Feb 2025 07:19:12 -0500 Subject: [PATCH 0900/1275] Add beatmap carousel to new song select screen --- osu.Game/Screens/SelectV2/SongSelectV2.cs | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/osu.Game/Screens/SelectV2/SongSelectV2.cs b/osu.Game/Screens/SelectV2/SongSelectV2.cs index 2f9667793f..88825d96e0 100644 --- a/osu.Game/Screens/SelectV2/SongSelectV2.cs +++ b/osu.Game/Screens/SelectV2/SongSelectV2.cs @@ -39,6 +39,20 @@ namespace osu.Game.Screens.SelectV2 { AddRangeInternal(new Drawable[] { + new Container + { + RelativeSizeAxes = Axes.Both, + Padding = new MarginPadding { Bottom = ScreenFooter.HEIGHT }, + Child = new BeatmapCarousel + { + Anchor = Anchor.TopRight, + Origin = Anchor.TopRight, + RelativeSizeAxes = Axes.Both, + Width = 0.5f, + // Push the carousel slightly off the right edge of the screen for the ends of the panels to be cut off. + X = 20f, + }, + }, modSelectOverlay, }); } From abce42b1c8fa48dc9fc64ff5e756b90f66d947a1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 5 Feb 2025 15:28:27 +0100 Subject: [PATCH 0901/1275] Improve bookmark controls - Bookmark menu items get disabled when they would do nothing. - Bookmark deletion only deletes the closest bookmark instead of all of them within the proximity of 2 seconds to current clock time. Action is only however *enabled* within 2 seconds of a bookmark. Additionally, logic was moved out of `Editor` because it's a huge class and I dislike huge classes if they can be at all avoided. --- osu.Game/Screens/Edit/BookmarkController.cs | 148 ++++++++++++++++++++ osu.Game/Screens/Edit/Editor.cs | 66 +-------- 2 files changed, 152 insertions(+), 62 deletions(-) create mode 100644 osu.Game/Screens/Edit/BookmarkController.cs diff --git a/osu.Game/Screens/Edit/BookmarkController.cs b/osu.Game/Screens/Edit/BookmarkController.cs new file mode 100644 index 0000000000..8c048ba871 --- /dev/null +++ b/osu.Game/Screens/Edit/BookmarkController.cs @@ -0,0 +1,148 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using System.Linq; +using osu.Framework.Allocation; +using osu.Framework.Bindables; +using osu.Framework.Graphics; +using osu.Framework.Graphics.UserInterface; +using osu.Framework.Input.Bindings; +using osu.Framework.Input.Events; +using osu.Game.Graphics.UserInterface; +using osu.Game.Input.Bindings; +using osu.Game.Localisation; +using osu.Game.Overlays; +using osu.Game.Screens.Edit.Components.Menus; + +namespace osu.Game.Screens.Edit +{ + public class BookmarkController : Component, IKeyBindingHandler + { + public EditorMenuItem Menu { get; private set; } + + [Resolved] + private EditorClock clock { get; set; } = null!; + + [Resolved] + private EditorBeatmap editorBeatmap { get; set; } = null!; + + [Resolved] + private IDialogOverlay? dialogOverlay { get; set; } + + private readonly BindableList bookmarks = new BindableList(); + + private readonly EditorMenuItem removeBookmarkMenuItem; + private readonly EditorMenuItem seekToPreviousBookmarkMenuItem; + private readonly EditorMenuItem seekToNextBookmarkMenuItem; + private readonly EditorMenuItem resetBookmarkMenuItem; + + public BookmarkController() + { + Menu = new EditorMenuItem(EditorStrings.Bookmarks) + { + Items = new MenuItem[] + { + new EditorMenuItem(EditorStrings.AddBookmark, MenuItemType.Standard, addBookmarkAtCurrentTime) + { + Hotkey = new Hotkey(GlobalAction.EditorAddBookmark), + }, + removeBookmarkMenuItem = new EditorMenuItem(EditorStrings.RemoveClosestBookmark, MenuItemType.Destructive, removeClosestBookmark) + { + Hotkey = new Hotkey(GlobalAction.EditorRemoveClosestBookmark) + }, + seekToPreviousBookmarkMenuItem = new EditorMenuItem(EditorStrings.SeekToPreviousBookmark, MenuItemType.Standard, () => seekBookmark(-1)) + { + Hotkey = new Hotkey(GlobalAction.EditorSeekToPreviousBookmark) + }, + seekToNextBookmarkMenuItem = new EditorMenuItem(EditorStrings.SeekToNextBookmark, MenuItemType.Standard, () => seekBookmark(1)) + { + Hotkey = new Hotkey(GlobalAction.EditorSeekToNextBookmark) + }, + resetBookmarkMenuItem = new EditorMenuItem(EditorStrings.ResetBookmarks, MenuItemType.Destructive, () => dialogOverlay?.Push(new BookmarkResetDialog(editorBeatmap))) + } + }; + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + bookmarks.BindTo(editorBeatmap.Bookmarks); + } + + protected override void Update() + { + base.Update(); + + bool hasAnyBookmark = bookmarks.Count > 0; + bool hasBookmarkCloseEnoughForDeletion = bookmarks.Any(b => Math.Abs(b - clock.CurrentTimeAccurate) < 2000); + + removeBookmarkMenuItem.Action.Disabled = !hasBookmarkCloseEnoughForDeletion; + seekToPreviousBookmarkMenuItem.Action.Disabled = !hasAnyBookmark; + seekToNextBookmarkMenuItem.Action.Disabled = !hasAnyBookmark; + resetBookmarkMenuItem.Action.Disabled = !hasAnyBookmark; + } + + private void addBookmarkAtCurrentTime() + { + int bookmark = (int)clock.CurrentTimeAccurate; + int idx = bookmarks.BinarySearch(bookmark); + if (idx < 0) + bookmarks.Insert(~idx, bookmark); + } + + private void removeClosestBookmark() + { + if (removeBookmarkMenuItem.Action.Disabled) + return; + + int closestBookmark = bookmarks.MinBy(b => Math.Abs(b - clock.CurrentTimeAccurate)); + bookmarks.Remove(closestBookmark); + } + + private void seekBookmark(int direction) + { + int? targetBookmark = direction < 1 + ? bookmarks.Cast().LastOrDefault(b => b < clock.CurrentTimeAccurate) + : bookmarks.Cast().FirstOrDefault(b => b > clock.CurrentTimeAccurate); + + if (targetBookmark != null) + clock.SeekSmoothlyTo(targetBookmark.Value); + } + + public bool OnPressed(KeyBindingPressEvent e) + { + switch (e.Action) + { + case GlobalAction.EditorSeekToPreviousBookmark: + seekBookmark(-1); + return true; + + case GlobalAction.EditorSeekToNextBookmark: + seekBookmark(1); + return true; + } + + if (e.Repeat) + return false; + + switch (e.Action) + { + case GlobalAction.EditorAddBookmark: + addBookmarkAtCurrentTime(); + return true; + + case GlobalAction.EditorRemoveClosestBookmark: + removeClosestBookmark(); + return true; + } + + return false; + } + + public void OnReleased(KeyBindingReleaseEvent e) + { + } + } +} diff --git a/osu.Game/Screens/Edit/Editor.cs b/osu.Game/Screens/Edit/Editor.cs index 3302fafbb8..a5dfda9c95 100644 --- a/osu.Game/Screens/Edit/Editor.cs +++ b/osu.Game/Screens/Edit/Editor.cs @@ -317,6 +317,9 @@ namespace osu.Game.Screens.Edit workingBeatmapUpdated = true; }); + var bookmarkController = new BookmarkController(); + AddInternal(bookmarkController); + OsuMenuItem undoMenuItem; OsuMenuItem redoMenuItem; @@ -442,29 +445,7 @@ namespace osu.Game.Screens.Edit Items = new MenuItem[] { new EditorMenuItem(EditorStrings.SetPreviewPointToCurrent, MenuItemType.Standard, SetPreviewPointToCurrentTime), - new EditorMenuItem(EditorStrings.Bookmarks) - { - Items = new MenuItem[] - { - new EditorMenuItem(EditorStrings.AddBookmark, MenuItemType.Standard, addBookmarkAtCurrentTime) - { - Hotkey = new Hotkey(GlobalAction.EditorAddBookmark), - }, - new EditorMenuItem(EditorStrings.RemoveClosestBookmark, MenuItemType.Destructive, removeBookmarksInProximityToCurrentTime) - { - Hotkey = new Hotkey(GlobalAction.EditorRemoveClosestBookmark) - }, - new EditorMenuItem(EditorStrings.SeekToPreviousBookmark, MenuItemType.Standard, () => seekBookmark(-1)) - { - Hotkey = new Hotkey(GlobalAction.EditorSeekToPreviousBookmark) - }, - new EditorMenuItem(EditorStrings.SeekToNextBookmark, MenuItemType.Standard, () => seekBookmark(1)) - { - Hotkey = new Hotkey(GlobalAction.EditorSeekToNextBookmark) - }, - new EditorMenuItem(EditorStrings.ResetBookmarks, MenuItemType.Destructive, () => dialogOverlay?.Push(new BookmarkResetDialog(editorBeatmap))) - } - } + bookmarkController.Menu, } } } @@ -800,14 +781,6 @@ namespace osu.Game.Screens.Edit case GlobalAction.EditorSeekToNextSamplePoint: seekSamplePoint(1); return true; - - case GlobalAction.EditorSeekToPreviousBookmark: - seekBookmark(-1); - return true; - - case GlobalAction.EditorSeekToNextBookmark: - seekBookmark(1); - return true; } if (e.Repeat) @@ -815,14 +788,6 @@ namespace osu.Game.Screens.Edit switch (e.Action) { - case GlobalAction.EditorAddBookmark: - addBookmarkAtCurrentTime(); - return true; - - case GlobalAction.EditorRemoveClosestBookmark: - removeBookmarksInProximityToCurrentTime(); - return true; - case GlobalAction.EditorCloneSelection: Clone(); return true; @@ -855,19 +820,6 @@ namespace osu.Game.Screens.Edit return false; } - private void addBookmarkAtCurrentTime() - { - int bookmark = (int)clock.CurrentTimeAccurate; - int idx = editorBeatmap.Bookmarks.BinarySearch(bookmark); - if (idx < 0) - editorBeatmap.Bookmarks.Insert(~idx, bookmark); - } - - private void removeBookmarksInProximityToCurrentTime() - { - editorBeatmap.Bookmarks.RemoveAll(b => Math.Abs(b - clock.CurrentTimeAccurate) < 2000); - } - public void OnReleased(KeyBindingReleaseEvent e) { } @@ -1202,16 +1154,6 @@ namespace osu.Game.Screens.Edit clock.SeekSmoothlyTo(found.StartTime); } - private void seekBookmark(int direction) - { - int? targetBookmark = direction < 1 - ? editorBeatmap.Bookmarks.Cast().LastOrDefault(b => b < clock.CurrentTimeAccurate) - : editorBeatmap.Bookmarks.Cast().FirstOrDefault(b => b > clock.CurrentTimeAccurate); - - if (targetBookmark != null) - clock.SeekSmoothlyTo(targetBookmark.Value); - } - private void seekSamplePoint(int direction) { double currentTime = clock.CurrentTimeAccurate; From 4cbfb5170790c301ecfa08214df9795e82b754e3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 5 Feb 2025 15:30:11 +0100 Subject: [PATCH 0902/1275] Fix undoing bookmark operations potentially making them unsorted Found in testing of previous commit. This would break seeking between bookmarks. Reproduction steps on `master`: - open map with bookmark - delete the first bookmark - undo the deletion of the first bookmark - seek to previous bookmark will now always seek to the first bookmark rather than closest preceding regardless of current clock time --- osu.Game/Screens/Edit/LegacyEditorBeatmapPatcher.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/osu.Game/Screens/Edit/LegacyEditorBeatmapPatcher.cs b/osu.Game/Screens/Edit/LegacyEditorBeatmapPatcher.cs index f3d58a3c3c..e84b6bfc72 100644 --- a/osu.Game/Screens/Edit/LegacyEditorBeatmapPatcher.cs +++ b/osu.Game/Screens/Edit/LegacyEditorBeatmapPatcher.cs @@ -115,7 +115,9 @@ namespace osu.Game.Screens.Edit if (editorBeatmap.Bookmarks.Contains(newBookmark)) continue; - editorBeatmap.Bookmarks.Add(newBookmark); + int idx = editorBeatmap.Bookmarks.BinarySearch(newBookmark); + if (idx < 0) + editorBeatmap.Bookmarks.Insert(~idx, newBookmark); } } From 10711e5e2721ab11b28c4fc5f00e6769d9aad3ec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 5 Feb 2025 15:39:36 +0100 Subject: [PATCH 0903/1275] Add missing `partial` --- osu.Game/Screens/Edit/BookmarkController.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/Edit/BookmarkController.cs b/osu.Game/Screens/Edit/BookmarkController.cs index 8c048ba871..3d2cb4663f 100644 --- a/osu.Game/Screens/Edit/BookmarkController.cs +++ b/osu.Game/Screens/Edit/BookmarkController.cs @@ -17,7 +17,7 @@ using osu.Game.Screens.Edit.Components.Menus; namespace osu.Game.Screens.Edit { - public class BookmarkController : Component, IKeyBindingHandler + public partial class BookmarkController : Component, IKeyBindingHandler { public EditorMenuItem Menu { get; private set; } From 29882a2542bb9895a163461055ff7f57f961f022 Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Wed, 5 Feb 2025 07:19:14 -0500 Subject: [PATCH 0904/1275] Allow importing real beatmaps in song select test scene --- .../SongSelectV2/TestSceneSongSelect.cs | 62 ++++++++++++++++++- 1 file changed, 60 insertions(+), 2 deletions(-) diff --git a/osu.Game.Tests/Visual/SongSelectV2/TestSceneSongSelect.cs b/osu.Game.Tests/Visual/SongSelectV2/TestSceneSongSelect.cs index d43026c960..33474d7449 100644 --- a/osu.Game.Tests/Visual/SongSelectV2/TestSceneSongSelect.cs +++ b/osu.Game.Tests/Visual/SongSelectV2/TestSceneSongSelect.cs @@ -9,15 +9,27 @@ using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Cursor; +using osu.Framework.Platform; using osu.Framework.Screens; using osu.Framework.Testing; +using osu.Game.Beatmaps; +using osu.Game.Configuration; +using osu.Game.Database; +using osu.Game.Online.API; +using osu.Game.Overlays; using osu.Game.Overlays.Mods; +using osu.Game.Rulesets; +using osu.Game.Rulesets.Catch; +using osu.Game.Rulesets.Mania; using osu.Game.Rulesets.Mods; +using osu.Game.Rulesets.Osu; using osu.Game.Rulesets.Osu.Mods; +using osu.Game.Rulesets.Taiko; using osu.Game.Screens; using osu.Game.Screens.Footer; using osu.Game.Screens.Menu; using osu.Game.Screens.SelectV2.Footer; +using osu.Game.Tests.Resources; using osuTK.Input; namespace osu.Game.Tests.Visual.SongSelectV2 @@ -30,6 +42,10 @@ namespace osu.Game.Tests.Visual.SongSelectV2 [Cached] private readonly OsuLogo logo; + private BeatmapManager beatmapManager = null!; + + protected override bool UseOnlineAPI => true; + public TestSceneSongSelect() { Children = new Drawable[] @@ -49,6 +65,35 @@ namespace osu.Game.Tests.Visual.SongSelectV2 }; } + [BackgroundDependencyLoader] + private void load(GameHost host, IAPIProvider onlineAPI) + { + BeatmapStore beatmapStore; + BeatmapUpdater beatmapUpdater; + BeatmapDifficultyCache difficultyCache; + + // These DI caches are required to ensure for interactive runs this test scene doesn't nuke all user beatmaps in the local install. + // At a point we have isolated interactive test runs enough, this can likely be removed. + Dependencies.Cache(new RealmRulesetStore(Realm)); + Dependencies.Cache(Realm); + Dependencies.Cache(difficultyCache = new BeatmapDifficultyCache()); + Dependencies.Cache(beatmapManager = new BeatmapManager(LocalStorage, Realm, onlineAPI, Audio, Resources, host, Beatmap.Default, difficultyCache)); + Dependencies.CacheAs(beatmapUpdater = new BeatmapUpdater(beatmapManager, difficultyCache, onlineAPI, LocalStorage)); + Dependencies.CacheAs(beatmapStore = new RealmDetachedBeatmapStore()); + + beatmapManager.ProcessBeatmap = (set, scope) => beatmapUpdater.Process(set, scope); + + MusicController music; + Dependencies.Cache(music = new MusicController()); + + // required to get bindables attached + Add(difficultyCache); + Add(music); + Add(beatmapStore); + + Dependencies.Cache(new OsuConfigManager(LocalStorage)); + } + protected override void LoadComplete() { base.LoadComplete(); @@ -64,6 +109,16 @@ namespace osu.Game.Tests.Visual.SongSelectV2 AddStep("load screen", () => Stack.Push(new Screens.SelectV2.SongSelectV2())); AddUntilStep("wait for load", () => Stack.CurrentScreen is Screens.SelectV2.SongSelectV2 songSelect && songSelect.IsLoaded); + AddStep("import test beatmap", () => beatmapManager.Import(TestResources.GetTestBeatmapForImport())); + } + + [Test] + public void TestRulesets() + { + AddStep("set osu ruleset", () => Ruleset.Value = new OsuRuleset().RulesetInfo); + AddStep("set taiko ruleset", () => Ruleset.Value = new TaikoRuleset().RulesetInfo); + AddStep("set catch ruleset", () => Ruleset.Value = new CatchRuleset().RulesetInfo); + AddStep("set mania ruleset", () => Ruleset.Value = new ManiaRuleset().RulesetInfo); } #region Footer @@ -80,8 +135,11 @@ namespace osu.Game.Tests.Visual.SongSelectV2 AddStep("modified", () => SelectedMods.Value = new List { new OsuModDoubleTime { SpeedChange = { Value = 1.2 } } }); AddStep("modified + one", () => SelectedMods.Value = new List { new OsuModHidden(), new OsuModDoubleTime { SpeedChange = { Value = 1.2 } } }); AddStep("modified + two", () => SelectedMods.Value = new List { new OsuModHidden(), new OsuModHardRock(), new OsuModDoubleTime { SpeedChange = { Value = 1.2 } } }); - AddStep("modified + three", () => SelectedMods.Value = new List { new OsuModHidden(), new OsuModHardRock(), new OsuModClassic(), new OsuModDoubleTime { SpeedChange = { Value = 1.2 } } }); - AddStep("modified + four", () => SelectedMods.Value = new List { new OsuModHidden(), new OsuModHardRock(), new OsuModClassic(), new OsuModDifficultyAdjust(), new OsuModDoubleTime { SpeedChange = { Value = 1.2 } } }); + AddStep("modified + three", + () => SelectedMods.Value = new List { new OsuModHidden(), new OsuModHardRock(), new OsuModClassic(), new OsuModDoubleTime { SpeedChange = { Value = 1.2 } } }); + AddStep("modified + four", + () => SelectedMods.Value = new List + { new OsuModHidden(), new OsuModHardRock(), new OsuModClassic(), new OsuModDifficultyAdjust(), new OsuModDoubleTime { SpeedChange = { Value = 1.2 } } }); AddStep("clear mods", () => SelectedMods.Value = Array.Empty()); AddWaitStep("wait", 3); From f9962f95f098bf3e4076839544090ad7556c3fcd Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Wed, 5 Feb 2025 08:16:51 -0500 Subject: [PATCH 0905/1275] Implement group panel design --- .../TestSceneBeatmapCarouselGroupPanel.cs | 80 +++++ osu.Game/Screens/SelectV2/BeatmapCarousel.cs | 1 + osu.Game/Screens/SelectV2/GroupPanel.cs | 220 ++++++++++--- osu.Game/Screens/SelectV2/StarsGroupPanel.cs | 288 ++++++++++++++++++ 4 files changed, 541 insertions(+), 48 deletions(-) create mode 100644 osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselGroupPanel.cs create mode 100644 osu.Game/Screens/SelectV2/StarsGroupPanel.cs diff --git a/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselGroupPanel.cs b/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselGroupPanel.cs new file mode 100644 index 0000000000..eea3870117 --- /dev/null +++ b/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselGroupPanel.cs @@ -0,0 +1,80 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Game.Screens.SelectV2; +using osu.Game.Tests.Visual.UserInterface; +using osuTK; + +namespace osu.Game.Tests.Visual.SongSelectV2 +{ + public partial class TestSceneBeatmapCarouselGroupPanel : ThemeComparisonTestScene + { + public TestSceneBeatmapCarouselGroupPanel() + : base(false) + { + } + + protected override Drawable CreateContent() + { + return new FillFlowContainer + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Width = 0.5f, + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Direction = FillDirection.Vertical, + Spacing = new Vector2(0f, 5f), + Children = new Drawable[] + { + new GroupPanel + { + Item = new CarouselItem(new GroupDefinition("Group A")) + }, + new GroupPanel + { + Item = new CarouselItem(new GroupDefinition("Group A")), + KeyboardSelected = { Value = true } + }, + new GroupPanel + { + Item = new CarouselItem(new GroupDefinition("Group A")), + Selected = { Value = true } + }, + new GroupPanel + { + Item = new CarouselItem(new GroupDefinition("Group A")), + KeyboardSelected = { Value = true }, + Selected = { Value = true } + }, + new StarsGroupPanel + { + Item = new CarouselItem(new StarsGroupDefinition(1)) + }, + new StarsGroupPanel + { + Item = new CarouselItem(new StarsGroupDefinition(3)), + }, + new StarsGroupPanel + { + Item = new CarouselItem(new StarsGroupDefinition(5)), + }, + new StarsGroupPanel + { + Item = new CarouselItem(new StarsGroupDefinition(7)), + }, + new StarsGroupPanel + { + Item = new CarouselItem(new StarsGroupDefinition(8)), + }, + new StarsGroupPanel + { + Item = new CarouselItem(new StarsGroupDefinition(9)), + }, + } + }; + } + } +} diff --git a/osu.Game/Screens/SelectV2/BeatmapCarousel.cs b/osu.Game/Screens/SelectV2/BeatmapCarousel.cs index 12660d8642..a49dcdd86c 100644 --- a/osu.Game/Screens/SelectV2/BeatmapCarousel.cs +++ b/osu.Game/Screens/SelectV2/BeatmapCarousel.cs @@ -264,4 +264,5 @@ namespace osu.Game.Screens.SelectV2 } public record GroupDefinition(string Title); + public record StarsGroupDefinition(int StarNumber); } diff --git a/osu.Game/Screens/SelectV2/GroupPanel.cs b/osu.Game/Screens/SelectV2/GroupPanel.cs index df930a3111..8995b93290 100644 --- a/osu.Game/Screens/SelectV2/GroupPanel.cs +++ b/osu.Game/Screens/SelectV2/GroupPanel.cs @@ -7,10 +7,14 @@ 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.Pooling; using osu.Framework.Graphics.Shapes; using osu.Framework.Input.Events; +using osu.Game.Graphics; using osu.Game.Graphics.Sprites; +using osu.Game.Graphics.UserInterface; +using osu.Game.Overlays; using osuTK; using osuTK.Graphics; @@ -18,15 +22,20 @@ namespace osu.Game.Screens.SelectV2 { public partial class GroupPanel : PoolableDrawable, ICarouselPanel { - public const float HEIGHT = CarouselItem.DEFAULT_HEIGHT * 2; + public const float HEIGHT = CarouselItem.DEFAULT_HEIGHT; + + private const float glow_offset = 10f; // extra space for the edge effect to not be cutoff by the right edge of the carousel. + private const float preselected_x_offset = 25f; + private const float selected_x_offset = 50f; + + private const float duration = 500; [Resolved] - private BeatmapCarousel carousel { get; set; } = null!; + private BeatmapCarousel? carousel { get; set; } private Box activationFlash = null!; - private OsuSpriteText text = null!; - - private Box box = null!; + private OsuSpriteText titleText = null!; + private Box hoverLayer = null!; public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) { @@ -39,56 +48,128 @@ namespace osu.Game.Screens.SelectV2 } [BackgroundDependencyLoader] - private void load() + private void load(OverlayColourProvider colourProvider, OsuColour colours) { - Size = new Vector2(500, HEIGHT); - Masking = true; + Anchor = Anchor.TopRight; + Origin = Anchor.TopRight; + RelativeSizeAxes = Axes.X; + Height = HEIGHT; - InternalChildren = new Drawable[] + InternalChild = new Container { - box = new Box + RelativeSizeAxes = Axes.Both, + CornerRadius = 10f, + Masking = true, + Children = new Drawable[] { - Colour = Color4.DarkBlue.Darken(5), - Alpha = 0.8f, - RelativeSizeAxes = Axes.Both, - }, - activationFlash = new Box - { - Colour = Color4.White, - Blending = BlendingParameters.Additive, - Alpha = 0, - RelativeSizeAxes = Axes.Both, - }, - text = new OsuSpriteText - { - Padding = new MarginPadding(5), - Anchor = Anchor.CentreLeft, - Origin = Anchor.CentreLeft, + new Container + { + RelativeSizeAxes = Axes.Both, + Padding = new MarginPadding { Left = 10f }, + Child = new Container + { + RelativeSizeAxes = Axes.Both, + CornerRadius = 10f, + Masking = true, + Children = new Drawable[] + { + new Box + { + RelativeSizeAxes = Axes.Both, + Colour = colourProvider.Background6, + }, + } + } + }, + new Box + { + RelativeSizeAxes = Axes.Both, + Colour = colourProvider.Background3, + }, + new Container + { + RelativeSizeAxes = Axes.Both, + Padding = new MarginPadding { Left = 10f }, + Child = new Container + { + RelativeSizeAxes = Axes.Both, + CornerRadius = 10f, + Masking = true, + Children = new Drawable[] + { + new Box + { + RelativeSizeAxes = Axes.Both, + Colour = colourProvider.Background5, + }, + titleText = new OsuSpriteText + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + X = 10f, + }, + new CircularContainer + { + Anchor = Anchor.CentreRight, + Origin = Anchor.CentreRight, + Size = new Vector2(50f, 14f), + Margin = new MarginPadding { Right = 30f }, + Masking = true, + Children = new Drawable[] + { + new Box + { + RelativeSizeAxes = Axes.Both, + Colour = Color4.Black.Opacity(0.7f), + }, + new OsuSpriteText + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Font = OsuFont.Torus.With(size: 14.4f, weight: FontWeight.Bold), + // TODO: requires Carousel/CarouselItem-side implementation + Text = "43", + UseFullGlyphHeight = false, + } + }, + }, + } + } + }, + activationFlash = new Box + { + Colour = Color4.White, + Blending = BlendingParameters.Additive, + Alpha = 0, + RelativeSizeAxes = Axes.Both, + }, + hoverLayer = new Box + { + Colour = colours.Blue.Opacity(0.1f), + Alpha = 0, + Blending = BlendingParameters.Additive, + RelativeSizeAxes = Axes.Both, + }, + new HoverSounds(), } }; + } - Selected.BindValueChanged(value => - { - activationFlash.FadeTo(value.NewValue ? 0.2f : 0, 500, Easing.OutQuint); - }); + protected override void LoadComplete() + { + base.LoadComplete(); - Expanded.BindValueChanged(value => - { - box.FadeColour(value.NewValue ? Color4.SkyBlue : Color4.DarkBlue.Darken(5), 500, Easing.OutQuint); - }); + Expanded.BindValueChanged(_ => updateExpandedDisplay(), true); + KeyboardSelected.BindValueChanged(_ => updateKeyboardSelectedDisplay(), true); + } - KeyboardSelected.BindValueChanged(value => - { - if (value.NewValue) - { - BorderThickness = 5; - BorderColour = Color4.Pink; - } - else - { - BorderThickness = 0; - } - }); + private void updateExpandedDisplay() + { + updatePanelPosition(); + + // todo: figma shares no extra visual feedback on this. + + activationFlash.FadeTo(0.2f).FadeTo(0f, 500, Easing.OutQuint); } protected override void PrepareForUse() @@ -99,17 +180,60 @@ namespace osu.Game.Screens.SelectV2 GroupDefinition group = (GroupDefinition)Item.Model; - text.Text = group.Title; + titleText.Text = group.Title; this.FadeInFromZero(500, Easing.OutQuint); } protected override bool OnClick(ClickEvent e) { - carousel.CurrentSelection = Item!.Model; + if (carousel != null) + carousel.CurrentSelection = Item!.Model; + return true; } + private void updateKeyboardSelectedDisplay() + { + updatePanelPosition(); + updateHover(); + } + + private void updatePanelPosition() + { + float x = glow_offset + selected_x_offset + preselected_x_offset; + + if (Expanded.Value) + x -= selected_x_offset; + + if (KeyboardSelected.Value) + x -= preselected_x_offset; + + this.TransformTo(nameof(Padding), new MarginPadding { Left = x }, duration, Easing.OutQuint); + } + + private void updateHover() + { + bool hovered = IsHovered || KeyboardSelected.Value; + + if (hovered) + hoverLayer.FadeIn(100, Easing.OutQuint); + else + hoverLayer.FadeOut(1000, Easing.OutQuint); + } + + protected override bool OnHover(HoverEvent e) + { + updateHover(); + return true; + } + + protected override void OnHoverLost(HoverLostEvent e) + { + updateHover(); + base.OnHoverLost(e); + } + #region ICarouselPanel public CarouselItem? Item { get; set; } diff --git a/osu.Game/Screens/SelectV2/StarsGroupPanel.cs b/osu.Game/Screens/SelectV2/StarsGroupPanel.cs new file mode 100644 index 0000000000..8ebf3fc7e8 --- /dev/null +++ b/osu.Game/Screens/SelectV2/StarsGroupPanel.cs @@ -0,0 +1,288 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using System.Diagnostics; +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.Pooling; +using osu.Framework.Graphics.Shapes; +using osu.Framework.Input.Events; +using osu.Game.Beatmaps; +using osu.Game.Beatmaps.Drawables; +using osu.Game.Graphics; +using osu.Game.Graphics.Sprites; +using osu.Game.Graphics.UserInterface; +using osu.Game.Overlays; +using osuTK; +using osuTK.Graphics; + +namespace osu.Game.Screens.SelectV2 +{ + public partial class StarsGroupPanel : PoolableDrawable, ICarouselPanel + { + public const float HEIGHT = CarouselItem.DEFAULT_HEIGHT; + + private const float glow_offset = 10f; // extra space for the edge effect to not be cutoff by the right edge of the carousel. + private const float preselected_x_offset = 25f; + private const float expanded_x_offset = 50f; + + private const float duration = 500; + + [Resolved] + private BeatmapCarousel? carousel { get; set; } + + [Resolved] + private OsuColour colours { get; set; } = null!; + + [Resolved] + private OverlayColourProvider colourProvider { get; set; } = null!; + + private Box activationFlash = null!; + private Box outerLayer = null!; + private Box innerLayer = null!; + private StarRatingDisplay starRatingDisplay = null!; + private StarCounter starCounter = null!; + private Box hoverLayer = null!; + + public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) + { + var inputRectangle = DrawRectangle; + + // Cover a gap introduced by the spacing between a GroupPanel and a BeatmapPanel either below/above it. + inputRectangle = inputRectangle.Inflate(new MarginPadding { Vertical = BeatmapCarousel.SPACING / 2f }); + + return inputRectangle.Contains(ToLocalSpace(screenSpacePos)); + } + + [BackgroundDependencyLoader] + private void load() + { + Anchor = Anchor.TopRight; + Origin = Anchor.TopRight; + RelativeSizeAxes = Axes.X; + Height = HEIGHT; + + InternalChild = new Container + { + RelativeSizeAxes = Axes.Both, + CornerRadius = 10f, + Masking = true, + Children = new Drawable[] + { + new Container + { + RelativeSizeAxes = Axes.Both, + Padding = new MarginPadding { Left = 10f }, + Child = new Container + { + RelativeSizeAxes = Axes.Both, + CornerRadius = 10f, + Masking = true, + Children = new Drawable[] + { + new Box + { + RelativeSizeAxes = Axes.Both, + Colour = colourProvider.Background6, + }, + } + } + }, + outerLayer = new Box + { + RelativeSizeAxes = Axes.Both, + Colour = colourProvider.Background3, + }, + new Container + { + RelativeSizeAxes = Axes.Both, + Padding = new MarginPadding { Left = 10f }, + Child = new Container + { + RelativeSizeAxes = Axes.Both, + CornerRadius = 10f, + Masking = true, + Children = new Drawable[] + { + innerLayer = new Box + { + RelativeSizeAxes = Axes.Both, + Colour = Color4.Black.Opacity(0.2f), + }, + new FillFlowContainer + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + AutoSizeAxes = Axes.Both, + Spacing = new Vector2(10f, 0f), + Margin = new MarginPadding { Left = 10f }, + Children = new Drawable[] + { + starRatingDisplay = new StarRatingDisplay(default, StarRatingDisplaySize.Small) + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + }, + starCounter = new StarCounter + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + Scale = new Vector2(8f / 20f), + }, + } + }, + new CircularContainer + { + Anchor = Anchor.CentreRight, + Origin = Anchor.CentreRight, + Size = new Vector2(50f, 14f), + Margin = new MarginPadding { Right = 30f }, + Masking = true, + Children = new Drawable[] + { + new Box + { + RelativeSizeAxes = Axes.Both, + Colour = Color4.Black.Opacity(0.7f), + }, + new OsuSpriteText + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Font = OsuFont.Torus.With(size: 14.4f, weight: FontWeight.Bold), + // TODO: requires Carousel/CarouselItem-side implementation + Text = "43", + UseFullGlyphHeight = false, + } + }, + }, + } + } + }, + activationFlash = new Box + { + Colour = Color4.White, + Blending = BlendingParameters.Additive, + Alpha = 0, + RelativeSizeAxes = Axes.Both, + }, + hoverLayer = new Box + { + Colour = colours.Blue.Opacity(0.1f), + Alpha = 0, + Blending = BlendingParameters.Additive, + RelativeSizeAxes = Axes.Both, + }, + new HoverSounds(), + } + }; + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + Expanded.BindValueChanged(_ => updateExpandedDisplay(), true); + KeyboardSelected.BindValueChanged(_ => updateKeyboardSelectedDisplay(), true); + } + + private void updateExpandedDisplay() + { + updatePanelPosition(); + + // todo: figma shares no extra visual feedback on this. + + activationFlash.FadeTo(0.2f).FadeTo(0f, 500, Easing.OutQuint); + } + + protected override void PrepareForUse() + { + base.PrepareForUse(); + + Debug.Assert(Item != null); + + StarsGroupDefinition group = (StarsGroupDefinition)Item.Model; + + Color4 colour = group.StarNumber >= 9 ? OsuColour.Gray(0.2f) : colours.ForStarDifficulty(group.StarNumber); + Color4 contentColour = group.StarNumber >= 7 ? colours.Orange1 : colourProvider.Background5; + + outerLayer.Colour = colour; + starCounter.Colour = contentColour; + + starRatingDisplay.Current.Value = new StarDifficulty(group.StarNumber, 0); + starCounter.Current = group.StarNumber; + + this.FadeInFromZero(500, Easing.OutQuint); + } + + protected override bool OnClick(ClickEvent e) + { + if (carousel != null) + carousel.CurrentSelection = Item!.Model; + + return true; + } + + private void updateKeyboardSelectedDisplay() + { + updatePanelPosition(); + updateHover(); + } + + private void updatePanelPosition() + { + float x = glow_offset + expanded_x_offset + preselected_x_offset; + + if (Expanded.Value) + x -= expanded_x_offset; + + if (KeyboardSelected.Value) + x -= preselected_x_offset; + + this.TransformTo(nameof(Padding), new MarginPadding { Left = x }, duration, Easing.OutQuint); + } + + private void updateHover() + { + bool hovered = IsHovered || KeyboardSelected.Value; + + if (hovered) + hoverLayer.FadeIn(100, Easing.OutQuint); + else + hoverLayer.FadeOut(1000, Easing.OutQuint); + } + + protected override bool OnHover(HoverEvent e) + { + updateHover(); + return true; + } + + protected override void OnHoverLost(HoverLostEvent e) + { + updateHover(); + base.OnHoverLost(e); + } + + #region ICarouselPanel + + public CarouselItem? Item { get; set; } + public BindableBool Selected { get; } = new BindableBool(); + public BindableBool Expanded { get; } = new BindableBool(); + public BindableBool KeyboardSelected { get; } = new BindableBool(); + + public double DrawYPosition { get; set; } + + public void Activated() + { + // sets should never be activated. + throw new InvalidOperationException(); + } + + #endregion + } +} From 04a3ee863c3f1b2f93d4868da9aebb79d2ec2d32 Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Wed, 5 Feb 2025 08:38:08 -0500 Subject: [PATCH 0906/1275] Fix design tests --- ...TestSceneBeatmapCarouselDifficultyPanel.cs | 26 +++++++++++-------- .../TestSceneBeatmapCarouselSetPanel.cs | 21 +++++++++------ ...TestSceneBeatmapCarouselStandalonePanel.cs | 26 +++++++++++-------- 3 files changed, 43 insertions(+), 30 deletions(-) diff --git a/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselDifficultyPanel.cs b/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselDifficultyPanel.cs index c0ecb06085..a9f73759f7 100644 --- a/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselDifficultyPanel.cs +++ b/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselDifficultyPanel.cs @@ -30,18 +30,20 @@ namespace osu.Game.Tests.Visual.SongSelectV2 { } + [SetUp] + public void SetUp() => Schedule(() => + { + var beatmapSet = beatmaps.GetAllUsableBeatmapSets().FirstOrDefault(b => b.OnlineID == 241526) + ?? beatmaps.GetAllUsableBeatmapSets().FirstOrDefault(b => !b.Protected) + ?? TestResources.CreateTestBeatmapSetInfo(); + + beatmap = beatmapSet.Beatmaps.First(); + }); + [Test] public void TestDisplay() { - AddStep("set beatmap", () => - { - var beatmapSet = beatmaps.GetAllUsableBeatmapSets().FirstOrDefault(b => b.OnlineID == 241526) - ?? beatmaps.GetAllUsableBeatmapSets().FirstOrDefault(b => !b.Protected) - ?? TestResources.CreateTestBeatmapSetInfo(); - - beatmap = beatmapSet.Beatmaps.First(); - CreateThemedContent(OverlayColourScheme.Aquamarine); - }); + AddStep("display", () => CreateThemedContent(OverlayColourScheme.Aquamarine)); } [Test] @@ -49,8 +51,10 @@ namespace osu.Game.Tests.Visual.SongSelectV2 { AddStep("random beatmap", () => { - beatmap = beatmaps.GetAllUsableBeatmapSets().OrderBy(_ => RNG.Next()) - .First().Beatmaps.OrderBy(_ => RNG.Next()).First(); + var randomSet = beatmaps.GetAllUsableBeatmapSets().OrderBy(_ => RNG.Next()).FirstOrDefault(); + randomSet ??= TestResources.CreateTestBeatmapSetInfo(); + beatmap = randomSet.Beatmaps.OrderBy(_ => RNG.Next()).First(); + CreateThemedContent(OverlayColourScheme.Aquamarine); }); } diff --git a/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselSetPanel.cs b/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselSetPanel.cs index 6b981d7b33..8f7cac2b58 100644 --- a/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselSetPanel.cs +++ b/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselSetPanel.cs @@ -28,16 +28,18 @@ namespace osu.Game.Tests.Visual.SongSelectV2 { } + [SetUp] + public void SetUp() => Schedule(() => + { + beatmapSet = beatmaps.GetAllUsableBeatmapSets().FirstOrDefault(b => b.OnlineID == 241526) + ?? beatmaps.GetAllUsableBeatmapSets().FirstOrDefault(b => !b.Protected) + ?? TestResources.CreateTestBeatmapSetInfo(); + }); + [Test] public void TestDisplay() { - AddStep("set beatmap", () => - { - beatmapSet = beatmaps.GetAllUsableBeatmapSets().FirstOrDefault(b => b.OnlineID == 241526) - ?? beatmaps.GetAllUsableBeatmapSets().FirstOrDefault(b => !b.Protected) - ?? TestResources.CreateTestBeatmapSetInfo(); - CreateThemedContent(OverlayColourScheme.Aquamarine); - }); + AddStep("display", () => CreateThemedContent(OverlayColourScheme.Aquamarine)); } [Test] @@ -45,7 +47,10 @@ namespace osu.Game.Tests.Visual.SongSelectV2 { AddStep("random beatmap", () => { - beatmapSet = beatmaps.GetAllUsableBeatmapSets().OrderBy(_ => RNG.Next()).First(); + var randomSet = beatmaps.GetAllUsableBeatmapSets().OrderBy(_ => RNG.Next()).FirstOrDefault(); + randomSet ??= TestResources.CreateTestBeatmapSetInfo(); + beatmapSet = randomSet; + CreateThemedContent(OverlayColourScheme.Aquamarine); }); } diff --git a/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselStandalonePanel.cs b/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselStandalonePanel.cs index 76dcfc9507..a34ac31d5d 100644 --- a/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselStandalonePanel.cs +++ b/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselStandalonePanel.cs @@ -30,18 +30,20 @@ namespace osu.Game.Tests.Visual.SongSelectV2 { } + [SetUp] + public void SetUp() => Schedule(() => + { + var beatmapSet = beatmaps.GetAllUsableBeatmapSets().FirstOrDefault(b => b.OnlineID == 241526) + ?? beatmaps.GetAllUsableBeatmapSets().FirstOrDefault(b => !b.Protected) + ?? TestResources.CreateTestBeatmapSetInfo(); + + beatmap = beatmapSet.Beatmaps.First(); + }); + [Test] public void TestDisplay() { - AddStep("set beatmap", () => - { - var beatmapSet = beatmaps.GetAllUsableBeatmapSets().FirstOrDefault(b => b.OnlineID == 241526) - ?? beatmaps.GetAllUsableBeatmapSets().FirstOrDefault(b => !b.Protected) - ?? TestResources.CreateTestBeatmapSetInfo(); - - beatmap = beatmapSet.Beatmaps.First(); - CreateThemedContent(OverlayColourScheme.Aquamarine); - }); + AddStep("display", () => CreateThemedContent(OverlayColourScheme.Aquamarine)); } [Test] @@ -49,8 +51,10 @@ namespace osu.Game.Tests.Visual.SongSelectV2 { AddStep("random beatmap", () => { - beatmap = beatmaps.GetAllUsableBeatmapSets().OrderBy(_ => RNG.Next()) - .First().Beatmaps.OrderBy(_ => RNG.Next()).First(); + var randomSet = beatmaps.GetAllUsableBeatmapSets().OrderBy(_ => RNG.Next()).FirstOrDefault(); + randomSet ??= TestResources.CreateTestBeatmapSetInfo(); + beatmap = randomSet.Beatmaps.OrderBy(_ => RNG.Next()).First(); + CreateThemedContent(OverlayColourScheme.Aquamarine); }); } From 467ea91105249569887ba2c12be021d6292a372e Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Wed, 5 Feb 2025 21:47:15 -0500 Subject: [PATCH 0907/1275] Fix basic code quality issues --- osu.Game/Screens/SelectV2/BeatmapCarousel.cs | 1 + osu.Game/Screens/SelectV2/StarsGroupPanel.cs | 3 +-- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/SelectV2/BeatmapCarousel.cs b/osu.Game/Screens/SelectV2/BeatmapCarousel.cs index a49dcdd86c..4de0041d36 100644 --- a/osu.Game/Screens/SelectV2/BeatmapCarousel.cs +++ b/osu.Game/Screens/SelectV2/BeatmapCarousel.cs @@ -264,5 +264,6 @@ namespace osu.Game.Screens.SelectV2 } public record GroupDefinition(string Title); + public record StarsGroupDefinition(int StarNumber); } diff --git a/osu.Game/Screens/SelectV2/StarsGroupPanel.cs b/osu.Game/Screens/SelectV2/StarsGroupPanel.cs index 8ebf3fc7e8..76e3da2500 100644 --- a/osu.Game/Screens/SelectV2/StarsGroupPanel.cs +++ b/osu.Game/Screens/SelectV2/StarsGroupPanel.cs @@ -43,7 +43,6 @@ namespace osu.Game.Screens.SelectV2 private Box activationFlash = null!; private Box outerLayer = null!; - private Box innerLayer = null!; private StarRatingDisplay starRatingDisplay = null!; private StarCounter starCounter = null!; private Box hoverLayer = null!; @@ -108,7 +107,7 @@ namespace osu.Game.Screens.SelectV2 Masking = true, Children = new Drawable[] { - innerLayer = new Box + new Box { RelativeSizeAxes = Axes.Both, Colour = Color4.Black.Opacity(0.2f), From 72a62b70c469407af7a91c7933ec7d6c803858da Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Wed, 5 Feb 2025 22:14:38 -0500 Subject: [PATCH 0908/1275] Simplify some code --- osu.Game/Screens/SelectV2/BeatmapPanel.cs | 10 ++++++---- osu.Game/Screens/SelectV2/BeatmapSetPanel.cs | 8 +++++--- osu.Game/Screens/SelectV2/BeatmapStandalonePanel.cs | 7 +++++-- osu.Game/Screens/SelectV2/GroupPanel.cs | 2 +- 4 files changed, 17 insertions(+), 10 deletions(-) diff --git a/osu.Game/Screens/SelectV2/BeatmapPanel.cs b/osu.Game/Screens/SelectV2/BeatmapPanel.cs index 180acffe80..896b8ea82a 100644 --- a/osu.Game/Screens/SelectV2/BeatmapPanel.cs +++ b/osu.Game/Screens/SelectV2/BeatmapPanel.cs @@ -36,8 +36,10 @@ namespace osu.Game.Screens.SelectV2 private const float colour_box_width = 30; private const float corner_radius = 10; - private const float glow_offset = 10f; // extra space for the edge effect to not be cutoff by the right edge of the carousel. - private const float difficulty_x_offset = 50f; // constant X offset for beatmap difficulty panels specifically. + // todo: this should be replaced with information from CarouselItem about how deep is BeatmapPanel in the carousel + // (i.e. whether it's under a beatmap set that's under a group, or just under a top-level beatmap set). + private const float difficulty_x_offset = 80f; // constant X offset for beatmap difficulty panels specifically. + private const float preselected_x_offset = 25f; private const float selected_x_offset = 50f; @@ -89,7 +91,7 @@ namespace osu.Game.Screens.SelectV2 Origin = Anchor.TopRight; RelativeSizeAxes = Axes.X; - Width = 0.9f; + Width = 1f; Height = HEIGHT; InternalChild = panel = new Container @@ -307,7 +309,7 @@ namespace osu.Game.Screens.SelectV2 private void updatePanelPosition() { - float x = glow_offset + difficulty_x_offset + selected_x_offset + preselected_x_offset; + float x = difficulty_x_offset + selected_x_offset + preselected_x_offset; if (Selected.Value) x -= selected_x_offset; diff --git a/osu.Game/Screens/SelectV2/BeatmapSetPanel.cs b/osu.Game/Screens/SelectV2/BeatmapSetPanel.cs index 4706ea487a..dff563339c 100644 --- a/osu.Game/Screens/SelectV2/BeatmapSetPanel.cs +++ b/osu.Game/Screens/SelectV2/BeatmapSetPanel.cs @@ -33,8 +33,10 @@ namespace osu.Game.Screens.SelectV2 private const float arrow_container_width = 20; private const float corner_radius = 10; - private const float glow_offset = 10f; // extra space for the edge effect to not be cutoff by the right edge of the carousel. - private const float set_x_offset = 20f; // constant X offset for beatmap set panels specifically. + // todo: this should be replaced with information from CarouselItem about how deep is BeatmapPanel in the carousel + // (i.e. whether it's under a beatmap set that's under a group, or just under a top-level beatmap set). + private const float set_x_offset = 20f; // constant X offset for beatmap set/standalone panels specifically. + private const float preselected_x_offset = 25f; private const float expanded_x_offset = 50f; @@ -269,7 +271,7 @@ namespace osu.Game.Screens.SelectV2 private void updatePanelPosition() { - float x = glow_offset + set_x_offset + expanded_x_offset + preselected_x_offset; + float x = set_x_offset + expanded_x_offset + preselected_x_offset; if (Expanded.Value) x -= expanded_x_offset; diff --git a/osu.Game/Screens/SelectV2/BeatmapStandalonePanel.cs b/osu.Game/Screens/SelectV2/BeatmapStandalonePanel.cs index 11fa22ab09..c3a773799a 100644 --- a/osu.Game/Screens/SelectV2/BeatmapStandalonePanel.cs +++ b/osu.Game/Screens/SelectV2/BeatmapStandalonePanel.cs @@ -38,7 +38,10 @@ namespace osu.Game.Screens.SelectV2 private const float difficulty_icon_container_width = 30; private const float corner_radius = 10; - private const float glow_offset = 10f; // extra space for the edge effect to not be cutoff by the right edge of the carousel. + // todo: this should be replaced with information from CarouselItem about how deep is BeatmapPanel in the carousel + // (i.e. whether it's under a beatmap set that's under a group, or just under a top-level beatmap set). + private const float set_x_offset = 20f; // constant X offset for beatmap set/standalone panels specifically. + private const float preselected_x_offset = 25f; private const float selected_x_offset = 50f; @@ -348,7 +351,7 @@ namespace osu.Game.Screens.SelectV2 private void updatePanelPosition() { - float x = glow_offset + selected_x_offset + preselected_x_offset; + float x = set_x_offset + selected_x_offset + preselected_x_offset; if (Selected.Value) x -= selected_x_offset; diff --git a/osu.Game/Screens/SelectV2/GroupPanel.cs b/osu.Game/Screens/SelectV2/GroupPanel.cs index 8995b93290..10d3b8934e 100644 --- a/osu.Game/Screens/SelectV2/GroupPanel.cs +++ b/osu.Game/Screens/SelectV2/GroupPanel.cs @@ -24,7 +24,7 @@ namespace osu.Game.Screens.SelectV2 { public const float HEIGHT = CarouselItem.DEFAULT_HEIGHT; - private const float glow_offset = 10f; // extra space for the edge effect to not be cutoff by the right edge of the carousel. + private const float glow_offset = 10f; // extra space for any edge effect to not be cutoff by the right edge of the carousel. private const float preselected_x_offset = 25f; private const float selected_x_offset = 50f; From 5e894a6f7e6faff59840d6b923ebd509a32853ee Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Wed, 5 Feb 2025 22:24:36 -0500 Subject: [PATCH 0909/1275] Fix carousel tests failing due to X offsets --- .../TestSceneBeatmapCarouselV2GroupSelection.cs | 10 +++++----- .../TestSceneBeatmapCarouselV2Selection.cs | 13 ++++++------- 2 files changed, 11 insertions(+), 12 deletions(-) diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2GroupSelection.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2GroupSelection.cs index ebdc54864e..4e6aa5d6c2 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2GroupSelection.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2GroupSelection.cs @@ -165,26 +165,26 @@ namespace osu.Game.Tests.Visual.SongSelect WaitForDrawablePanels(); SelectNextGroup(); - clickOnPanel(0, 1, p => p.LayoutRectangle.TopLeft + new Vector2(20f, -1f)); + clickOnPanel(0, 1, p => p.LayoutRectangle.TopLeft + new Vector2(p.LayoutRectangle.Centre.X, -1f)); WaitForGroupSelection(0, 1); - clickOnPanel(0, 0, p => p.LayoutRectangle.BottomLeft + new Vector2(20f, 1f)); + clickOnPanel(0, 0, p => p.LayoutRectangle.BottomLeft + new Vector2(p.LayoutRectangle.Centre.X, 1f)); WaitForGroupSelection(0, 0); SelectNextPanel(); Select(); WaitForGroupSelection(0, 1); - clickOnGroup(0, p => p.LayoutRectangle.BottomLeft + new Vector2(20f, 1f)); + clickOnGroup(0, p => p.LayoutRectangle.BottomLeft + new Vector2(p.LayoutRectangle.Centre.X, 1f)); AddAssert("group 0 collapsed", () => this.ChildrenOfType().OrderBy(g => g.Y).ElementAt(0).Expanded.Value, () => Is.False); clickOnGroup(0, p => p.LayoutRectangle.Centre); AddAssert("group 0 expanded", () => this.ChildrenOfType().OrderBy(g => g.Y).ElementAt(0).Expanded.Value, () => Is.True); AddStep("scroll to end", () => Scroll.ScrollToEnd(false)); - clickOnPanel(0, 4, p => p.LayoutRectangle.BottomLeft + new Vector2(20f, 1f)); + clickOnPanel(0, 4, p => p.LayoutRectangle.BottomLeft + new Vector2(p.LayoutRectangle.Centre.X, 1f)); WaitForGroupSelection(0, 4); - clickOnGroup(1, p => p.LayoutRectangle.TopLeft + new Vector2(20f, -1f)); + clickOnGroup(1, p => p.LayoutRectangle.TopLeft + new Vector2(p.LayoutRectangle.Centre.X, -1f)); AddAssert("group 1 expanded", () => this.ChildrenOfType().OrderBy(g => g.Y).ElementAt(1).Expanded.Value, () => Is.True); } diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2Selection.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2Selection.cs index 5541e217cf..3566b5e95f 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2Selection.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2Selection.cs @@ -187,24 +187,23 @@ namespace osu.Game.Tests.Visual.SongSelect WaitForDrawablePanels(); SelectNextGroup(); - clickOnDifficulty(0, 1, p => p.LayoutRectangle.TopLeft + new Vector2(20f, -1f)); + clickOnDifficulty(0, 1, p => new Vector2(p.LayoutRectangle.Centre.X, -1f)); WaitForSelection(0, 1); - clickOnDifficulty(0, 0, p => p.LayoutRectangle.BottomLeft + new Vector2(20f, 1f)); + clickOnDifficulty(0, 0, p => new Vector2(p.LayoutRectangle.Centre.X, BeatmapPanel.HEIGHT + 1f)); WaitForSelection(0, 0); - SelectNextPanel(); - Select(); + clickOnDifficulty(0, 1, p => p.LayoutRectangle.Centre); WaitForSelection(0, 1); - clickOnSet(0, p => p.LayoutRectangle.BottomLeft + new Vector2(20f, 1f)); + clickOnSet(0, p => new Vector2(p.LayoutRectangle.Centre.X, BeatmapSetPanel.HEIGHT + 1f)); WaitForSelection(0, 0); AddStep("scroll to end", () => Scroll.ScrollToEnd(false)); - clickOnDifficulty(0, 4, p => p.LayoutRectangle.BottomLeft + new Vector2(20f, 1f)); + clickOnDifficulty(0, 4, p => new Vector2(p.LayoutRectangle.Centre.X, BeatmapPanel.HEIGHT + 1f)); WaitForSelection(0, 4); - clickOnSet(1, p => p.LayoutRectangle.TopLeft + new Vector2(20f, -1f)); + clickOnSet(1, p => new Vector2(p.LayoutRectangle.Centre.X, -1f)); WaitForSelection(1, 0); } From aab4a79ce4e87ebc24481398b378cc939b64cd7c Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Wed, 5 Feb 2025 22:37:03 -0500 Subject: [PATCH 0910/1275] Push all beatmap panels to hide their tails --- osu.Game/Screens/SelectV2/BeatmapPanel.cs | 3 ++- osu.Game/Screens/SelectV2/BeatmapSetPanel.cs | 1 + .../Screens/SelectV2/BeatmapStandalonePanel.cs | 1 + osu.Game/Screens/SelectV2/GroupPanel.cs | 16 ++++++++++------ osu.Game/Screens/SelectV2/SongSelectV2.cs | 4 +--- 5 files changed, 15 insertions(+), 10 deletions(-) diff --git a/osu.Game/Screens/SelectV2/BeatmapPanel.cs b/osu.Game/Screens/SelectV2/BeatmapPanel.cs index 896b8ea82a..e5b612b1b2 100644 --- a/osu.Game/Screens/SelectV2/BeatmapPanel.cs +++ b/osu.Game/Screens/SelectV2/BeatmapPanel.cs @@ -38,7 +38,7 @@ namespace osu.Game.Screens.SelectV2 // todo: this should be replaced with information from CarouselItem about how deep is BeatmapPanel in the carousel // (i.e. whether it's under a beatmap set that's under a group, or just under a top-level beatmap set). - private const float difficulty_x_offset = 80f; // constant X offset for beatmap difficulty panels specifically. + private const float difficulty_x_offset = 100f; // constant X offset for beatmap difficulty panels specifically. private const float preselected_x_offset = 25f; private const float selected_x_offset = 50f; @@ -99,6 +99,7 @@ namespace osu.Game.Screens.SelectV2 Masking = true, CornerRadius = corner_radius, RelativeSizeAxes = Axes.Both, + X = corner_radius, EdgeEffect = new EdgeEffectParameters { Type = EdgeEffectType.Shadow, diff --git a/osu.Game/Screens/SelectV2/BeatmapSetPanel.cs b/osu.Game/Screens/SelectV2/BeatmapSetPanel.cs index dff563339c..aabc39f27f 100644 --- a/osu.Game/Screens/SelectV2/BeatmapSetPanel.cs +++ b/osu.Game/Screens/SelectV2/BeatmapSetPanel.cs @@ -81,6 +81,7 @@ namespace osu.Game.Screens.SelectV2 Masking = true, CornerRadius = corner_radius, RelativeSizeAxes = Axes.Both, + X = corner_radius, EdgeEffect = new EdgeEffectParameters { Type = EdgeEffectType.Shadow, diff --git a/osu.Game/Screens/SelectV2/BeatmapStandalonePanel.cs b/osu.Game/Screens/SelectV2/BeatmapStandalonePanel.cs index c3a773799a..c0a5f828f4 100644 --- a/osu.Game/Screens/SelectV2/BeatmapStandalonePanel.cs +++ b/osu.Game/Screens/SelectV2/BeatmapStandalonePanel.cs @@ -105,6 +105,7 @@ namespace osu.Game.Screens.SelectV2 Masking = true, CornerRadius = corner_radius, RelativeSizeAxes = Axes.Both, + X = corner_radius, EdgeEffect = new EdgeEffectParameters { Type = EdgeEffectType.Shadow, diff --git a/osu.Game/Screens/SelectV2/GroupPanel.cs b/osu.Game/Screens/SelectV2/GroupPanel.cs index 10d3b8934e..b5fa338f82 100644 --- a/osu.Game/Screens/SelectV2/GroupPanel.cs +++ b/osu.Game/Screens/SelectV2/GroupPanel.cs @@ -24,6 +24,8 @@ namespace osu.Game.Screens.SelectV2 { public const float HEIGHT = CarouselItem.DEFAULT_HEIGHT; + private const float corner_radius = 10; + private const float glow_offset = 10f; // extra space for any edge effect to not be cutoff by the right edge of the carousel. private const float preselected_x_offset = 25f; private const float selected_x_offset = 50f; @@ -33,18 +35,19 @@ namespace osu.Game.Screens.SelectV2 [Resolved] private BeatmapCarousel? carousel { get; set; } + private Container panel = null!; private Box activationFlash = null!; private OsuSpriteText titleText = null!; private Box hoverLayer = null!; public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) { - var inputRectangle = DrawRectangle; + var inputRectangle = panel.DrawRectangle; // Cover a gap introduced by the spacing between a GroupPanel and a BeatmapPanel either below/above it. inputRectangle = inputRectangle.Inflate(new MarginPadding { Vertical = BeatmapCarousel.SPACING / 2f }); - return inputRectangle.Contains(ToLocalSpace(screenSpacePos)); + return inputRectangle.Contains(panel.ToLocalSpace(screenSpacePos)); } [BackgroundDependencyLoader] @@ -55,11 +58,12 @@ namespace osu.Game.Screens.SelectV2 RelativeSizeAxes = Axes.X; Height = HEIGHT; - InternalChild = new Container + InternalChild = panel = new Container { RelativeSizeAxes = Axes.Both, - CornerRadius = 10f, + CornerRadius = corner_radius, Masking = true, + X = corner_radius, Children = new Drawable[] { new Container @@ -69,7 +73,7 @@ namespace osu.Game.Screens.SelectV2 Child = new Container { RelativeSizeAxes = Axes.Both, - CornerRadius = 10f, + CornerRadius = corner_radius, Masking = true, Children = new Drawable[] { @@ -93,7 +97,7 @@ namespace osu.Game.Screens.SelectV2 Child = new Container { RelativeSizeAxes = Axes.Both, - CornerRadius = 10f, + CornerRadius = corner_radius, Masking = true, Children = new Drawable[] { diff --git a/osu.Game/Screens/SelectV2/SongSelectV2.cs b/osu.Game/Screens/SelectV2/SongSelectV2.cs index 88825d96e0..3943d059f9 100644 --- a/osu.Game/Screens/SelectV2/SongSelectV2.cs +++ b/osu.Game/Screens/SelectV2/SongSelectV2.cs @@ -48,9 +48,7 @@ namespace osu.Game.Screens.SelectV2 Anchor = Anchor.TopRight, Origin = Anchor.TopRight, RelativeSizeAxes = Axes.Both, - Width = 0.5f, - // Push the carousel slightly off the right edge of the screen for the ends of the panels to be cut off. - X = 20f, + Width = 0.6f, }, }, modSelectOverlay, From ecc3aeadf2f5fbb17008ff1919ba9e68a0e0b77d Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Wed, 5 Feb 2025 22:39:42 -0500 Subject: [PATCH 0911/1275] Make `BeatmapPanel` appear hovered on keyboard selection even if selected Was an intentional choice but appeared weird to others instead. The feedback itself probably needs changing. --- osu.Game/Screens/SelectV2/BeatmapPanel.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/osu.Game/Screens/SelectV2/BeatmapPanel.cs b/osu.Game/Screens/SelectV2/BeatmapPanel.cs index e5b612b1b2..c36a23e51f 100644 --- a/osu.Game/Screens/SelectV2/BeatmapPanel.cs +++ b/osu.Game/Screens/SelectV2/BeatmapPanel.cs @@ -299,7 +299,6 @@ namespace osu.Game.Screens.SelectV2 updatePanelPosition(); updateEdgeEffectColour(); - updateHover(); } private void updateKeyboardSelectedDisplay() @@ -323,7 +322,7 @@ namespace osu.Game.Screens.SelectV2 private void updateHover() { - bool hovered = IsHovered || (KeyboardSelected.Value && !Selected.Value); + bool hovered = IsHovered || KeyboardSelected.Value; if (hovered) hoverLayer.FadeIn(100, Easing.OutQuint); From 84206e9ad8253ae0acc5169787fb6d6b516e16ff Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Thu, 6 Feb 2025 13:29:16 +0900 Subject: [PATCH 0912/1275] Initial support for freemod+freestyle --- .../Screens/OnlinePlay/Match/RoomSubScreen.cs | 11 +-- .../Multiplayer/MultiplayerMatchSubScreen.cs | 89 ++++++++---------- .../Playlists/PlaylistsRoomSubScreen.cs | 93 +++++++++---------- 3 files changed, 86 insertions(+), 107 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs b/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs index ce51bb3c21..312253774f 100644 --- a/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs @@ -4,7 +4,6 @@ using System; using System.Collections.Generic; using System.ComponentModel; -using System.Diagnostics; using System.Linq; using osu.Framework.Allocation; using osu.Framework.Audio; @@ -441,7 +440,9 @@ namespace osu.Game.Screens.OnlinePlay.Match var rulesetInstance = GetGameplayRuleset().CreateInstance(); // Remove any user mods that are no longer allowed. - Mod[] allowedMods = item.AllowedMods.Select(m => m.ToMod(rulesetInstance)).ToArray(); + Mod[] allowedMods = item.Freestyle + ? rulesetInstance.CreateAllMods().Where(m => ModUtils.IsValidFreeModForMatchType(m, Room.Type)).ToArray() + : item.AllowedMods.Select(m => m.ToMod(rulesetInstance)).ToArray(); Mod[] newUserMods = UserMods.Value.Where(m => allowedMods.Any(a => m.GetType() == a.GetType())).ToArray(); if (!newUserMods.SequenceEqual(UserMods.Value)) UserMods.Value = UserMods.Value.Where(m => allowedMods.Any(a => m.GetType() == a.GetType())).ToList(); @@ -455,12 +456,8 @@ namespace osu.Game.Screens.OnlinePlay.Match Mods.Value = GetGameplayMods().Select(m => m.ToMod(rulesetInstance)).ToArray(); Ruleset.Value = GetGameplayRuleset(); - bool freeMod = item.AllowedMods.Any(); bool freestyle = item.Freestyle; - - // For now, the game can never be in a state where freemod and freestyle are on at the same time. - // This will change, but due to the current implementation if this was to occur drawables will overlap so let's assert. - Debug.Assert(!freeMod || !freestyle); + bool freeMod = freestyle || item.AllowedMods.Any(); if (freeMod) { diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs index b803c5f28b..a16c5c9442 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs @@ -98,7 +98,6 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer { new Drawable?[] { - // Participants column new GridContainer { RelativeSizeAxes = Axes.Both, @@ -118,9 +117,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer } } }, - // Spacer null, - // Beatmap column new GridContainer { RelativeSizeAxes = Axes.Both, @@ -147,67 +144,63 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer SelectedItem = SelectedItem } }, - new Drawable[] + new[] { - new Container + UserModsSection = new FillFlowContainer { RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y, Margin = new MarginPadding { Top = 10 }, - Children = new[] + Alpha = 0, + Children = new Drawable[] { - UserModsSection = new FillFlowContainer + new OverlinedHeader("Extra mods"), + new FillFlowContainer { - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, - Alpha = 0, + AutoSizeAxes = Axes.Both, + Direction = FillDirection.Horizontal, + Spacing = new Vector2(10, 0), Children = new Drawable[] { - new OverlinedHeader("Extra mods"), - new FillFlowContainer + new UserModSelectButton { - AutoSizeAxes = Axes.Both, - Direction = FillDirection.Horizontal, - Spacing = new Vector2(10, 0), - Children = new Drawable[] - { - new UserModSelectButton - { - Anchor = Anchor.CentreLeft, - Origin = Anchor.CentreLeft, - Width = 90, - Text = "Select", - Action = ShowUserModSelect, - }, - new ModDisplay - { - Anchor = Anchor.CentreLeft, - Origin = Anchor.CentreLeft, - Current = UserMods, - Scale = new Vector2(0.8f), - }, - } + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + Width = 90, + Text = "Select", + Action = ShowUserModSelect, }, - } - }, - UserStyleSection = new FillFlowContainer - { - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, - Alpha = 0, - Children = new Drawable[] - { - new OverlinedHeader("Difficulty"), - UserStyleDisplayContainer = new Container + new ModDisplay { - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y - } + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + Current = UserMods, + Scale = new Vector2(0.8f), + }, } }, } } }, + new[] + { + UserStyleSection = new FillFlowContainer + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Margin = new MarginPadding { Top = 10 }, + Alpha = 0, + Children = new Drawable[] + { + new OverlinedHeader("Difficulty"), + UserStyleDisplayContainer = new Container + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y + } + } + }, + }, }, RowDimensions = new[] { @@ -218,9 +211,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer new Dimension(GridSizeMode.AutoSize), } }, - // Spacer null, - // Main right column new GridContainer { RelativeSizeAxes = Axes.Both, diff --git a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs index 2195ed4722..957a51c467 100644 --- a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs @@ -146,7 +146,6 @@ namespace osu.Game.Screens.OnlinePlay.Playlists { new Drawable?[] { - // Playlist items column new GridContainer { RelativeSizeAxes = Axes.Both, @@ -176,73 +175,66 @@ namespace osu.Game.Screens.OnlinePlay.Playlists new Dimension(), } }, - // Spacer null, - // Middle column (mods and leaderboard) new GridContainer { RelativeSizeAxes = Axes.Both, Content = new[] { - new Drawable[] + new[] { - new Container + UserModsSection = new FillFlowContainer { RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y, Margin = new MarginPadding { Bottom = 10 }, - Children = new[] + Alpha = 0, + Children = new Drawable[] { - UserModsSection = new FillFlowContainer + new OverlinedHeader("Extra mods"), + new FillFlowContainer { - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, - Alpha = 0, - Margin = new MarginPadding { Bottom = 10 }, + AutoSizeAxes = Axes.Both, + Direction = FillDirection.Horizontal, + Spacing = new Vector2(10, 0), Children = new Drawable[] { - new OverlinedHeader("Extra mods"), - new FillFlowContainer + new UserModSelectButton { - AutoSizeAxes = Axes.Both, - Direction = FillDirection.Horizontal, - Spacing = new Vector2(10, 0), - Children = new Drawable[] - { - new UserModSelectButton - { - Anchor = Anchor.CentreLeft, - Origin = Anchor.CentreLeft, - Width = 90, - Text = "Select", - Action = ShowUserModSelect, - }, - new ModDisplay - { - Anchor = Anchor.CentreLeft, - Origin = Anchor.CentreLeft, - Current = UserMods, - Scale = new Vector2(0.8f), - }, - } - } + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + Width = 90, + Text = "Select", + Action = ShowUserModSelect, + }, + new ModDisplay + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + Current = UserMods, + Scale = new Vector2(0.8f), + }, } - }, - UserStyleSection = new FillFlowContainer + } + } + }, + }, + new[] + { + UserStyleSection = new FillFlowContainer + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Margin = new MarginPadding { Bottom = 10 }, + Alpha = 0, + Children = new Drawable[] + { + new OverlinedHeader("Difficulty"), + UserStyleDisplayContainer = new Container { RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, - Alpha = 0, - Children = new Drawable[] - { - new OverlinedHeader("Difficulty"), - UserStyleDisplayContainer = new Container - { - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y - } - } - }, + AutoSizeAxes = Axes.Y + } } }, }, @@ -273,12 +265,11 @@ namespace osu.Game.Screens.OnlinePlay.Playlists new Dimension(GridSizeMode.AutoSize), new Dimension(GridSizeMode.AutoSize), new Dimension(GridSizeMode.AutoSize), + new Dimension(GridSizeMode.AutoSize), new Dimension(), } }, - // Spacer null, - // Main right column new GridContainer { RelativeSizeAxes = Axes.Both, From 9cc90a51df7aa2a0043690ff873ed836741993b0 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 6 Feb 2025 13:32:11 +0900 Subject: [PATCH 0913/1275] Adjust xmldoc and avoid LINQ overheads --- osu.Game/Screens/SelectV2/Carousel.cs | 7 +++---- osu.Game/Screens/SelectV2/CarouselItem.cs | 5 ++--- 2 files changed, 5 insertions(+), 7 deletions(-) diff --git a/osu.Game/Screens/SelectV2/Carousel.cs b/osu.Game/Screens/SelectV2/Carousel.cs index 5dc8d80476..07d9c988f5 100644 --- a/osu.Game/Screens/SelectV2/Carousel.cs +++ b/osu.Game/Screens/SelectV2/Carousel.cs @@ -550,8 +550,7 @@ namespace osu.Game.Screens.SelectV2 updateDisplayedRange(range); } - double selectedYPos = currentSelection?.CarouselItem?.CarouselYPosition ?? 0; - double maximumDistanceFromSelection = scroll.Panels.Select(p => Math.Abs(((ICarouselPanel)p).DrawYPosition - selectedYPos)).DefaultIfEmpty().Max(); + double selectedYPos = currentSelection.CarouselItem?.CarouselYPosition ?? 0; foreach (var panel in scroll.Panels) { @@ -561,8 +560,8 @@ namespace osu.Game.Screens.SelectV2 if (c.Item == null) continue; - float normalisedDepth = (float)(Math.Abs(selectedYPos - c.DrawYPosition) / maximumDistanceFromSelection); - scroll.Panels.ChangeChildDepth(panel, normalisedDepth + c.Item.DepthLayer); + float normalisedDepth = (float)(Math.Abs(selectedYPos - c.DrawYPosition) / DrawHeight); + scroll.Panels.ChangeChildDepth(panel, c.Item.DepthLayer + normalisedDepth); if (c.DrawYPosition != c.Item.CarouselYPosition) c.DrawYPosition = Interpolation.DampContinuously(c.DrawYPosition, c.Item.CarouselYPosition, 50, Time.Elapsed); diff --git a/osu.Game/Screens/SelectV2/CarouselItem.cs b/osu.Game/Screens/SelectV2/CarouselItem.cs index e497c3890c..0ac8180028 100644 --- a/osu.Game/Screens/SelectV2/CarouselItem.cs +++ b/osu.Game/Screens/SelectV2/CarouselItem.cs @@ -30,10 +30,9 @@ namespace osu.Game.Screens.SelectV2 public float DrawHeight { get; set; } = DEFAULT_HEIGHT; /// - /// A number that defines the layer which this should be placed on depth-wise. - /// The higher the number, the farther the panel associated with this item is taken to the background. + /// Defines the display depth relative to other s. /// - public int DepthLayer { get; set; } = 0; + public int DepthLayer { get; set; } /// /// Whether this item is visible or hidden. From d9b370e3a1d384758dee89ef704dd0c38a694ec8 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 6 Feb 2025 13:41:16 +0900 Subject: [PATCH 0914/1275] Add xmldoc for menu implying external consumption --- osu.Game/Screens/Edit/BookmarkController.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/osu.Game/Screens/Edit/BookmarkController.cs b/osu.Game/Screens/Edit/BookmarkController.cs index 3d2cb4663f..80e77364e5 100644 --- a/osu.Game/Screens/Edit/BookmarkController.cs +++ b/osu.Game/Screens/Edit/BookmarkController.cs @@ -19,6 +19,9 @@ namespace osu.Game.Screens.Edit { public partial class BookmarkController : Component, IKeyBindingHandler { + /// + /// Bookmarks menu item (with submenu containing options). Should be added to the 's global menu. + /// public EditorMenuItem Menu { get; private set; } [Resolved] From 0257b8c2ffd2dffa2b81fbf41ad88889db0ff14a Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 6 Feb 2025 13:57:38 +0900 Subject: [PATCH 0915/1275] Move metadata randomisation local to usage --- osu.Game.Tests/Resources/TestResources.cs | 29 +++++------------- .../SongSelect/BeatmapCarouselV2TestScene.cs | 30 +++++++++++++++++-- .../TestSceneBeatmapCarouselV2Basics.cs | 3 +- 3 files changed, 37 insertions(+), 25 deletions(-) diff --git a/osu.Game.Tests/Resources/TestResources.cs b/osu.Game.Tests/Resources/TestResources.cs index bf08097ffd..e0572e604c 100644 --- a/osu.Game.Tests/Resources/TestResources.cs +++ b/osu.Game.Tests/Resources/TestResources.cs @@ -85,8 +85,7 @@ namespace osu.Game.Tests.Resources /// /// Number of difficulties. If null, a random number between 1 and 20 will be used. /// Rulesets to cycle through when creating difficulties. If null, osu! ruleset will be used. - /// Whether to randomise metadata to create a better distribution. - public static BeatmapSetInfo CreateTestBeatmapSetInfo(int? difficultyCount = null, RulesetInfo[] rulesets = null, bool randomiseMetadata = false) + public static BeatmapSetInfo CreateTestBeatmapSetInfo(int? difficultyCount = null, RulesetInfo[] rulesets = null) { int j = 0; @@ -96,27 +95,13 @@ namespace osu.Game.Tests.Resources int setId = GetNextTestID(); - char getRandomCharacter() + var metadata = new BeatmapMetadata { - const string chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz*"; - return chars[RNG.Next(chars.Length)]; - } - - var metadata = randomiseMetadata - ? new BeatmapMetadata - { - // Create random metadata, then we can check if sorting works based on these - Artist = $"{getRandomCharacter()}ome Artist " + RNG.Next(0, 9), - Title = $"{getRandomCharacter()}ome Song (set id {setId:000}) {Guid.NewGuid()}", - Author = { Username = $"{getRandomCharacter()}ome Guy " + RNG.Next(0, 9) }, - } - : new BeatmapMetadata - { - // Create random metadata, then we can check if sorting works based on these - Artist = "Some Artist " + RNG.Next(0, 9), - Title = $"Some Song (set id {setId:000}) {Guid.NewGuid()}", - Author = { Username = "Some Guy " + RNG.Next(0, 9) }, - }; + // Create random metadata, then we can check if sorting works based on these + Artist = "Some Artist " + RNG.Next(0, 9), + Title = $"Some Song (set id {setId:000}) {Guid.NewGuid()}", + Author = { Username = "Some Guy " + RNG.Next(0, 9) }, + }; Logger.Log($"🛠️ Generating beatmap set \"{metadata}\" for test consumption."); diff --git a/osu.Game.Tests/Visual/SongSelect/BeatmapCarouselV2TestScene.cs b/osu.Game.Tests/Visual/SongSelect/BeatmapCarouselV2TestScene.cs index f5ea959c51..a55f79c42e 100644 --- a/osu.Game.Tests/Visual/SongSelect/BeatmapCarouselV2TestScene.cs +++ b/osu.Game.Tests/Visual/SongSelect/BeatmapCarouselV2TestScene.cs @@ -1,6 +1,7 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System; using System.Linq; using NUnit.Framework; using osu.Framework.Allocation; @@ -190,12 +191,37 @@ namespace osu.Game.Tests.Visual.SongSelect /// /// The count of beatmap sets to add. /// If not null, the number of difficulties per set. If null, randomised difficulty count will be used. - protected void AddBeatmaps(int count, int? fixedDifficultiesPerSet = null) => AddStep($"add {count} beatmaps", () => + /// Whether to randomise the metadata to make groupings more uniform. + protected void AddBeatmaps(int count, int? fixedDifficultiesPerSet = null, bool randomMetadata = false) => AddStep($"add {count} beatmaps{(randomMetadata ? " with random data" : "")}", () => { for (int i = 0; i < count; i++) - BeatmapSets.Add(TestResources.CreateTestBeatmapSetInfo(fixedDifficultiesPerSet ?? RNG.Next(1, 4), randomiseMetadata: true)); + { + var beatmapSetInfo = TestResources.CreateTestBeatmapSetInfo(fixedDifficultiesPerSet ?? RNG.Next(1, 4)); + + if (randomMetadata) + { + var metadata = new BeatmapMetadata + { + // Create random metadata, then we can check if sorting works based on these + Artist = $"{getRandomCharacter()}ome Artist " + RNG.Next(0, 9), + Title = $"{getRandomCharacter()}ome Song (set id {beatmapSetInfo.OnlineID:000}) {Guid.NewGuid()}", + Author = { Username = $"{getRandomCharacter()}ome Guy " + RNG.Next(0, 9) }, + }; + + foreach (var beatmap in beatmapSetInfo.Beatmaps) + beatmap.Metadata = metadata.DeepClone(); + } + + BeatmapSets.Add(beatmapSetInfo); + } }); + private static char getRandomCharacter() + { + const string chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz*"; + return chars[RNG.Next(chars.Length)]; + } + protected void RemoveAllBeatmaps() => AddStep("clear all beatmaps", () => BeatmapSets.Clear()); protected void RemoveFirstBeatmap() => diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2Basics.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2Basics.cs index a173920dc6..41ceff3183 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2Basics.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2Basics.cs @@ -26,8 +26,9 @@ namespace osu.Game.Tests.Visual.SongSelect [Test] public void TestBasics() { - AddBeatmaps(1); AddBeatmaps(10); + AddBeatmaps(10, randomMetadata: true); + AddBeatmaps(1); RemoveFirstBeatmap(); RemoveAllBeatmaps(); } From d93f7509b6545489f405faf8e9a60f4800b7e040 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Thu, 6 Feb 2025 14:12:15 +0900 Subject: [PATCH 0916/1275] Fix participant panels not displaying mods from other rulesets correctly --- .../TestSceneMultiplayerParticipantsList.cs | 37 +++++++++++++++++++ .../Participants/ParticipantPanel.cs | 22 ++++++----- 2 files changed, 49 insertions(+), 10 deletions(-) diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerParticipantsList.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerParticipantsList.cs index 238a716f91..d3c967a8d5 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerParticipantsList.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerParticipantsList.cs @@ -12,11 +12,14 @@ using osu.Framework.Utils; using osu.Game.Graphics.Cursor; using osu.Game.Graphics.UserInterface; using osu.Game.Online; +using osu.Game.Online.API; using osu.Game.Online.API.Requests.Responses; using osu.Game.Online.Multiplayer; using osu.Game.Online.Rooms; +using osu.Game.Rulesets.Catch.Mods; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Osu.Mods; +using osu.Game.Rulesets.Taiko.Mods; using osu.Game.Screens.OnlinePlay.Multiplayer.Participants; using osu.Game.Users; using osuTK; @@ -393,6 +396,40 @@ namespace osu.Game.Tests.Visual.Multiplayer }); } + [Test] + public void TestModsAndRuleset() + { + AddStep("add another user", () => + { + MultiplayerClient.AddUser(new APIUser + { + Id = 0, + Username = "User 0", + RulesetsStatistics = new Dictionary + { + { + Ruleset.Value.ShortName, + new UserStatistics { GlobalRank = RNG.Next(1, 100000), } + } + }, + CoverUrl = @"https://osu.ppy.sh/images/headers/profile-covers/c3.jpg", + }); + + MultiplayerClient.ChangeUserBeatmapAvailability(0, BeatmapAvailability.LocallyAvailable()); + }); + + AddStep("set user styles", () => + { + MultiplayerClient.ChangeUserStyle(API.LocalUser.Value.OnlineID, 259, 1); + MultiplayerClient.ChangeUserMods(API.LocalUser.Value.OnlineID, + [new APIMod(new TaikoModConstantSpeed()), new APIMod(new TaikoModHidden()), new APIMod(new TaikoModFlashlight()), new APIMod(new TaikoModHardRock())]); + + MultiplayerClient.ChangeUserStyle(0, 259, 2); + MultiplayerClient.ChangeUserMods(0, + [new APIMod(new CatchModFloatingFruits()), new APIMod(new CatchModHidden()), new APIMod(new CatchModMirror())]); + }); + } + private void createNewParticipantsList() { ParticipantsList? participantsList = null; diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Participants/ParticipantPanel.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Participants/ParticipantPanel.cs index a2657019a3..d6666de2b6 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Participants/ParticipantPanel.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Participants/ParticipantPanel.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; +using System.Diagnostics; using System.Linq; using System.Threading; using System.Threading.Tasks; @@ -27,7 +28,6 @@ using osu.Game.Online.API; using osu.Game.Online.Multiplayer; using osu.Game.Online.Rooms; using osu.Game.Rulesets; -using osu.Game.Rulesets.Mods; using osu.Game.Screens.Play.HUD; using osu.Game.Users; using osu.Game.Users.Drawables; @@ -210,13 +210,18 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Participants const double fade_time = 50; MultiplayerPlaylistItem? currentItem = client.Room.GetCurrentItem(); - Ruleset? ruleset = currentItem != null ? rulesets.GetRuleset(currentItem.RulesetID)?.CreateInstance() : null; + Debug.Assert(currentItem != null); - int? currentModeRank = ruleset != null ? User.User?.RulesetsStatistics?.GetValueOrDefault(ruleset.ShortName)?.GlobalRank : null; - userRankText.Text = currentModeRank != null ? $"#{currentModeRank.Value:N0}" : string.Empty; + int userBeatmapId = User.BeatmapId ?? currentItem.BeatmapID; + int userRulesetId = User.RulesetId ?? currentItem.RulesetID; + Ruleset? userRuleset = rulesets.GetRuleset(userRulesetId)?.CreateInstance(); + Debug.Assert(userRuleset != null); userStateDisplay.UpdateStatus(User.State, User.BeatmapAvailability); + int? currentModeRank = User.User?.RulesetsStatistics?.GetValueOrDefault(userRuleset.ShortName)?.GlobalRank; + userRankText.Text = currentModeRank != null ? $"#{currentModeRank.Value:N0}" : string.Empty; + if ((User.BeatmapAvailability.State == DownloadState.LocallyAvailable) && (User.State != MultiplayerUserState.Spectating)) { userModsDisplay.FadeIn(fade_time); @@ -228,20 +233,17 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Participants userStyleDisplay.FadeOut(fade_time); } - if ((User.BeatmapId == null && User.RulesetId == null) || (User.BeatmapId == currentItem?.BeatmapID && User.RulesetId == currentItem?.RulesetID)) + if (userBeatmapId == currentItem.BeatmapID && userRulesetId == currentItem.RulesetID) userStyleDisplay.Style = null; else - userStyleDisplay.Style = (User.BeatmapId ?? currentItem?.BeatmapID ?? 0, User.RulesetId ?? currentItem?.RulesetID ?? 0); + userStyleDisplay.Style = (userBeatmapId, userRulesetId); kickButton.Alpha = client.IsHost && !User.Equals(client.LocalUser) ? 1 : 0; crown.Alpha = client.Room.Host?.Equals(User) == true ? 1 : 0; // If the mods are updated at the end of the frame, the flow container will skip a reflow cycle: https://github.com/ppy/osu-framework/issues/4187 // This looks particularly jarring here, so re-schedule the update to that start of our frame as a fix. - Schedule(() => - { - userModsDisplay.Current.Value = ruleset != null ? User.Mods.Select(m => m.ToMod(ruleset)).ToList() : Array.Empty(); - }); + Schedule(() => userModsDisplay.Current.Value = User.Mods.Select(m => m.ToMod(userRuleset)).ToList()); } public MenuItem[]? ContextMenuItems From 885ae7c735a82740710fce395a456d8e1280abf9 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Thu, 6 Feb 2025 14:25:08 +0900 Subject: [PATCH 0917/1275] Adjust styling --- .../Multiplayer/Participants/ParticipantPanel.cs | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Participants/ParticipantPanel.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Participants/ParticipantPanel.cs index d6666de2b6..51ff52c63e 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Participants/ParticipantPanel.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Participants/ParticipantPanel.cs @@ -161,11 +161,18 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Participants Origin = Anchor.CentreRight, AutoSizeAxes = Axes.Both, Margin = new MarginPadding { Right = 70 }, + Spacing = new Vector2(2), Children = new Drawable[] { - userStyleDisplay = new StyleDisplayIcon(), + userStyleDisplay = new StyleDisplayIcon + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + }, userModsDisplay = new ModDisplay { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, Scale = new Vector2(0.5f), ExpansionMode = ExpansionMode.AlwaysContracted, } From 88ad87a78e36a7170d0ce05dd0a0a29433977f88 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 6 Feb 2025 14:30:15 +0900 Subject: [PATCH 0918/1275] Expose set grouping state --- osu.Game/Screens/SelectV2/BeatmapCarousel.cs | 2 +- .../SelectV2/BeatmapCarouselFilterGrouping.cs | 12 ++++++------ 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/osu.Game/Screens/SelectV2/BeatmapCarousel.cs b/osu.Game/Screens/SelectV2/BeatmapCarousel.cs index e7311fbfbc..36e57c9067 100644 --- a/osu.Game/Screens/SelectV2/BeatmapCarousel.cs +++ b/osu.Game/Screens/SelectV2/BeatmapCarousel.cs @@ -140,7 +140,7 @@ namespace osu.Game.Screens.SelectV2 return true; case BeatmapInfo: - return Criteria.SplitOutDifficulties; + return !grouping.BeatmapSetsGroupedTogether; case GroupDefinition: return false; diff --git a/osu.Game/Screens/SelectV2/BeatmapCarouselFilterGrouping.cs b/osu.Game/Screens/SelectV2/BeatmapCarouselFilterGrouping.cs index d4e0a166ab..29c534cbe2 100644 --- a/osu.Game/Screens/SelectV2/BeatmapCarouselFilterGrouping.cs +++ b/osu.Game/Screens/SelectV2/BeatmapCarouselFilterGrouping.cs @@ -14,6 +14,8 @@ namespace osu.Game.Screens.SelectV2 { public class BeatmapCarouselFilterGrouping : ICarouselFilter { + public bool BeatmapSetsGroupedTogether { get; private set; } + /// /// Beatmap sets contain difficulties as related panels. This dictionary holds the relationships between set-difficulties to allow expanding them on selection. /// @@ -36,8 +38,6 @@ namespace osu.Game.Screens.SelectV2 public async Task> Run(IEnumerable items, CancellationToken cancellationToken) => await Task.Run(() => { - bool groupSetsTogether; - setItems.Clear(); groupItems.Clear(); @@ -48,12 +48,12 @@ namespace osu.Game.Screens.SelectV2 switch (criteria.Group) { default: - groupSetsTogether = true; + BeatmapSetsGroupedTogether = true; newItems.AddRange(items); break; case GroupMode.Artist: - groupSetsTogether = true; + BeatmapSetsGroupedTogether = true; char groupChar = (char)0; foreach (var item in items) @@ -80,7 +80,7 @@ namespace osu.Game.Screens.SelectV2 break; case GroupMode.Difficulty: - groupSetsTogether = false; + BeatmapSetsGroupedTogether = false; int starGroup = int.MinValue; foreach (var item in items) @@ -108,7 +108,7 @@ namespace osu.Game.Screens.SelectV2 // Add set headers wherever required. CarouselItem? lastItem = null; - if (groupSetsTogether) + if (BeatmapSetsGroupedTogether) { for (int i = 0; i < newItems.Count; i++) { From 342a66b9e21e619c9192a4bd63bb2f32563c2e20 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 6 Feb 2025 14:39:11 +0900 Subject: [PATCH 0919/1275] Fix keyboard traversal on a collapsed group not working as intended --- osu.Game/Screens/SelectV2/Carousel.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/osu.Game/Screens/SelectV2/Carousel.cs b/osu.Game/Screens/SelectV2/Carousel.cs index 608ef207d9..6b7b1f3a9b 100644 --- a/osu.Game/Screens/SelectV2/Carousel.cs +++ b/osu.Game/Screens/SelectV2/Carousel.cs @@ -378,7 +378,7 @@ namespace osu.Game.Screens.SelectV2 { TryActivateSelection(); - // There's a chance this couldn't resolve, at which point continue with standard traversal. + // Is the selection actually changed, then we should not perform any further traversal. if (currentSelection.CarouselItem == currentKeyboardSelection.CarouselItem) return; } @@ -386,20 +386,20 @@ namespace osu.Game.Screens.SelectV2 int originalIndex; int newIndex; - if (currentSelection.Index == null) + if (currentKeyboardSelection.Index == null) { // If there's no current selection, start from either end of the full list. newIndex = originalIndex = direction > 0 ? carouselItems.Count - 1 : 0; } else { - newIndex = originalIndex = currentSelection.Index.Value; + newIndex = originalIndex = currentKeyboardSelection.Index.Value; // As a second special case, if we're group selecting backwards and the current selection isn't a group, // make sure to go back to the group header this item belongs to, so that the block below doesn't find it and stop too early. if (direction < 0) { - while (!CheckValidForGroupSelection(carouselItems[newIndex])) + while (newIndex > 0 && !CheckValidForGroupSelection(carouselItems[newIndex])) newIndex--; } } From bf377e081ad36ab88b1c7b6ef415bcb2db888bdd Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 6 Feb 2025 14:38:51 +0900 Subject: [PATCH 0920/1275] Reorganise tests to make more logical when manually testing --- .../SongSelect/BeatmapCarouselV2TestScene.cs | 23 ++--- ...asics.cs => TestSceneBeatmapCarouselV2.cs} | 93 ++++++------------- ...eneBeatmapCarouselV2DifficultyGrouping.cs} | 25 ++--- ...> TestSceneBeatmapCarouselV2NoGrouping.cs} | 12 ++- .../TestSceneBeatmapCarouselV2Scrolling.cs | 65 +++++++++++++ 5 files changed, 118 insertions(+), 100 deletions(-) rename osu.Game.Tests/Visual/SongSelect/{TestSceneBeatmapCarouselV2Basics.cs => TestSceneBeatmapCarouselV2.cs} (52%) rename osu.Game.Tests/Visual/SongSelect/{TestSceneBeatmapCarouselV2GroupSelection.cs => TestSceneBeatmapCarouselV2DifficultyGrouping.cs} (92%) rename osu.Game.Tests/Visual/SongSelect/{TestSceneBeatmapCarouselV2Selection.cs => TestSceneBeatmapCarouselV2NoGrouping.cs} (94%) create mode 100644 osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2Scrolling.cs diff --git a/osu.Game.Tests/Visual/SongSelect/BeatmapCarouselV2TestScene.cs b/osu.Game.Tests/Visual/SongSelect/BeatmapCarouselV2TestScene.cs index a55f79c42e..36226a13cc 100644 --- a/osu.Game.Tests/Visual/SongSelect/BeatmapCarouselV2TestScene.cs +++ b/osu.Game.Tests/Visual/SongSelect/BeatmapCarouselV2TestScene.cs @@ -17,7 +17,6 @@ using osu.Game.Database; using osu.Game.Graphics; using osu.Game.Graphics.Containers; using osu.Game.Screens.Select; -using osu.Game.Screens.Select.Filter; using osu.Game.Screens.SelectV2; using osu.Game.Tests.Beatmaps; using osu.Game.Tests.Resources; @@ -54,16 +53,6 @@ namespace osu.Game.Tests.Visual.SongSelect Scheduler.AddDelayed(updateStats, 100, true); } - [SetUpSteps] - public virtual void SetUpSteps() - { - RemoveAllBeatmaps(); - - CreateCarousel(); - - SortBy(new FilterCriteria { Sort = SortMode.Title }); - } - protected void CreateCarousel() { AddStep("create components", () => @@ -200,12 +189,14 @@ namespace osu.Game.Tests.Visual.SongSelect if (randomMetadata) { + char randomCharacter = getRandomCharacter(); + var metadata = new BeatmapMetadata { // Create random metadata, then we can check if sorting works based on these - Artist = $"{getRandomCharacter()}ome Artist " + RNG.Next(0, 9), - Title = $"{getRandomCharacter()}ome Song (set id {beatmapSetInfo.OnlineID:000}) {Guid.NewGuid()}", - Author = { Username = $"{getRandomCharacter()}ome Guy " + RNG.Next(0, 9) }, + Artist = $"{randomCharacter}ome Artist " + RNG.Next(0, 9), + Title = $"{randomCharacter}ome Song (set id {beatmapSetInfo.OnlineID:000}) {Guid.NewGuid()}", + Author = { Username = $"{randomCharacter}ome Guy " + RNG.Next(0, 9) }, }; foreach (var beatmap in beatmapSetInfo.Beatmaps) @@ -216,10 +207,12 @@ namespace osu.Game.Tests.Visual.SongSelect } }); + private static long randomCharPointer; + private static char getRandomCharacter() { const string chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz*"; - return chars[RNG.Next(chars.Length)]; + return chars[(int)((randomCharPointer++ / 2) % chars.Length)]; } protected void RemoveAllBeatmaps() => AddStep("clear all beatmaps", () => BeatmapSets.Clear()); diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2Basics.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2.cs similarity index 52% rename from osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2Basics.cs rename to osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2.cs index 41ceff3183..3c5cf16e92 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2Basics.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2.cs @@ -2,102 +2,65 @@ // See the LICENCE file in the repository root for full licence text. using System.Collections.Generic; -using System.Linq; using System.Threading.Tasks; using NUnit.Framework; -using osu.Framework.Graphics.Primitives; -using osu.Framework.Testing; using osu.Framework.Utils; using osu.Game.Beatmaps; using osu.Game.Screens.Select; using osu.Game.Screens.Select.Filter; -using osu.Game.Screens.SelectV2; using osu.Game.Tests.Resources; namespace osu.Game.Tests.Visual.SongSelect { /// - /// Currently covers adding and removing of items and scrolling. - /// If we add more tests here, these two categories can likely be split out into separate scenes. + /// Covers common steps which can be used for manual testing. /// [TestFixture] - public partial class TestSceneBeatmapCarouselV2Basics : BeatmapCarouselV2TestScene + public partial class TestSceneBeatmapCarouselV2 : BeatmapCarouselV2TestScene { [Test] + [Explicit] public void TestBasics() { - AddBeatmaps(10); + CreateCarousel(); + RemoveAllBeatmaps(); + AddBeatmaps(10, randomMetadata: true); + AddBeatmaps(10); AddBeatmaps(1); + } + + [Test] + [Explicit] + public void TestSorting() + { + SortBy(new FilterCriteria { Sort = SortMode.Artist }); + SortBy(new FilterCriteria { Group = GroupMode.Difficulty, Sort = SortMode.Difficulty }); + SortBy(new FilterCriteria { Group = GroupMode.Artist, Sort = SortMode.Artist }); + } + + [Test] + [Explicit] + public void TestRemovals() + { RemoveFirstBeatmap(); RemoveAllBeatmaps(); } [Test] - public void TestOffScreenLoading() - { - AddStep("disable masking", () => Scroll.Masking = false); - AddStep("enable masking", () => Scroll.Masking = true); - } - - [Test] - public void TestAddRemoveOneByOne() + [Explicit] + public void TestAddRemoveRepeatedOps() { AddRepeatStep("add beatmaps", () => BeatmapSets.Add(TestResources.CreateTestBeatmapSetInfo(RNG.Next(1, 4))), 20); AddRepeatStep("remove beatmaps", () => BeatmapSets.RemoveAt(RNG.Next(0, BeatmapSets.Count)), 20); } [Test] - public void TestSorting() + [Explicit] + public void TestMasking() { - AddBeatmaps(10); - SortBy(new FilterCriteria { Group = GroupMode.Difficulty, Sort = SortMode.Difficulty }); - SortBy(new FilterCriteria { Group = GroupMode.Artist, Sort = SortMode.Artist }); - SortBy(new FilterCriteria { Sort = SortMode.Artist }); - } - - [Test] - public void TestScrollPositionMaintainedOnAddSecondSelected() - { - Quad positionBefore = default; - - AddBeatmaps(10); - WaitForDrawablePanels(); - - AddStep("select middle beatmap", () => Carousel.CurrentSelection = BeatmapSets.ElementAt(BeatmapSets.Count - 2)); - AddStep("scroll to selected item", () => Scroll.ScrollTo(Scroll.ChildrenOfType().Single(p => p.Selected.Value))); - - WaitForScrolling(); - - AddStep("save selected screen position", () => positionBefore = Carousel.ChildrenOfType().FirstOrDefault(p => p.Selected.Value)!.ScreenSpaceDrawQuad); - - RemoveFirstBeatmap(); - WaitForSorting(); - - AddAssert("select screen position unchanged", () => Carousel.ChildrenOfType().Single(p => p.Selected.Value).ScreenSpaceDrawQuad, - () => Is.EqualTo(positionBefore)); - } - - [Test] - public void TestScrollPositionMaintainedOnAddLastSelected() - { - Quad positionBefore = default; - - AddBeatmaps(10); - WaitForDrawablePanels(); - - AddStep("scroll to last item", () => Scroll.ScrollToEnd(false)); - - AddStep("select last beatmap", () => Carousel.CurrentSelection = BeatmapSets.Last()); - - WaitForScrolling(); - - AddStep("save selected screen position", () => positionBefore = Carousel.ChildrenOfType().FirstOrDefault(p => p.Selected.Value)!.ScreenSpaceDrawQuad); - - RemoveFirstBeatmap(); - WaitForSorting(); - AddAssert("select screen position unchanged", () => Carousel.ChildrenOfType().Single(p => p.Selected.Value).ScreenSpaceDrawQuad, - () => Is.EqualTo(positionBefore)); + AddStep("disable masking", () => Scroll.Masking = false); + AddStep("enable masking", () => Scroll.Masking = true); } [Test] diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2GroupSelection.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2DifficultyGrouping.cs similarity index 92% rename from osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2GroupSelection.cs rename to osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2DifficultyGrouping.cs index f4d97be5a5..e861d8bf30 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2GroupSelection.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2DifficultyGrouping.cs @@ -12,23 +12,22 @@ using osu.Game.Screens.SelectV2; namespace osu.Game.Tests.Visual.SongSelect { [TestFixture] - public partial class TestSceneBeatmapCarouselV2GroupSelection : BeatmapCarouselV2TestScene + public partial class TestSceneBeatmapCarouselV2DifficultyGrouping : BeatmapCarouselV2TestScene { - public override void SetUpSteps() + [SetUpSteps] + public void SetUpSteps() { RemoveAllBeatmaps(); - CreateCarousel(); - SortBy(new FilterCriteria { Group = GroupMode.Difficulty, Sort = SortMode.Difficulty }); + + AddBeatmaps(10, 3); + WaitForDrawablePanels(); } [Test] public void TestOpenCloseGroupWithNoSelectionMouse() { - AddBeatmaps(10, 5); - WaitForDrawablePanels(); - AddAssert("no beatmaps visible", () => Carousel.ChildrenOfType().Count(p => p.Alpha > 0), () => Is.Zero); CheckNoSelection(); @@ -44,9 +43,6 @@ namespace osu.Game.Tests.Visual.SongSelect [Test] public void TestOpenCloseGroupWithNoSelectionKeyboard() { - AddBeatmaps(10, 5); - WaitForDrawablePanels(); - AddAssert("no beatmaps visible", () => Carousel.ChildrenOfType().Count(p => p.Alpha > 0), () => Is.Zero); CheckNoSelection(); @@ -67,9 +63,6 @@ namespace osu.Game.Tests.Visual.SongSelect [Test] public void TestCarouselRemembersSelection() { - AddBeatmaps(10); - WaitForDrawablePanels(); - SelectNextGroup(); object? selection = null; @@ -107,9 +100,6 @@ namespace osu.Game.Tests.Visual.SongSelect [Test] public void TestGroupSelectionOnHeader() { - AddBeatmaps(10, 3); - WaitForDrawablePanels(); - SelectNextGroup(); WaitForGroupSelection(0, 0); @@ -121,9 +111,6 @@ namespace osu.Game.Tests.Visual.SongSelect [Test] public void TestKeyboardSelection() { - AddBeatmaps(10, 3); - WaitForDrawablePanels(); - SelectNextPanel(); SelectNextPanel(); SelectNextPanel(); diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2Selection.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2NoGrouping.cs similarity index 94% rename from osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2Selection.cs rename to osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2NoGrouping.cs index b087c252e4..82f35af0ec 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2Selection.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2NoGrouping.cs @@ -5,14 +5,24 @@ using System.Linq; using NUnit.Framework; using osu.Framework.Testing; using osu.Game.Beatmaps; +using osu.Game.Screens.Select; +using osu.Game.Screens.Select.Filter; using osu.Game.Screens.SelectV2; using osuTK.Input; namespace osu.Game.Tests.Visual.SongSelect { [TestFixture] - public partial class TestSceneBeatmapCarouselV2Selection : BeatmapCarouselV2TestScene + public partial class TestSceneBeatmapCarouselV2NoGrouping : BeatmapCarouselV2TestScene { + [SetUpSteps] + public void SetUpSteps() + { + RemoveAllBeatmaps(); + CreateCarousel(); + SortBy(new FilterCriteria { Sort = SortMode.Title }); + } + /// /// Keyboard selection via up and down arrows doesn't actually change the selection until /// the select key is pressed. diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2Scrolling.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2Scrolling.cs new file mode 100644 index 0000000000..1d5d8e2a2d --- /dev/null +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2Scrolling.cs @@ -0,0 +1,65 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Linq; +using NUnit.Framework; +using osu.Framework.Graphics.Primitives; +using osu.Framework.Testing; +using osu.Game.Screens.Select; +using osu.Game.Screens.SelectV2; + +namespace osu.Game.Tests.Visual.SongSelect +{ + [TestFixture] + public partial class TestSceneBeatmapCarouselV2Scrolling : BeatmapCarouselV2TestScene + { + [SetUpSteps] + public void SetUpSteps() + { + RemoveAllBeatmaps(); + CreateCarousel(); + SortBy(new FilterCriteria()); + + AddBeatmaps(10); + WaitForDrawablePanels(); + } + + [Test] + public void TestScrollPositionMaintainedOnAddSecondSelected() + { + Quad positionBefore = default; + + AddStep("select middle beatmap", () => Carousel.CurrentSelection = BeatmapSets.ElementAt(BeatmapSets.Count - 2)); + AddStep("scroll to selected item", () => Scroll.ScrollTo(Scroll.ChildrenOfType().Single(p => p.Selected.Value))); + + WaitForScrolling(); + + AddStep("save selected screen position", () => positionBefore = Carousel.ChildrenOfType().FirstOrDefault(p => p.Selected.Value)!.ScreenSpaceDrawQuad); + + RemoveFirstBeatmap(); + WaitForSorting(); + + AddAssert("select screen position unchanged", () => Carousel.ChildrenOfType().Single(p => p.Selected.Value).ScreenSpaceDrawQuad, + () => Is.EqualTo(positionBefore)); + } + + [Test] + public void TestScrollPositionMaintainedOnAddLastSelected() + { + Quad positionBefore = default; + + AddStep("scroll to last item", () => Scroll.ScrollToEnd(false)); + + AddStep("select last beatmap", () => Carousel.CurrentSelection = BeatmapSets.Last()); + + WaitForScrolling(); + + AddStep("save selected screen position", () => positionBefore = Carousel.ChildrenOfType().FirstOrDefault(p => p.Selected.Value)!.ScreenSpaceDrawQuad); + + RemoveFirstBeatmap(); + WaitForSorting(); + AddAssert("select screen position unchanged", () => Carousel.ChildrenOfType().Single(p => p.Selected.Value).ScreenSpaceDrawQuad, + () => Is.EqualTo(positionBefore)); + } + } +} From 5b8b9589d8347fbf4a6d4d0ff9f89f24ed3274a5 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Thu, 6 Feb 2025 15:25:14 +0900 Subject: [PATCH 0921/1275] Add ruleset icon to expanded score panel --- .../Expanded/ExpandedPanelMiddleContent.cs | 49 +++++++++---------- 1 file changed, 24 insertions(+), 25 deletions(-) diff --git a/osu.Game/Screens/Ranking/Expanded/ExpandedPanelMiddleContent.cs b/osu.Game/Screens/Ranking/Expanded/ExpandedPanelMiddleContent.cs index d1dc1a81db..4bc559694a 100644 --- a/osu.Game/Screens/Ranking/Expanded/ExpandedPanelMiddleContent.cs +++ b/osu.Game/Screens/Ranking/Expanded/ExpandedPanelMiddleContent.cs @@ -41,7 +41,6 @@ namespace osu.Game.Screens.Ranking.Expanded private readonly List statisticDisplays = new List(); - private FillFlowContainer starAndModDisplay; private RollingCounter scoreCounter; [Resolved] @@ -139,12 +138,35 @@ namespace osu.Game.Screens.Ranking.Expanded Alpha = 0, AlwaysPresent = true }, - starAndModDisplay = new FillFlowContainer + new FillFlowContainer { Anchor = Anchor.TopCentre, Origin = Anchor.TopCentre, AutoSizeAxes = Axes.Both, Spacing = new Vector2(5, 0), + Children = new Drawable[] + { + new StarRatingDisplay(beatmapDifficultyCache.GetDifficultyAsync(beatmap, score.Ruleset, score.Mods).GetResultSafely() ?? default) + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft + }, + new DifficultyIcon(beatmap, score.Ruleset) + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + Size = new Vector2(20), + TooltipType = DifficultyIconTooltipType.Extended, + }, + new ModDisplay + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + ExpansionMode = ExpansionMode.AlwaysExpanded, + Scale = new Vector2(0.5f), + Current = { Value = score.Mods } + } + } }, new FillFlowContainer { @@ -225,29 +247,6 @@ namespace osu.Game.Screens.Ranking.Expanded if (score.Date != default) AddInternal(new PlayedOnText(score.Date)); - - var starDifficulty = beatmapDifficultyCache.GetDifficultyAsync(beatmap, score.Ruleset, score.Mods).GetResultSafely(); - - if (starDifficulty != null) - { - starAndModDisplay.Add(new StarRatingDisplay(starDifficulty.Value) - { - Anchor = Anchor.CentreLeft, - Origin = Anchor.CentreLeft - }); - } - - if (score.Mods.Any()) - { - starAndModDisplay.Add(new ModDisplay - { - Anchor = Anchor.CentreLeft, - Origin = Anchor.CentreLeft, - ExpansionMode = ExpansionMode.AlwaysExpanded, - Scale = new Vector2(0.5f), - Current = { Value = score.Mods } - }); - } } protected override void LoadComplete() From 134e62c39afb3aa4a36d790c509aff24a7b5bead Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Thu, 6 Feb 2025 00:10:42 -0500 Subject: [PATCH 0922/1275] Abstractify beatmap panel piece and update all panel implementations --- osu.Game/Screens/SelectV2/BeatmapPanel.cs | 249 +++---------- osu.Game/Screens/SelectV2/BeatmapSetPanel.cs | 278 ++++---------- .../SelectV2/BeatmapStandalonePanel.cs | 342 ++++++------------ .../Screens/SelectV2/CarouselPanelPiece.cs | 240 ++++++++++++ osu.Game/Screens/SelectV2/GroupPanel.cs | 212 +++-------- osu.Game/Screens/SelectV2/StarsGroupPanel.cs | 234 ++++-------- 6 files changed, 608 insertions(+), 947 deletions(-) create mode 100644 osu.Game/Screens/SelectV2/CarouselPanelPiece.cs diff --git a/osu.Game/Screens/SelectV2/BeatmapPanel.cs b/osu.Game/Screens/SelectV2/BeatmapPanel.cs index c36a23e51f..bd4cf6d7cf 100644 --- a/osu.Game/Screens/SelectV2/BeatmapPanel.cs +++ b/osu.Game/Screens/SelectV2/BeatmapPanel.cs @@ -6,13 +6,9 @@ using System.Diagnostics; using System.Threading; using osu.Framework.Allocation; using osu.Framework.Bindables; -using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; -using osu.Framework.Graphics.Colour; using osu.Framework.Graphics.Containers; -using osu.Framework.Graphics.Effects; using osu.Framework.Graphics.Pooling; -using osu.Framework.Graphics.Shapes; using osu.Framework.Input.Events; using osu.Game.Beatmaps; using osu.Game.Beatmaps.Drawables; @@ -25,7 +21,6 @@ using osu.Game.Resources.Localisation.Web; using osu.Game.Rulesets; using osu.Game.Rulesets.Mods; using osuTK; -using osuTK.Graphics; namespace osu.Game.Screens.SelectV2 { @@ -33,36 +28,23 @@ namespace osu.Game.Screens.SelectV2 { public const float HEIGHT = CarouselItem.DEFAULT_HEIGHT; - private const float colour_box_width = 30; - private const float corner_radius = 10; - // todo: this should be replaced with information from CarouselItem about how deep is BeatmapPanel in the carousel // (i.e. whether it's under a beatmap set that's under a group, or just under a top-level beatmap set). private const float difficulty_x_offset = 100f; // constant X offset for beatmap difficulty panels specifically. - private const float preselected_x_offset = 25f; - private const float selected_x_offset = 50f; - private const float duration = 500; - [Resolved] - private BeatmapCarousel? carousel { get; set; } - - [Resolved] - private IBindable ruleset { get; set; } = null!; - - [Resolved] - private IBindable> mods { get; set; } = null!; - - private Container panel = null!; + private CarouselPanelPiece panel = null!; private StarCounter starCounter = null!; - private ConstrainedIconContainer iconContainer = null!; - private Box hoverLayer = null!; - private Box activationFlash = null!; - - private Box backgroundBorder = null!; - + private ConstrainedIconContainer difficultyIcon = null!; + private OsuSpriteText keyCountText = null!; private StarRatingDisplay starRatingDisplay = null!; + private TopLocalRankV2 difficultyRank = null!; + private OsuSpriteText difficultyText = null!; + private OsuSpriteText authorText = null!; + + private IBindable? starDifficultyBindable; + private CancellationTokenSource? starDifficultyCancellationSource; [Resolved] private OverlayColourProvider colourProvider { get; set; } = null!; @@ -73,16 +55,24 @@ namespace osu.Game.Screens.SelectV2 [Resolved] private BeatmapDifficultyCache difficultyCache { get; set; } = null!; - private OsuSpriteText keyCountText = null!; + [Resolved] + private BeatmapCarousel? carousel { get; set; } - private IBindable? starDifficultyBindable; - private CancellationTokenSource? starDifficultyCancellationSource; + [Resolved] + private IBindable ruleset { get; set; } = null!; - private Container rightContainer = null!; - private Box starRatingGradient = null!; - private TopLocalRankV2 difficultyRank = null!; - private OsuSpriteText difficultyText = null!; - private OsuSpriteText authorText = null!; + [Resolved] + private IBindable> mods { get; set; } = null!; + + protected override bool ReceivePositionalInputAtSubTree(Vector2 screenSpacePos) + { + var inputRectangle = panel.TopLevelContent.DrawRectangle; + + // Cover the gaps introduced by the spacing between BeatmapPanels. + inputRectangle = inputRectangle.Inflate(new MarginPadding { Vertical = BeatmapCarousel.SPACING / 2f }); + + return inputRectangle.Contains(panel.TopLevelContent.ToLocalSpace(screenSpacePos)); + } [BackgroundDependencyLoader] private void load(OverlayColourProvider colourProvider) @@ -94,67 +84,21 @@ namespace osu.Game.Screens.SelectV2 Width = 1f; Height = HEIGHT; - InternalChild = panel = new Container + InternalChild = panel = new CarouselPanelPiece(difficulty_x_offset) { - Masking = true, - CornerRadius = corner_radius, - RelativeSizeAxes = Axes.Both, - X = corner_radius, - EdgeEffect = new EdgeEffectParameters + Icon = difficultyIcon = new ConstrainedIconContainer { - Type = EdgeEffectType.Shadow, - Offset = new Vector2(1f), - Radius = 10, + Size = new Vector2(20), + Margin = new MarginPadding { Horizontal = 5f }, + Colour = colourProvider.Background5, }, - Children = new Drawable[] + Children = new[] { - new BufferedContainer - { - RelativeSizeAxes = Axes.Both, - Children = new Drawable[] - { - backgroundBorder = new Box - { - RelativeSizeAxes = Axes.Both, - Colour = colours.ForStarDifficulty(0), - EdgeSmoothness = new Vector2(2, 0), - }, - rightContainer = new Container - { - Anchor = Anchor.CentreLeft, - Origin = Anchor.CentreLeft, - Masking = true, - CornerRadius = corner_radius, - RelativeSizeAxes = Axes.X, - Height = HEIGHT, - X = colour_box_width, - Children = new Drawable[] - { - new Box - { - RelativeSizeAxes = Axes.Both, - Colour = ColourInfo.GradientHorizontal(colourProvider.Background3, colourProvider.Background4), - }, - starRatingGradient = new Box - { - RelativeSizeAxes = Axes.Both, - Alpha = 0, - }, - }, - }, - } - }, - iconContainer = new ConstrainedIconContainer - { - X = colour_box_width / 2, - Origin = Anchor.Centre, - Anchor = Anchor.CentreLeft, - Size = new Vector2(20), - Colour = colourProvider.Background5, - }, new FillFlowContainer { - Padding = new MarginPadding { Top = 8, Left = colour_box_width + corner_radius }, + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + Padding = new MarginPadding { Left = 10f }, Direction = FillDirection.Vertical, AutoSizeAxes = Axes.Both, Children = new Drawable[] @@ -216,34 +160,10 @@ namespace osu.Game.Screens.SelectV2 } } }, - hoverLayer = new Box - { - Colour = colours.Blue.Opacity(0.1f), - Alpha = 0, - Blending = BlendingParameters.Additive, - RelativeSizeAxes = Axes.Both, - }, - activationFlash = new Box - { - Blending = BlendingParameters.Additive, - Alpha = 0f, - RelativeSizeAxes = Axes.Both, - }, - new HoverSounds(), } }; } - public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) - { - var inputRectangle = panel.DrawRectangle; - - // Cover the gaps introduced by the spacing between BeatmapPanels. - inputRectangle = inputRectangle.Inflate(new MarginPadding { Vertical = BeatmapCarousel.SPACING / 2f }); - - return inputRectangle.Contains(panel.ToLocalSpace(screenSpacePos)); - } - protected override void LoadComplete() { base.LoadComplete(); @@ -260,8 +180,8 @@ namespace osu.Game.Screens.SelectV2 updateKeyCount(); }, true); - Selected.BindValueChanged(_ => updateSelectionDisplay(), true); - KeyboardSelected.BindValueChanged(_ => updateKeyboardSelectedDisplay(), true); + Selected.BindValueChanged(s => panel.Active.Value = s.NewValue, true); + KeyboardSelected.BindValueChanged(k => panel.KeyboardActive.Value = k.NewValue, true); } protected override void PrepareForUse() @@ -271,63 +191,25 @@ namespace osu.Game.Screens.SelectV2 Debug.Assert(Item != null); var beatmap = (BeatmapInfo)Item.Model; - iconContainer.Icon = beatmap.Ruleset.CreateInstance().CreateIcon(); + difficultyIcon.Icon = beatmap.Ruleset.CreateInstance().CreateIcon(); difficultyRank.Beatmap = beatmap; difficultyText.Text = beatmap.DifficultyName; authorText.Text = BeatmapsetsStrings.ShowDetailsMappedBy(beatmap.Metadata.Author.Username); - starDifficultyBindable = null; - computeStarRating(); updateKeyCount(); - updateSelectionDisplay(); FinishTransforms(true); - this.FadeInFromZero(duration, Easing.OutQuint); - - // todo: only do this when visible. - // starCounter.ReplayAnimation(); } - private void updateSelectionDisplay() + protected override void FreeAfterUse() { - bool selected = Selected.Value; + base.FreeAfterUse(); - rightContainer.ResizeHeightTo(selected ? HEIGHT - 4 : HEIGHT, duration, Easing.OutQuint); - - updatePanelPosition(); - updateEdgeEffectColour(); - } - - private void updateKeyboardSelectedDisplay() - { - updatePanelPosition(); - updateHover(); - } - - private void updatePanelPosition() - { - float x = difficulty_x_offset + selected_x_offset + preselected_x_offset; - - if (Selected.Value) - x -= selected_x_offset; - - if (KeyboardSelected.Value) - x -= preselected_x_offset; - - this.TransformTo(nameof(Padding), new MarginPadding { Left = x }, duration, Easing.OutQuint); - } - - private void updateHover() - { - bool hovered = IsHovered || KeyboardSelected.Value; - - if (hovered) - hoverLayer.FadeIn(100, Easing.OutQuint); - else - hoverLayer.FadeOut(1000, Easing.OutQuint); + difficultyRank.Beatmap = null; + starDifficultyBindable = null; } private void computeStarRating() @@ -341,34 +223,7 @@ namespace osu.Game.Screens.SelectV2 var beatmap = (BeatmapInfo)Item.Model; starDifficultyBindable = difficultyCache.GetBindableDifficulty(beatmap, starDifficultyCancellationSource.Token); - starDifficultyBindable.BindValueChanged(d => - { - var value = d.NewValue ?? default; - - starRatingDisplay.Current.Value = value; - starCounter.Current = (float)value.Stars; - - iconContainer.FadeColour(value.Stars > 6.5f ? colours.Orange1 : colourProvider.Background5, duration, Easing.OutQuint); - - var starRatingColour = colours.ForStarDifficulty(value.Stars); - - backgroundBorder.FadeColour(starRatingColour, duration, Easing.OutQuint); - starCounter.FadeColour(starRatingColour, duration, Easing.OutQuint); - starRatingGradient.FadeColour(ColourInfo.GradientHorizontal(starRatingColour.Opacity(0.25f), starRatingColour.Opacity(0)), duration, Easing.OutQuint); - starRatingGradient.FadeIn(duration, Easing.OutQuint); - - // todo: this doesn't work for dark star rating colours, still not sure how to fix. - activationFlash.FadeColour(starRatingColour, duration, Easing.OutQuint); - - updateEdgeEffectColour(); - }, true); - } - - private void updateEdgeEffectColour() - { - panel.FadeEdgeEffectTo(Selected.Value - ? colours.ForStarDifficulty(starDifficultyBindable?.Value?.Stars ?? 0f).Opacity(0.5f) - : Color4.Black.Opacity(0.4f), duration, Easing.OutQuint); + starDifficultyBindable.BindValueChanged(_ => updateDisplay(), true); } private void updateKeyCount() @@ -392,16 +247,18 @@ namespace osu.Game.Screens.SelectV2 keyCountText.Alpha = 0; } - protected override bool OnHover(HoverEvent e) + private void updateDisplay() { - updateHover(); - return true; - } + var starDifficulty = starDifficultyBindable?.Value ?? default; - protected override void OnHoverLost(HoverLostEvent e) - { - updateHover(); - base.OnHoverLost(e); + starRatingDisplay.Current.Value = starDifficulty; + starCounter.Current = (float)starDifficulty.Stars; + + difficultyIcon.FadeColour(starDifficulty.Stars > 6.5f ? colours.Orange1 : colourProvider.Background5, duration, Easing.OutQuint); + + var starRatingColour = colours.ForStarDifficulty(starDifficulty.Stars); + starCounter.FadeColour(starRatingColour, duration, Easing.OutQuint); + panel.AccentColour = starRatingColour; } protected override bool OnClick(ClickEvent e) @@ -430,7 +287,7 @@ namespace osu.Game.Screens.SelectV2 public void Activated() { - activationFlash.FadeOutFromOne(500, Easing.OutQuint); + panel.Flash(); } #endregion diff --git a/osu.Game/Screens/SelectV2/BeatmapSetPanel.cs b/osu.Game/Screens/SelectV2/BeatmapSetPanel.cs index aabc39f27f..f5d7e0594b 100644 --- a/osu.Game/Screens/SelectV2/BeatmapSetPanel.cs +++ b/osu.Game/Screens/SelectV2/BeatmapSetPanel.cs @@ -6,12 +6,9 @@ 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.Pooling; -using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Sprites; using osu.Framework.Input.Events; using osu.Framework.Localisation; @@ -19,10 +16,8 @@ using osu.Game.Beatmaps; using osu.Game.Beatmaps.Drawables; using osu.Game.Graphics; using osu.Game.Graphics.Sprites; -using osu.Game.Graphics.UserInterface; using osu.Game.Overlays; using osuTK; -using osuTK.Graphics; namespace osu.Game.Screens.SelectV2 { @@ -30,18 +25,22 @@ namespace osu.Game.Screens.SelectV2 { public const float HEIGHT = CarouselItem.DEFAULT_HEIGHT * 1.6f; - private const float arrow_container_width = 20; - private const float corner_radius = 10; - // todo: this should be replaced with information from CarouselItem about how deep is BeatmapPanel in the carousel // (i.e. whether it's under a beatmap set that's under a group, or just under a top-level beatmap set). private const float set_x_offset = 20f; // constant X offset for beatmap set/standalone panels specifically. - private const float preselected_x_offset = 25f; - private const float expanded_x_offset = 50f; - private const float duration = 500; + private CarouselPanelPiece panel = null!; + private BeatmapSetPanelBackground background = null!; + + private OsuSpriteText titleText = null!; + private OsuSpriteText artistText = null!; + private Drawable chevronIcon = null!; + private UpdateBeatmapSetButtonV2 updateButton = null!; + private BeatmapSetOnlineStatusPill statusPill = null!; + private DifficultySpectrumDisplay difficultiesDisplay = null!; + [Resolved] private BeatmapCarousel? carousel { get; set; } @@ -51,22 +50,15 @@ namespace osu.Game.Screens.SelectV2 [Resolved] private BeatmapManager beatmaps { get; set; } = null!; - [Resolved] - private OsuColour colours { get; set; } = null!; + protected override bool ReceivePositionalInputAtSubTree(Vector2 screenSpacePos) + { + var inputRectangle = panel.TopLevelContent.DrawRectangle; - private Container panel = null!; - private Box backgroundBorder = null!; - private BeatmapSetPanelBackground background = null!; - private Container backgroundContainer = null!; - private FillFlowContainer mainFlowContainer = null!; - private SpriteIcon chevronIcon = null!; - private Box hoverLayer = null!; + // Cover a gap introduced by the spacing between a BeatmapSetPanel and a BeatmapPanel either above it or below it. + inputRectangle = inputRectangle.Inflate(new MarginPadding { Vertical = BeatmapCarousel.SPACING / 2f }); - private OsuSpriteText titleText = null!; - private OsuSpriteText artistText = null!; - private UpdateBeatmapSetButtonV2 updateButton = null!; - private BeatmapSetOnlineStatusPill statusPill = null!; - private DifficultySpectrumDisplay difficultiesDisplay = null!; + return inputRectangle.Contains(panel.TopLevelContent.ToLocalSpace(screenSpacePos)); + } [BackgroundDependencyLoader] private void load() @@ -76,137 +68,89 @@ namespace osu.Game.Screens.SelectV2 RelativeSizeAxes = Axes.X; Height = HEIGHT; - InternalChild = panel = new Container + InternalChild = panel = new CarouselPanelPiece(set_x_offset) { - Masking = true, - CornerRadius = corner_radius, - RelativeSizeAxes = Axes.Both, - X = corner_radius, - EdgeEffect = new EdgeEffectParameters + Icon = chevronIcon = new Container { - Type = EdgeEffectType.Shadow, - Radius = 10, - }, - Children = new Drawable[] - { - new BufferedContainer + Size = new Vector2(22), + Child = new SpriteIcon { - RelativeSizeAxes = Axes.Both, - Children = new Drawable[] - { - backgroundBorder = new Box - { - RelativeSizeAxes = Axes.Y, - Alpha = 0, - EdgeSmoothness = new Vector2(2, 0), - }, - backgroundContainer = new Container - { - Masking = true, - CornerRadius = corner_radius, - RelativeSizeAxes = Axes.X, - MaskingSmoothness = 2, - Anchor = Anchor.CentreLeft, - Origin = Anchor.CentreLeft, - Children = new Drawable[] - { - background = new BeatmapSetPanelBackground - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - RelativeSizeAxes = Axes.Both, - }, - }, - }, - } - }, - chevronIcon = new SpriteIcon - { - X = arrow_container_width / 2, + Anchor = Anchor.Centre, Origin = Anchor.Centre, - Anchor = Anchor.CentreLeft, Icon = FontAwesome.Solid.ChevronRight, Size = new Vector2(12), + X = 1f, Colour = colourProvider.Background5, }, - mainFlowContainer = new FillFlowContainer + }, + Background = background = new BeatmapSetPanelBackground + { + RelativeSizeAxes = Axes.Both, + }, + Child = new FillFlowContainer + { + AutoSizeAxes = Axes.Both, + Direction = FillDirection.Vertical, + Padding = new MarginPadding { Top = 7.5f, Left = 15, Bottom = 5 }, + Children = new Drawable[] { - AutoSizeAxes = Axes.Both, - Direction = FillDirection.Vertical, - Padding = new MarginPadding { Top = 7.5f, Left = 15, Bottom = 5 }, - Children = new Drawable[] + titleText = new OsuSpriteText { - titleText = new OsuSpriteText + Font = OsuFont.GetFont(weight: FontWeight.Bold, size: 22, italics: true), + }, + artistText = new OsuSpriteText + { + Font = OsuFont.GetFont(weight: FontWeight.SemiBold, size: 17, italics: true), + }, + new FillFlowContainer + { + Direction = FillDirection.Horizontal, + AutoSizeAxes = Axes.Both, + Margin = new MarginPadding { Top = 5f }, + Children = new Drawable[] { - Font = OsuFont.GetFont(weight: FontWeight.Bold, size: 22, italics: true), - Shadow = true, - }, - artistText = new OsuSpriteText - { - Font = OsuFont.GetFont(weight: FontWeight.SemiBold, size: 17, italics: true), - Shadow = true, - }, - new FillFlowContainer - { - Direction = FillDirection.Horizontal, - AutoSizeAxes = Axes.Both, - Margin = new MarginPadding { Top = 5f }, - Children = new Drawable[] + updateButton = new UpdateBeatmapSetButtonV2 { - updateButton = new UpdateBeatmapSetButtonV2 - { - Anchor = Anchor.CentreLeft, - Origin = Anchor.CentreLeft, - Margin = new MarginPadding { Right = 5f, Top = -2f }, - }, - statusPill = new BeatmapSetOnlineStatusPill - { - AutoSizeAxes = Axes.Both, - Origin = Anchor.CentreLeft, - Anchor = Anchor.CentreLeft, - TextSize = 11, - TextPadding = new MarginPadding { Horizontal = 8, Vertical = 2 }, - Margin = new MarginPadding { Right = 5f }, - }, - difficultiesDisplay = new DifficultySpectrumDisplay - { - DotSize = new Vector2(5, 10), - DotSpacing = 2, - Anchor = Anchor.CentreLeft, - Origin = Anchor.CentreLeft, - }, + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + Margin = new MarginPadding { Right = 5f, Top = -2f }, }, - } + statusPill = new BeatmapSetOnlineStatusPill + { + AutoSizeAxes = Axes.Both, + Origin = Anchor.CentreLeft, + Anchor = Anchor.CentreLeft, + TextSize = 11, + TextPadding = new MarginPadding { Horizontal = 8, Vertical = 2 }, + Margin = new MarginPadding { Right = 5f }, + }, + difficultiesDisplay = new DifficultySpectrumDisplay + { + DotSize = new Vector2(5, 10), + DotSpacing = 2, + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + }, + }, } - }, - hoverLayer = new Box - { - Colour = colours.Blue.Opacity(0.1f), - Alpha = 0, - Blending = BlendingParameters.Additive, - RelativeSizeAxes = Axes.Both, - }, - new HoverSounds(), + } } }; } - public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) - { - var inputRectangle = panel.DrawRectangle; - - // Cover a gap introduced by the spacing between a BeatmapSetPanel and a BeatmapPanel either above it or below it. - inputRectangle = inputRectangle.Inflate(new MarginPadding { Vertical = BeatmapCarousel.SPACING / 2f }); - - return inputRectangle.Contains(panel.ToLocalSpace(screenSpacePos)); - } - protected override void LoadComplete() { base.LoadComplete(); - Expanded.BindValueChanged(_ => updateExpandedDisplay(), true); - KeyboardSelected.BindValueChanged(_ => updateKeyboardSelectedDisplay(), true); + Expanded.BindValueChanged(_ => onExpanded(), true); + KeyboardSelected.BindValueChanged(k => panel.KeyboardActive.Value = k.NewValue, true); + } + + private void onExpanded() + { + panel.Active.Value = Expanded.Value; + chevronIcon.ResizeWidthTo(Expanded.Value ? 22 : 0f, duration, Easing.OutQuint); + chevronIcon.FadeTo(Expanded.Value ? 1f : 0f, duration, Easing.OutQuint); } protected override void PrepareForUse() @@ -226,9 +170,7 @@ namespace osu.Game.Screens.SelectV2 statusPill.Status = beatmapSet.Status; difficultiesDisplay.BeatmapSet = beatmapSet; - updateExpandedDisplay(); FinishTransforms(true); - this.FadeInFromZero(duration, Easing.OutQuint); } @@ -241,70 +183,6 @@ namespace osu.Game.Screens.SelectV2 difficultiesDisplay.BeatmapSet = null; } - private void updateExpandedDisplay() - { - if (Item == null) - return; - - updatePanelPosition(); - - backgroundBorder.RelativeSizeAxes = Expanded.Value ? Axes.Both : Axes.Y; - backgroundBorder.Width = Expanded.Value ? 1 : arrow_container_width + corner_radius; - backgroundBorder.FadeTo(Expanded.Value ? 1 : 0, duration, Easing.OutQuint); - chevronIcon.FadeTo(Expanded.Value ? 1 : 0, duration, Easing.OutQuint); - - backgroundContainer.ResizeHeightTo(Expanded.Value ? HEIGHT - 4 : HEIGHT, duration, Easing.OutQuint); - backgroundContainer.MoveToX(Expanded.Value ? arrow_container_width : 0, duration, Easing.OutQuint); - mainFlowContainer.MoveToX(Expanded.Value ? arrow_container_width : 0, duration, Easing.OutQuint); - - panel.EdgeEffect = panel.EdgeEffect with { Radius = Expanded.Value ? 15 : 10 }; - - panel.FadeEdgeEffectTo(Expanded.Value - ? Color4Extensions.FromHex(@"4EBFFF").Opacity(0.5f) - : Color4.Black.Opacity(0.4f), duration, Easing.OutQuint); - } - - private void updateKeyboardSelectedDisplay() - { - updatePanelPosition(); - updateHover(); - } - - private void updatePanelPosition() - { - float x = set_x_offset + expanded_x_offset + preselected_x_offset; - - if (Expanded.Value) - x -= expanded_x_offset; - - if (KeyboardSelected.Value) - x -= preselected_x_offset; - - this.TransformTo(nameof(Padding), new MarginPadding { Left = x }, duration, Easing.OutQuint); - } - - private void updateHover() - { - bool hovered = IsHovered || KeyboardSelected.Value; - - if (hovered) - hoverLayer.FadeIn(100, Easing.OutQuint); - else - hoverLayer.FadeOut(1000, Easing.OutQuint); - } - - protected override bool OnHover(HoverEvent e) - { - updateHover(); - return true; - } - - protected override void OnHoverLost(HoverLostEvent e) - { - updateHover(); - base.OnHoverLost(e); - } - protected override bool OnClick(ClickEvent e) { if (carousel != null) diff --git a/osu.Game/Screens/SelectV2/BeatmapStandalonePanel.cs b/osu.Game/Screens/SelectV2/BeatmapStandalonePanel.cs index c0a5f828f4..a8fa2224d7 100644 --- a/osu.Game/Screens/SelectV2/BeatmapStandalonePanel.cs +++ b/osu.Game/Screens/SelectV2/BeatmapStandalonePanel.cs @@ -8,12 +8,9 @@ using System.Linq; using System.Threading; 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.Pooling; -using osu.Framework.Graphics.Shapes; using osu.Framework.Input.Events; using osu.Framework.Localisation; using osu.Game.Beatmaps; @@ -21,13 +18,11 @@ using osu.Game.Beatmaps.Drawables; using osu.Game.Graphics; using osu.Game.Graphics.Containers; using osu.Game.Graphics.Sprites; -using osu.Game.Graphics.UserInterface; using osu.Game.Overlays; using osu.Game.Resources.Localisation.Web; using osu.Game.Rulesets; using osu.Game.Rulesets.Mods; using osuTK; -using osuTK.Graphics; namespace osu.Game.Screens.SelectV2 { @@ -35,15 +30,9 @@ namespace osu.Game.Screens.SelectV2 { public const float HEIGHT = CarouselItem.DEFAULT_HEIGHT * 1.6f; - private const float difficulty_icon_container_width = 30; - private const float corner_radius = 10; - // todo: this should be replaced with information from CarouselItem about how deep is BeatmapPanel in the carousel // (i.e. whether it's under a beatmap set that's under a group, or just under a top-level beatmap set). - private const float set_x_offset = 20f; // constant X offset for beatmap set/standalone panels specifically. - - private const float preselected_x_offset = 25f; - private const float selected_x_offset = 50f; + private const float standalone_x_offset = 20f; // constant X offset for beatmap set/standalone panels specifically. private const float duration = 500; @@ -71,12 +60,8 @@ namespace osu.Game.Screens.SelectV2 private IBindable? starDifficultyBindable; private CancellationTokenSource? starDifficultyCancellationSource; - private Container panel = null!; - private Box backgroundBorder = null!; + private CarouselPanelPiece panel = null!; private BeatmapSetPanelBackground background = null!; - private Container backgroundContainer = null!; - private FillFlowContainer mainFlowContainer = null!; - private Box hoverLayer = null!; private OsuSpriteText titleText = null!; private OsuSpriteText artistText = null!; @@ -91,6 +76,16 @@ namespace osu.Game.Screens.SelectV2 private OsuSpriteText difficultyName = null!; private OsuSpriteText difficultyAuthor = null!; + protected override bool ReceivePositionalInputAtSubTree(Vector2 screenSpacePos) + { + var inputRectangle = panel.TopLevelContent.DrawRectangle; + + // Cover a gap introduced by the spacing between a BeatmapSetPanel and a BeatmapPanel either above it or below it. + inputRectangle = inputRectangle.Inflate(new MarginPadding { Vertical = BeatmapCarousel.SPACING / 2f }); + + return inputRectangle.Contains(panel.TopLevelContent.ToLocalSpace(screenSpacePos)); + } + [BackgroundDependencyLoader] private void load() { @@ -100,167 +95,109 @@ namespace osu.Game.Screens.SelectV2 Width = 1f; Height = HEIGHT; - InternalChild = panel = new Container + InternalChild = panel = new CarouselPanelPiece(standalone_x_offset) { - Masking = true, - CornerRadius = corner_radius, - RelativeSizeAxes = Axes.Both, - X = corner_radius, - EdgeEffect = new EdgeEffectParameters + Icon = difficultyIcon = new ConstrainedIconContainer { - Type = EdgeEffectType.Shadow, - Radius = 10, + Size = new Vector2(20), + Margin = new MarginPadding { Horizontal = 5f }, + Colour = colourProvider.Background5, }, - Children = new Drawable[] + Background = background = new BeatmapSetPanelBackground { - new BufferedContainer + RelativeSizeAxes = Axes.Both, + }, + Child = new FillFlowContainer + { + AutoSizeAxes = Axes.Both, + Direction = FillDirection.Vertical, + Padding = new MarginPadding { Top = 7.5f, Left = 15, Bottom = 5 }, + Children = new Drawable[] { - RelativeSizeAxes = Axes.Both, - Children = new Drawable[] + titleText = new OsuSpriteText { - backgroundBorder = new Box + Font = OsuFont.GetFont(weight: FontWeight.Bold, size: 22, italics: true), + Shadow = true, + }, + artistText = new OsuSpriteText + { + Font = OsuFont.GetFont(weight: FontWeight.SemiBold, size: 17, italics: true), + Shadow = true, + }, + new FillFlowContainer + { + Direction = FillDirection.Horizontal, + AutoSizeAxes = Axes.Both, + Margin = new MarginPadding { Top = 5f }, + Children = new Drawable[] { - RelativeSizeAxes = Axes.Y, - Alpha = 0, - EdgeSmoothness = new Vector2(2, 0), - }, - backgroundContainer = new Container - { - Masking = true, - CornerRadius = corner_radius, - RelativeSizeAxes = Axes.X, - MaskingSmoothness = 2, - Anchor = Anchor.CentreLeft, - Origin = Anchor.CentreLeft, - Children = new Drawable[] + updateButton = new UpdateBeatmapSetButtonV2 { - background = new BeatmapSetPanelBackground - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - RelativeSizeAxes = Axes.Both, - }, + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + Margin = new MarginPadding { Right = 5f, Top = -2f }, }, - }, - } - }, - difficultyIcon = new ConstrainedIconContainer - { - X = difficulty_icon_container_width / 2, - Origin = Anchor.Centre, - Anchor = Anchor.CentreLeft, - Size = new Vector2(20), - }, - mainFlowContainer = new FillFlowContainer - { - AutoSizeAxes = Axes.Both, - Direction = FillDirection.Vertical, - Padding = new MarginPadding { Top = 7.5f, Left = 15, Bottom = 5 }, - Children = new Drawable[] - { - titleText = new OsuSpriteText - { - Font = OsuFont.GetFont(weight: FontWeight.Bold, size: 22, italics: true), - Shadow = true, - }, - artistText = new OsuSpriteText - { - Font = OsuFont.GetFont(weight: FontWeight.SemiBold, size: 17, italics: true), - Shadow = true, - }, - new FillFlowContainer - { - Direction = FillDirection.Horizontal, - AutoSizeAxes = Axes.Both, - Margin = new MarginPadding { Top = 5f }, - Children = new Drawable[] + statusPill = new BeatmapSetOnlineStatusPill { - updateButton = new UpdateBeatmapSetButtonV2 + AutoSizeAxes = Axes.Both, + Origin = Anchor.CentreLeft, + Anchor = Anchor.CentreLeft, + TextSize = 11, + TextPadding = new MarginPadding { Horizontal = 8, Vertical = 2 }, + Margin = new MarginPadding { Right = 5f }, + }, + difficultyLine = new FillFlowContainer + { + Direction = FillDirection.Horizontal, + AutoSizeAxes = Axes.Both, + Children = new Drawable[] { - Anchor = Anchor.CentreLeft, - Origin = Anchor.CentreLeft, - Margin = new MarginPadding { Right = 5f, Top = -2f }, - }, - statusPill = new BeatmapSetOnlineStatusPill - { - AutoSizeAxes = Axes.Both, - Origin = Anchor.CentreLeft, - Anchor = Anchor.CentreLeft, - TextSize = 11, - TextPadding = new MarginPadding { Horizontal = 8, Vertical = 2 }, - Margin = new MarginPadding { Right = 5f }, - }, - difficultyLine = new FillFlowContainer - { - Direction = FillDirection.Horizontal, - AutoSizeAxes = Axes.Both, - Children = new Drawable[] + difficultyStarRating = new StarRatingDisplay(default, StarRatingDisplaySize.Small) { - difficultyStarRating = new StarRatingDisplay(default, StarRatingDisplaySize.Small) - { - Origin = Anchor.CentreLeft, - Anchor = Anchor.CentreLeft, - Scale = new Vector2(8f / 9f), - Margin = new MarginPadding { Right = 5f }, - }, - difficultyRank = new TopLocalRankV2 - { - Scale = new Vector2(8f / 11), - Origin = Anchor.CentreLeft, - Anchor = Anchor.CentreLeft, - Margin = new MarginPadding { Right = 5f }, - }, - difficultyKeyCountText = new OsuSpriteText - { - Font = OsuFont.GetFont(size: 18, weight: FontWeight.SemiBold), - Anchor = Anchor.BottomLeft, - Origin = Anchor.BottomLeft, - Alpha = 0, - Margin = new MarginPadding { Bottom = 2f }, - }, - difficultyName = new OsuSpriteText - { - Font = OsuFont.GetFont(size: 18, weight: FontWeight.SemiBold), - Origin = Anchor.BottomLeft, - Anchor = Anchor.BottomLeft, - Margin = new MarginPadding { Right = 5f, Bottom = 2f }, - }, - difficultyAuthor = new OsuSpriteText - { - Colour = colourProvider.Content2, - Font = OsuFont.GetFont(weight: FontWeight.SemiBold), - Origin = Anchor.BottomLeft, - Anchor = Anchor.BottomLeft, - Margin = new MarginPadding { Right = 5f, Bottom = 2f }, - } + Origin = Anchor.CentreLeft, + Anchor = Anchor.CentreLeft, + Scale = new Vector2(8f / 9f), + Margin = new MarginPadding { Right = 5f }, + }, + difficultyRank = new TopLocalRankV2 + { + Scale = new Vector2(8f / 11), + Origin = Anchor.CentreLeft, + Anchor = Anchor.CentreLeft, + Margin = new MarginPadding { Right = 5f }, + }, + difficultyKeyCountText = new OsuSpriteText + { + Font = OsuFont.GetFont(size: 18, weight: FontWeight.SemiBold), + Anchor = Anchor.BottomLeft, + Origin = Anchor.BottomLeft, + Alpha = 0, + Margin = new MarginPadding { Bottom = 2f }, + }, + difficultyName = new OsuSpriteText + { + Font = OsuFont.GetFont(size: 18, weight: FontWeight.SemiBold), + Origin = Anchor.BottomLeft, + Anchor = Anchor.BottomLeft, + Margin = new MarginPadding { Right = 5f, Bottom = 2f }, + }, + difficultyAuthor = new OsuSpriteText + { + Colour = colourProvider.Content2, + Font = OsuFont.GetFont(weight: FontWeight.SemiBold), + Origin = Anchor.BottomLeft, + Anchor = Anchor.BottomLeft, + Margin = new MarginPadding { Right = 5f, Bottom = 2f }, } - }, + } }, - } + }, } - }, - hoverLayer = new Box - { - Colour = colours.Blue.Opacity(0.1f), - Alpha = 0, - Blending = BlendingParameters.Additive, - RelativeSizeAxes = Axes.Both, - }, - new HoverSounds(), - } + } + }, }; } - public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) - { - var inputRectangle = panel.DrawRectangle; - - // Cover a gap introduced by the spacing between a BeatmapSetPanel and a BeatmapPanel either above it or below it. - inputRectangle = inputRectangle.Inflate(new MarginPadding { Vertical = BeatmapCarousel.SPACING / 2f }); - - return inputRectangle.Contains(panel.ToLocalSpace(screenSpacePos)); - } - protected override void LoadComplete() { base.LoadComplete(); @@ -277,8 +214,8 @@ namespace osu.Game.Screens.SelectV2 updateKeyCount(); }, true); - Selected.BindValueChanged(_ => updateSelectedDisplay(), true); - KeyboardSelected.BindValueChanged(_ => updateKeyboardSelectedDisplay(), true); + Selected.BindValueChanged(s => panel.Active.Value = s.NewValue, true); + KeyboardSelected.BindValueChanged(k => panel.KeyboardActive.Value = k.NewValue, true); } protected override void PrepareForUse() @@ -308,7 +245,6 @@ namespace osu.Game.Screens.SelectV2 computeStarRating(); - updateSelectedDisplay(); FinishTransforms(true); this.FadeInFromZero(duration, Easing.OutQuint); @@ -324,55 +260,6 @@ namespace osu.Game.Screens.SelectV2 starDifficultyBindable = null; } - private void updateSelectedDisplay() - { - if (Item == null) - return; - - updatePanelPosition(); - - backgroundBorder.RelativeSizeAxes = Selected.Value ? Axes.Both : Axes.Y; - backgroundBorder.Width = Selected.Value ? 1 : difficulty_icon_container_width + corner_radius; - backgroundBorder.FadeTo(Selected.Value ? 1 : 0, duration, Easing.OutQuint); - difficultyIcon.FadeTo(Selected.Value ? 1 : 0, duration, Easing.OutQuint); - - backgroundContainer.ResizeHeightTo(Selected.Value ? HEIGHT - 4 : HEIGHT, duration, Easing.OutQuint); - backgroundContainer.MoveToX(Selected.Value ? difficulty_icon_container_width : 0, duration, Easing.OutQuint); - mainFlowContainer.MoveToX(Selected.Value ? difficulty_icon_container_width : 0, duration, Easing.OutQuint); - - panel.EdgeEffect = panel.EdgeEffect with { Radius = Selected.Value ? 15 : 10 }; - updateEdgeEffectColour(); - } - - private void updateKeyboardSelectedDisplay() - { - updatePanelPosition(); - updateHover(); - } - - private void updatePanelPosition() - { - float x = set_x_offset + selected_x_offset + preselected_x_offset; - - if (Selected.Value) - x -= selected_x_offset; - - if (KeyboardSelected.Value) - x -= preselected_x_offset; - - this.TransformTo(nameof(Padding), new MarginPadding { Left = x }, duration, Easing.OutQuint); - } - - private void updateHover() - { - bool hovered = IsHovered || KeyboardSelected.Value; - - if (hovered) - hoverLayer.FadeIn(100, Easing.OutQuint); - else - hoverLayer.FadeOut(1000, Easing.OutQuint); - } - private void computeStarRating() { starDifficultyCancellationSource?.Cancel(); @@ -384,23 +271,7 @@ namespace osu.Game.Screens.SelectV2 var beatmap = (BeatmapInfo)Item.Model; starDifficultyBindable = difficultyCache.GetBindableDifficulty(beatmap, starDifficultyCancellationSource.Token); - starDifficultyBindable.BindValueChanged(d => - { - var value = d.NewValue ?? default; - - backgroundBorder.FadeColour(colours.ForStarDifficulty(value.Stars), duration, Easing.OutQuint); - difficultyIcon.FadeColour(value.Stars > 6.5f ? colours.Orange1 : colourProvider.Background5, duration, Easing.OutQuint); - difficultyStarRating.Current.Value = value; - - updateEdgeEffectColour(); - }, true); - } - - private void updateEdgeEffectColour() - { - panel.FadeEdgeEffectTo(Selected.Value - ? colours.ForStarDifficulty(starDifficultyBindable?.Value?.Stars ?? 0f).Opacity(0.5f) - : Color4.Black.Opacity(0.4f), duration, Easing.OutQuint); + starDifficultyBindable.BindValueChanged(_ => updateDisplay(), true); } private void updateKeyCount() @@ -424,16 +295,13 @@ namespace osu.Game.Screens.SelectV2 difficultyKeyCountText.Alpha = 0; } - protected override bool OnHover(HoverEvent e) + private void updateDisplay() { - updateHover(); - return true; - } + var starDifficulty = starDifficultyBindable?.Value ?? default; - protected override void OnHoverLost(HoverLostEvent e) - { - updateHover(); - base.OnHoverLost(e); + panel.AccentColour = colours.ForStarDifficulty(starDifficulty.Stars); + difficultyIcon.FadeColour(starDifficulty.Stars > 6.5f ? colours.Orange1 : colourProvider.Background5, duration, Easing.OutQuint); + difficultyStarRating.Current.Value = starDifficulty; } protected override bool OnClick(ClickEvent e) diff --git a/osu.Game/Screens/SelectV2/CarouselPanelPiece.cs b/osu.Game/Screens/SelectV2/CarouselPanelPiece.cs new file mode 100644 index 0000000000..a7f2b3a163 --- /dev/null +++ b/osu.Game/Screens/SelectV2/CarouselPanelPiece.cs @@ -0,0 +1,240 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Allocation; +using osu.Framework.Bindables; +using osu.Framework.Extensions.Color4Extensions; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Colour; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Effects; +using osu.Framework.Graphics.Shapes; +using osu.Framework.Input.Events; +using osu.Game.Graphics; +using osu.Game.Graphics.UserInterface; +using osu.Game.Overlays; +using osuTK; +using osuTK.Graphics; + +namespace osu.Game.Screens.SelectV2 +{ + public partial class CarouselPanelPiece : Container + { + private const float corner_radius = 10; + + private const float left_edge_x_offset = 20f; + private const float keyboard_active_x_offset = 25f; + private const float active_x_offset = 50f; + + private const float duration = 500; + + private readonly float panelXOffset; + + private readonly Box backgroundBorder; + private readonly Box backgroundGradient; + private readonly Box backgroundAccentGradient; + private readonly Container backgroundLayer; + private readonly Container backgroundLayerHorizontalPadding; + private readonly Container backgroundContainer; + private readonly Container iconContainer; + private readonly Box activationFlash; + private readonly Box hoverLayer; + + public Container TopLevelContent { get; } + + protected override Container Content { get; } + + public Drawable Background + { + set => backgroundContainer.Child = value; + } + + public Drawable Icon + { + set => iconContainer.Child = value; + } + + private Color4? accentColour; + + public Color4? AccentColour + { + get => accentColour; + set + { + accentColour = value; + updateDisplay(); + } + } + + public readonly BindableBool Active = new BindableBool(); + public readonly BindableBool KeyboardActive = new BindableBool(); + + public CarouselPanelPiece(float panelXOffset) + { + this.panelXOffset = panelXOffset; + + RelativeSizeAxes = Axes.Both; + + InternalChild = TopLevelContent = new Container + { + Masking = true, + CornerRadius = corner_radius, + RelativeSizeAxes = Axes.Both, + X = corner_radius, + EdgeEffect = new EdgeEffectParameters + { + Type = EdgeEffectType.Shadow, + Offset = new Vector2(1f), + Radius = 10, + }, + Children = new Drawable[] + { + new BufferedContainer + { + RelativeSizeAxes = Axes.Both, + Children = new Drawable[] + { + backgroundBorder = new Box + { + RelativeSizeAxes = Axes.Both, + Colour = Color4.White, + }, + backgroundLayerHorizontalPadding = new Container + { + RelativeSizeAxes = Axes.Both, + Child = backgroundLayer = new Container + { + RelativeSizeAxes = Axes.Both, + Child = new Container + { + Masking = true, + CornerRadius = corner_radius, + RelativeSizeAxes = Axes.Both, + Children = new Drawable[] + { + backgroundGradient = new Box + { + RelativeSizeAxes = Axes.Both, + }, + backgroundAccentGradient = new Box + { + RelativeSizeAxes = Axes.Both, + }, + backgroundContainer = new Container + { + RelativeSizeAxes = Axes.Both, + }, + } + }, + }, + } + }, + }, + iconContainer = new Container + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + AutoSizeAxes = Axes.Both, + }, + Content = new Container + { + RelativeSizeAxes = Axes.Both, + Padding = new MarginPadding { Right = panelXOffset + corner_radius }, + }, + hoverLayer = new Box + { + Alpha = 0, + Blending = BlendingParameters.Additive, + RelativeSizeAxes = Axes.Both, + }, + activationFlash = new Box + { + Colour = Color4.White.Opacity(0.4f), + Blending = BlendingParameters.Additive, + Alpha = 0f, + RelativeSizeAxes = Axes.Both, + }, + new HoverSounds(), + } + }; + } + + [BackgroundDependencyLoader] + private void load(OverlayColourProvider colourProvider, OsuColour colours) + { + hoverLayer.Colour = colours.Blue.Opacity(0.1f); + backgroundGradient.Colour = ColourInfo.GradientHorizontal(colourProvider.Background3, colourProvider.Background4); + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + Active.BindValueChanged(_ => updateDisplay()); + KeyboardActive.BindValueChanged(_ => updateDisplay(), true); + } + + public void Flash() + { + activationFlash.FadeOutFromOne(500, Easing.OutQuint); + } + + private void updateDisplay() + { + backgroundLayer.TransformTo(nameof(Padding), backgroundLayer.Padding with { Vertical = Active.Value ? 2f : 0f }, duration, Easing.OutQuint); + + var backgroundColour = accentColour ?? Color4.White; + var edgeEffectColour = accentColour ?? Color4Extensions.FromHex(@"4EBFFF"); + + backgroundAccentGradient.FadeColour(ColourInfo.GradientHorizontal(backgroundColour.Opacity(0.25f), backgroundColour.Opacity(0f)), duration, Easing.OutQuint); + backgroundBorder.FadeColour(backgroundColour, duration, Easing.OutQuint); + + TopLevelContent.FadeEdgeEffectTo(Active.Value ? edgeEffectColour.Opacity(0.5f) : Color4.Black.Opacity(0.4f), duration, Easing.OutQuint); + + updateXOffset(); + updateHover(); + } + + private void updateXOffset() + { + float x = panelXOffset + active_x_offset + keyboard_active_x_offset + left_edge_x_offset; + + if (Active.Value) + x -= active_x_offset; + + if (KeyboardActive.Value) + x -= keyboard_active_x_offset; + + this.TransformTo(nameof(Padding), new MarginPadding { Left = x }, duration, Easing.OutQuint); + } + + private void updateHover() + { + bool hovered = IsHovered || KeyboardActive.Value; + + if (hovered) + hoverLayer.FadeIn(100, Easing.OutQuint); + else + hoverLayer.FadeOut(1000, Easing.OutQuint); + } + + protected override bool OnHover(HoverEvent e) + { + updateDisplay(); + return base.OnHover(e); + } + + protected override void OnHoverLost(HoverLostEvent e) + { + updateDisplay(); + base.OnHoverLost(e); + } + + protected override void Update() + { + base.Update(); + Content.Padding = Content.Padding with { Left = iconContainer.DrawWidth }; + backgroundLayerHorizontalPadding.Padding = new MarginPadding { Left = iconContainer.DrawWidth }; + } + } +} diff --git a/osu.Game/Screens/SelectV2/GroupPanel.cs b/osu.Game/Screens/SelectV2/GroupPanel.cs index b5fa338f82..12c4df830c 100644 --- a/osu.Game/Screens/SelectV2/GroupPanel.cs +++ b/osu.Game/Screens/SelectV2/GroupPanel.cs @@ -10,10 +10,10 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Pooling; using osu.Framework.Graphics.Shapes; +using osu.Framework.Graphics.Sprites; using osu.Framework.Input.Events; using osu.Game.Graphics; using osu.Game.Graphics.Sprites; -using osu.Game.Graphics.UserInterface; using osu.Game.Overlays; using osuTK; using osuTK.Graphics; @@ -24,137 +24,83 @@ namespace osu.Game.Screens.SelectV2 { public const float HEIGHT = CarouselItem.DEFAULT_HEIGHT; - private const float corner_radius = 10; - - private const float glow_offset = 10f; // extra space for any edge effect to not be cutoff by the right edge of the carousel. - private const float preselected_x_offset = 25f; - private const float selected_x_offset = 50f; - private const float duration = 500; [Resolved] private BeatmapCarousel? carousel { get; set; } - private Container panel = null!; - private Box activationFlash = null!; + private CarouselPanelPiece panel = null!; + private Drawable chevronIcon = null!; private OsuSpriteText titleText = null!; - private Box hoverLayer = null!; - public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) + protected override bool ReceivePositionalInputAtSubTree(Vector2 screenSpacePos) { - var inputRectangle = panel.DrawRectangle; + var inputRectangle = panel.TopLevelContent.DrawRectangle; - // Cover a gap introduced by the spacing between a GroupPanel and a BeatmapPanel either below/above it. + // Cover a gap introduced by the spacing between a GroupPanel and other panel types either below/above it. inputRectangle = inputRectangle.Inflate(new MarginPadding { Vertical = BeatmapCarousel.SPACING / 2f }); - return inputRectangle.Contains(panel.ToLocalSpace(screenSpacePos)); + return inputRectangle.Contains(panel.TopLevelContent.ToLocalSpace(screenSpacePos)); } [BackgroundDependencyLoader] - private void load(OverlayColourProvider colourProvider, OsuColour colours) + private void load(OverlayColourProvider colourProvider) { Anchor = Anchor.TopRight; Origin = Anchor.TopRight; RelativeSizeAxes = Axes.X; Height = HEIGHT; - InternalChild = panel = new Container + InternalChild = panel = new CarouselPanelPiece(0) { - RelativeSizeAxes = Axes.Both, - CornerRadius = corner_radius, - Masking = true, - X = corner_radius, + Icon = chevronIcon = new SpriteIcon + { + AlwaysPresent = true, + Icon = FontAwesome.Solid.ChevronDown, + Size = new Vector2(12), + Margin = new MarginPadding { Horizontal = 5f }, + X = 2f, + Colour = colourProvider.Background3, + }, + Background = new Box + { + RelativeSizeAxes = Axes.Both, + Colour = colourProvider.Dark1, + }, + AccentColour = colourProvider.Highlight1, Children = new Drawable[] { - new Container + titleText = new OsuSpriteText { - RelativeSizeAxes = Axes.Both, - Padding = new MarginPadding { Left = 10f }, - Child = new Container + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + X = 10f, + }, + new CircularContainer + { + Anchor = Anchor.CentreRight, + Origin = Anchor.CentreRight, + Size = new Vector2(50f, 14f), + Margin = new MarginPadding { Right = 20f }, + Masking = true, + Children = new Drawable[] { - RelativeSizeAxes = Axes.Both, - CornerRadius = corner_radius, - Masking = true, - Children = new Drawable[] + new Box { - new Box - { - RelativeSizeAxes = Axes.Both, - Colour = colourProvider.Background6, - }, - } - } - }, - new Box - { - RelativeSizeAxes = Axes.Both, - Colour = colourProvider.Background3, - }, - new Container - { - RelativeSizeAxes = Axes.Both, - Padding = new MarginPadding { Left = 10f }, - Child = new Container - { - RelativeSizeAxes = Axes.Both, - CornerRadius = corner_radius, - Masking = true, - Children = new Drawable[] + RelativeSizeAxes = Axes.Both, + Colour = Color4.Black.Opacity(0.7f), + }, + new OsuSpriteText { - new Box - { - RelativeSizeAxes = Axes.Both, - Colour = colourProvider.Background5, - }, - titleText = new OsuSpriteText - { - Anchor = Anchor.CentreLeft, - Origin = Anchor.CentreLeft, - X = 10f, - }, - new CircularContainer - { - Anchor = Anchor.CentreRight, - Origin = Anchor.CentreRight, - Size = new Vector2(50f, 14f), - Margin = new MarginPadding { Right = 30f }, - Masking = true, - Children = new Drawable[] - { - new Box - { - RelativeSizeAxes = Axes.Both, - Colour = Color4.Black.Opacity(0.7f), - }, - new OsuSpriteText - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Font = OsuFont.Torus.With(size: 14.4f, weight: FontWeight.Bold), - // TODO: requires Carousel/CarouselItem-side implementation - Text = "43", - UseFullGlyphHeight = false, - } - }, - }, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Font = OsuFont.Torus.With(size: 14.4f, weight: FontWeight.Bold), + // TODO: requires Carousel/CarouselItem-side implementation + Text = "43", + UseFullGlyphHeight = false, } - } - }, - activationFlash = new Box - { - Colour = Color4.White, - Blending = BlendingParameters.Additive, - Alpha = 0, - RelativeSizeAxes = Axes.Both, - }, - hoverLayer = new Box - { - Colour = colours.Blue.Opacity(0.1f), - Alpha = 0, - Blending = BlendingParameters.Additive, - RelativeSizeAxes = Axes.Both, - }, - new HoverSounds(), + }, + } } }; } @@ -163,17 +109,17 @@ namespace osu.Game.Screens.SelectV2 { base.LoadComplete(); - Expanded.BindValueChanged(_ => updateExpandedDisplay(), true); - KeyboardSelected.BindValueChanged(_ => updateKeyboardSelectedDisplay(), true); + Expanded.BindValueChanged(_ => onExpanded(), true); + KeyboardSelected.BindValueChanged(k => panel.KeyboardActive.Value = k.NewValue, true); } - private void updateExpandedDisplay() + private void onExpanded() { - updatePanelPosition(); + panel.Active.Value = Expanded.Value; + panel.Flash(); - // todo: figma shares no extra visual feedback on this. - - activationFlash.FadeTo(0.2f).FadeTo(0f, 500, Easing.OutQuint); + chevronIcon.ResizeWidthTo(Expanded.Value ? 12f : 0f, duration, Easing.OutQuint); + chevronIcon.FadeTo(Expanded.Value ? 1f : 0f, duration, Easing.OutQuint); } protected override void PrepareForUse() @@ -186,6 +132,7 @@ namespace osu.Game.Screens.SelectV2 titleText.Text = group.Title; + FinishTransforms(true); this.FadeInFromZero(500, Easing.OutQuint); } @@ -197,47 +144,6 @@ namespace osu.Game.Screens.SelectV2 return true; } - private void updateKeyboardSelectedDisplay() - { - updatePanelPosition(); - updateHover(); - } - - private void updatePanelPosition() - { - float x = glow_offset + selected_x_offset + preselected_x_offset; - - if (Expanded.Value) - x -= selected_x_offset; - - if (KeyboardSelected.Value) - x -= preselected_x_offset; - - this.TransformTo(nameof(Padding), new MarginPadding { Left = x }, duration, Easing.OutQuint); - } - - private void updateHover() - { - bool hovered = IsHovered || KeyboardSelected.Value; - - if (hovered) - hoverLayer.FadeIn(100, Easing.OutQuint); - else - hoverLayer.FadeOut(1000, Easing.OutQuint); - } - - protected override bool OnHover(HoverEvent e) - { - updateHover(); - return true; - } - - protected override void OnHoverLost(HoverLostEvent e) - { - updateHover(); - base.OnHoverLost(e); - } - #region ICarouselPanel public CarouselItem? Item { get; set; } @@ -249,7 +155,7 @@ namespace osu.Game.Screens.SelectV2 public void Activated() { - // sets should never be activated. + // groups should never be activated. throw new InvalidOperationException(); } diff --git a/osu.Game/Screens/SelectV2/StarsGroupPanel.cs b/osu.Game/Screens/SelectV2/StarsGroupPanel.cs index 76e3da2500..8e179ec5c1 100644 --- a/osu.Game/Screens/SelectV2/StarsGroupPanel.cs +++ b/osu.Game/Screens/SelectV2/StarsGroupPanel.cs @@ -10,6 +10,7 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Pooling; using osu.Framework.Graphics.Shapes; +using osu.Framework.Graphics.Sprites; using osu.Framework.Input.Events; using osu.Game.Beatmaps; using osu.Game.Beatmaps.Drawables; @@ -26,10 +27,6 @@ namespace osu.Game.Screens.SelectV2 { public const float HEIGHT = CarouselItem.DEFAULT_HEIGHT; - private const float glow_offset = 10f; // extra space for the edge effect to not be cutoff by the right edge of the carousel. - private const float preselected_x_offset = 25f; - private const float expanded_x_offset = 50f; - private const float duration = 500; [Resolved] @@ -41,20 +38,20 @@ namespace osu.Game.Screens.SelectV2 [Resolved] private OverlayColourProvider colourProvider { get; set; } = null!; - private Box activationFlash = null!; - private Box outerLayer = null!; + private CarouselPanelPiece panel = null!; + private Drawable chevronIcon = null!; + private Box contentBackground = null!; private StarRatingDisplay starRatingDisplay = null!; private StarCounter starCounter = null!; - private Box hoverLayer = null!; - public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) + protected override bool ReceivePositionalInputAtSubTree(Vector2 screenSpacePos) { - var inputRectangle = DrawRectangle; + var inputRectangle = panel.TopLevelContent.DrawRectangle; - // Cover a gap introduced by the spacing between a GroupPanel and a BeatmapPanel either below/above it. + // Cover a gap introduced by the spacing between a GroupPanel and other panel types either below/above it. inputRectangle = inputRectangle.Inflate(new MarginPadding { Vertical = BeatmapCarousel.SPACING / 2f }); - return inputRectangle.Contains(ToLocalSpace(screenSpacePos)); + return inputRectangle.Contains(panel.TopLevelContent.ToLocalSpace(screenSpacePos)); } [BackgroundDependencyLoader] @@ -65,118 +62,71 @@ namespace osu.Game.Screens.SelectV2 RelativeSizeAxes = Axes.X; Height = HEIGHT; - InternalChild = new Container + InternalChild = panel = new CarouselPanelPiece(0) { - RelativeSizeAxes = Axes.Both, - CornerRadius = 10f, - Masking = true, + Icon = chevronIcon = new SpriteIcon + { + AlwaysPresent = true, + Icon = FontAwesome.Solid.ChevronDown, + Size = new Vector2(12), + Margin = new MarginPadding { Horizontal = 5f }, + X = 2f, + }, + Background = contentBackground = new Box + { + RelativeSizeAxes = Axes.Both, + Colour = colourProvider.Dark1, + }, + AccentColour = colourProvider.Highlight1, Children = new Drawable[] { - new Container + new FillFlowContainer { - RelativeSizeAxes = Axes.Both, - Padding = new MarginPadding { Left = 10f }, - Child = new Container + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + AutoSizeAxes = Axes.Both, + Spacing = new Vector2(10f, 0f), + Margin = new MarginPadding { Left = 10f }, + Children = new Drawable[] { - RelativeSizeAxes = Axes.Both, - CornerRadius = 10f, - Masking = true, - Children = new Drawable[] + starRatingDisplay = new StarRatingDisplay(default, StarRatingDisplaySize.Small) { - new Box - { - RelativeSizeAxes = Axes.Both, - Colour = colourProvider.Background6, - }, - } + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + }, + starCounter = new StarCounter + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + Scale = new Vector2(8f / 20f), + }, } }, - outerLayer = new Box + new CircularContainer { - RelativeSizeAxes = Axes.Both, - Colour = colourProvider.Background3, - }, - new Container - { - RelativeSizeAxes = Axes.Both, - Padding = new MarginPadding { Left = 10f }, - Child = new Container + Anchor = Anchor.CentreRight, + Origin = Anchor.CentreRight, + Size = new Vector2(50f, 14f), + Margin = new MarginPadding { Right = 20f }, + Masking = true, + Children = new Drawable[] { - RelativeSizeAxes = Axes.Both, - CornerRadius = 10f, - Masking = true, - Children = new Drawable[] + new Box { - new Box - { - RelativeSizeAxes = Axes.Both, - Colour = Color4.Black.Opacity(0.2f), - }, - new FillFlowContainer - { - Anchor = Anchor.CentreLeft, - Origin = Anchor.CentreLeft, - AutoSizeAxes = Axes.Both, - Spacing = new Vector2(10f, 0f), - Margin = new MarginPadding { Left = 10f }, - Children = new Drawable[] - { - starRatingDisplay = new StarRatingDisplay(default, StarRatingDisplaySize.Small) - { - Anchor = Anchor.CentreLeft, - Origin = Anchor.CentreLeft, - }, - starCounter = new StarCounter - { - Anchor = Anchor.CentreLeft, - Origin = Anchor.CentreLeft, - Scale = new Vector2(8f / 20f), - }, - } - }, - new CircularContainer - { - Anchor = Anchor.CentreRight, - Origin = Anchor.CentreRight, - Size = new Vector2(50f, 14f), - Margin = new MarginPadding { Right = 30f }, - Masking = true, - Children = new Drawable[] - { - new Box - { - RelativeSizeAxes = Axes.Both, - Colour = Color4.Black.Opacity(0.7f), - }, - new OsuSpriteText - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Font = OsuFont.Torus.With(size: 14.4f, weight: FontWeight.Bold), - // TODO: requires Carousel/CarouselItem-side implementation - Text = "43", - UseFullGlyphHeight = false, - } - }, - }, + RelativeSizeAxes = Axes.Both, + Colour = Color4.Black.Opacity(0.7f), + }, + new OsuSpriteText + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Font = OsuFont.Torus.With(size: 14.4f, weight: FontWeight.Bold), + // TODO: requires Carousel/CarouselItem-side implementation + Text = "43", + UseFullGlyphHeight = false, } - } - }, - activationFlash = new Box - { - Colour = Color4.White, - Blending = BlendingParameters.Additive, - Alpha = 0, - RelativeSizeAxes = Axes.Both, - }, - hoverLayer = new Box - { - Colour = colours.Blue.Opacity(0.1f), - Alpha = 0, - Blending = BlendingParameters.Additive, - RelativeSizeAxes = Axes.Both, - }, - new HoverSounds(), + }, + } } }; } @@ -185,17 +135,17 @@ namespace osu.Game.Screens.SelectV2 { base.LoadComplete(); - Expanded.BindValueChanged(_ => updateExpandedDisplay(), true); - KeyboardSelected.BindValueChanged(_ => updateKeyboardSelectedDisplay(), true); + Expanded.BindValueChanged(_ => onExpanded(), true); + KeyboardSelected.BindValueChanged(k => panel.KeyboardActive.Value = k.NewValue, true); } - private void updateExpandedDisplay() + private void onExpanded() { - updatePanelPosition(); + panel.Active.Value = Expanded.Value; + panel.Flash(); - // todo: figma shares no extra visual feedback on this. - - activationFlash.FadeTo(0.2f).FadeTo(0f, 500, Easing.OutQuint); + chevronIcon.ResizeWidthTo(Expanded.Value ? 12f : 0f, duration, Easing.OutQuint); + chevronIcon.FadeTo(Expanded.Value ? 1f : 0f, duration, Easing.OutQuint); } protected override void PrepareForUse() @@ -209,12 +159,15 @@ namespace osu.Game.Screens.SelectV2 Color4 colour = group.StarNumber >= 9 ? OsuColour.Gray(0.2f) : colours.ForStarDifficulty(group.StarNumber); Color4 contentColour = group.StarNumber >= 7 ? colours.Orange1 : colourProvider.Background5; - outerLayer.Colour = colour; - starCounter.Colour = contentColour; + panel.AccentColour = colour; + contentBackground.Colour = colour.Darken(0.3f); starRatingDisplay.Current.Value = new StarDifficulty(group.StarNumber, 0); starCounter.Current = group.StarNumber; + chevronIcon.Colour = contentColour; + starCounter.Colour = contentColour; + this.FadeInFromZero(500, Easing.OutQuint); } @@ -226,47 +179,6 @@ namespace osu.Game.Screens.SelectV2 return true; } - private void updateKeyboardSelectedDisplay() - { - updatePanelPosition(); - updateHover(); - } - - private void updatePanelPosition() - { - float x = glow_offset + expanded_x_offset + preselected_x_offset; - - if (Expanded.Value) - x -= expanded_x_offset; - - if (KeyboardSelected.Value) - x -= preselected_x_offset; - - this.TransformTo(nameof(Padding), new MarginPadding { Left = x }, duration, Easing.OutQuint); - } - - private void updateHover() - { - bool hovered = IsHovered || KeyboardSelected.Value; - - if (hovered) - hoverLayer.FadeIn(100, Easing.OutQuint); - else - hoverLayer.FadeOut(1000, Easing.OutQuint); - } - - protected override bool OnHover(HoverEvent e) - { - updateHover(); - return true; - } - - protected override void OnHoverLost(HoverLostEvent e) - { - updateHover(); - base.OnHoverLost(e); - } - #region ICarouselPanel public CarouselItem? Item { get; set; } From 3ab208bb4643e6bd0512bd5b274d958cbef3a8fc Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Thu, 6 Feb 2025 02:21:44 -0500 Subject: [PATCH 0923/1275] Fix group visual test scene --- .../SongSelectV2/TestSceneBeatmapCarouselGroupPanel.cs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselGroupPanel.cs b/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselGroupPanel.cs index eea3870117..d9f4a1630f 100644 --- a/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselGroupPanel.cs +++ b/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselGroupPanel.cs @@ -41,13 +41,13 @@ namespace osu.Game.Tests.Visual.SongSelectV2 new GroupPanel { Item = new CarouselItem(new GroupDefinition("Group A")), - Selected = { Value = true } + Expanded = { Value = true } }, new GroupPanel { Item = new CarouselItem(new GroupDefinition("Group A")), KeyboardSelected = { Value = true }, - Selected = { Value = true } + Expanded = { Value = true } }, new StarsGroupPanel { @@ -56,6 +56,7 @@ namespace osu.Game.Tests.Visual.SongSelectV2 new StarsGroupPanel { Item = new CarouselItem(new StarsGroupDefinition(3)), + Expanded = { Value = true } }, new StarsGroupPanel { @@ -64,6 +65,7 @@ namespace osu.Game.Tests.Visual.SongSelectV2 new StarsGroupPanel { Item = new CarouselItem(new StarsGroupDefinition(7)), + Expanded = { Value = true } }, new StarsGroupPanel { @@ -72,6 +74,7 @@ namespace osu.Game.Tests.Visual.SongSelectV2 new StarsGroupPanel { Item = new CarouselItem(new StarsGroupDefinition(9)), + Expanded = { Value = true } }, } }; From e1d6ce5ff44569c0b911540ebabbf116319b0eab Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Thu, 6 Feb 2025 02:25:12 -0500 Subject: [PATCH 0924/1275] Add V2 suffix for easier test browsing --- ...yPanel.cs => TestSceneBeatmapCarouselV2DifficultyPanel.cs} | 4 ++-- ...lGroupPanel.cs => TestSceneBeatmapCarouselV2GroupPanel.cs} | 4 ++-- ...ouselSetPanel.cs => TestSceneBeatmapCarouselV2SetPanel.cs} | 4 ++-- ...ePanel.cs => TestSceneBeatmapCarouselV2StandalonePanel.cs} | 4 ++-- 4 files changed, 8 insertions(+), 8 deletions(-) rename osu.Game.Tests/Visual/SongSelectV2/{TestSceneBeatmapCarouselDifficultyPanel.cs => TestSceneBeatmapCarouselV2DifficultyPanel.cs} (95%) rename osu.Game.Tests/Visual/SongSelectV2/{TestSceneBeatmapCarouselGroupPanel.cs => TestSceneBeatmapCarouselV2GroupPanel.cs} (95%) rename osu.Game.Tests/Visual/SongSelectV2/{TestSceneBeatmapCarouselSetPanel.cs => TestSceneBeatmapCarouselV2SetPanel.cs} (95%) rename osu.Game.Tests/Visual/SongSelectV2/{TestSceneBeatmapCarouselStandalonePanel.cs => TestSceneBeatmapCarouselV2StandalonePanel.cs} (95%) diff --git a/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselDifficultyPanel.cs b/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselV2DifficultyPanel.cs similarity index 95% rename from osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselDifficultyPanel.cs rename to osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselV2DifficultyPanel.cs index a9f73759f7..93472e7b81 100644 --- a/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselDifficultyPanel.cs +++ b/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselV2DifficultyPanel.cs @@ -18,14 +18,14 @@ using osuTK; namespace osu.Game.Tests.Visual.SongSelectV2 { - public partial class TestSceneBeatmapCarouselDifficultyPanel : ThemeComparisonTestScene + public partial class TestSceneBeatmapCarouselV2DifficultyPanel : ThemeComparisonTestScene { [Resolved] private BeatmapManager beatmaps { get; set; } = null!; private BeatmapInfo beatmap = null!; - public TestSceneBeatmapCarouselDifficultyPanel() + public TestSceneBeatmapCarouselV2DifficultyPanel() : base(false) { } diff --git a/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselGroupPanel.cs b/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselV2GroupPanel.cs similarity index 95% rename from osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselGroupPanel.cs rename to osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselV2GroupPanel.cs index d9f4a1630f..9808e41f73 100644 --- a/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselGroupPanel.cs +++ b/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselV2GroupPanel.cs @@ -9,9 +9,9 @@ using osuTK; namespace osu.Game.Tests.Visual.SongSelectV2 { - public partial class TestSceneBeatmapCarouselGroupPanel : ThemeComparisonTestScene + public partial class TestSceneBeatmapCarouselV2GroupPanel : ThemeComparisonTestScene { - public TestSceneBeatmapCarouselGroupPanel() + public TestSceneBeatmapCarouselV2GroupPanel() : base(false) { } diff --git a/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselSetPanel.cs b/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselV2SetPanel.cs similarity index 95% rename from osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselSetPanel.cs rename to osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselV2SetPanel.cs index 8f7cac2b58..540eae3be0 100644 --- a/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselSetPanel.cs +++ b/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselV2SetPanel.cs @@ -16,14 +16,14 @@ using osuTK; namespace osu.Game.Tests.Visual.SongSelectV2 { - public partial class TestSceneBeatmapCarouselSetPanel : ThemeComparisonTestScene + public partial class TestSceneBeatmapCarouselV2SetPanel : ThemeComparisonTestScene { [Resolved] private BeatmapManager beatmaps { get; set; } = null!; private BeatmapSetInfo beatmapSet = null!; - public TestSceneBeatmapCarouselSetPanel() + public TestSceneBeatmapCarouselV2SetPanel() : base(false) { } diff --git a/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselStandalonePanel.cs b/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselV2StandalonePanel.cs similarity index 95% rename from osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselStandalonePanel.cs rename to osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselV2StandalonePanel.cs index a34ac31d5d..72f7a9e98c 100644 --- a/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselStandalonePanel.cs +++ b/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselV2StandalonePanel.cs @@ -18,14 +18,14 @@ using osuTK; namespace osu.Game.Tests.Visual.SongSelectV2 { - public partial class TestSceneBeatmapCarouselStandalonePanel : ThemeComparisonTestScene + public partial class TestSceneBeatmapCarouselV2StandalonePanel : ThemeComparisonTestScene { [Resolved] private BeatmapManager beatmaps { get; set; } = null!; private BeatmapInfo beatmap = null!; - public TestSceneBeatmapCarouselStandalonePanel() + public TestSceneBeatmapCarouselV2StandalonePanel() : base(false) { } From 5e74d82fc101f03d945033be96e184b0199016a3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 6 Feb 2025 08:32:08 +0100 Subject: [PATCH 0925/1275] Suppress inspections for now --- osu.Game/Online/API/Requests/PatchBeatmapPackageRequest.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/osu.Game/Online/API/Requests/PatchBeatmapPackageRequest.cs b/osu.Game/Online/API/Requests/PatchBeatmapPackageRequest.cs index 85981448da..bb9d32f77b 100644 --- a/osu.Game/Online/API/Requests/PatchBeatmapPackageRequest.cs +++ b/osu.Game/Online/API/Requests/PatchBeatmapPackageRequest.cs @@ -25,7 +25,11 @@ namespace osu.Game.Online.API.Requests protected override string Target => throw new NotSupportedException(); public uint BeatmapSetID { get; } + + // ReSharper disable once CollectionNeverUpdated.Global public Dictionary FilesChanged { get; } = new Dictionary(); + + // ReSharper disable once CollectionNeverUpdated.Global public HashSet FilesDeleted { get; } = new HashSet(); public PatchBeatmapPackageRequest(uint beatmapSetId) From e1a146d487300feb616adcf100563945aa3d17e8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 6 Feb 2025 08:38:28 +0100 Subject: [PATCH 0926/1275] Remove unnecessary suppressions --- osu.Game/Online/API/Requests/PatchBeatmapPackageRequest.cs | 2 -- 1 file changed, 2 deletions(-) diff --git a/osu.Game/Online/API/Requests/PatchBeatmapPackageRequest.cs b/osu.Game/Online/API/Requests/PatchBeatmapPackageRequest.cs index bb9d32f77b..a59a708079 100644 --- a/osu.Game/Online/API/Requests/PatchBeatmapPackageRequest.cs +++ b/osu.Game/Online/API/Requests/PatchBeatmapPackageRequest.cs @@ -26,10 +26,8 @@ namespace osu.Game.Online.API.Requests public uint BeatmapSetID { get; } - // ReSharper disable once CollectionNeverUpdated.Global public Dictionary FilesChanged { get; } = new Dictionary(); - // ReSharper disable once CollectionNeverUpdated.Global public HashSet FilesDeleted { get; } = new HashSet(); public PatchBeatmapPackageRequest(uint beatmapSetId) From 78cd093a47f70403428eb40f020c8a8beffc522e Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Thu, 6 Feb 2025 02:44:40 -0500 Subject: [PATCH 0927/1275] Fix broken input handling with structural changes --- osu.Game/Screens/SelectV2/BeatmapPanel.cs | 24 ++++--------------- osu.Game/Screens/SelectV2/BeatmapSetPanel.cs | 16 ++----------- .../SelectV2/BeatmapStandalonePanel.cs | 23 +++++++----------- .../Screens/SelectV2/CarouselPanelPiece.cs | 21 +++++++++++++++- osu.Game/Screens/SelectV2/GroupPanel.cs | 16 ++----------- osu.Game/Screens/SelectV2/StarsGroupPanel.cs | 16 ++----------- 6 files changed, 39 insertions(+), 77 deletions(-) diff --git a/osu.Game/Screens/SelectV2/BeatmapPanel.cs b/osu.Game/Screens/SelectV2/BeatmapPanel.cs index bd4cf6d7cf..a878f966b8 100644 --- a/osu.Game/Screens/SelectV2/BeatmapPanel.cs +++ b/osu.Game/Screens/SelectV2/BeatmapPanel.cs @@ -9,7 +9,6 @@ using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Pooling; -using osu.Framework.Input.Events; using osu.Game.Beatmaps; using osu.Game.Beatmaps.Drawables; using osu.Game.Graphics; @@ -64,16 +63,6 @@ namespace osu.Game.Screens.SelectV2 [Resolved] private IBindable> mods { get; set; } = null!; - protected override bool ReceivePositionalInputAtSubTree(Vector2 screenSpacePos) - { - var inputRectangle = panel.TopLevelContent.DrawRectangle; - - // Cover the gaps introduced by the spacing between BeatmapPanels. - inputRectangle = inputRectangle.Inflate(new MarginPadding { Vertical = BeatmapCarousel.SPACING / 2f }); - - return inputRectangle.Contains(panel.TopLevelContent.ToLocalSpace(screenSpacePos)); - } - [BackgroundDependencyLoader] private void load(OverlayColourProvider colourProvider) { @@ -86,6 +75,7 @@ namespace osu.Game.Screens.SelectV2 InternalChild = panel = new CarouselPanelPiece(difficulty_x_offset) { + Action = onAction, Icon = difficultyIcon = new ConstrainedIconContainer { Size = new Vector2(20), @@ -261,19 +251,15 @@ namespace osu.Game.Screens.SelectV2 panel.AccentColour = starRatingColour; } - protected override bool OnClick(ClickEvent e) + private void onAction() { if (carousel == null) - return true; + return; if (carousel.CurrentSelection != Item!.Model) - { carousel.CurrentSelection = Item!.Model; - return true; - } - - carousel.TryActivateSelection(); - return true; + else + carousel.TryActivateSelection(); } #region ICarouselPanel diff --git a/osu.Game/Screens/SelectV2/BeatmapSetPanel.cs b/osu.Game/Screens/SelectV2/BeatmapSetPanel.cs index f5d7e0594b..951e76e0bc 100644 --- a/osu.Game/Screens/SelectV2/BeatmapSetPanel.cs +++ b/osu.Game/Screens/SelectV2/BeatmapSetPanel.cs @@ -10,7 +10,6 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Pooling; using osu.Framework.Graphics.Sprites; -using osu.Framework.Input.Events; using osu.Framework.Localisation; using osu.Game.Beatmaps; using osu.Game.Beatmaps.Drawables; @@ -50,16 +49,6 @@ namespace osu.Game.Screens.SelectV2 [Resolved] private BeatmapManager beatmaps { get; set; } = null!; - protected override bool ReceivePositionalInputAtSubTree(Vector2 screenSpacePos) - { - var inputRectangle = panel.TopLevelContent.DrawRectangle; - - // Cover a gap introduced by the spacing between a BeatmapSetPanel and a BeatmapPanel either above it or below it. - inputRectangle = inputRectangle.Inflate(new MarginPadding { Vertical = BeatmapCarousel.SPACING / 2f }); - - return inputRectangle.Contains(panel.TopLevelContent.ToLocalSpace(screenSpacePos)); - } - [BackgroundDependencyLoader] private void load() { @@ -70,6 +59,7 @@ namespace osu.Game.Screens.SelectV2 InternalChild = panel = new CarouselPanelPiece(set_x_offset) { + Action = onAction, Icon = chevronIcon = new Container { Size = new Vector2(22), @@ -183,12 +173,10 @@ namespace osu.Game.Screens.SelectV2 difficultiesDisplay.BeatmapSet = null; } - protected override bool OnClick(ClickEvent e) + private void onAction() { if (carousel != null) carousel.CurrentSelection = Item!.Model; - - return true; } #region ICarouselPanel diff --git a/osu.Game/Screens/SelectV2/BeatmapStandalonePanel.cs b/osu.Game/Screens/SelectV2/BeatmapStandalonePanel.cs index a8fa2224d7..8e201ec5bc 100644 --- a/osu.Game/Screens/SelectV2/BeatmapStandalonePanel.cs +++ b/osu.Game/Screens/SelectV2/BeatmapStandalonePanel.cs @@ -11,7 +11,6 @@ using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Pooling; -using osu.Framework.Input.Events; using osu.Framework.Localisation; using osu.Game.Beatmaps; using osu.Game.Beatmaps.Drawables; @@ -76,16 +75,6 @@ namespace osu.Game.Screens.SelectV2 private OsuSpriteText difficultyName = null!; private OsuSpriteText difficultyAuthor = null!; - protected override bool ReceivePositionalInputAtSubTree(Vector2 screenSpacePos) - { - var inputRectangle = panel.TopLevelContent.DrawRectangle; - - // Cover a gap introduced by the spacing between a BeatmapSetPanel and a BeatmapPanel either above it or below it. - inputRectangle = inputRectangle.Inflate(new MarginPadding { Vertical = BeatmapCarousel.SPACING / 2f }); - - return inputRectangle.Contains(panel.TopLevelContent.ToLocalSpace(screenSpacePos)); - } - [BackgroundDependencyLoader] private void load() { @@ -97,6 +86,7 @@ namespace osu.Game.Screens.SelectV2 InternalChild = panel = new CarouselPanelPiece(standalone_x_offset) { + Action = onAction, Icon = difficultyIcon = new ConstrainedIconContainer { Size = new Vector2(20), @@ -304,12 +294,15 @@ namespace osu.Game.Screens.SelectV2 difficultyStarRating.Current.Value = starDifficulty; } - protected override bool OnClick(ClickEvent e) + private void onAction() { - if (carousel != null) - carousel.CurrentSelection = Item!.Model; + if (carousel == null) + return; - return true; + if (carousel.CurrentSelection != Item!.Model) + carousel.CurrentSelection = Item!.Model; + else + carousel.TryActivateSelection(); } #region ICarouselPanel diff --git a/osu.Game/Screens/SelectV2/CarouselPanelPiece.cs b/osu.Game/Screens/SelectV2/CarouselPanelPiece.cs index a7f2b3a163..4b533e362a 100644 --- a/osu.Game/Screens/SelectV2/CarouselPanelPiece.cs +++ b/osu.Game/Screens/SelectV2/CarouselPanelPiece.cs @@ -1,6 +1,7 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Extensions.Color4Extensions; @@ -69,6 +70,18 @@ namespace osu.Game.Screens.SelectV2 public readonly BindableBool Active = new BindableBool(); public readonly BindableBool KeyboardActive = new BindableBool(); + public Action? Action; + + public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) + { + var inputRectangle = TopLevelContent.DrawRectangle; + + // Cover potential gaps introduced by the spacing between panels. + inputRectangle = inputRectangle.Inflate(new MarginPadding { Vertical = BeatmapCarousel.SPACING / 2f }); + + return inputRectangle.Contains(TopLevelContent.ToLocalSpace(screenSpacePos)); + } + public CarouselPanelPiece(float panelXOffset) { this.panelXOffset = panelXOffset; @@ -221,7 +234,7 @@ namespace osu.Game.Screens.SelectV2 protected override bool OnHover(HoverEvent e) { updateDisplay(); - return base.OnHover(e); + return true; } protected override void OnHoverLost(HoverLostEvent e) @@ -230,6 +243,12 @@ namespace osu.Game.Screens.SelectV2 base.OnHoverLost(e); } + protected override bool OnClick(ClickEvent e) + { + Action?.Invoke(); + return true; + } + protected override void Update() { base.Update(); diff --git a/osu.Game/Screens/SelectV2/GroupPanel.cs b/osu.Game/Screens/SelectV2/GroupPanel.cs index 12c4df830c..a757293e57 100644 --- a/osu.Game/Screens/SelectV2/GroupPanel.cs +++ b/osu.Game/Screens/SelectV2/GroupPanel.cs @@ -11,7 +11,6 @@ using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Pooling; using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Sprites; -using osu.Framework.Input.Events; using osu.Game.Graphics; using osu.Game.Graphics.Sprites; using osu.Game.Overlays; @@ -33,16 +32,6 @@ namespace osu.Game.Screens.SelectV2 private Drawable chevronIcon = null!; private OsuSpriteText titleText = null!; - protected override bool ReceivePositionalInputAtSubTree(Vector2 screenSpacePos) - { - var inputRectangle = panel.TopLevelContent.DrawRectangle; - - // Cover a gap introduced by the spacing between a GroupPanel and other panel types either below/above it. - inputRectangle = inputRectangle.Inflate(new MarginPadding { Vertical = BeatmapCarousel.SPACING / 2f }); - - return inputRectangle.Contains(panel.TopLevelContent.ToLocalSpace(screenSpacePos)); - } - [BackgroundDependencyLoader] private void load(OverlayColourProvider colourProvider) { @@ -53,6 +42,7 @@ namespace osu.Game.Screens.SelectV2 InternalChild = panel = new CarouselPanelPiece(0) { + Action = onAction, Icon = chevronIcon = new SpriteIcon { AlwaysPresent = true, @@ -136,12 +126,10 @@ namespace osu.Game.Screens.SelectV2 this.FadeInFromZero(500, Easing.OutQuint); } - protected override bool OnClick(ClickEvent e) + private void onAction() { if (carousel != null) carousel.CurrentSelection = Item!.Model; - - return true; } #region ICarouselPanel diff --git a/osu.Game/Screens/SelectV2/StarsGroupPanel.cs b/osu.Game/Screens/SelectV2/StarsGroupPanel.cs index 8e179ec5c1..d345f9687e 100644 --- a/osu.Game/Screens/SelectV2/StarsGroupPanel.cs +++ b/osu.Game/Screens/SelectV2/StarsGroupPanel.cs @@ -11,7 +11,6 @@ using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Pooling; using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Sprites; -using osu.Framework.Input.Events; using osu.Game.Beatmaps; using osu.Game.Beatmaps.Drawables; using osu.Game.Graphics; @@ -44,16 +43,6 @@ namespace osu.Game.Screens.SelectV2 private StarRatingDisplay starRatingDisplay = null!; private StarCounter starCounter = null!; - protected override bool ReceivePositionalInputAtSubTree(Vector2 screenSpacePos) - { - var inputRectangle = panel.TopLevelContent.DrawRectangle; - - // Cover a gap introduced by the spacing between a GroupPanel and other panel types either below/above it. - inputRectangle = inputRectangle.Inflate(new MarginPadding { Vertical = BeatmapCarousel.SPACING / 2f }); - - return inputRectangle.Contains(panel.TopLevelContent.ToLocalSpace(screenSpacePos)); - } - [BackgroundDependencyLoader] private void load() { @@ -64,6 +53,7 @@ namespace osu.Game.Screens.SelectV2 InternalChild = panel = new CarouselPanelPiece(0) { + Action = onAction, Icon = chevronIcon = new SpriteIcon { AlwaysPresent = true, @@ -171,12 +161,10 @@ namespace osu.Game.Screens.SelectV2 this.FadeInFromZero(500, Easing.OutQuint); } - protected override bool OnClick(ClickEvent e) + private void onAction() { if (carousel != null) carousel.CurrentSelection = Item!.Model; - - return true; } #region ICarouselPanel From aa9727c020051e38d3ffc45f462e8f062ff11752 Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Thu, 6 Feb 2025 02:44:52 -0500 Subject: [PATCH 0928/1275] Fix helper method in carousel test scene --- osu.Game.Tests/Visual/SongSelect/BeatmapCarouselV2TestScene.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/osu.Game.Tests/Visual/SongSelect/BeatmapCarouselV2TestScene.cs b/osu.Game.Tests/Visual/SongSelect/BeatmapCarouselV2TestScene.cs index a3f6eaf152..9f7b4468dc 100644 --- a/osu.Game.Tests/Visual/SongSelect/BeatmapCarouselV2TestScene.cs +++ b/osu.Game.Tests/Visual/SongSelect/BeatmapCarouselV2TestScene.cs @@ -185,6 +185,7 @@ namespace osu.Game.Tests.Visual.SongSelect .Where(p => ((ICarouselPanel)p).Item?.IsVisible == true) .OrderBy(p => p.Y) .ElementAt(index) + .ChildrenOfType().Single() .TriggerClick(); }); } From a25e1f4f9b3e9796905419b8aa310a356a3276e1 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 6 Feb 2025 15:13:17 +0900 Subject: [PATCH 0929/1275] Add test coverage of artist grouping --- ...estSceneBeatmapCarouselV2ArtistGrouping.cs | 170 ++++++++++++++++++ 1 file changed, 170 insertions(+) create mode 100644 osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2ArtistGrouping.cs diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2ArtistGrouping.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2ArtistGrouping.cs new file mode 100644 index 0000000000..c7ab9de5e5 --- /dev/null +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2ArtistGrouping.cs @@ -0,0 +1,170 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Linq; +using NUnit.Framework; +using osu.Framework.Testing; +using osu.Game.Beatmaps; +using osu.Game.Screens.Select; +using osu.Game.Screens.Select.Filter; +using osu.Game.Screens.SelectV2; + +namespace osu.Game.Tests.Visual.SongSelect +{ + [TestFixture] + public partial class TestSceneBeatmapCarouselV2ArtistGrouping : BeatmapCarouselV2TestScene + { + [SetUpSteps] + public void SetUpSteps() + { + RemoveAllBeatmaps(); + CreateCarousel(); + SortBy(new FilterCriteria { Group = GroupMode.Artist, Sort = SortMode.Artist }); + + AddBeatmaps(10, 3, true); + WaitForDrawablePanels(); + } + + [Test] + public void TestOpenCloseGroupWithNoSelectionMouse() + { + AddAssert("no beatmaps visible", () => Carousel.ChildrenOfType().Count(p => p.Alpha > 0), () => Is.Zero); + AddUntilStep("no sets visible", () => Carousel.ChildrenOfType().Count(p => p.Alpha > 0), () => Is.Zero); + CheckNoSelection(); + + ClickVisiblePanel(0); + + AddUntilStep("some sets visible", () => Carousel.ChildrenOfType().Count(p => p.Alpha > 0), () => Is.GreaterThan(0)); + AddUntilStep("no beatmaps visible", () => Carousel.ChildrenOfType().Count(p => p.Alpha > 0), () => Is.Zero); + CheckNoSelection(); + + ClickVisiblePanel(0); + + AddUntilStep("no sets visible", () => Carousel.ChildrenOfType().Count(p => p.Alpha > 0), () => Is.Zero); + AddUntilStep("no beatmaps visible", () => Carousel.ChildrenOfType().Count(p => p.Alpha > 0), () => Is.Zero); + CheckNoSelection(); + } + + [Test] + public void TestOpenCloseGroupWithNoSelectionKeyboard() + { + AddAssert("no beatmaps visible", () => Carousel.ChildrenOfType().Count(p => p.Alpha > 0), () => Is.Zero); + AddUntilStep("no sets visible", () => Carousel.ChildrenOfType().Count(p => p.Alpha > 0), () => Is.Zero); + CheckNoSelection(); + + SelectNextPanel(); + Select(); + + AddUntilStep("some sets visible", () => Carousel.ChildrenOfType().Count(p => p.Alpha > 0), () => Is.GreaterThan(0)); + AddUntilStep("no beatmaps visible", () => Carousel.ChildrenOfType().Count(p => p.Alpha > 0), () => Is.Zero); + AddAssert("keyboard selected is expanded", () => getKeyboardSelectedPanel()?.Expanded.Value, () => Is.True); + CheckNoSelection(); + + Select(); + + AddUntilStep("no sets visible", () => Carousel.ChildrenOfType().Count(p => p.Alpha > 0), () => Is.Zero); + AddUntilStep("no beatmaps visible", () => Carousel.ChildrenOfType().Count(p => p.Alpha > 0), () => Is.Zero); + AddAssert("keyboard selected is collapsed", () => getKeyboardSelectedPanel()?.Expanded.Value, () => Is.False); + CheckNoSelection(); + + GroupPanel? getKeyboardSelectedPanel() => Carousel.ChildrenOfType().SingleOrDefault(p => p.KeyboardSelected.Value); + } + + [Test] + public void TestCarouselRemembersSelection() + { + SelectNextGroup(); + + object? selection = null; + + AddStep("store drawable selection", () => selection = getSelectedPanel()?.Item?.Model); + + CheckHasSelection(); + AddAssert("drawable selection non-null", () => selection, () => Is.Not.Null); + AddAssert("drawable selection matches carousel selection", () => selection, () => Is.EqualTo(Carousel.CurrentSelection)); + + RemoveAllBeatmaps(); + AddUntilStep("no drawable selection", getSelectedPanel, () => Is.Null); + + AddBeatmaps(10); + WaitForDrawablePanels(); + + CheckHasSelection(); + AddAssert("no drawable selection", getSelectedPanel, () => Is.Null); + + AddStep("add previous selection", () => BeatmapSets.Add(((BeatmapInfo)selection!).BeatmapSet!)); + + AddAssert("selection matches original carousel selection", () => selection, () => Is.EqualTo(Carousel.CurrentSelection)); + AddUntilStep("drawable selection restored", () => getSelectedPanel()?.Item?.Model, () => Is.EqualTo(selection)); + AddAssert("carousel item is visible", () => getSelectedPanel()?.Item?.IsVisible, () => Is.True); + + ClickVisiblePanel(0); + AddUntilStep("carousel item not visible", getSelectedPanel, () => Is.Null); + + ClickVisiblePanel(0); + AddUntilStep("carousel item is visible", () => getSelectedPanel()?.Item?.IsVisible, () => Is.True); + + BeatmapPanel? getSelectedPanel() => Carousel.ChildrenOfType().SingleOrDefault(p => p.Selected.Value); + } + + [Test] + public void TestGroupSelectionOnHeader() + { + SelectNextGroup(); + WaitForGroupSelection(0, 1); + + SelectPrevPanel(); + SelectPrevGroup(); + WaitForGroupSelection(4, 5); + } + + [Test] + public void TestKeyboardSelection() + { + SelectNextPanel(); + SelectNextPanel(); + SelectNextPanel(); + SelectNextPanel(); + CheckNoSelection(); + + // open first group + Select(); + CheckNoSelection(); + AddUntilStep("some beatmaps visible", () => Carousel.ChildrenOfType().Count(p => p.Alpha > 0), () => Is.GreaterThan(0)); + + SelectNextPanel(); + Select(); + WaitForGroupSelection(3, 1); + + SelectNextGroup(); + WaitForGroupSelection(3, 5); + + SelectNextGroup(); + WaitForGroupSelection(4, 1); + + SelectPrevGroup(); + WaitForGroupSelection(3, 5); + + SelectNextGroup(); + WaitForGroupSelection(4, 1); + + SelectNextGroup(); + WaitForGroupSelection(4, 5); + + SelectNextGroup(); + WaitForGroupSelection(0, 1); + + SelectNextPanel(); + SelectNextPanel(); + SelectNextPanel(); + SelectNextPanel(); + + SelectNextGroup(); + WaitForGroupSelection(0, 1); + + SelectNextPanel(); + SelectNextGroup(); + WaitForGroupSelection(1, 1); + } + } +} From 4026ca84f887979555d32484f32ec8f20f178c7d Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 6 Feb 2025 15:41:57 +0900 Subject: [PATCH 0930/1275] Move selected retrieval functions to base class --- .../SongSelect/BeatmapCarouselV2TestScene.cs | 3 +++ ...estSceneBeatmapCarouselV2ArtistGrouping.cs | 22 +++++++---------- ...ceneBeatmapCarouselV2DifficultyGrouping.cs | 24 ++++++++----------- .../TestSceneBeatmapCarouselV2NoGrouping.cs | 14 ++++------- 4 files changed, 27 insertions(+), 36 deletions(-) diff --git a/osu.Game.Tests/Visual/SongSelect/BeatmapCarouselV2TestScene.cs b/osu.Game.Tests/Visual/SongSelect/BeatmapCarouselV2TestScene.cs index 36226a13cc..5ace306c7d 100644 --- a/osu.Game.Tests/Visual/SongSelect/BeatmapCarouselV2TestScene.cs +++ b/osu.Game.Tests/Visual/SongSelect/BeatmapCarouselV2TestScene.cs @@ -136,6 +136,9 @@ namespace osu.Game.Tests.Visual.SongSelect protected void CheckNoSelection() => AddAssert("has no selection", () => Carousel.CurrentSelection, () => Is.Null); protected void CheckHasSelection() => AddAssert("has selection", () => Carousel.CurrentSelection, () => Is.Not.Null); + protected BeatmapPanel? GetSelectedPanel() => Carousel.ChildrenOfType().SingleOrDefault(p => p.Selected.Value); + protected GroupPanel? GetKeyboardSelectedPanel() => Carousel.ChildrenOfType().SingleOrDefault(p => p.KeyboardSelected.Value); + protected void WaitForGroupSelection(int group, int panel) { AddUntilStep($"selected is group{group} panel{panel}", () => diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2ArtistGrouping.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2ArtistGrouping.cs index c7ab9de5e5..3c518fc7a6 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2ArtistGrouping.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2ArtistGrouping.cs @@ -57,17 +57,15 @@ namespace osu.Game.Tests.Visual.SongSelect AddUntilStep("some sets visible", () => Carousel.ChildrenOfType().Count(p => p.Alpha > 0), () => Is.GreaterThan(0)); AddUntilStep("no beatmaps visible", () => Carousel.ChildrenOfType().Count(p => p.Alpha > 0), () => Is.Zero); - AddAssert("keyboard selected is expanded", () => getKeyboardSelectedPanel()?.Expanded.Value, () => Is.True); + AddAssert("keyboard selected is expanded", () => GetKeyboardSelectedPanel()?.Expanded.Value, () => Is.True); CheckNoSelection(); Select(); AddUntilStep("no sets visible", () => Carousel.ChildrenOfType().Count(p => p.Alpha > 0), () => Is.Zero); AddUntilStep("no beatmaps visible", () => Carousel.ChildrenOfType().Count(p => p.Alpha > 0), () => Is.Zero); - AddAssert("keyboard selected is collapsed", () => getKeyboardSelectedPanel()?.Expanded.Value, () => Is.False); + AddAssert("keyboard selected is collapsed", () => GetKeyboardSelectedPanel()?.Expanded.Value, () => Is.False); CheckNoSelection(); - - GroupPanel? getKeyboardSelectedPanel() => Carousel.ChildrenOfType().SingleOrDefault(p => p.KeyboardSelected.Value); } [Test] @@ -77,34 +75,32 @@ namespace osu.Game.Tests.Visual.SongSelect object? selection = null; - AddStep("store drawable selection", () => selection = getSelectedPanel()?.Item?.Model); + AddStep("store drawable selection", () => selection = GetSelectedPanel()?.Item?.Model); CheckHasSelection(); AddAssert("drawable selection non-null", () => selection, () => Is.Not.Null); AddAssert("drawable selection matches carousel selection", () => selection, () => Is.EqualTo(Carousel.CurrentSelection)); RemoveAllBeatmaps(); - AddUntilStep("no drawable selection", getSelectedPanel, () => Is.Null); + AddUntilStep("no drawable selection", GetSelectedPanel, () => Is.Null); AddBeatmaps(10); WaitForDrawablePanels(); CheckHasSelection(); - AddAssert("no drawable selection", getSelectedPanel, () => Is.Null); + AddAssert("no drawable selection", GetSelectedPanel, () => Is.Null); AddStep("add previous selection", () => BeatmapSets.Add(((BeatmapInfo)selection!).BeatmapSet!)); AddAssert("selection matches original carousel selection", () => selection, () => Is.EqualTo(Carousel.CurrentSelection)); - AddUntilStep("drawable selection restored", () => getSelectedPanel()?.Item?.Model, () => Is.EqualTo(selection)); - AddAssert("carousel item is visible", () => getSelectedPanel()?.Item?.IsVisible, () => Is.True); + AddUntilStep("drawable selection restored", () => GetSelectedPanel()?.Item?.Model, () => Is.EqualTo(selection)); + AddAssert("carousel item is visible", () => GetSelectedPanel()?.Item?.IsVisible, () => Is.True); ClickVisiblePanel(0); - AddUntilStep("carousel item not visible", getSelectedPanel, () => Is.Null); + AddUntilStep("carousel item not visible", GetSelectedPanel, () => Is.Null); ClickVisiblePanel(0); - AddUntilStep("carousel item is visible", () => getSelectedPanel()?.Item?.IsVisible, () => Is.True); - - BeatmapPanel? getSelectedPanel() => Carousel.ChildrenOfType().SingleOrDefault(p => p.Selected.Value); + AddUntilStep("carousel item is visible", () => GetSelectedPanel()?.Item?.IsVisible, () => Is.True); } [Test] diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2DifficultyGrouping.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2DifficultyGrouping.cs index e861d8bf30..da3ef75487 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2DifficultyGrouping.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2DifficultyGrouping.cs @@ -49,15 +49,13 @@ namespace osu.Game.Tests.Visual.SongSelect SelectNextPanel(); Select(); AddUntilStep("some beatmaps visible", () => Carousel.ChildrenOfType().Count(p => p.Alpha > 0), () => Is.GreaterThan(0)); - AddAssert("keyboard selected is expanded", () => getKeyboardSelectedPanel()?.Expanded.Value, () => Is.True); + AddAssert("keyboard selected is expanded", () => GetKeyboardSelectedPanel()?.Expanded.Value, () => Is.True); CheckNoSelection(); Select(); AddUntilStep("no beatmaps visible", () => Carousel.ChildrenOfType().Count(p => p.Alpha > 0), () => Is.Zero); - AddAssert("keyboard selected is collapsed", () => getKeyboardSelectedPanel()?.Expanded.Value, () => Is.False); + AddAssert("keyboard selected is collapsed", () => GetKeyboardSelectedPanel()?.Expanded.Value, () => Is.False); CheckNoSelection(); - - GroupPanel? getKeyboardSelectedPanel() => Carousel.ChildrenOfType().SingleOrDefault(p => p.KeyboardSelected.Value); } [Test] @@ -67,34 +65,32 @@ namespace osu.Game.Tests.Visual.SongSelect object? selection = null; - AddStep("store drawable selection", () => selection = getSelectedPanel()?.Item?.Model); + AddStep("store drawable selection", () => selection = GetSelectedPanel()?.Item?.Model); CheckHasSelection(); AddAssert("drawable selection non-null", () => selection, () => Is.Not.Null); AddAssert("drawable selection matches carousel selection", () => selection, () => Is.EqualTo(Carousel.CurrentSelection)); RemoveAllBeatmaps(); - AddUntilStep("no drawable selection", getSelectedPanel, () => Is.Null); + AddUntilStep("no drawable selection", GetSelectedPanel, () => Is.Null); AddBeatmaps(10); WaitForDrawablePanels(); CheckHasSelection(); - AddAssert("no drawable selection", getSelectedPanel, () => Is.Null); + AddAssert("no drawable selection", GetSelectedPanel, () => Is.Null); AddStep("add previous selection", () => BeatmapSets.Add(((BeatmapInfo)selection!).BeatmapSet!)); AddAssert("selection matches original carousel selection", () => selection, () => Is.EqualTo(Carousel.CurrentSelection)); - AddUntilStep("drawable selection restored", () => getSelectedPanel()?.Item?.Model, () => Is.EqualTo(selection)); - AddAssert("carousel item is visible", () => getSelectedPanel()?.Item?.IsVisible, () => Is.True); + AddUntilStep("drawable selection restored", () => GetSelectedPanel()?.Item?.Model, () => Is.EqualTo(selection)); + AddAssert("carousel item is visible", () => GetSelectedPanel()?.Item?.IsVisible, () => Is.True); ClickVisiblePanel(0); - AddUntilStep("carousel item not visible", getSelectedPanel, () => Is.Null); + AddUntilStep("carousel item not visible", GetSelectedPanel, () => Is.Null); ClickVisiblePanel(0); - AddUntilStep("carousel item is visible", () => getSelectedPanel()?.Item?.IsVisible, () => Is.True); - - BeatmapPanel? getSelectedPanel() => Carousel.ChildrenOfType().SingleOrDefault(p => p.Selected.Value); + AddUntilStep("carousel item is visible", () => GetSelectedPanel()?.Item?.IsVisible, () => Is.True); } [Test] @@ -105,7 +101,7 @@ namespace osu.Game.Tests.Visual.SongSelect SelectPrevPanel(); SelectPrevGroup(); - WaitForGroupSelection(2, 9); + WaitForGroupSelection(0, 0); } [Test] diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2NoGrouping.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2NoGrouping.cs index 82f35af0ec..56bc7790bf 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2NoGrouping.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2NoGrouping.cs @@ -1,13 +1,11 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using System.Linq; using NUnit.Framework; using osu.Framework.Testing; using osu.Game.Beatmaps; using osu.Game.Screens.Select; using osu.Game.Screens.Select.Filter; -using osu.Game.Screens.SelectV2; using osuTK.Input; namespace osu.Game.Tests.Visual.SongSelect @@ -87,28 +85,26 @@ namespace osu.Game.Tests.Visual.SongSelect object? selection = null; - AddStep("store drawable selection", () => selection = getSelectedPanel()?.Item?.Model); + AddStep("store drawable selection", () => selection = GetSelectedPanel()?.Item?.Model); CheckHasSelection(); AddAssert("drawable selection non-null", () => selection, () => Is.Not.Null); AddAssert("drawable selection matches carousel selection", () => selection, () => Is.EqualTo(Carousel.CurrentSelection)); RemoveAllBeatmaps(); - AddUntilStep("no drawable selection", getSelectedPanel, () => Is.Null); + AddUntilStep("no drawable selection", GetSelectedPanel, () => Is.Null); AddBeatmaps(10); WaitForDrawablePanels(); CheckHasSelection(); - AddAssert("no drawable selection", getSelectedPanel, () => Is.Null); + AddAssert("no drawable selection", GetSelectedPanel, () => Is.Null); AddStep("add previous selection", () => BeatmapSets.Add(((BeatmapInfo)selection!).BeatmapSet!)); AddAssert("selection matches original carousel selection", () => selection, () => Is.EqualTo(Carousel.CurrentSelection)); - AddUntilStep("drawable selection restored", () => getSelectedPanel()?.Item?.Model, () => Is.EqualTo(selection)); - AddAssert("carousel item is visible", () => getSelectedPanel()?.Item?.IsVisible, () => Is.True); - - BeatmapPanel? getSelectedPanel() => Carousel.ChildrenOfType().SingleOrDefault(p => p.Selected.Value); + AddUntilStep("drawable selection restored", () => GetSelectedPanel()?.Item?.Model, () => Is.EqualTo(selection)); + AddAssert("carousel item is visible", () => GetSelectedPanel()?.Item?.IsVisible, () => Is.True); } [Test] From 024fbde0fd723a721eba48279085e0539bec0dde Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 6 Feb 2025 16:21:18 +0900 Subject: [PATCH 0931/1275] Refactor selection and activation handling MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit I had a bit of a struggle getting header traversal logic to work well. The constraints I had in place were a bit weird: - Group panels should toggle or potentially fall into the prev/next group - Set panels should just traverse around them The current method of using `CheckValidForGroupSelection` return type for traversal did not mesh with the above two cases. Just trust me on this one since it's quite hard to explain in words. After some re-thinking, I've gone with a simpler approach with one important change to UX: Now when group traversing with a beatmap set header currently keyboard focused, the first operation will be to reset keyboard selection to the selected beatmap, rather than traverse. I find this non-offensive – at most it means a user will need to press their group traversal key one extra time. I've also changed group headers to always toggle expansion when doing group traversal with them selected. To make all this work, the meaning of `Activation` has changed somewhat. It is now the primary path for carousel implementations to change selection of an item. It is what the `Drawable` panels call when they are clicked. Selection changes are not performed implicitly by `Carousel` – an implementation should decide when it actually wants to change the selection, usually in `HandleItemActivated`. Having less things mutating `CurrentSelection` is better in my eyes, as we see this variable as only being mutated internally when utmost required (ie the user has requested the change). With this change, `CurrentSelection` can no longer become of a non-`T` type (in the beatmap carousel implementation at least). This might pave a path forward for making `CurrentSelection` typed, but that comes with a few other concerns so I'll look at that as a follow-up. --- ...estSceneBeatmapCarouselV2ArtistGrouping.cs | 13 ++- ...ceneBeatmapCarouselV2DifficultyGrouping.cs | 9 ++ .../TestSceneBeatmapCarouselV2NoGrouping.cs | 6 +- .../TestSceneBeatmapCarouselV2Scrolling.cs | 4 +- osu.Game/Screens/SelectV2/BeatmapCarousel.cs | 33 ++++--- osu.Game/Screens/SelectV2/BeatmapPanel.cs | 8 +- osu.Game/Screens/SelectV2/BeatmapSetPanel.cs | 5 +- osu.Game/Screens/SelectV2/Carousel.cs | 98 +++++++++---------- osu.Game/Screens/SelectV2/GroupPanel.cs | 5 +- 9 files changed, 98 insertions(+), 83 deletions(-) diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2ArtistGrouping.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2ArtistGrouping.cs index 3c518fc7a6..d3eeee151a 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2ArtistGrouping.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2ArtistGrouping.cs @@ -110,8 +110,19 @@ namespace osu.Game.Tests.Visual.SongSelect WaitForGroupSelection(0, 1); SelectPrevPanel(); + SelectPrevPanel(); + + AddAssert("keyboard selected panel is expanded", () => GetKeyboardSelectedPanel()?.Expanded.Value, () => Is.True); + SelectPrevGroup(); - WaitForGroupSelection(4, 5); + + WaitForGroupSelection(0, 1); + AddAssert("keyboard selected panel is contracted", () => GetKeyboardSelectedPanel()?.Expanded.Value, () => Is.False); + + SelectPrevGroup(); + + WaitForGroupSelection(0, 1); + AddAssert("keyboard selected panel is expanded", () => GetKeyboardSelectedPanel()?.Expanded.Value, () => Is.True); } [Test] diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2DifficultyGrouping.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2DifficultyGrouping.cs index da3ef75487..151f1f5fec 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2DifficultyGrouping.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2DifficultyGrouping.cs @@ -100,8 +100,17 @@ namespace osu.Game.Tests.Visual.SongSelect WaitForGroupSelection(0, 0); SelectPrevPanel(); + AddAssert("keyboard selected panel is expanded", () => GetKeyboardSelectedPanel()?.Expanded.Value, () => Is.True); + SelectPrevGroup(); + WaitForGroupSelection(0, 0); + AddAssert("keyboard selected panel is contracted", () => GetKeyboardSelectedPanel()?.Expanded.Value, () => Is.False); + + SelectPrevGroup(); + + WaitForGroupSelection(0, 0); + AddAssert("keyboard selected panel is expanded", () => GetKeyboardSelectedPanel()?.Expanded.Value, () => Is.True); } [Test] diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2NoGrouping.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2NoGrouping.cs index 56bc7790bf..34bdd1265d 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2NoGrouping.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2NoGrouping.cs @@ -147,7 +147,11 @@ namespace osu.Game.Tests.Visual.SongSelect SelectPrevPanel(); SelectPrevGroup(); - WaitForSelection(0, 0); + WaitForSelection(1, 0); + + SelectPrevPanel(); + SelectNextGroup(); + WaitForSelection(1, 0); } [Test] diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2Scrolling.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2Scrolling.cs index 1d5d8e2a2d..ee6c11595a 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2Scrolling.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2Scrolling.cs @@ -29,7 +29,7 @@ namespace osu.Game.Tests.Visual.SongSelect { Quad positionBefore = default; - AddStep("select middle beatmap", () => Carousel.CurrentSelection = BeatmapSets.ElementAt(BeatmapSets.Count - 2)); + AddStep("select middle beatmap", () => Carousel.CurrentSelection = BeatmapSets.ElementAt(BeatmapSets.Count - 2).Beatmaps.First()); AddStep("scroll to selected item", () => Scroll.ScrollTo(Scroll.ChildrenOfType().Single(p => p.Selected.Value))); WaitForScrolling(); @@ -50,7 +50,7 @@ namespace osu.Game.Tests.Visual.SongSelect AddStep("scroll to last item", () => Scroll.ScrollToEnd(false)); - AddStep("select last beatmap", () => Carousel.CurrentSelection = BeatmapSets.Last()); + AddStep("select last beatmap", () => Carousel.CurrentSelection = BeatmapSets.Last().Beatmaps.Last()); WaitForScrolling(); diff --git a/osu.Game/Screens/SelectV2/BeatmapCarousel.cs b/osu.Game/Screens/SelectV2/BeatmapCarousel.cs index 36e57c9067..6032989ad0 100644 --- a/osu.Game/Screens/SelectV2/BeatmapCarousel.cs +++ b/osu.Game/Screens/SelectV2/BeatmapCarousel.cs @@ -95,11 +95,9 @@ namespace osu.Game.Screens.SelectV2 private GroupDefinition? lastSelectedGroup; private BeatmapInfo? lastSelectedBeatmap; - protected override bool HandleItemSelected(object? model) + protected override void HandleItemActivated(CarouselItem item) { - base.HandleItemSelected(model); - - switch (model) + switch (item.Model) { case GroupDefinition group: // Special case – collapsing an open group. @@ -107,16 +105,32 @@ namespace osu.Game.Screens.SelectV2 { setExpansionStateOfGroup(lastSelectedGroup, false); lastSelectedGroup = null; - return false; + return; } setExpandedGroup(group); - return false; + return; case BeatmapSetInfo setInfo: // Selecting a set isn't valid – let's re-select the first difficulty. CurrentSelection = setInfo.Beatmaps.First(); - return false; + return; + + case BeatmapInfo beatmapInfo: + CurrentSelection = beatmapInfo; + return; + } + } + + protected override void HandleItemSelected(object? model) + { + base.HandleItemSelected(model); + + switch (model) + { + case BeatmapSetInfo: + case GroupDefinition: + throw new InvalidOperationException("Groups should never become selected"); case BeatmapInfo beatmapInfo: // Find any containing group. There should never be too many groups so iterating is efficient enough. @@ -125,11 +139,8 @@ namespace osu.Game.Screens.SelectV2 if (containingGroup != null) setExpandedGroup(containingGroup); setExpandedSet(beatmapInfo); - - return true; + break; } - - return true; } protected override bool CheckValidForGroupSelection(CarouselItem item) diff --git a/osu.Game/Screens/SelectV2/BeatmapPanel.cs b/osu.Game/Screens/SelectV2/BeatmapPanel.cs index 3edfd4203b..9280e1c2c1 100644 --- a/osu.Game/Screens/SelectV2/BeatmapPanel.cs +++ b/osu.Game/Screens/SelectV2/BeatmapPanel.cs @@ -86,13 +86,7 @@ namespace osu.Game.Screens.SelectV2 protected override bool OnClick(ClickEvent e) { - if (carousel.CurrentSelection != Item!.Model) - { - carousel.CurrentSelection = Item!.Model; - return true; - } - - carousel.TryActivateSelection(); + carousel.Activate(Item!); return true; } diff --git a/osu.Game/Screens/SelectV2/BeatmapSetPanel.cs b/osu.Game/Screens/SelectV2/BeatmapSetPanel.cs index 79ffe0f68a..f6c9324077 100644 --- a/osu.Game/Screens/SelectV2/BeatmapSetPanel.cs +++ b/osu.Game/Screens/SelectV2/BeatmapSetPanel.cs @@ -1,7 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using System; using System.Diagnostics; using osu.Framework.Allocation; using osu.Framework.Bindables; @@ -83,7 +82,7 @@ namespace osu.Game.Screens.SelectV2 protected override bool OnClick(ClickEvent e) { - carousel.CurrentSelection = Item!.Model; + carousel.Activate(Item!); return true; } @@ -98,8 +97,6 @@ namespace osu.Game.Screens.SelectV2 public void Activated() { - // sets should never be activated. - throw new InvalidOperationException(); } #endregion diff --git a/osu.Game/Screens/SelectV2/Carousel.cs b/osu.Game/Screens/SelectV2/Carousel.cs index 6b7b1f3a9b..603a792847 100644 --- a/osu.Game/Screens/SelectV2/Carousel.cs +++ b/osu.Game/Screens/SelectV2/Carousel.cs @@ -94,26 +94,39 @@ namespace osu.Game.Screens.SelectV2 public object? CurrentSelection { get => currentSelection.Model; - set => setSelection(value); + set + { + if (currentSelection.Model != value) + { + HandleItemSelected(value); + + if (currentSelection.Model != null) + HandleItemDeselected(currentSelection.Model); + + currentKeyboardSelection = new Selection(value); + currentSelection = currentKeyboardSelection; + selectionValid.Invalidate(); + } + else if (currentKeyboardSelection.Model != value) + { + // Even if the current selection matches, let's ensure the keyboard selection is reset + // to the newly selected object. This matches user expectations (for now). + currentKeyboardSelection = currentSelection; + selectionValid.Invalidate(); + } + } } /// - /// Activate the current selection, if a selection exists and matches keyboard selection. - /// If keyboard selection does not match selection, this will transfer the selection on first invocation. + /// Activate the specified item. /// - public void TryActivateSelection() + /// + public void Activate(CarouselItem item) { - if (currentSelection.CarouselItem != currentKeyboardSelection.CarouselItem) - { - CurrentSelection = currentKeyboardSelection.Model; - return; - } + (GetMaterialisedDrawableForItem(item) as ICarouselPanel)?.Activated(); + HandleItemActivated(item); - if (currentSelection.CarouselItem != null) - { - (GetMaterialisedDrawableForItem(currentSelection.CarouselItem) as ICarouselPanel)?.Activated(); - HandleItemActivated(currentSelection.CarouselItem); - } + selectionValid.Invalidate(); } #endregion @@ -176,30 +189,28 @@ namespace osu.Game.Screens.SelectV2 protected virtual bool CheckValidForGroupSelection(CarouselItem item) => true; /// - /// Called when an item is "selected". + /// Called after an item becomes the . + /// Should be used to handle any group expansion, item visibility changes, etc. /// - /// Whether the item should be selected. - protected virtual bool HandleItemSelected(object? model) => true; + protected virtual void HandleItemSelected(object? model) { } /// - /// Called when an item is "deselected". + /// Called when the changes to a new selection. + /// Should be used to handle any group expansion, item visibility changes, etc. /// - protected virtual void HandleItemDeselected(object? model) - { - } + protected virtual void HandleItemDeselected(object? model) { } /// - /// Called when an item is "activated". + /// Called when an item is activated via user input (keyboard traversal or a mouse click). /// /// - /// An activated item should for instance: - /// - Open or close a folder - /// - Start gameplay on a beatmap difficulty. + /// An activated item should decide to perform an action, such as: + /// - Change its expanded state (and show / hide children items). + /// - Set the item to the . + /// - Start gameplay on a beatmap difficulty if already selected. /// /// The carousel item which was activated. - protected virtual void HandleItemActivated(CarouselItem item) - { - } + protected virtual void HandleItemActivated(CarouselItem item) { } #endregion @@ -305,7 +316,8 @@ namespace osu.Game.Screens.SelectV2 switch (e.Action) { case GlobalAction.Select: - TryActivateSelection(); + if (currentKeyboardSelection.CarouselItem != null) + Activate(currentKeyboardSelection.CarouselItem); return true; case GlobalAction.SelectNext: @@ -374,13 +386,10 @@ namespace osu.Game.Screens.SelectV2 // If the user has a different keyboard selection and requests // group selection, first transfer the keyboard selection to actual selection. - if (currentSelection.CarouselItem != currentKeyboardSelection.CarouselItem) + if (currentKeyboardSelection.CarouselItem != null && currentSelection.CarouselItem != currentKeyboardSelection.CarouselItem) { - TryActivateSelection(); - - // Is the selection actually changed, then we should not perform any further traversal. - if (currentSelection.CarouselItem == currentKeyboardSelection.CarouselItem) - return; + Activate(currentKeyboardSelection.CarouselItem); + return; } int originalIndex; @@ -413,7 +422,7 @@ namespace osu.Game.Screens.SelectV2 if (CheckValidForGroupSelection(newItem)) { - setSelection(newItem.Model); + HandleItemActivated(newItem); return; } } while (newIndex != originalIndex); @@ -428,23 +437,6 @@ namespace osu.Game.Screens.SelectV2 private Selection currentKeyboardSelection = new Selection(); private Selection currentSelection = new Selection(); - private void setSelection(object? model) - { - if (currentSelection.Model == model) - return; - - if (HandleItemSelected(model)) - { - if (currentSelection.Model != null) - HandleItemDeselected(currentSelection.Model); - - currentKeyboardSelection = new Selection(model); - currentSelection = currentKeyboardSelection; - } - - selectionValid.Invalidate(); - } - private void setKeyboardSelection(object? model) { currentKeyboardSelection = new Selection(model); diff --git a/osu.Game/Screens/SelectV2/GroupPanel.cs b/osu.Game/Screens/SelectV2/GroupPanel.cs index 7ed256ca6a..e10521f63e 100644 --- a/osu.Game/Screens/SelectV2/GroupPanel.cs +++ b/osu.Game/Screens/SelectV2/GroupPanel.cs @@ -1,7 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using System; using System.Diagnostics; using osu.Framework.Allocation; using osu.Framework.Bindables; @@ -96,7 +95,7 @@ namespace osu.Game.Screens.SelectV2 protected override bool OnClick(ClickEvent e) { - carousel.CurrentSelection = Item!.Model; + carousel.Activate(Item!); return true; } @@ -111,8 +110,6 @@ namespace osu.Game.Screens.SelectV2 public void Activated() { - // sets should never be activated. - throw new InvalidOperationException(); } #endregion From 05a9160884a6426159539c9b9b7b326156cbeabd Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Thu, 6 Feb 2025 03:10:21 -0500 Subject: [PATCH 0932/1275] Simplify LINQ expressions to appease CI don't ask me --- .../SongSelectV2/TestSceneBeatmapCarouselV2DifficultyPanel.cs | 4 ++-- .../Visual/SongSelectV2/TestSceneBeatmapCarouselV2SetPanel.cs | 2 +- .../SongSelectV2/TestSceneBeatmapCarouselV2StandalonePanel.cs | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselV2DifficultyPanel.cs b/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselV2DifficultyPanel.cs index 93472e7b81..f843c2cded 100644 --- a/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselV2DifficultyPanel.cs +++ b/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselV2DifficultyPanel.cs @@ -51,9 +51,9 @@ namespace osu.Game.Tests.Visual.SongSelectV2 { AddStep("random beatmap", () => { - var randomSet = beatmaps.GetAllUsableBeatmapSets().OrderBy(_ => RNG.Next()).FirstOrDefault(); + var randomSet = beatmaps.GetAllUsableBeatmapSets().MinBy(_ => RNG.Next()); randomSet ??= TestResources.CreateTestBeatmapSetInfo(); - beatmap = randomSet.Beatmaps.OrderBy(_ => RNG.Next()).First(); + beatmap = randomSet.Beatmaps.MinBy(_ => RNG.Next())!; CreateThemedContent(OverlayColourScheme.Aquamarine); }); diff --git a/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselV2SetPanel.cs b/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselV2SetPanel.cs index 540eae3be0..382357b67e 100644 --- a/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselV2SetPanel.cs +++ b/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselV2SetPanel.cs @@ -47,7 +47,7 @@ namespace osu.Game.Tests.Visual.SongSelectV2 { AddStep("random beatmap", () => { - var randomSet = beatmaps.GetAllUsableBeatmapSets().OrderBy(_ => RNG.Next()).FirstOrDefault(); + var randomSet = beatmaps.GetAllUsableBeatmapSets().MinBy(_ => RNG.Next()); randomSet ??= TestResources.CreateTestBeatmapSetInfo(); beatmapSet = randomSet; diff --git a/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselV2StandalonePanel.cs b/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselV2StandalonePanel.cs index 72f7a9e98c..41eb5c3683 100644 --- a/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselV2StandalonePanel.cs +++ b/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselV2StandalonePanel.cs @@ -51,9 +51,9 @@ namespace osu.Game.Tests.Visual.SongSelectV2 { AddStep("random beatmap", () => { - var randomSet = beatmaps.GetAllUsableBeatmapSets().OrderBy(_ => RNG.Next()).FirstOrDefault(); + var randomSet = beatmaps.GetAllUsableBeatmapSets().MinBy(_ => RNG.Next()); randomSet ??= TestResources.CreateTestBeatmapSetInfo(); - beatmap = randomSet.Beatmaps.OrderBy(_ => RNG.Next()).First(); + beatmap = randomSet.Beatmaps.MinBy(_ => RNG.Next())!; CreateThemedContent(OverlayColourScheme.Aquamarine); }); From bff686f01289f68ec8b12de2bb62107ddb49d76a Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 6 Feb 2025 17:09:58 +0900 Subject: [PATCH 0933/1275] Avoid double iteration when updating group states --- osu.Game/Screens/SelectV2/BeatmapCarousel.cs | 47 +++++++++++++++----- 1 file changed, 35 insertions(+), 12 deletions(-) diff --git a/osu.Game/Screens/SelectV2/BeatmapCarousel.cs b/osu.Game/Screens/SelectV2/BeatmapCarousel.cs index 6032989ad0..4126889892 100644 --- a/osu.Game/Screens/SelectV2/BeatmapCarousel.cs +++ b/osu.Game/Screens/SelectV2/BeatmapCarousel.cs @@ -173,22 +173,45 @@ namespace osu.Game.Screens.SelectV2 { if (grouping.GroupItems.TryGetValue(group, out var items)) { - // First pass ignoring set groupings. - foreach (var i in items) - { - if (i.Model is GroupDefinition) - i.IsExpanded = expanded; - else - i.IsVisible = expanded; - } - - // Second pass to hide set children when not meant to be displayed. if (expanded) { foreach (var i in items) { - if (i.Model is BeatmapSetInfo set) - setExpansionStateOfSetItems(set, i.IsExpanded); + switch (i.Model) + { + case GroupDefinition: + i.IsExpanded = true; + break; + + case BeatmapSetInfo set: + // Case where there are set headers, header should be visible + // and items should use the set's expanded state. + i.IsVisible = true; + setExpansionStateOfSetItems(set, i.IsExpanded); + break; + + default: + // Case where there are no set headers, all items should be visible. + if (!grouping.BeatmapSetsGroupedTogether) + i.IsVisible = true; + break; + } + } + } + else + { + foreach (var i in items) + { + switch (i.Model) + { + case GroupDefinition: + i.IsExpanded = false; + break; + + default: + i.IsVisible = false; + break; + } } } } From cb42ef95c57cf6f86c66dd882962b1532401d823 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 6 Feb 2025 17:48:42 +0900 Subject: [PATCH 0934/1275] Add invalidation on draw size change in beatmap carousel v2 Matching old implementation. --- osu.Game/Screens/SelectV2/Carousel.cs | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/osu.Game/Screens/SelectV2/Carousel.cs b/osu.Game/Screens/SelectV2/Carousel.cs index 1fd2f0a9b0..4248641a43 100644 --- a/osu.Game/Screens/SelectV2/Carousel.cs +++ b/osu.Game/Screens/SelectV2/Carousel.cs @@ -16,6 +16,7 @@ using osu.Framework.Graphics.Cursor; using osu.Framework.Graphics.Pooling; using osu.Framework.Input.Bindings; using osu.Framework.Input.Events; +using osu.Framework.Layout; using osu.Framework.Logging; using osu.Framework.Utils; using osu.Game.Graphics.Containers; @@ -678,6 +679,15 @@ namespace osu.Game.Screens.SelectV2 carouselPanel.Expanded.Value = false; } + protected override bool OnInvalidate(Invalidation invalidation, InvalidationSource source) + { + // handles the vertical size of the carousel changing (ie. on window resize when aspect ratio has changed). + if (invalidation.HasFlag(Invalidation.DrawSize)) + selectionValid.Invalidate(); + + return base.OnInvalidate(invalidation, source); + } + #endregion #region Internal helper classes From b7483b9442596fa367105f62effe81addb8bd8ec Mon Sep 17 00:00:00 2001 From: Layendan Date: Thu, 6 Feb 2025 07:25:45 -0700 Subject: [PATCH 0935/1275] Add playlist collection button w/ tests --- .../TestSceneAddPlaylistToCollectionButton.cs | 94 +++++++++++++++++++ .../AddPlaylistToCollectionButton.cs | 78 +++++++++++++++ .../Playlists/PlaylistsRoomSubScreen.cs | 10 ++ 3 files changed, 182 insertions(+) create mode 100644 osu.Game.Tests/Visual/Playlists/TestSceneAddPlaylistToCollectionButton.cs create mode 100644 osu.Game/Screens/OnlinePlay/Playlists/AddPlaylistToCollectionButton.cs diff --git a/osu.Game.Tests/Visual/Playlists/TestSceneAddPlaylistToCollectionButton.cs b/osu.Game.Tests/Visual/Playlists/TestSceneAddPlaylistToCollectionButton.cs new file mode 100644 index 0000000000..acf2c4b3f9 --- /dev/null +++ b/osu.Game.Tests/Visual/Playlists/TestSceneAddPlaylistToCollectionButton.cs @@ -0,0 +1,94 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using System.Diagnostics; +using System.Linq; +using osu.Framework.Allocation; +using osu.Framework.Audio; +using osu.Framework.Graphics; +using osu.Framework.Platform; +using osu.Framework.Testing; +using osu.Game.Beatmaps; +using osu.Game.Database; +using osu.Game.Online.Rooms; +using osu.Game.Overlays; +using osu.Game.Rulesets; +using osu.Game.Rulesets.Osu; +using osu.Game.Screens.OnlinePlay.Playlists; +using osuTK; + +namespace osu.Game.Tests.Visual.Playlists +{ + public partial class TestSceneAddPlaylistToCollectionButton : OsuTestScene + { + private BeatmapManager manager = null!; + private BeatmapSetInfo importedBeatmap = null!; + private Room room = null!; + + [BackgroundDependencyLoader] + private void load(GameHost host, AudioManager audio) + { + Dependencies.Cache(new RealmRulesetStore(Realm)); + Dependencies.Cache(manager = new BeatmapManager(LocalStorage, Realm, API, audio, Resources, host, Beatmap.Default)); + Dependencies.Cache(Realm); + } + + [Cached(typeof(INotificationOverlay))] + private NotificationOverlay notificationOverlay = new NotificationOverlay + { + Anchor = Anchor.TopRight, + Origin = Anchor.TopRight, + }; + + [SetUpSteps] + public void SetUpSteps() + { + importBeatmap(); + + setupRoom(); + + AddStep("create button", () => + { + AddRange(new Drawable[] + { + notificationOverlay, + new AddPlaylistToCollectionButton(room) + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Size = new Vector2(300, 40), + } + }); + }); + } + + private void importBeatmap() => AddStep("import beatmap", () => + { + var beatmap = CreateBeatmap(new OsuRuleset().RulesetInfo); + + Debug.Assert(beatmap.BeatmapInfo.BeatmapSet != null); + + importedBeatmap = manager.Import(beatmap.BeatmapInfo.BeatmapSet)!.Value.Detach(); + }); + + private void setupRoom() => AddStep("setup room", () => + { + room = new Room + { + Name = "my awesome room", + MaxAttempts = 5, + Host = API.LocalUser.Value + }; + room.RecentParticipants = [room.Host]; + room.EndDate = DateTimeOffset.Now.AddMinutes(5); + room.Playlist = + [ + new PlaylistItem(importedBeatmap.Beatmaps.First()) + { + RulesetID = new OsuRuleset().RulesetInfo.OnlineID + } + ]; + }); + } +} diff --git a/osu.Game/Screens/OnlinePlay/Playlists/AddPlaylistToCollectionButton.cs b/osu.Game/Screens/OnlinePlay/Playlists/AddPlaylistToCollectionButton.cs new file mode 100644 index 0000000000..643e274335 --- /dev/null +++ b/osu.Game/Screens/OnlinePlay/Playlists/AddPlaylistToCollectionButton.cs @@ -0,0 +1,78 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Linq; +using System.Threading.Tasks; +using osu.Framework.Allocation; +using osu.Framework.Extensions; +using osu.Game.Collections; +using osu.Game.Database; +using osu.Game.Graphics; +using osu.Game.Graphics.UserInterfaceV2; +using osu.Game.Online.Rooms; +using osu.Game.Overlays; +using osu.Game.Overlays.Notifications; + +namespace osu.Game.Screens.OnlinePlay.Playlists +{ + public partial class AddPlaylistToCollectionButton : RoundedButton + { + private readonly Room room; + + [Resolved] + private RealmAccess realmAccess { get; set; } = null!; + + [Resolved] + private BeatmapLookupCache beatmapLookupCache { get; set; } = null!; + + [Resolved(canBeNull: true)] + private INotificationOverlay? notifications { get; set; } + + public AddPlaylistToCollectionButton(Room room) + { + this.room = room; + Text = "Add Maps to Collection"; + } + + [BackgroundDependencyLoader] + private void load(OsuColour colours) + { + BackgroundColour = colours.Gray5; + + Action = () => + { + int[] ids = room.Playlist.Select(item => item.Beatmap.OnlineID).Where(onlineId => onlineId > 0).ToArray(); + + if (ids.Length == 0) + { + notifications?.Post(new SimpleErrorNotification { Text = "Cannot add local beatmaps" }); + return; + } + + beatmapLookupCache.GetBeatmapsAsync(ids).ContinueWith(task => Schedule(() => + { + var beatmaps = task.GetResultSafely().Where(item => item?.BeatmapSet != null).ToList(); + + var collection = realmAccess.Realm.All().FirstOrDefault(c => c.Name == room.Name); + + if (collection == null) + { + collection = new BeatmapCollection(room.Name, beatmaps.Select(i => i!.MD5Hash).Distinct().ToList()); + realmAccess.Realm.Write(() => realmAccess.Realm.Add(collection)); + notifications?.Post(new SimpleNotification { Text = $"Created new playlist: {room.Name}" }); + } + else + { + collection.ToLive(realmAccess).PerformWrite(c => + { + beatmaps = beatmaps.Where(i => !c.BeatmapMD5Hashes.Contains(i!.MD5Hash)).ToList(); + foreach (var item in beatmaps) + c.BeatmapMD5Hashes.Add(item!.MD5Hash); + notifications?.Post(new SimpleNotification { Text = $"Updated playlist: {room.Name}" }); + }); + } + }), TaskContinuationOptions.OnlyOnRanToCompletion); + }; + } + } +} diff --git a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs index 9b4630ac0b..afab8a9721 100644 --- a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs @@ -153,11 +153,21 @@ namespace osu.Game.Screens.OnlinePlay.Playlists } } }, + new Drawable[] + { + new AddPlaylistToCollectionButton(Room) + { + Margin = new MarginPadding { Top = 5 }, + RelativeSizeAxes = Axes.X, + Size = new Vector2(1, 40) + } + } }, RowDimensions = new[] { new Dimension(GridSizeMode.AutoSize), new Dimension(), + new Dimension(GridSizeMode.AutoSize), } }, // Spacer From 6769a74c92937eead5628a4a3b0080059c2d2e85 Mon Sep 17 00:00:00 2001 From: Layendan Date: Thu, 6 Feb 2025 17:23:06 -0700 Subject: [PATCH 0936/1275] Add loading in case cache lookup takes longer than expected --- .../Playlists/AddPlaylistToCollectionButton.cs | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/osu.Game/Screens/OnlinePlay/Playlists/AddPlaylistToCollectionButton.cs b/osu.Game/Screens/OnlinePlay/Playlists/AddPlaylistToCollectionButton.cs index 643e274335..d28776cac2 100644 --- a/osu.Game/Screens/OnlinePlay/Playlists/AddPlaylistToCollectionButton.cs +++ b/osu.Game/Screens/OnlinePlay/Playlists/AddPlaylistToCollectionButton.cs @@ -8,6 +8,7 @@ using osu.Framework.Extensions; using osu.Game.Collections; using osu.Game.Database; using osu.Game.Graphics; +using osu.Game.Graphics.UserInterface; using osu.Game.Graphics.UserInterfaceV2; using osu.Game.Online.Rooms; using osu.Game.Overlays; @@ -19,6 +20,8 @@ namespace osu.Game.Screens.OnlinePlay.Playlists { private readonly Room room; + private LoadingLayer loading = null!; + [Resolved] private RealmAccess realmAccess { get; set; } = null!; @@ -39,6 +42,8 @@ namespace osu.Game.Screens.OnlinePlay.Playlists { BackgroundColour = colours.Gray5; + Add(loading = new LoadingLayer(true, false)); + Action = () => { int[] ids = room.Playlist.Select(item => item.Beatmap.OnlineID).Where(onlineId => onlineId > 0).ToArray(); @@ -49,6 +54,8 @@ namespace osu.Game.Screens.OnlinePlay.Playlists return; } + Enabled.Value = false; + loading.Show(); beatmapLookupCache.GetBeatmapsAsync(ids).ContinueWith(task => Schedule(() => { var beatmaps = task.GetResultSafely().Where(item => item?.BeatmapSet != null).ToList(); @@ -71,6 +78,9 @@ namespace osu.Game.Screens.OnlinePlay.Playlists notifications?.Post(new SimpleNotification { Text = $"Updated playlist: {room.Name}" }); }); } + + loading.Hide(); + Enabled.Value = true; }), TaskContinuationOptions.OnlyOnRanToCompletion); }; } From 2aa930a36c87d579c1cde09a11a56342f8ca960f Mon Sep 17 00:00:00 2001 From: Layendan Date: Thu, 6 Feb 2025 17:46:49 -0700 Subject: [PATCH 0937/1275] Corrected notification strings --- .../OnlinePlay/Playlists/AddPlaylistToCollectionButton.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/Playlists/AddPlaylistToCollectionButton.cs b/osu.Game/Screens/OnlinePlay/Playlists/AddPlaylistToCollectionButton.cs index d28776cac2..ab3e481f9f 100644 --- a/osu.Game/Screens/OnlinePlay/Playlists/AddPlaylistToCollectionButton.cs +++ b/osu.Game/Screens/OnlinePlay/Playlists/AddPlaylistToCollectionButton.cs @@ -66,7 +66,7 @@ namespace osu.Game.Screens.OnlinePlay.Playlists { collection = new BeatmapCollection(room.Name, beatmaps.Select(i => i!.MD5Hash).Distinct().ToList()); realmAccess.Realm.Write(() => realmAccess.Realm.Add(collection)); - notifications?.Post(new SimpleNotification { Text = $"Created new playlist: {room.Name}" }); + notifications?.Post(new SimpleNotification { Text = $"Created new collection: {room.Name}" }); } else { @@ -75,7 +75,7 @@ namespace osu.Game.Screens.OnlinePlay.Playlists beatmaps = beatmaps.Where(i => !c.BeatmapMD5Hashes.Contains(i!.MD5Hash)).ToList(); foreach (var item in beatmaps) c.BeatmapMD5Hashes.Add(item!.MD5Hash); - notifications?.Post(new SimpleNotification { Text = $"Updated playlist: {room.Name}" }); + notifications?.Post(new SimpleNotification { Text = $"Updated collection: {room.Name}" }); }); } From 4f6fd68a9195d170eeca5983e0a76d5e5fcc78b4 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 7 Feb 2025 13:54:35 +0900 Subject: [PATCH 0938/1275] Fix inspections --- .../Difficulty/Preprocessing/Rhythm/TaikoRhythmData.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/TaikoRhythmData.cs b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/TaikoRhythmData.cs index 6c4a332624..247fb06dc0 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/TaikoRhythmData.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/TaikoRhythmData.cs @@ -48,7 +48,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Rhythm } double actualRatio = current.DeltaTime / previous.DeltaTime; - double closestRatio = common_ratios.OrderBy(r => Math.Abs(r - actualRatio)).First(); + double closestRatio = common_ratios.MinBy(r => Math.Abs(r - actualRatio)); Ratio = closestRatio; } @@ -63,8 +63,8 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Rhythm /// speeding up is generally harder than slowing down (with exceptions of rhythm changes requiring a hand switch). /// /// - private static readonly double[] common_ratios = new[] - { + private static readonly double[] common_ratios = + [ 1.0 / 1, 2.0 / 1, 1.0 / 2, @@ -74,6 +74,6 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Rhythm 2.0 / 3, 5.0 / 4, 4.0 / 5 - }; + ]; } } From 25846b232748ae71b288fec35a43534512bdf5ed Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 7 Feb 2025 14:21:43 +0900 Subject: [PATCH 0939/1275] Adjust results screen designs and tests slightly --- .../Visual/Ranking/TestSceneResultsScreen.cs | 16 ++++++++++------ .../Contracted/ContractedPanelMiddleContent.cs | 6 +++--- 2 files changed, 13 insertions(+), 9 deletions(-) diff --git a/osu.Game.Tests/Visual/Ranking/TestSceneResultsScreen.cs b/osu.Game.Tests/Visual/Ranking/TestSceneResultsScreen.cs index fca1d0f82a..3a08756090 100644 --- a/osu.Game.Tests/Visual/Ranking/TestSceneResultsScreen.cs +++ b/osu.Game.Tests/Visual/Ranking/TestSceneResultsScreen.cs @@ -62,12 +62,6 @@ namespace osu.Game.Tests.Visual.Ranking if (beatmapInfo != null) Beatmap.Value = beatmaps.GetWorkingBeatmap(beatmapInfo); }); - - AddToggleStep("toggle legacy classic skin", v => - { - if (skins != null) - skins.CurrentSkinInfo.Value = v ? skins.DefaultClassicSkin.SkinInfo : skins.CurrentSkinInfo.Default; - }); } [SetUp] @@ -84,6 +78,16 @@ namespace osu.Game.Tests.Visual.Ranking })); } + [Test] + public void TestLegacySkin() + { + AddToggleStep("toggle legacy classic skin", v => + { + if (skins != null) + skins.CurrentSkinInfo.Value = v ? skins.DefaultClassicSkin.SkinInfo : skins.CurrentSkinInfo.Default; + }); + } + private int onlineScoreID = 1; [TestCase(1, ScoreRank.X, 0)] diff --git a/osu.Game/Screens/Ranking/Contracted/ContractedPanelMiddleContent.cs b/osu.Game/Screens/Ranking/Contracted/ContractedPanelMiddleContent.cs index cfb6465e62..2f863a95ec 100644 --- a/osu.Game/Screens/Ranking/Contracted/ContractedPanelMiddleContent.cs +++ b/osu.Game/Screens/Ranking/Contracted/ContractedPanelMiddleContent.cs @@ -67,7 +67,7 @@ namespace osu.Game.Screens.Ranking.Contracted Colour = Color4.Black.Opacity(0.25f), Type = EdgeEffectType.Shadow, Radius = 1, - Offset = new Vector2(0, 4) + Offset = new Vector2(0, 2) }, Children = new Drawable[] { @@ -100,10 +100,10 @@ namespace osu.Game.Screens.Ranking.Contracted CornerRadius = 20, EdgeEffect = new EdgeEffectParameters { - Colour = Color4.Black.Opacity(0.25f), + Colour = Color4.Black.Opacity(0.15f), Type = EdgeEffectType.Shadow, Radius = 8, - Offset = new Vector2(0, 4), + Offset = new Vector2(0, 1), } }, new OsuSpriteText From 975c35f5ac48735367ba792784d5572b69fe9a4c Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 7 Feb 2025 14:27:37 +0900 Subject: [PATCH 0940/1275] Also add difficulty icon to contracted panel --- .../ContractedPanelMiddleContent.cs | 31 ++++++++++++++++--- 1 file changed, 26 insertions(+), 5 deletions(-) diff --git a/osu.Game/Screens/Ranking/Contracted/ContractedPanelMiddleContent.cs b/osu.Game/Screens/Ranking/Contracted/ContractedPanelMiddleContent.cs index 2f863a95ec..e9d0bf3403 100644 --- a/osu.Game/Screens/Ranking/Contracted/ContractedPanelMiddleContent.cs +++ b/osu.Game/Screens/Ranking/Contracted/ContractedPanelMiddleContent.cs @@ -11,13 +11,15 @@ using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Effects; using osu.Framework.Graphics.Shapes; using osu.Framework.Localisation; +using osu.Game.Beatmaps.Drawables; using osu.Game.Graphics; using osu.Game.Graphics.Sprites; using osu.Game.Online.Leaderboards; using osu.Game.Resources.Localisation.Web; +using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Scoring; +using osu.Game.Rulesets.UI; using osu.Game.Scoring; -using osu.Game.Screens.Play.HUD; using osu.Game.Users; using osu.Game.Users.Drawables; using osu.Game.Utils; @@ -134,14 +136,33 @@ namespace osu.Game.Screens.Ranking.Contracted createStatistic(BeatmapsetsStrings.ShowScoreboardHeadersAccuracy, $"{score.Accuracy.FormatAccuracy()}"), } }, - new ModFlowDisplay + new FillFlowContainer { Anchor = Anchor.TopCentre, Origin = Anchor.TopCentre, - AutoSizeAxes = Axes.Y, RelativeSizeAxes = Axes.X, - Current = { Value = score.Mods }, - IconScale = 0.5f, + AutoSizeAxes = Axes.Y, + Direction = FillDirection.Full, + Spacing = new Vector2(3), + ChildrenEnumerable = + [ + new DifficultyIcon(score.BeatmapInfo!, score.Ruleset) + { + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre, + Size = new Vector2(20), + TooltipType = DifficultyIconTooltipType.Extended, + Margin = new MarginPadding { Right = 2 } + }, + .. + score.Mods.AsOrdered().Select(m => new ModIcon(m) + { + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre, + Scale = new Vector2(0.3f), + Margin = new MarginPadding { Top = -6 } + }) + ] } } } From d73f275143a7c36a8b629f15ea061678cba5ba1e Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 7 Feb 2025 15:15:58 +0900 Subject: [PATCH 0941/1275] Don't inflate set / group panels for simplicity --- osu.Game/Screens/SelectV2/BeatmapPanel.cs | 7 +++++-- osu.Game/Screens/SelectV2/BeatmapSetPanel.cs | 10 ---------- osu.Game/Screens/SelectV2/GroupPanel.cs | 10 ---------- 3 files changed, 5 insertions(+), 22 deletions(-) diff --git a/osu.Game/Screens/SelectV2/BeatmapPanel.cs b/osu.Game/Screens/SelectV2/BeatmapPanel.cs index b690e35a48..ddf2fdcb57 100644 --- a/osu.Game/Screens/SelectV2/BeatmapPanel.cs +++ b/osu.Game/Screens/SelectV2/BeatmapPanel.cs @@ -28,8 +28,11 @@ namespace osu.Game.Screens.SelectV2 { var inputRectangle = DrawRectangle; - // Cover the gaps introduced by the spacing between BeatmapPanels. - inputRectangle = inputRectangle.Inflate(new MarginPadding { Vertical = BeatmapCarousel.SPACING / 2f }); + // Cover the gaps introduced by the spacing between BeatmapPanels so that clicks will not fall through the carousel. + // + // Caveat is that for simplicity, we are covering the full spacing, so panels with frontmost depth will have a slightly + // larger hit target. + inputRectangle = inputRectangle.Inflate(new MarginPadding { Vertical = BeatmapCarousel.SPACING }); return inputRectangle.Contains(ToLocalSpace(screenSpacePos)); } diff --git a/osu.Game/Screens/SelectV2/BeatmapSetPanel.cs b/osu.Game/Screens/SelectV2/BeatmapSetPanel.cs index d869e0af75..f6c9324077 100644 --- a/osu.Game/Screens/SelectV2/BeatmapSetPanel.cs +++ b/osu.Game/Screens/SelectV2/BeatmapSetPanel.cs @@ -26,16 +26,6 @@ namespace osu.Game.Screens.SelectV2 private OsuSpriteText text = null!; private Box box = null!; - public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) - { - var inputRectangle = DrawRectangle; - - // Cover a gap introduced by the spacing between a BeatmapSetPanel and a BeatmapPanel either below/above it. - inputRectangle = inputRectangle.Inflate(new MarginPadding { Vertical = BeatmapCarousel.SPACING / 2f }); - - return inputRectangle.Contains(ToLocalSpace(screenSpacePos)); - } - [BackgroundDependencyLoader] private void load() { diff --git a/osu.Game/Screens/SelectV2/GroupPanel.cs b/osu.Game/Screens/SelectV2/GroupPanel.cs index c5e5c7745f..e10521f63e 100644 --- a/osu.Game/Screens/SelectV2/GroupPanel.cs +++ b/osu.Game/Screens/SelectV2/GroupPanel.cs @@ -27,16 +27,6 @@ namespace osu.Game.Screens.SelectV2 private Box box = null!; - public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) - { - var inputRectangle = DrawRectangle; - - // Cover a gap introduced by the spacing between a GroupPanel and a BeatmapPanel either below/above it. - inputRectangle = inputRectangle.Inflate(new MarginPadding { Vertical = BeatmapCarousel.SPACING / 2f }); - - return inputRectangle.Contains(ToLocalSpace(screenSpacePos)); - } - [BackgroundDependencyLoader] private void load() { From d505c529cd217abfbf697a5e9f9f8c1ebb2da14c Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 7 Feb 2025 15:06:21 +0900 Subject: [PATCH 0942/1275] Adjust tests in line with new expectations --- .../SongSelect/BeatmapCarouselV2TestScene.cs | 28 ++++++++ ...ceneBeatmapCarouselV2DifficultyGrouping.cs | 64 ++++--------------- .../TestSceneBeatmapCarouselV2NoGrouping.cs | 54 +++++----------- 3 files changed, 58 insertions(+), 88 deletions(-) diff --git a/osu.Game.Tests/Visual/SongSelect/BeatmapCarouselV2TestScene.cs b/osu.Game.Tests/Visual/SongSelect/BeatmapCarouselV2TestScene.cs index f17f312e9f..be0d0bf79a 100644 --- a/osu.Game.Tests/Visual/SongSelect/BeatmapCarouselV2TestScene.cs +++ b/osu.Game.Tests/Visual/SongSelect/BeatmapCarouselV2TestScene.cs @@ -2,6 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using System; +using System.Collections.Generic; using System.Linq; using NUnit.Framework; using osu.Framework.Allocation; @@ -20,6 +21,7 @@ using osu.Game.Screens.Select; using osu.Game.Screens.SelectV2; using osu.Game.Tests.Beatmaps; using osu.Game.Tests.Resources; +using osuTK; using osuTK.Graphics; using osuTK.Input; using BeatmapCarousel = osu.Game.Screens.SelectV2.BeatmapCarousel; @@ -164,6 +166,15 @@ namespace osu.Game.Tests.Visual.SongSelect }); } + protected IEnumerable GetVisiblePanels() + where T : Drawable + { + return Carousel.ChildrenOfType().Single() + .ChildrenOfType() + .Where(p => ((ICarouselPanel)p).Item?.IsVisible == true) + .OrderBy(p => p.Y); + } + protected void ClickVisiblePanel(int index) where T : Drawable { @@ -178,6 +189,23 @@ namespace osu.Game.Tests.Visual.SongSelect }); } + protected void ClickVisiblePanelWithOffset(int index, Vector2 positionOffsetFromCentre) + where T : Drawable + { + AddStep($"move mouse to panel {index} with offset {positionOffsetFromCentre}", () => + { + var panel = Carousel.ChildrenOfType().Single() + .ChildrenOfType() + .Where(p => ((ICarouselPanel)p).Item?.IsVisible == true) + .OrderBy(p => p.Y) + .ElementAt(index); + + InputManager.MoveMouseTo(panel.ScreenSpaceDrawQuad.Centre + panel.ToScreenSpace(positionOffsetFromCentre) - panel.ToScreenSpace(Vector2.Zero)); + }); + + AddStep("click", () => InputManager.Click(MouseButton.Left)); + } + /// /// Add requested beatmap sets count to list. /// diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2DifficultyGrouping.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2DifficultyGrouping.cs index 83e0e77fa6..f631dfc562 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2DifficultyGrouping.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2DifficultyGrouping.cs @@ -1,7 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using System; using System.Linq; using NUnit.Framework; using osu.Framework.Testing; @@ -10,7 +9,6 @@ using osu.Game.Screens.Select; using osu.Game.Screens.Select.Filter; using osu.Game.Screens.SelectV2; using osuTK; -using osuTK.Input; namespace osu.Game.Tests.Visual.SongSelect { @@ -153,60 +151,24 @@ namespace osu.Game.Tests.Visual.SongSelect [Test] public void TestInputHandlingWithinGaps() { - AddBeatmaps(5, 2); - WaitForDrawablePanels(); - SelectNextGroup(); + AddAssert("no beatmaps visible", () => !GetVisiblePanels().Any()); - clickOnPanel(0, 1, p => p.LayoutRectangle.TopLeft + new Vector2(20f, -1f)); - WaitForGroupSelection(0, 1); + // Clicks just above the first group panel should not actuate any action. + ClickVisiblePanelWithOffset(0, new Vector2(0, -(GroupPanel.HEIGHT / 2 + 1))); - clickOnPanel(0, 0, p => p.LayoutRectangle.BottomLeft + new Vector2(20f, 1f)); + AddAssert("no beatmaps visible", () => !GetVisiblePanels().Any()); + + ClickVisiblePanelWithOffset(0, new Vector2(0, -(GroupPanel.HEIGHT / 2))); + + AddUntilStep("wait for beatmaps visible", () => GetVisiblePanels().Any()); + CheckNoSelection(); + + // Beatmap panels expand their selection area to cover holes from spacing. + ClickVisiblePanelWithOffset(0, new Vector2(0, -(CarouselItem.DEFAULT_HEIGHT / 2 + 1))); WaitForGroupSelection(0, 0); - SelectNextPanel(); - Select(); + ClickVisiblePanelWithOffset(1, new Vector2(0, (CarouselItem.DEFAULT_HEIGHT / 2 + 1))); WaitForGroupSelection(0, 1); - - clickOnGroup(0, p => p.LayoutRectangle.BottomLeft + new Vector2(20f, 1f)); - AddAssert("group 0 collapsed", () => this.ChildrenOfType().OrderBy(g => g.Y).ElementAt(0).Expanded.Value, () => Is.False); - clickOnGroup(0, p => p.LayoutRectangle.Centre); - AddAssert("group 0 expanded", () => this.ChildrenOfType().OrderBy(g => g.Y).ElementAt(0).Expanded.Value, () => Is.True); - - AddStep("scroll to end", () => Scroll.ScrollToEnd(false)); - clickOnPanel(0, 4, p => p.LayoutRectangle.BottomLeft + new Vector2(20f, 1f)); - WaitForGroupSelection(0, 4); - - clickOnGroup(1, p => p.LayoutRectangle.TopLeft + new Vector2(20f, -1f)); - AddAssert("group 1 expanded", () => this.ChildrenOfType().OrderBy(g => g.Y).ElementAt(1).Expanded.Value, () => Is.True); - } - - private void clickOnGroup(int group, Func pos) - { - AddStep($"click on group{group}", () => - { - var groupingFilter = Carousel.Filters.OfType().Single(); - var model = groupingFilter.GroupItems.Keys.ElementAt(group); - - var panel = this.ChildrenOfType().Single(b => ReferenceEquals(b.Item!.Model, model)); - InputManager.MoveMouseTo(panel.ToScreenSpace(pos(panel))); - InputManager.Click(MouseButton.Left); - }); - } - - private void clickOnPanel(int group, int panel, Func pos) - { - AddStep($"click on group{group} panel{panel}", () => - { - var groupingFilter = Carousel.Filters.OfType().Single(); - - var g = groupingFilter.GroupItems.Keys.ElementAt(group); - // offset by one because the group itself is included in the items list. - object model = groupingFilter.GroupItems[g].ElementAt(panel + 1).Model; - - var p = this.ChildrenOfType().Single(b => ReferenceEquals(b.Item!.Model, model)); - InputManager.MoveMouseTo(p.ToScreenSpace(pos(p))); - InputManager.Click(MouseButton.Left); - }); } } } diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2NoGrouping.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2NoGrouping.cs index 566c2f1798..1359b5c58e 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2NoGrouping.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2NoGrouping.cs @@ -1,7 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using System; using System.Linq; using NUnit.Framework; using osu.Framework.Testing; @@ -209,31 +208,34 @@ namespace osu.Game.Tests.Visual.SongSelect } [Test] + [Solo] public void TestInputHandlingWithinGaps() { AddBeatmaps(2, 5); WaitForDrawablePanels(); - SelectNextGroup(); - clickOnDifficulty(0, 1, p => p.LayoutRectangle.TopLeft + new Vector2(20f, -1f)); - WaitForSelection(0, 1); + AddAssert("no beatmaps visible", () => !GetVisiblePanels().Any()); - clickOnDifficulty(0, 0, p => p.LayoutRectangle.BottomLeft + new Vector2(20f, 1f)); + // Clicks just above the first group panel should not actuate any action. + ClickVisiblePanelWithOffset(0, new Vector2(0, -(BeatmapSetPanel.HEIGHT / 2 + 1))); + + AddAssert("no beatmaps visible", () => !GetVisiblePanels().Any()); + + ClickVisiblePanelWithOffset(0, new Vector2(0, -(BeatmapSetPanel.HEIGHT / 2))); + + AddUntilStep("wait for beatmaps visible", () => GetVisiblePanels().Any()); WaitForSelection(0, 0); - SelectNextPanel(); - Select(); - WaitForSelection(0, 1); - - clickOnSet(0, p => p.LayoutRectangle.BottomLeft + new Vector2(20f, 1f)); + // Beatmap panels expand their selection area to cover holes from spacing. + ClickVisiblePanelWithOffset(1, new Vector2(0, -(CarouselItem.DEFAULT_HEIGHT / 2 + 1))); WaitForSelection(0, 0); - AddStep("scroll to end", () => Scroll.ScrollToEnd(false)); - clickOnDifficulty(0, 4, p => p.LayoutRectangle.BottomLeft + new Vector2(20f, 1f)); - WaitForSelection(0, 4); + // Panels with higher depth will handle clicks in the gutters for simplicity. + ClickVisiblePanelWithOffset(2, new Vector2(0, (CarouselItem.DEFAULT_HEIGHT / 2 + 1))); + WaitForSelection(0, 2); - clickOnSet(1, p => p.LayoutRectangle.TopLeft + new Vector2(20f, -1f)); - WaitForSelection(1, 0); + ClickVisiblePanelWithOffset(3, new Vector2(0, (CarouselItem.DEFAULT_HEIGHT / 2 + 1))); + WaitForSelection(0, 3); } private void checkSelectionIterating(bool isIterating) @@ -249,27 +251,5 @@ namespace osu.Game.Tests.Visual.SongSelect AddUntilStep("selection not changed", () => Carousel.CurrentSelection == selection); } } - - private void clickOnSet(int set, Func pos) - { - AddStep($"click on set{set}", () => - { - var model = BeatmapSets[set]; - var panel = this.ChildrenOfType().Single(b => ReferenceEquals(b.Item!.Model, model)); - InputManager.MoveMouseTo(panel.ToScreenSpace(pos(panel))); - InputManager.Click(MouseButton.Left); - }); - } - - private void clickOnDifficulty(int set, int diff, Func pos) - { - AddStep($"click on set{set} diff{diff}", () => - { - var model = BeatmapSets[set].Beatmaps[diff]; - var panel = this.ChildrenOfType().Single(b => ReferenceEquals(b.Item!.Model, model)); - InputManager.MoveMouseTo(panel.ToScreenSpace(pos(panel))); - InputManager.Click(MouseButton.Left); - }); - } } } From aa329f397e684f41e5ca040d4d33a13026d464a5 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 7 Feb 2025 15:30:31 +0900 Subject: [PATCH 0943/1275] Remove stray `[Solo]`s --- osu.Game.Tests/Visual/Editing/TestScenePlacementBlueprint.cs | 1 - .../Visual/Navigation/TestSceneBeatmapEditorNavigation.cs | 1 - .../Visual/SongSelect/TestSceneBeatmapCarouselV2NoGrouping.cs | 1 - osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs | 1 - 4 files changed, 4 deletions(-) diff --git a/osu.Game.Tests/Visual/Editing/TestScenePlacementBlueprint.cs b/osu.Game.Tests/Visual/Editing/TestScenePlacementBlueprint.cs index 966e6513bb..4953cf83c9 100644 --- a/osu.Game.Tests/Visual/Editing/TestScenePlacementBlueprint.cs +++ b/osu.Game.Tests/Visual/Editing/TestScenePlacementBlueprint.cs @@ -74,7 +74,6 @@ namespace osu.Game.Tests.Visual.Editing } [Test] - [Solo] public void TestCommitPlacementViaRightClick() { Playfield playfield = null!; diff --git a/osu.Game.Tests/Visual/Navigation/TestSceneBeatmapEditorNavigation.cs b/osu.Game.Tests/Visual/Navigation/TestSceneBeatmapEditorNavigation.cs index d76e0290ef..ee5b1797ed 100644 --- a/osu.Game.Tests/Visual/Navigation/TestSceneBeatmapEditorNavigation.cs +++ b/osu.Game.Tests/Visual/Navigation/TestSceneBeatmapEditorNavigation.cs @@ -165,7 +165,6 @@ namespace osu.Game.Tests.Visual.Navigation } [Test] - [Solo] public void TestEditorGameplayTestAlwaysUsesOriginalRuleset() { prepareBeatmap(); diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2NoGrouping.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2NoGrouping.cs index 1359b5c58e..09ded342c3 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2NoGrouping.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2NoGrouping.cs @@ -208,7 +208,6 @@ namespace osu.Game.Tests.Visual.SongSelect } [Test] - [Solo] public void TestInputHandlingWithinGaps() { AddBeatmaps(2, 5); diff --git a/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs b/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs index c415fc876f..d8ab367ebd 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs @@ -1239,7 +1239,6 @@ namespace osu.Game.Tests.Visual.SongSelect } [Test] - [Solo] public void TestHardDeleteHandledCorrectly() { createSongSelect(); From 4d1167fdccbfee3d0ecf425a969925c9baf5b222 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 7 Feb 2025 15:36:59 +0900 Subject: [PATCH 0944/1275] Don't attempt to submit zero scores --- osu.Game/Screens/Play/SubmittingPlayer.cs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/osu.Game/Screens/Play/SubmittingPlayer.cs b/osu.Game/Screens/Play/SubmittingPlayer.cs index 0a230ea00b..b667963a70 100644 --- a/osu.Game/Screens/Play/SubmittingPlayer.cs +++ b/osu.Game/Screens/Play/SubmittingPlayer.cs @@ -284,6 +284,13 @@ namespace osu.Game.Screens.Play return Task.CompletedTask; } + // zero scores should also never be submitted. + if (score.ScoreInfo.TotalScore == 0) + { + Logger.Log("Zero score, skipping score submission"); + return Task.CompletedTask; + } + // mind the timing of this. // once `scoreSubmissionSource` is created, it is presumed that submission is taking place in the background, // so all exceptional circumstances that would disallow submission must be handled above. From 75ef6f6a0e02e1bf4b898186141376d2ccf7b80a Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 7 Feb 2025 02:10:08 +0900 Subject: [PATCH 0945/1275] Use random generation in carousel stress test --- .../SongSelect/BeatmapCarouselV2TestScene.cs | 45 ++++++++++--------- .../SongSelect/TestSceneBeatmapCarouselV2.cs | 2 +- 2 files changed, 25 insertions(+), 22 deletions(-) diff --git a/osu.Game.Tests/Visual/SongSelect/BeatmapCarouselV2TestScene.cs b/osu.Game.Tests/Visual/SongSelect/BeatmapCarouselV2TestScene.cs index f17f312e9f..db433b93d2 100644 --- a/osu.Game.Tests/Visual/SongSelect/BeatmapCarouselV2TestScene.cs +++ b/osu.Game.Tests/Visual/SongSelect/BeatmapCarouselV2TestScene.cs @@ -187,29 +187,32 @@ namespace osu.Game.Tests.Visual.SongSelect protected void AddBeatmaps(int count, int? fixedDifficultiesPerSet = null, bool randomMetadata = false) => AddStep($"add {count} beatmaps{(randomMetadata ? " with random data" : "")}", () => { for (int i = 0; i < count; i++) - { - var beatmapSetInfo = TestResources.CreateTestBeatmapSetInfo(fixedDifficultiesPerSet ?? RNG.Next(1, 4)); - - if (randomMetadata) - { - char randomCharacter = getRandomCharacter(); - - var metadata = new BeatmapMetadata - { - // Create random metadata, then we can check if sorting works based on these - Artist = $"{randomCharacter}ome Artist " + RNG.Next(0, 9), - Title = $"{randomCharacter}ome Song (set id {beatmapSetInfo.OnlineID:000}) {Guid.NewGuid()}", - Author = { Username = $"{randomCharacter}ome Guy " + RNG.Next(0, 9) }, - }; - - foreach (var beatmap in beatmapSetInfo.Beatmaps) - beatmap.Metadata = metadata.DeepClone(); - } - - BeatmapSets.Add(beatmapSetInfo); - } + BeatmapSets.Add(CreateTestBeatmapSetInfo(fixedDifficultiesPerSet, randomMetadata)); }); + protected static BeatmapSetInfo CreateTestBeatmapSetInfo(int? fixedDifficultiesPerSet, bool randomMetadata) + { + var beatmapSetInfo = TestResources.CreateTestBeatmapSetInfo(fixedDifficultiesPerSet ?? RNG.Next(1, 4)); + + if (randomMetadata) + { + char randomCharacter = getRandomCharacter(); + + var metadata = new BeatmapMetadata + { + // Create random metadata, then we can check if sorting works based on these + Artist = $"{randomCharacter}ome Artist " + RNG.Next(0, 9), + Title = $"{randomCharacter}ome Song (set id {beatmapSetInfo.OnlineID:000}) {Guid.NewGuid()}", + Author = { Username = $"{randomCharacter}ome Guy " + RNG.Next(0, 9) }, + }; + + foreach (var beatmap in beatmapSetInfo.Beatmaps) + beatmap.Metadata = metadata.DeepClone(); + } + + return beatmapSetInfo; + } + private static long randomCharPointer; private static char getRandomCharacter() diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2.cs index 3c5cf16e92..30ca26ce68 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2.cs @@ -77,7 +77,7 @@ namespace osu.Game.Tests.Visual.SongSelect Task.Run(() => { for (int j = 0; j < count; j++) - generated.Add(TestResources.CreateTestBeatmapSetInfo(RNG.Next(1, 4))); + generated.Add(CreateTestBeatmapSetInfo(3, true)); }).ConfigureAwait(true); }); From 50d880e2ae3e3abfd58a795d26461ff39aa82070 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 7 Feb 2025 02:10:38 +0900 Subject: [PATCH 0946/1275] Fix unnecessary `BeatmapSet.Metadata` lookups --- osu.Game/Screens/SelectV2/BeatmapCarouselFilterSorting.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/SelectV2/BeatmapCarouselFilterSorting.cs b/osu.Game/Screens/SelectV2/BeatmapCarouselFilterSorting.cs index 0298616aa8..3cdbbb4fed 100644 --- a/osu.Game/Screens/SelectV2/BeatmapCarouselFilterSorting.cs +++ b/osu.Game/Screens/SelectV2/BeatmapCarouselFilterSorting.cs @@ -36,7 +36,7 @@ namespace osu.Game.Screens.SelectV2 switch (criteria.Sort) { case SortMode.Artist: - comparison = OrdinalSortByCaseStringComparer.DEFAULT.Compare(ab.BeatmapSet!.Metadata.Artist, bb.BeatmapSet!.Metadata.Artist); + comparison = OrdinalSortByCaseStringComparer.DEFAULT.Compare(ab.Metadata.Artist, bb.Metadata.Artist); if (comparison == 0) goto case SortMode.Title; break; @@ -46,7 +46,7 @@ namespace osu.Game.Screens.SelectV2 break; case SortMode.Title: - comparison = OrdinalSortByCaseStringComparer.DEFAULT.Compare(ab.BeatmapSet!.Metadata.Title, bb.BeatmapSet!.Metadata.Title); + comparison = OrdinalSortByCaseStringComparer.DEFAULT.Compare(ab.Metadata.Title, bb.Metadata.Title); break; default: From a49b1b61b4dfe7ff394f8f68c70d0e61bbc657d0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 7 Feb 2025 08:21:34 +0100 Subject: [PATCH 0947/1275] Add test coverage for scores with zero total not submitting --- .../TestScenePlayerScoreSubmission.cs | 26 +++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/osu.Game.Tests/Visual/Gameplay/TestScenePlayerScoreSubmission.cs b/osu.Game.Tests/Visual/Gameplay/TestScenePlayerScoreSubmission.cs index c382f0828b..381f49d9eb 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestScenePlayerScoreSubmission.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestScenePlayerScoreSubmission.cs @@ -16,6 +16,7 @@ using osu.Game.Online.API; using osu.Game.Online.Rooms; using osu.Game.Online.Solo; using osu.Game.Rulesets; +using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Mania; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Objects; @@ -234,6 +235,31 @@ namespace osu.Game.Tests.Visual.Gameplay AddAssert("ensure no submission", () => Player.SubmittedScore == null); } + [Test] + public void TestNoSubmissionWhenScoreZero() + { + prepareTestAPI(true); + + createPlayerTest(); + + AddUntilStep("wait for token request", () => Player.TokenCreationRequested); + + AddUntilStep("wait for track to start running", () => Beatmap.Value.Track.IsRunning); + AddUntilStep("wait for first result", () => Player.Results.Count > 0); + + AddStep("add fake non-scoring hit", () => + { + Player.ScoreProcessor.RevertResult(Player.Results.First()); + Player.ScoreProcessor.ApplyResult(new OsuJudgementResult(Beatmap.Value.Beatmap.HitObjects.First(), new IgnoreJudgement()) + { + Type = HitResult.IgnoreHit, + }); + }); + + AddStep("exit", () => Player.Exit()); + AddAssert("ensure no submission", () => Player.SubmittedScore == null); + } + [Test] public void TestSubmissionOnExit() { From 9d979dc3f4adb523269fb14f6d049986dab9d61b Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 7 Feb 2025 02:37:16 +0900 Subject: [PATCH 0948/1275] Refactor grouping to be much more efficient --- osu.Game/Screens/SelectV2/BeatmapCarousel.cs | 2 +- .../SelectV2/BeatmapCarouselFilterGrouping.cs | 207 ++++++++---------- 2 files changed, 93 insertions(+), 116 deletions(-) diff --git a/osu.Game/Screens/SelectV2/BeatmapCarousel.cs b/osu.Game/Screens/SelectV2/BeatmapCarousel.cs index 4126889892..137a8e8eab 100644 --- a/osu.Game/Screens/SelectV2/BeatmapCarousel.cs +++ b/osu.Game/Screens/SelectV2/BeatmapCarousel.cs @@ -289,5 +289,5 @@ namespace osu.Game.Screens.SelectV2 #endregion } - public record GroupDefinition(string Title); + public record GroupDefinition(object Data, string Title); } diff --git a/osu.Game/Screens/SelectV2/BeatmapCarouselFilterGrouping.cs b/osu.Game/Screens/SelectV2/BeatmapCarouselFilterGrouping.cs index a8caebad7a..8838ce67ad 100644 --- a/osu.Game/Screens/SelectV2/BeatmapCarouselFilterGrouping.cs +++ b/osu.Game/Screens/SelectV2/BeatmapCarouselFilterGrouping.cs @@ -3,7 +3,6 @@ using System; using System.Collections.Generic; -using System.Linq; using System.Threading; using System.Threading.Tasks; using osu.Game.Beatmaps; @@ -36,137 +35,115 @@ namespace osu.Game.Screens.SelectV2 this.getCriteria = getCriteria; } - public async Task> Run(IEnumerable items, CancellationToken cancellationToken) => await Task.Run(() => + public async Task> Run(IEnumerable items, CancellationToken cancellationToken) { - setItems.Clear(); - groupItems.Clear(); + return await Task.Run(() => + { + setItems.Clear(); + groupItems.Clear(); - var criteria = getCriteria(); - var newItems = new List(items.Count()); + var criteria = getCriteria(); + var newItems = new List(); - // Add criteria groups. + BeatmapInfo? lastBeatmap = null; + GroupDefinition? lastGroup = null; + + HashSet? groupRefItems = null; + HashSet? setRefItems = null; + + switch (criteria.Group) + { + default: + BeatmapSetsGroupedTogether = true; + break; + + case GroupMode.Difficulty: + BeatmapSetsGroupedTogether = false; + break; + } + + foreach (var item in items) + { + cancellationToken.ThrowIfCancellationRequested(); + + var beatmap = (BeatmapInfo)item.Model; + + if (createGroupIfRequired(criteria, beatmap, lastGroup) is GroupDefinition newGroup) + { + // When reaching a new group, ensure we reset any beatmap set tracking. + setRefItems = null; + lastBeatmap = null; + + groupItems[newGroup] = groupRefItems = new HashSet(); + lastGroup = newGroup; + + addItem(new CarouselItem(newGroup) + { + DrawHeight = GroupPanel.HEIGHT, + DepthLayer = -2, + }); + } + + if (BeatmapSetsGroupedTogether) + { + bool newBeatmapSet = lastBeatmap?.BeatmapSet!.ID != beatmap.BeatmapSet!.ID; + + if (newBeatmapSet) + { + setItems[beatmap.BeatmapSet!] = setRefItems = new HashSet(); + + addItem(new CarouselItem(beatmap.BeatmapSet!) + { + DrawHeight = BeatmapSetPanel.HEIGHT, + DepthLayer = -1 + }); + } + } + + addItem(item); + lastBeatmap = beatmap; + + void addItem(CarouselItem i) + { + newItems.Add(i); + + groupRefItems?.Add(i); + setRefItems?.Add(i); + + i.IsVisible = i.Model is GroupDefinition || (lastGroup == null && (i.Model is BeatmapSetInfo || setRefItems == null)); + } + } + + return newItems; + }, cancellationToken).ConfigureAwait(false); + } + + private GroupDefinition? createGroupIfRequired(FilterCriteria criteria, BeatmapInfo beatmap, GroupDefinition? lastGroup) + { switch (criteria.Group) { - default: - BeatmapSetsGroupedTogether = true; - newItems.AddRange(items); - break; - case GroupMode.Artist: - BeatmapSetsGroupedTogether = true; - char groupChar = (char)0; + char groupChar = lastGroup?.Data as char? ?? (char)0; + char beatmapFirstChar = char.ToUpperInvariant(beatmap.Metadata.Artist[0]); - foreach (var item in items) - { - cancellationToken.ThrowIfCancellationRequested(); - - var b = (BeatmapInfo)item.Model; - - char beatmapFirstChar = char.ToUpperInvariant(b.Metadata.Artist[0]); - - if (beatmapFirstChar > groupChar) - { - groupChar = beatmapFirstChar; - var groupDefinition = new GroupDefinition($"{groupChar}"); - var groupItem = new CarouselItem(groupDefinition) { DrawHeight = GroupPanel.HEIGHT }; - - newItems.Add(groupItem); - groupItems[groupDefinition] = new HashSet { groupItem }; - } - - newItems.Add(item); - } + if (beatmapFirstChar > groupChar) + return new GroupDefinition(beatmapFirstChar, $"{beatmapFirstChar}"); break; case GroupMode.Difficulty: - BeatmapSetsGroupedTogether = false; - int starGroup = int.MinValue; + int starGroup = lastGroup?.Data as int? ?? -1; - foreach (var item in items) + if (beatmap.StarRating > starGroup) { - cancellationToken.ThrowIfCancellationRequested(); - - var b = (BeatmapInfo)item.Model; - - if (b.StarRating > starGroup) - { - starGroup = (int)Math.Floor(b.StarRating); - var groupDefinition = new GroupDefinition($"{starGroup} - {++starGroup} *"); - - var groupItem = new CarouselItem(groupDefinition) - { - DrawHeight = GroupPanel.HEIGHT, - DepthLayer = -2 - }; - - newItems.Add(groupItem); - groupItems[groupDefinition] = new HashSet { groupItem }; - } - - newItems.Add(item); + starGroup = (int)Math.Floor(beatmap.StarRating); + return new GroupDefinition(starGroup + 1, $"{starGroup} - {starGroup + 1} *"); } break; } - // Add set headers wherever required. - CarouselItem? lastItem = null; - - if (BeatmapSetsGroupedTogether) - { - for (int i = 0; i < newItems.Count; i++) - { - cancellationToken.ThrowIfCancellationRequested(); - - var item = newItems[i]; - - if (item.Model is BeatmapInfo beatmap) - { - bool newBeatmapSet = lastItem?.Model is not BeatmapInfo lastBeatmap || lastBeatmap.BeatmapSet!.ID != beatmap.BeatmapSet!.ID; - - if (newBeatmapSet) - { - var setItem = new CarouselItem(beatmap.BeatmapSet!) - { - DrawHeight = BeatmapSetPanel.HEIGHT, - DepthLayer = -1 - }; - - setItems[beatmap.BeatmapSet!] = new HashSet { setItem }; - newItems.Insert(i, setItem); - i++; - } - - setItems[beatmap.BeatmapSet!].Add(item); - item.IsVisible = false; - } - - lastItem = item; - } - } - - // Link group items to their headers. - GroupDefinition? lastGroup = null; - - foreach (var item in newItems) - { - cancellationToken.ThrowIfCancellationRequested(); - - if (item.Model is GroupDefinition group) - { - lastGroup = group; - continue; - } - - if (lastGroup != null) - { - groupItems[lastGroup].Add(item); - item.IsVisible = false; - } - } - - return newItems; - }, cancellationToken).ConfigureAwait(false); + return null; + } } } From c935c3154b33739020c42b597fbd83480e6cd0e5 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 7 Feb 2025 16:54:30 +0900 Subject: [PATCH 0949/1275] Always transfer keyboard selection on activation --- .../SongSelect/BeatmapCarouselV2TestScene.cs | 4 ++-- ...ceneBeatmapCarouselV2DifficultyGrouping.cs | 22 ++++++++++++++++++- osu.Game/Screens/SelectV2/Carousel.cs | 5 +++++ 3 files changed, 28 insertions(+), 3 deletions(-) diff --git a/osu.Game.Tests/Visual/SongSelect/BeatmapCarouselV2TestScene.cs b/osu.Game.Tests/Visual/SongSelect/BeatmapCarouselV2TestScene.cs index f17f312e9f..cfef2882be 100644 --- a/osu.Game.Tests/Visual/SongSelect/BeatmapCarouselV2TestScene.cs +++ b/osu.Game.Tests/Visual/SongSelect/BeatmapCarouselV2TestScene.cs @@ -136,8 +136,8 @@ namespace osu.Game.Tests.Visual.SongSelect protected void CheckNoSelection() => AddAssert("has no selection", () => Carousel.CurrentSelection, () => Is.Null); protected void CheckHasSelection() => AddAssert("has selection", () => Carousel.CurrentSelection, () => Is.Not.Null); - protected BeatmapPanel? GetSelectedPanel() => Carousel.ChildrenOfType().SingleOrDefault(p => p.Selected.Value); - protected GroupPanel? GetKeyboardSelectedPanel() => Carousel.ChildrenOfType().SingleOrDefault(p => p.KeyboardSelected.Value); + protected ICarouselPanel? GetSelectedPanel() => Carousel.ChildrenOfType().SingleOrDefault(p => p.Selected.Value); + protected ICarouselPanel? GetKeyboardSelectedPanel() => Carousel.ChildrenOfType().SingleOrDefault(p => p.KeyboardSelected.Value); protected void WaitForGroupSelection(int group, int panel) { diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2DifficultyGrouping.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2DifficultyGrouping.cs index 151f1f5fec..f46e79caf7 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2DifficultyGrouping.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2DifficultyGrouping.cs @@ -94,7 +94,7 @@ namespace osu.Game.Tests.Visual.SongSelect } [Test] - public void TestGroupSelectionOnHeader() + public void TestGroupSelectionOnHeaderKeyboard() { SelectNextGroup(); WaitForGroupSelection(0, 0); @@ -113,6 +113,26 @@ namespace osu.Game.Tests.Visual.SongSelect AddAssert("keyboard selected panel is expanded", () => GetKeyboardSelectedPanel()?.Expanded.Value, () => Is.True); } + [Test] + public void TestGroupSelectionOnHeaderMouse() + { + SelectNextGroup(); + WaitForGroupSelection(0, 0); + + AddAssert("keyboard selected panel is beatmap", GetKeyboardSelectedPanel, Is.TypeOf); + AddAssert("selected panel is beatmap", GetSelectedPanel, Is.TypeOf); + + ClickVisiblePanel(0); + AddAssert("keyboard selected panel is group", GetKeyboardSelectedPanel, Is.TypeOf); + AddAssert("keyboard selected panel is contracted", () => GetKeyboardSelectedPanel()?.Expanded.Value, () => Is.False); + + ClickVisiblePanel(0); + AddAssert("keyboard selected panel is group", GetKeyboardSelectedPanel, Is.TypeOf); + AddAssert("keyboard selected panel is expanded", () => GetKeyboardSelectedPanel()?.Expanded.Value, () => Is.True); + + AddAssert("selected panel is still beatmap", GetSelectedPanel, Is.TypeOf); + } + [Test] public void TestKeyboardSelection() { diff --git a/osu.Game/Screens/SelectV2/Carousel.cs b/osu.Game/Screens/SelectV2/Carousel.cs index 5220781ce8..89dec6a7ae 100644 --- a/osu.Game/Screens/SelectV2/Carousel.cs +++ b/osu.Game/Screens/SelectV2/Carousel.cs @@ -124,6 +124,11 @@ namespace osu.Game.Screens.SelectV2 /// public void Activate(CarouselItem item) { + // Regardless of how the item handles activation, update keyboard selection to the activated panel. + // In other words, when a panel is clicked, keyboard selection should default to matching the clicked + // item. + setKeyboardSelection(item.Model); + (GetMaterialisedDrawableForItem(item) as ICarouselPanel)?.Activated(); HandleItemActivated(item); From cef9d2eac50ac11bdbc964fcb243d1225c06dae3 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 7 Feb 2025 16:55:02 +0900 Subject: [PATCH 0950/1275] Reduce number of beatmaps added in selection test This is because with the new keyboard selection logic, adding too many can cause the re-added selection to be off-screen in the headless test setup. --- .../SongSelect/TestSceneBeatmapCarouselV2DifficultyGrouping.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2DifficultyGrouping.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2DifficultyGrouping.cs index f46e79caf7..33d9d3a363 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2DifficultyGrouping.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2DifficultyGrouping.cs @@ -74,7 +74,7 @@ namespace osu.Game.Tests.Visual.SongSelect RemoveAllBeatmaps(); AddUntilStep("no drawable selection", GetSelectedPanel, () => Is.Null); - AddBeatmaps(10); + AddBeatmaps(5); WaitForDrawablePanels(); CheckHasSelection(); From 41c8f648063d2cef2ba36021095cb7ca4e5fd0c0 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 7 Feb 2025 17:33:32 +0900 Subject: [PATCH 0951/1275] Simplify naming of endpoints --- osu.Desktop/DiscordRichPresence.cs | 2 +- .../Visual/Menus/TestSceneMainMenu.cs | 2 +- .../Online/TestSceneWikiMarkdownContainer.cs | 10 ++--- osu.Game/Beatmaps/BeatmapInfoExtensions.cs | 2 +- osu.Game/Beatmaps/BeatmapSetInfoExtensions.cs | 4 +- osu.Game/Online/API/APIAccess.cs | 12 +++--- osu.Game/Online/API/APIRequest.cs | 2 +- osu.Game/Online/API/DummyAPIAccess.cs | 6 +-- osu.Game/Online/API/IAPIProvider.cs | 2 +- .../Requests/PatchBeatmapPackageRequest.cs | 4 +- .../API/Requests/PutBeatmapSetRequest.cs | 4 +- .../Requests/ReplaceBeatmapPackageRequest.cs | 4 +- osu.Game/Online/Chat/ExternalLinkOpener.cs | 4 +- osu.Game/Online/Chat/NowPlayingCommand.cs | 2 +- .../DevelopmentEndpointConfiguration.cs | 8 ++-- osu.Game/Online/EndpointConfiguration.cs | 38 +++++++++---------- .../Online/Leaderboards/LeaderboardScore.cs | 2 +- .../Online/Metadata/OnlineMetadataClient.cs | 2 +- .../Multiplayer/OnlineMultiplayerClient.cs | 2 +- .../Online/ProductionEndpointConfiguration.cs | 8 ++-- .../Online/Spectator/OnlineSpectatorClient.cs | 2 +- osu.Game/OsuGameBase.cs | 2 +- osu.Game/Overlays/Comments/DrawableComment.cs | 2 +- osu.Game/Overlays/Login/LoginForm.cs | 2 +- .../Overlays/Login/SecondFactorAuthForm.cs | 2 +- .../Profile/Header/BottomHeaderContainer.cs | 4 +- .../Components/DrawableTournamentBanner.cs | 2 +- .../Profile/Header/TopHeaderContainer.cs | 2 +- .../Sections/Recent/DrawableRecentActivity.cs | 2 +- osu.Game/Overlays/Wiki/WikiPanelContainer.cs | 2 +- osu.Game/Overlays/WikiOverlay.cs | 4 +- .../ScreenFrequentlyAskedQuestions.cs | 4 +- .../Lounge/Components/DrawableRoom.cs | 2 +- .../Leaderboards/LeaderboardScoreV2.cs | 2 +- osu.Game/Utils/SentryLogger.cs | 2 +- 35 files changed, 78 insertions(+), 78 deletions(-) diff --git a/osu.Desktop/DiscordRichPresence.cs b/osu.Desktop/DiscordRichPresence.cs index cf56fe6115..668f63b910 100644 --- a/osu.Desktop/DiscordRichPresence.cs +++ b/osu.Desktop/DiscordRichPresence.cs @@ -173,7 +173,7 @@ namespace osu.Desktop new Button { Label = "View beatmap", - Url = $@"{api.EndpointConfiguration.WebsiteRootUrl}/beatmaps/{beatmapId}?mode={ruleset.Value.ShortName}" + Url = $@"{api.Endpoints.WebsiteUrl}/beatmaps/{beatmapId}?mode={ruleset.Value.ShortName}" } }; } diff --git a/osu.Game.Tests/Visual/Menus/TestSceneMainMenu.cs b/osu.Game.Tests/Visual/Menus/TestSceneMainMenu.cs index e2d5bc2917..cd391519f4 100644 --- a/osu.Game.Tests/Visual/Menus/TestSceneMainMenu.cs +++ b/osu.Game.Tests/Visual/Menus/TestSceneMainMenu.cs @@ -80,7 +80,7 @@ namespace osu.Game.Tests.Visual.Menus new APIMenuImage { Image = @"https://assets.ppy.sh/main-menu/project-loved-2@2x.png", - Url = $@"{API.EndpointConfiguration.WebsiteRootUrl}/home/news/2023-12-21-project-loved-december-2023", + Url = $@"{API.Endpoints.WebsiteUrl}/home/news/2023-12-21-project-loved-december-2023", } } }); diff --git a/osu.Game.Tests/Visual/Online/TestSceneWikiMarkdownContainer.cs b/osu.Game.Tests/Visual/Online/TestSceneWikiMarkdownContainer.cs index cee3f37aea..e453a32652 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneWikiMarkdownContainer.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneWikiMarkdownContainer.cs @@ -67,19 +67,19 @@ namespace osu.Game.Tests.Visual.Online [Test] public void TestLink() { - AddStep("set current path", () => markdownContainer.CurrentPath = $"{API.EndpointConfiguration.WebsiteRootUrl}/wiki/Article_styling_criteria/"); + AddStep("set current path", () => markdownContainer.CurrentPath = $"{API.Endpoints.WebsiteUrl}/wiki/Article_styling_criteria/"); AddStep("set '/wiki/Main_page''", () => markdownContainer.Text = "[wiki main page](/wiki/Main_page)"); - AddAssert("check url", () => markdownContainer.Link.Url == $"{API.EndpointConfiguration.WebsiteRootUrl}/wiki/Main_page"); + AddAssert("check url", () => markdownContainer.Link.Url == $"{API.Endpoints.WebsiteUrl}/wiki/Main_page"); AddStep("set '../FAQ''", () => markdownContainer.Text = "[FAQ](../FAQ)"); - AddAssert("check url", () => markdownContainer.Link.Url == $"{API.EndpointConfiguration.WebsiteRootUrl}/wiki/FAQ"); + AddAssert("check url", () => markdownContainer.Link.Url == $"{API.Endpoints.WebsiteUrl}/wiki/FAQ"); AddStep("set './Writing''", () => markdownContainer.Text = "[wiki writing guidline](./Writing)"); - AddAssert("check url", () => markdownContainer.Link.Url == $"{API.EndpointConfiguration.WebsiteRootUrl}/wiki/Article_styling_criteria/Writing"); + AddAssert("check url", () => markdownContainer.Link.Url == $"{API.Endpoints.WebsiteUrl}/wiki/Article_styling_criteria/Writing"); AddStep("set 'Formatting''", () => markdownContainer.Text = "[wiki formatting guidline](Formatting)"); - AddAssert("check url", () => markdownContainer.Link.Url == $"{API.EndpointConfiguration.WebsiteRootUrl}/wiki/Article_styling_criteria/Formatting"); + AddAssert("check url", () => markdownContainer.Link.Url == $"{API.Endpoints.WebsiteUrl}/wiki/Article_styling_criteria/Formatting"); } [Test] diff --git a/osu.Game/Beatmaps/BeatmapInfoExtensions.cs b/osu.Game/Beatmaps/BeatmapInfoExtensions.cs index d0625c64e3..16b4b04ce4 100644 --- a/osu.Game/Beatmaps/BeatmapInfoExtensions.cs +++ b/osu.Game/Beatmaps/BeatmapInfoExtensions.cs @@ -59,7 +59,7 @@ namespace osu.Game.Beatmaps if (beatmapInfo.OnlineID <= 0 || beatmapInfo.BeatmapSet == null) return null; - return $@"{api.EndpointConfiguration.WebsiteRootUrl}/beatmapsets/{beatmapInfo.BeatmapSet.OnlineID}#{ruleset?.ShortName ?? beatmapInfo.Ruleset.ShortName}/{beatmapInfo.OnlineID}"; + return $@"{api.Endpoints.WebsiteUrl}/beatmapsets/{beatmapInfo.BeatmapSet.OnlineID}#{ruleset?.ShortName ?? beatmapInfo.Ruleset.ShortName}/{beatmapInfo.OnlineID}"; } } } diff --git a/osu.Game/Beatmaps/BeatmapSetInfoExtensions.cs b/osu.Game/Beatmaps/BeatmapSetInfoExtensions.cs index ac191d36a9..1af0e7a9ee 100644 --- a/osu.Game/Beatmaps/BeatmapSetInfoExtensions.cs +++ b/osu.Game/Beatmaps/BeatmapSetInfoExtensions.cs @@ -41,9 +41,9 @@ namespace osu.Game.Beatmaps return null; if (ruleset != null) - return $@"{api.EndpointConfiguration.WebsiteRootUrl}/beatmapsets/{beatmapSetInfo.OnlineID}#{ruleset.ShortName}"; + return $@"{api.Endpoints.WebsiteUrl}/beatmapsets/{beatmapSetInfo.OnlineID}#{ruleset.ShortName}"; - return $@"{api.EndpointConfiguration.WebsiteRootUrl}/beatmapsets/{beatmapSetInfo.OnlineID}"; + return $@"{api.Endpoints.WebsiteUrl}/beatmapsets/{beatmapSetInfo.OnlineID}"; } } } diff --git a/osu.Game/Online/API/APIAccess.cs b/osu.Game/Online/API/APIAccess.cs index ef7b49868c..88f9b3f242 100644 --- a/osu.Game/Online/API/APIAccess.cs +++ b/osu.Game/Online/API/APIAccess.cs @@ -40,7 +40,7 @@ namespace osu.Game.Online.API private readonly Queue queue = new Queue(); - public EndpointConfiguration EndpointConfiguration { get; } + public EndpointConfiguration Endpoints { get; } /// /// The API response version. @@ -73,7 +73,7 @@ namespace osu.Game.Online.API private readonly CancellationTokenSource cancellationToken = new CancellationTokenSource(); private readonly Logger log; - public APIAccess(OsuGameBase game, OsuConfigManager config, EndpointConfiguration endpointConfiguration, string versionHash) + public APIAccess(OsuGameBase game, OsuConfigManager config, EndpointConfiguration endpoints, string versionHash) { this.game = game; this.config = config; @@ -87,13 +87,13 @@ namespace osu.Game.Online.API APIVersion = now.Year * 10000 + now.Month * 100 + now.Day; } - EndpointConfiguration = endpointConfiguration; + Endpoints = endpoints; NotificationsClient = setUpNotificationsClient(); - authentication = new OAuth(endpointConfiguration.APIClientID, endpointConfiguration.APIClientSecret, EndpointConfiguration.APIEndpointUrl); + authentication = new OAuth(endpoints.APIClientID, endpoints.APIClientSecret, Endpoints.APIUrl); log = Logger.GetLogger(LoggingTarget.Network); - log.Add($@"API endpoint root: {EndpointConfiguration.APIEndpointUrl}"); + log.Add($@"API endpoint root: {Endpoints.APIUrl}"); log.Add($@"API request version: {APIVersion}"); ProvidedUsername = config.Get(OsuSetting.Username); @@ -405,7 +405,7 @@ namespace osu.Game.Online.API var req = new RegistrationRequest { - Url = $@"{EndpointConfiguration.APIEndpointUrl}/users", + Url = $@"{Endpoints.APIUrl}/users", Method = HttpMethod.Post, Username = username, Email = email, diff --git a/osu.Game/Online/API/APIRequest.cs b/osu.Game/Online/API/APIRequest.cs index 575e6f8a10..9d9873cc6f 100644 --- a/osu.Game/Online/API/APIRequest.cs +++ b/osu.Game/Online/API/APIRequest.cs @@ -71,7 +71,7 @@ namespace osu.Game.Online.API protected virtual WebRequest CreateWebRequest() => new OsuWebRequest(Uri); - protected virtual string Uri => $@"{API!.EndpointConfiguration.APIEndpointUrl}/api/v2/{Target}"; + protected virtual string Uri => $@"{API!.Endpoints.APIUrl}/api/v2/{Target}"; protected IAPIProvider? API; diff --git a/osu.Game/Online/API/DummyAPIAccess.cs b/osu.Game/Online/API/DummyAPIAccess.cs index 7b3a8f357b..f9649cdd88 100644 --- a/osu.Game/Online/API/DummyAPIAccess.cs +++ b/osu.Game/Online/API/DummyAPIAccess.cs @@ -41,10 +41,10 @@ namespace osu.Game.Online.API public string ProvidedUsername => LocalUser.Value.Username; - public EndpointConfiguration EndpointConfiguration { get; } = new EndpointConfiguration + public EndpointConfiguration Endpoints { get; } = new EndpointConfiguration { - APIEndpointUrl = "http://localhost", - WebsiteRootUrl = "http://localhost", + APIUrl = "http://localhost", + WebsiteUrl = "http://localhost", }; public int APIVersion => int.Parse(DateTime.Now.ToString("yyyyMMdd")); diff --git a/osu.Game/Online/API/IAPIProvider.cs b/osu.Game/Online/API/IAPIProvider.cs index 048193def7..54eaaaafc2 100644 --- a/osu.Game/Online/API/IAPIProvider.cs +++ b/osu.Game/Online/API/IAPIProvider.cs @@ -53,7 +53,7 @@ namespace osu.Game.Online.API /// /// Holds configuration for online endpoints. /// - EndpointConfiguration EndpointConfiguration { get; } + EndpointConfiguration Endpoints { get; } /// /// The version of the API. diff --git a/osu.Game/Online/API/Requests/PatchBeatmapPackageRequest.cs b/osu.Game/Online/API/Requests/PatchBeatmapPackageRequest.cs index bb9d32f77b..ffe7b5d1ec 100644 --- a/osu.Game/Online/API/Requests/PatchBeatmapPackageRequest.cs +++ b/osu.Game/Online/API/Requests/PatchBeatmapPackageRequest.cs @@ -15,10 +15,10 @@ namespace osu.Game.Online.API.Requests get { // can be removed once the service has been successfully deployed to production - if (API!.EndpointConfiguration.BeatmapSubmissionServiceUrl == null) + if (API!.Endpoints.BeatmapSubmissionServiceUrl == null) throw new NotSupportedException("Beatmap submission not supported in this configuration!"); - return $@"{API!.EndpointConfiguration.BeatmapSubmissionServiceUrl!}/beatmapsets/{BeatmapSetID}"; + return $@"{API!.Endpoints.BeatmapSubmissionServiceUrl!}/beatmapsets/{BeatmapSetID}"; } } diff --git a/osu.Game/Online/API/Requests/PutBeatmapSetRequest.cs b/osu.Game/Online/API/Requests/PutBeatmapSetRequest.cs index 03b8397681..fb25749786 100644 --- a/osu.Game/Online/API/Requests/PutBeatmapSetRequest.cs +++ b/osu.Game/Online/API/Requests/PutBeatmapSetRequest.cs @@ -21,10 +21,10 @@ namespace osu.Game.Online.API.Requests get { // can be removed once the service has been successfully deployed to production - if (API!.EndpointConfiguration.BeatmapSubmissionServiceUrl == null) + if (API!.Endpoints.BeatmapSubmissionServiceUrl == null) throw new NotSupportedException("Beatmap submission not supported in this configuration!"); - return $@"{API!.EndpointConfiguration.BeatmapSubmissionServiceUrl}/beatmapsets"; + return $@"{API!.Endpoints.BeatmapSubmissionServiceUrl}/beatmapsets"; } } diff --git a/osu.Game/Online/API/Requests/ReplaceBeatmapPackageRequest.cs b/osu.Game/Online/API/Requests/ReplaceBeatmapPackageRequest.cs index c9dd12d61e..2e224ce602 100644 --- a/osu.Game/Online/API/Requests/ReplaceBeatmapPackageRequest.cs +++ b/osu.Game/Online/API/Requests/ReplaceBeatmapPackageRequest.cs @@ -14,10 +14,10 @@ namespace osu.Game.Online.API.Requests get { // can be removed once the service has been successfully deployed to production - if (API!.EndpointConfiguration.BeatmapSubmissionServiceUrl == null) + if (API!.Endpoints.BeatmapSubmissionServiceUrl == null) throw new NotSupportedException("Beatmap submission not supported in this configuration!"); - return $@"{API!.EndpointConfiguration.BeatmapSubmissionServiceUrl}/beatmapsets/{BeatmapSetID}"; + return $@"{API!.Endpoints.BeatmapSubmissionServiceUrl}/beatmapsets/{BeatmapSetID}"; } } diff --git a/osu.Game/Online/Chat/ExternalLinkOpener.cs b/osu.Game/Online/Chat/ExternalLinkOpener.cs index 1615b72033..258cca2ad5 100644 --- a/osu.Game/Online/Chat/ExternalLinkOpener.cs +++ b/osu.Game/Online/Chat/ExternalLinkOpener.cs @@ -49,12 +49,12 @@ namespace osu.Game.Online.Chat if (url.StartsWith('/')) { - url = $"{api.EndpointConfiguration.WebsiteRootUrl}{url}"; + url = $"{api.Endpoints.WebsiteUrl}{url}"; isTrustedDomain = true; } else { - isTrustedDomain = url.StartsWith(api.EndpointConfiguration.WebsiteRootUrl, StringComparison.Ordinal); + isTrustedDomain = url.StartsWith(api.Endpoints.WebsiteUrl, StringComparison.Ordinal); } if (!url.CheckIsValidUrl()) diff --git a/osu.Game/Online/Chat/NowPlayingCommand.cs b/osu.Game/Online/Chat/NowPlayingCommand.cs index 5e71980a55..43452a768c 100644 --- a/osu.Game/Online/Chat/NowPlayingCommand.cs +++ b/osu.Game/Online/Chat/NowPlayingCommand.cs @@ -95,7 +95,7 @@ namespace osu.Game.Online.Chat string getBeatmapPart() { - return beatmapOnlineID > 0 ? $"[{api.EndpointConfiguration.WebsiteRootUrl}/b/{beatmapOnlineID} {beatmapDisplayTitle}]" : beatmapDisplayTitle; + return beatmapOnlineID > 0 ? $"[{api.Endpoints.WebsiteUrl}/b/{beatmapOnlineID} {beatmapDisplayTitle}]" : beatmapDisplayTitle; } string getRulesetPart() diff --git a/osu.Game/Online/DevelopmentEndpointConfiguration.cs b/osu.Game/Online/DevelopmentEndpointConfiguration.cs index 5f3c353f4d..f4e1b257ee 100644 --- a/osu.Game/Online/DevelopmentEndpointConfiguration.cs +++ b/osu.Game/Online/DevelopmentEndpointConfiguration.cs @@ -7,12 +7,12 @@ namespace osu.Game.Online { public DevelopmentEndpointConfiguration() { - WebsiteRootUrl = APIEndpointUrl = @"https://dev.ppy.sh"; + WebsiteUrl = APIUrl = @"https://dev.ppy.sh"; APIClientSecret = @"3LP2mhUrV89xxzD1YKNndXHEhWWCRLPNKioZ9ymT"; APIClientID = "5"; - SpectatorEndpointUrl = $@"{APIEndpointUrl}/signalr/spectator"; - MultiplayerEndpointUrl = $@"{APIEndpointUrl}/signalr/multiplayer"; - MetadataEndpointUrl = $@"{APIEndpointUrl}/signalr/metadata"; + SpectatorUrl = $@"{APIUrl}/signalr/spectator"; + MultiplayerUrl = $@"{APIUrl}/signalr/multiplayer"; + MetadataUrl = $@"{APIUrl}/signalr/metadata"; } } } diff --git a/osu.Game/Online/EndpointConfiguration.cs b/osu.Game/Online/EndpointConfiguration.cs index 39dd72d41a..2d5ea32345 100644 --- a/osu.Game/Online/EndpointConfiguration.cs +++ b/osu.Game/Online/EndpointConfiguration.cs @@ -8,16 +8,6 @@ namespace osu.Game.Online /// public class EndpointConfiguration { - /// - /// The base URL for the website. Does not include a trailing slash. - /// - public string WebsiteRootUrl { get; set; } = string.Empty; - - /// - /// The endpoint for the main (osu-web) API. Does not include a trailing slash. - /// - public string APIEndpointUrl { get; set; } = string.Empty; - /// /// The OAuth client secret. /// @@ -29,23 +19,33 @@ namespace osu.Game.Online public string APIClientID { get; set; } = string.Empty; /// - /// The endpoint for the SignalR spectator server. + /// The base URL for the website. Does not include a trailing slash. /// - public string SpectatorEndpointUrl { get; set; } = string.Empty; + public string WebsiteUrl { get; set; } = string.Empty; /// - /// The endpoint for the SignalR multiplayer server. + /// The endpoint for the main (osu-web) API. Does not include a trailing slash. /// - public string MultiplayerEndpointUrl { get; set; } = string.Empty; - - /// - /// The endpoint for the SignalR metadata server. - /// - public string MetadataEndpointUrl { get; set; } = string.Empty; + public string APIUrl { get; set; } = string.Empty; /// /// The root URL for the service handling beatmap submission. Does not include a trailing slash. /// public string? BeatmapSubmissionServiceUrl { get; set; } + + /// + /// The endpoint for the SignalR spectator server. + /// + public string SpectatorUrl { get; set; } = string.Empty; + + /// + /// The endpoint for the SignalR multiplayer server. + /// + public string MultiplayerUrl { get; set; } = string.Empty; + + /// + /// The endpoint for the SignalR metadata server. + /// + public string MetadataUrl { get; set; } = string.Empty; } } diff --git a/osu.Game/Online/Leaderboards/LeaderboardScore.cs b/osu.Game/Online/Leaderboards/LeaderboardScore.cs index f7efa08969..52074119b8 100644 --- a/osu.Game/Online/Leaderboards/LeaderboardScore.cs +++ b/osu.Game/Online/Leaderboards/LeaderboardScore.cs @@ -436,7 +436,7 @@ namespace osu.Game.Online.Leaderboards items.Add(new OsuMenuItem("Use these mods", MenuItemType.Highlighted, () => songSelect.Mods.Value = Score.Mods)); if (Score.OnlineID > 0) - items.Add(new OsuMenuItem(CommonStrings.CopyLink, MenuItemType.Standard, () => clipboard?.SetText($@"{api.EndpointConfiguration.WebsiteRootUrl}/scores/{Score.OnlineID}"))); + items.Add(new OsuMenuItem(CommonStrings.CopyLink, MenuItemType.Standard, () => clipboard?.SetText($@"{api.Endpoints.WebsiteUrl}/scores/{Score.OnlineID}"))); if (Score.Files.Count > 0) { diff --git a/osu.Game/Online/Metadata/OnlineMetadataClient.cs b/osu.Game/Online/Metadata/OnlineMetadataClient.cs index c7c7dfc58b..6637fc8dba 100644 --- a/osu.Game/Online/Metadata/OnlineMetadataClient.cs +++ b/osu.Game/Online/Metadata/OnlineMetadataClient.cs @@ -47,7 +47,7 @@ namespace osu.Game.Online.Metadata public OnlineMetadataClient(EndpointConfiguration endpoints) { - endpoint = endpoints.MetadataEndpointUrl; + endpoint = endpoints.MetadataUrl; } [BackgroundDependencyLoader] diff --git a/osu.Game/Online/Multiplayer/OnlineMultiplayerClient.cs b/osu.Game/Online/Multiplayer/OnlineMultiplayerClient.cs index 2660cd94e4..a485a6b262 100644 --- a/osu.Game/Online/Multiplayer/OnlineMultiplayerClient.cs +++ b/osu.Game/Online/Multiplayer/OnlineMultiplayerClient.cs @@ -32,7 +32,7 @@ namespace osu.Game.Online.Multiplayer public OnlineMultiplayerClient(EndpointConfiguration endpoints) { - endpoint = endpoints.MultiplayerEndpointUrl; + endpoint = endpoints.MultiplayerUrl; } [BackgroundDependencyLoader] diff --git a/osu.Game/Online/ProductionEndpointConfiguration.cs b/osu.Game/Online/ProductionEndpointConfiguration.cs index 0244761b65..6e06abbeed 100644 --- a/osu.Game/Online/ProductionEndpointConfiguration.cs +++ b/osu.Game/Online/ProductionEndpointConfiguration.cs @@ -7,12 +7,12 @@ namespace osu.Game.Online { public ProductionEndpointConfiguration() { - WebsiteRootUrl = APIEndpointUrl = @"https://osu.ppy.sh"; + WebsiteUrl = APIUrl = @"https://osu.ppy.sh"; APIClientSecret = @"FGc9GAtyHzeQDshWP5Ah7dega8hJACAJpQtw6OXk"; APIClientID = "5"; - SpectatorEndpointUrl = "https://spectator.ppy.sh/spectator"; - MultiplayerEndpointUrl = "https://spectator.ppy.sh/multiplayer"; - MetadataEndpointUrl = "https://spectator.ppy.sh/metadata"; + SpectatorUrl = "https://spectator.ppy.sh/spectator"; + MultiplayerUrl = "https://spectator.ppy.sh/multiplayer"; + MetadataUrl = "https://spectator.ppy.sh/metadata"; } } } diff --git a/osu.Game/Online/Spectator/OnlineSpectatorClient.cs b/osu.Game/Online/Spectator/OnlineSpectatorClient.cs index 645d7054dc..29d174f8e3 100644 --- a/osu.Game/Online/Spectator/OnlineSpectatorClient.cs +++ b/osu.Game/Online/Spectator/OnlineSpectatorClient.cs @@ -24,7 +24,7 @@ namespace osu.Game.Online.Spectator public OnlineSpectatorClient(EndpointConfiguration endpoints) { - endpoint = endpoints.SpectatorEndpointUrl; + endpoint = endpoints.SpectatorUrl; } [BackgroundDependencyLoader] diff --git a/osu.Game/OsuGameBase.cs b/osu.Game/OsuGameBase.cs index 5e247ca877..7d35207bbe 100644 --- a/osu.Game/OsuGameBase.cs +++ b/osu.Game/OsuGameBase.cs @@ -295,7 +295,7 @@ namespace osu.Game EndpointConfiguration endpoints = CreateEndpoints(); - MessageFormatter.WebsiteRootUrl = endpoints.WebsiteRootUrl; + MessageFormatter.WebsiteRootUrl = endpoints.WebsiteUrl; frameworkLocale = frameworkConfig.GetBindable(FrameworkSetting.Locale); frameworkLocale.BindValueChanged(_ => updateLanguage()); diff --git a/osu.Game/Overlays/Comments/DrawableComment.cs b/osu.Game/Overlays/Comments/DrawableComment.cs index b06be3e74a..0d566174bb 100644 --- a/osu.Game/Overlays/Comments/DrawableComment.cs +++ b/osu.Game/Overlays/Comments/DrawableComment.cs @@ -419,7 +419,7 @@ namespace osu.Game.Overlays.Comments private void copyUrl() { - clipboard.SetText($@"{api.EndpointConfiguration.APIEndpointUrl}/comments/{Comment.Id}"); + clipboard.SetText($@"{api.Endpoints.APIUrl}/comments/{Comment.Id}"); onScreenDisplay?.Display(new CopyUrlToast()); } diff --git a/osu.Game/Overlays/Login/LoginForm.cs b/osu.Game/Overlays/Login/LoginForm.cs index 2b6d523b95..215a946b42 100644 --- a/osu.Game/Overlays/Login/LoginForm.cs +++ b/osu.Game/Overlays/Login/LoginForm.cs @@ -129,7 +129,7 @@ namespace osu.Game.Overlays.Login } }; - forgottenPasswordLink.AddLink(LayoutStrings.PopupLoginLoginForgot, $"{api.EndpointConfiguration.WebsiteRootUrl}/home/password-reset"); + forgottenPasswordLink.AddLink(LayoutStrings.PopupLoginLoginForgot, $"{api.Endpoints.WebsiteUrl}/home/password-reset"); password.OnCommit += (_, _) => performLogin(); diff --git a/osu.Game/Overlays/Login/SecondFactorAuthForm.cs b/osu.Game/Overlays/Login/SecondFactorAuthForm.cs index e36d62f827..74db58e225 100644 --- a/osu.Game/Overlays/Login/SecondFactorAuthForm.cs +++ b/osu.Game/Overlays/Login/SecondFactorAuthForm.cs @@ -98,7 +98,7 @@ namespace osu.Game.Overlays.Login explainText.AddParagraph(UserVerificationStrings.BoxInfoCheckSpam); // We can't support localisable strings with nested links yet. Not sure if we even can (probably need to allow markdown link formatting or something). explainText.AddParagraph("If you can't access your email or have forgotten what you used, please follow the "); - explainText.AddLink(UserVerificationStrings.BoxInfoRecoverLink, $"{api.EndpointConfiguration.WebsiteRootUrl}/home/password-reset"); + explainText.AddLink(UserVerificationStrings.BoxInfoRecoverLink, $"{api.Endpoints.WebsiteUrl}/home/password-reset"); explainText.AddText(". You can also "); explainText.AddLink(UserVerificationStrings.BoxInfoReissueLink, () => { diff --git a/osu.Game/Overlays/Profile/Header/BottomHeaderContainer.cs b/osu.Game/Overlays/Profile/Header/BottomHeaderContainer.cs index d9d23f16fd..03c849052b 100644 --- a/osu.Game/Overlays/Profile/Header/BottomHeaderContainer.cs +++ b/osu.Game/Overlays/Profile/Header/BottomHeaderContainer.cs @@ -124,12 +124,12 @@ namespace osu.Game.Overlays.Profile.Header } topLinkContainer.AddText("Contributed "); - topLinkContainer.AddLink("forum post".ToQuantity(user.PostCount, "#,##0"), $"{api.EndpointConfiguration.WebsiteRootUrl}/users/{user.Id}/posts", creationParameters: embolden); + topLinkContainer.AddLink("forum post".ToQuantity(user.PostCount, "#,##0"), $"{api.Endpoints.WebsiteUrl}/users/{user.Id}/posts", creationParameters: embolden); addSpacer(topLinkContainer); topLinkContainer.AddText("Posted "); - topLinkContainer.AddLink("comment".ToQuantity(user.CommentsCount, "#,##0"), $"{api.EndpointConfiguration.WebsiteRootUrl}/comments?user_id={user.Id}", creationParameters: embolden); + topLinkContainer.AddLink("comment".ToQuantity(user.CommentsCount, "#,##0"), $"{api.Endpoints.WebsiteUrl}/comments?user_id={user.Id}", creationParameters: embolden); string websiteWithoutProtocol = user.Website; diff --git a/osu.Game/Overlays/Profile/Header/Components/DrawableTournamentBanner.cs b/osu.Game/Overlays/Profile/Header/Components/DrawableTournamentBanner.cs index a66a5c8fe9..b036b0a305 100644 --- a/osu.Game/Overlays/Profile/Header/Components/DrawableTournamentBanner.cs +++ b/osu.Game/Overlays/Profile/Header/Components/DrawableTournamentBanner.cs @@ -33,7 +33,7 @@ namespace osu.Game.Overlays.Profile.Header.Components Texture = textures.Get(banner.Image), }; - Action = () => game?.OpenUrlExternally($@"{api.EndpointConfiguration.WebsiteRootUrl}/community/tournaments/{banner.TournamentId}"); + Action = () => game?.OpenUrlExternally($@"{api.Endpoints.WebsiteUrl}/community/tournaments/{banner.TournamentId}"); } protected override void LoadComplete() diff --git a/osu.Game/Overlays/Profile/Header/TopHeaderContainer.cs b/osu.Game/Overlays/Profile/Header/TopHeaderContainer.cs index fb1bdca57c..ba2cd5b705 100644 --- a/osu.Game/Overlays/Profile/Header/TopHeaderContainer.cs +++ b/osu.Game/Overlays/Profile/Header/TopHeaderContainer.cs @@ -213,7 +213,7 @@ namespace osu.Game.Overlays.Profile.Header cover.User = user; avatar.User = user; usernameText.Text = user?.Username ?? string.Empty; - openUserExternally.Link = $@"{api.EndpointConfiguration.WebsiteRootUrl}/users/{user?.Id ?? 0}"; + openUserExternally.Link = $@"{api.Endpoints.WebsiteUrl}/users/{user?.Id ?? 0}"; userFlag.CountryCode = user?.CountryCode ?? default; userCountryText.Text = (user?.CountryCode ?? default).GetDescription(); userCountryContainer.Action = () => rankingsOverlay?.ShowCountry(user?.CountryCode ?? default); diff --git a/osu.Game/Overlays/Profile/Sections/Recent/DrawableRecentActivity.cs b/osu.Game/Overlays/Profile/Sections/Recent/DrawableRecentActivity.cs index a0bcf2dc47..05762f29f9 100644 --- a/osu.Game/Overlays/Profile/Sections/Recent/DrawableRecentActivity.cs +++ b/osu.Game/Overlays/Profile/Sections/Recent/DrawableRecentActivity.cs @@ -223,7 +223,7 @@ namespace osu.Game.Overlays.Profile.Sections.Recent private void addBeatmapsetLink() => content.AddLink(activity.Beatmapset.AsNonNull().Title, LinkAction.OpenBeatmapSet, getLinkArgument(activity.Beatmapset.AsNonNull().Url), creationParameters: t => t.Font = getLinkFont()); - private object getLinkArgument(string url) => MessageFormatter.GetLinkDetails($"{api.EndpointConfiguration.WebsiteRootUrl}{url}").Argument.AsNonNull(); + private object getLinkArgument(string url) => MessageFormatter.GetLinkDetails($"{api.Endpoints.WebsiteUrl}{url}").Argument.AsNonNull(); private FontUsage getLinkFont(FontWeight fontWeight = FontWeight.Regular) => OsuFont.GetFont(size: font_size, weight: fontWeight, italics: true); diff --git a/osu.Game/Overlays/Wiki/WikiPanelContainer.cs b/osu.Game/Overlays/Wiki/WikiPanelContainer.cs index 773dde6436..81bdae5525 100644 --- a/osu.Game/Overlays/Wiki/WikiPanelContainer.cs +++ b/osu.Game/Overlays/Wiki/WikiPanelContainer.cs @@ -54,7 +54,7 @@ namespace osu.Game.Overlays.Wiki Padding = new MarginPadding(padding), Child = new WikiPanelMarkdownContainer(isFullWidth) { - CurrentPath = $@"{api.EndpointConfiguration.WebsiteRootUrl}/wiki/", + CurrentPath = $@"{api.Endpoints.WebsiteUrl}/wiki/", Text = text, RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y diff --git a/osu.Game/Overlays/WikiOverlay.cs b/osu.Game/Overlays/WikiOverlay.cs index c360d1eb9e..e9099f1deb 100644 --- a/osu.Game/Overlays/WikiOverlay.cs +++ b/osu.Game/Overlays/WikiOverlay.cs @@ -167,7 +167,7 @@ namespace osu.Game.Overlays } else { - LoadDisplay(articlePage = new WikiArticlePage($@"{api.EndpointConfiguration.WebsiteRootUrl}/wiki/{path.Value}/", response.Markdown)); + LoadDisplay(articlePage = new WikiArticlePage($@"{api.Endpoints.WebsiteUrl}/wiki/{path.Value}/", response.Markdown)); } } @@ -176,7 +176,7 @@ namespace osu.Game.Overlays wikiData.Value = null; path.Value = "error"; - LoadDisplay(articlePage = new WikiArticlePage($@"{api.EndpointConfiguration.WebsiteRootUrl}/wiki/", + LoadDisplay(articlePage = new WikiArticlePage($@"{api.Endpoints.WebsiteUrl}/wiki/", $"Something went wrong when trying to fetch page \"{originalPath}\".\n\n[Return to the main page]({INDEX_PATH}).")); } diff --git a/osu.Game/Screens/Edit/Submission/ScreenFrequentlyAskedQuestions.cs b/osu.Game/Screens/Edit/Submission/ScreenFrequentlyAskedQuestions.cs index ff9cb07e2d..861c5051f4 100644 --- a/osu.Game/Screens/Edit/Submission/ScreenFrequentlyAskedQuestions.cs +++ b/osu.Game/Screens/Edit/Submission/ScreenFrequentlyAskedQuestions.cs @@ -46,14 +46,14 @@ namespace osu.Game.Screens.Edit.Submission RelativeSizeAxes = Axes.X, Caption = BeatmapSubmissionStrings.MappingHelpForumDescription, ButtonText = BeatmapSubmissionStrings.MappingHelpForum, - Action = () => game?.OpenUrlExternally($@"{api.EndpointConfiguration.WebsiteRootUrl}/community/forums/56"), + Action = () => game?.OpenUrlExternally($@"{api.Endpoints.WebsiteUrl}/community/forums/56"), }, new FormButton { RelativeSizeAxes = Axes.X, Caption = BeatmapSubmissionStrings.ModdingQueuesForumDescription, ButtonText = BeatmapSubmissionStrings.ModdingQueuesForum, - Action = () => game?.OpenUrlExternally($@"{api.EndpointConfiguration.WebsiteRootUrl}/community/forums/60"), + Action = () => game?.OpenUrlExternally($@"{api.Endpoints.WebsiteUrl}/community/forums/60"), }, }, }); diff --git a/osu.Game/Screens/OnlinePlay/Lounge/Components/DrawableRoom.cs b/osu.Game/Screens/OnlinePlay/Lounge/Components/DrawableRoom.cs index 7b2e2c02f7..de5813ce0d 100644 --- a/osu.Game/Screens/OnlinePlay/Lounge/Components/DrawableRoom.cs +++ b/osu.Game/Screens/OnlinePlay/Lounge/Components/DrawableRoom.cs @@ -361,7 +361,7 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components return items.ToArray(); - string formatRoomUrl(long id) => $@"{api.EndpointConfiguration.WebsiteRootUrl}/multiplayer/rooms/{id}"; + string formatRoomUrl(long id) => $@"{api.Endpoints.WebsiteUrl}/multiplayer/rooms/{id}"; } } diff --git a/osu.Game/Screens/SelectV2/Leaderboards/LeaderboardScoreV2.cs b/osu.Game/Screens/SelectV2/Leaderboards/LeaderboardScoreV2.cs index 2460fbe6f8..a2253b413c 100644 --- a/osu.Game/Screens/SelectV2/Leaderboards/LeaderboardScoreV2.cs +++ b/osu.Game/Screens/SelectV2/Leaderboards/LeaderboardScoreV2.cs @@ -778,7 +778,7 @@ namespace osu.Game.Screens.SelectV2.Leaderboards items.Add(new OsuMenuItem("Use these mods", MenuItemType.Highlighted, () => SelectedMods.Value = score.Mods.Where(m => IsValidMod.Invoke(m)).ToArray())); if (score.OnlineID > 0) - items.Add(new OsuMenuItem(CommonStrings.CopyLink, MenuItemType.Standard, () => clipboard?.SetText($@"{api.EndpointConfiguration.WebsiteRootUrl}/scores/{score.OnlineID}"))); + items.Add(new OsuMenuItem(CommonStrings.CopyLink, MenuItemType.Standard, () => clipboard?.SetText($@"{api.Endpoints.WebsiteUrl}/scores/{score.OnlineID}"))); if (score.Files.Count <= 0) return items.ToArray(); diff --git a/osu.Game/Utils/SentryLogger.cs b/osu.Game/Utils/SentryLogger.cs index ed644bf5cb..2172ea895e 100644 --- a/osu.Game/Utils/SentryLogger.cs +++ b/osu.Game/Utils/SentryLogger.cs @@ -41,7 +41,7 @@ namespace osu.Game.Utils { this.game = game; - if (!game.IsDeployedBuild || !game.CreateEndpoints().WebsiteRootUrl.EndsWith(@".ppy.sh", StringComparison.Ordinal)) + if (!game.IsDeployedBuild || !game.CreateEndpoints().WebsiteUrl.EndsWith(@".ppy.sh", StringComparison.Ordinal)) return; sentrySession = SentrySdk.Init(options => From 3da615481eb59a2aad22501e74121f2b0e06323e Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 7 Feb 2025 17:38:24 +0900 Subject: [PATCH 0952/1275] Change `switch` to simple conditional for now --- .../Screens/SelectV2/BeatmapCarouselFilterGrouping.cs | 11 +---------- 1 file changed, 1 insertion(+), 10 deletions(-) diff --git a/osu.Game/Screens/SelectV2/BeatmapCarouselFilterGrouping.cs b/osu.Game/Screens/SelectV2/BeatmapCarouselFilterGrouping.cs index 8838ce67ad..db407fd647 100644 --- a/osu.Game/Screens/SelectV2/BeatmapCarouselFilterGrouping.cs +++ b/osu.Game/Screens/SelectV2/BeatmapCarouselFilterGrouping.cs @@ -51,16 +51,7 @@ namespace osu.Game.Screens.SelectV2 HashSet? groupRefItems = null; HashSet? setRefItems = null; - switch (criteria.Group) - { - default: - BeatmapSetsGroupedTogether = true; - break; - - case GroupMode.Difficulty: - BeatmapSetsGroupedTogether = false; - break; - } + BeatmapSetsGroupedTogether = criteria.Group != GroupMode.Difficulty; foreach (var item in items) { From 29b0b62ffa55ebb4ac4b107a691c95a3f72f516d Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 7 Feb 2025 17:39:38 +0900 Subject: [PATCH 0953/1275] Rename variables to something more sane --- .../SelectV2/BeatmapCarouselFilterGrouping.cs | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/osu.Game/Screens/SelectV2/BeatmapCarouselFilterGrouping.cs b/osu.Game/Screens/SelectV2/BeatmapCarouselFilterGrouping.cs index db407fd647..cb5a40918c 100644 --- a/osu.Game/Screens/SelectV2/BeatmapCarouselFilterGrouping.cs +++ b/osu.Game/Screens/SelectV2/BeatmapCarouselFilterGrouping.cs @@ -48,8 +48,8 @@ namespace osu.Game.Screens.SelectV2 BeatmapInfo? lastBeatmap = null; GroupDefinition? lastGroup = null; - HashSet? groupRefItems = null; - HashSet? setRefItems = null; + HashSet? currentGroupItems = null; + HashSet? currentSetItems = null; BeatmapSetsGroupedTogether = criteria.Group != GroupMode.Difficulty; @@ -62,10 +62,10 @@ namespace osu.Game.Screens.SelectV2 if (createGroupIfRequired(criteria, beatmap, lastGroup) is GroupDefinition newGroup) { // When reaching a new group, ensure we reset any beatmap set tracking. - setRefItems = null; + currentSetItems = null; lastBeatmap = null; - groupItems[newGroup] = groupRefItems = new HashSet(); + groupItems[newGroup] = currentGroupItems = new HashSet(); lastGroup = newGroup; addItem(new CarouselItem(newGroup) @@ -81,7 +81,7 @@ namespace osu.Game.Screens.SelectV2 if (newBeatmapSet) { - setItems[beatmap.BeatmapSet!] = setRefItems = new HashSet(); + setItems[beatmap.BeatmapSet!] = currentSetItems = new HashSet(); addItem(new CarouselItem(beatmap.BeatmapSet!) { @@ -98,10 +98,10 @@ namespace osu.Game.Screens.SelectV2 { newItems.Add(i); - groupRefItems?.Add(i); - setRefItems?.Add(i); + currentGroupItems?.Add(i); + currentSetItems?.Add(i); - i.IsVisible = i.Model is GroupDefinition || (lastGroup == null && (i.Model is BeatmapSetInfo || setRefItems == null)); + i.IsVisible = i.Model is GroupDefinition || (lastGroup == null && (i.Model is BeatmapSetInfo || currentSetItems == null)); } } From bf57fef4125bba86595850a6ec13f5f1fcb3f980 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 7 Feb 2025 17:50:32 +0900 Subject: [PATCH 0954/1275] Fix missing cached settings in `BetamapSubmissionOverlay` test --- .../Visual/Editing/TestSceneBeatmapSubmissionOverlay.cs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/osu.Game.Tests/Visual/Editing/TestSceneBeatmapSubmissionOverlay.cs b/osu.Game.Tests/Visual/Editing/TestSceneBeatmapSubmissionOverlay.cs index e3e8c0de39..f83d424d56 100644 --- a/osu.Game.Tests/Visual/Editing/TestSceneBeatmapSubmissionOverlay.cs +++ b/osu.Game.Tests/Visual/Editing/TestSceneBeatmapSubmissionOverlay.cs @@ -24,7 +24,11 @@ namespace osu.Game.Tests.Visual.Editing Child = new DependencyProvidingContainer { RelativeSizeAxes = Axes.Both, - CachedDependencies = new[] { (typeof(ScreenFooter), (object)footer) }, + CachedDependencies = new[] + { + (typeof(ScreenFooter), (object)footer), + (typeof(BeatmapSubmissionSettings), new BeatmapSubmissionSettings()), + }, Children = new Drawable[] { receptor, From 46290ae76b81d953253b670c752968906ced6e5f Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 7 Feb 2025 18:05:47 +0900 Subject: [PATCH 0955/1275] Disallow changing beatmap / ruleset while submitting beatmap --- osu.Game/Screens/Edit/Submission/BeatmapSubmissionScreen.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/osu.Game/Screens/Edit/Submission/BeatmapSubmissionScreen.cs b/osu.Game/Screens/Edit/Submission/BeatmapSubmissionScreen.cs index 9794402061..4c7ea39c35 100644 --- a/osu.Game/Screens/Edit/Submission/BeatmapSubmissionScreen.cs +++ b/osu.Game/Screens/Edit/Submission/BeatmapSubmissionScreen.cs @@ -40,6 +40,8 @@ namespace osu.Game.Screens.Edit.Submission public override bool AllowUserExit => false; + public override bool DisallowExternalBeatmapRulesetChanges => true; + [Cached] private readonly OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Aquamarine); From 12881f3f366625ecdd861c66e24120541c428995 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 7 Feb 2025 18:06:31 +0900 Subject: [PATCH 0956/1275] Don't show informational screens for subsequent submissions These are historically only presented to the user when uploading a new beatmap for the first time. --- .../Edit/Submission/BeatmapSubmissionOverlay.cs | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/osu.Game/Screens/Edit/Submission/BeatmapSubmissionOverlay.cs b/osu.Game/Screens/Edit/Submission/BeatmapSubmissionOverlay.cs index da2abd8c23..cf2fef25d5 100644 --- a/osu.Game/Screens/Edit/Submission/BeatmapSubmissionOverlay.cs +++ b/osu.Game/Screens/Edit/Submission/BeatmapSubmissionOverlay.cs @@ -2,6 +2,8 @@ // See the LICENCE file in the repository root for full licence text. using osu.Framework.Allocation; +using osu.Framework.Bindables; +using osu.Game.Beatmaps; using osu.Game.Overlays; using osu.Game.Localisation; @@ -15,10 +17,14 @@ namespace osu.Game.Screens.Edit.Submission } [BackgroundDependencyLoader] - private void load() + private void load(IBindable beatmap) { - AddStep(); - AddStep(); + if (beatmap.Value.BeatmapSetInfo.OnlineID <= 0) + { + AddStep(); + AddStep(); + } + AddStep(); Header.Title = BeatmapSubmissionStrings.BeatmapSubmissionTitle; From 95967a2fde5ae2015c206d35f3edc86eff318388 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 7 Feb 2025 18:17:49 +0900 Subject: [PATCH 0957/1275] Adjust beatmap stream creation to make a bit more sense --- .../Edit/Submission/BeatmapSubmissionScreen.cs | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/Edit/Submission/BeatmapSubmissionScreen.cs b/osu.Game/Screens/Edit/Submission/BeatmapSubmissionScreen.cs index 4c7ea39c35..44b2778869 100644 --- a/osu.Game/Screens/Edit/Submission/BeatmapSubmissionScreen.cs +++ b/osu.Game/Screens/Edit/Submission/BeatmapSubmissionScreen.cs @@ -76,10 +76,10 @@ namespace osu.Game.Screens.Edit.Submission private RoundedButton backButton = null!; private uint? beatmapSetId; + private MemoryStream? beatmapPackageStream; private SubmissionBeatmapExporter legacyBeatmapExporter = null!; private ProgressNotification? exportProgressNotification; - private MemoryStream beatmapPackageStream = null!; private ProgressNotification? updateProgressNotification; [BackgroundDependencyLoader] @@ -189,7 +189,6 @@ namespace osu.Game.Screens.Edit.Submission } } }); - beatmapPackageStream = new MemoryStream(); } private void createBeatmapSet() @@ -239,10 +238,12 @@ namespace osu.Game.Screens.Edit.Submission private async Task createBeatmapPackage(ICollection onlineFiles) { Debug.Assert(ThreadSafety.IsUpdateThread); + exportStep.SetInProgress(); try { + beatmapPackageStream = new MemoryStream(); await legacyBeatmapExporter.ExportToStreamAsync(Beatmap.Value.BeatmapSetInfo.ToLive(realmAccess), beatmapPackageStream, exportProgressNotification = new ProgressNotification()) .ConfigureAwait(true); } @@ -266,6 +267,7 @@ namespace osu.Game.Screens.Edit.Submission private async Task patchBeatmapSet(ICollection onlineFiles) { Debug.Assert(beatmapSetId != null); + Debug.Assert(beatmapPackageStream != null); var onlineFilesByFilename = onlineFiles.ToDictionary(f => f.Filename, f => f.SHA2Hash); @@ -320,6 +322,7 @@ namespace osu.Game.Screens.Edit.Submission private void replaceBeatmapSet() { Debug.Assert(beatmapSetId != null); + Debug.Assert(beatmapPackageStream != null); var uploadRequest = new ReplaceBeatmapPackageRequest(beatmapSetId.Value, beatmapPackageStream.ToArray()); @@ -347,6 +350,8 @@ namespace osu.Game.Screens.Edit.Submission private async Task updateLocalBeatmap() { Debug.Assert(beatmapSetId != null); + Debug.Assert(beatmapPackageStream != null); + updateStep.SetInProgress(); Live? importedSet; @@ -420,5 +425,12 @@ namespace osu.Game.Screens.Edit.Submission overlay.Show(); } + + protected override void Dispose(bool isDisposing) + { + base.Dispose(isDisposing); + + beatmapPackageStream?.Dispose(); + } } } From 783ef0078533c7bf90f13675861a88c03c4242e5 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 7 Feb 2025 18:34:48 +0900 Subject: [PATCH 0958/1275] Change `BeatmapSubmissionScreen` to use global back button instead of custom implementation --- .../Submission/BeatmapSubmissionScreen.cs | 81 ++++++++++--------- 1 file changed, 44 insertions(+), 37 deletions(-) diff --git a/osu.Game/Screens/Edit/Submission/BeatmapSubmissionScreen.cs b/osu.Game/Screens/Edit/Submission/BeatmapSubmissionScreen.cs index 44b2778869..8536ba5f02 100644 --- a/osu.Game/Screens/Edit/Submission/BeatmapSubmissionScreen.cs +++ b/osu.Game/Screens/Edit/Submission/BeatmapSubmissionScreen.cs @@ -21,7 +21,6 @@ using osu.Game.Beatmaps.Drawables.Cards; using osu.Game.Configuration; using osu.Game.Database; using osu.Game.Extensions; -using osu.Game.Graphics.UserInterfaceV2; using osu.Game.IO.Archives; using osu.Game.Localisation; using osu.Game.Online.API; @@ -38,8 +37,6 @@ namespace osu.Game.Screens.Edit.Submission { private BeatmapSubmissionOverlay overlay = null!; - public override bool AllowUserExit => false; - public override bool DisallowExternalBeatmapRulesetChanges => true; [Cached] @@ -73,7 +70,6 @@ namespace osu.Game.Screens.Edit.Submission private SubmissionStageProgress updateStep = null!; private Container successContainer = null!; private Container flashLayer = null!; - private RoundedButton backButton = null!; private uint? beatmapSetId; private MemoryStream? beatmapPackageStream; @@ -82,6 +78,8 @@ namespace osu.Game.Screens.Edit.Submission private ProgressNotification? exportProgressNotification; private ProgressNotification? updateProgressNotification; + private Live? importedSet; + [BackgroundDependencyLoader] private void load() { @@ -161,15 +159,6 @@ namespace osu.Game.Screens.Edit.Submission } } }, - backButton = new RoundedButton - { - Text = CommonStrings.Back, - Width = 150, - Action = this.Exit, - Enabled = { Value = false }, - Anchor = Anchor.TopCentre, - Origin = Anchor.TopCentre, - } } } } @@ -181,7 +170,10 @@ namespace osu.Game.Screens.Edit.Submission if (overlay.State.Value == Visibility.Hidden) { if (!overlay.Completed) + { + allowExit(); this.Exit(); + } else { submissionProgress.FadeIn(200, Easing.OutQuint); @@ -227,8 +219,8 @@ namespace osu.Game.Screens.Edit.Submission createRequest.Failure += ex => { createSetStep.SetFailed(ex.Message); - backButton.Enabled.Value = true; Logger.Log($"Beatmap set submission failed on creation: {ex}"); + allowExit(); }; createSetStep.SetInProgress(); @@ -250,9 +242,9 @@ namespace osu.Game.Screens.Edit.Submission catch (Exception ex) { exportStep.SetFailed(ex.Message); - Logger.Log($"Beatmap set submission failed on export: {ex}"); - backButton.Enabled.Value = true; exportProgressNotification = null; + Logger.Log($"Beatmap set submission failed on export: {ex}"); + allowExit(); } exportStep.SetCompleted(); @@ -311,7 +303,7 @@ namespace osu.Game.Screens.Edit.Submission { uploadStep.SetFailed(ex.Message); Logger.Log($"Beatmap submission failed on upload: {ex}"); - backButton.Enabled.Value = true; + allowExit(); }; patchRequest.Progressed += (current, total) => uploadStep.SetInProgress((float)current / total); @@ -339,7 +331,7 @@ namespace osu.Game.Screens.Edit.Submission { uploadStep.SetFailed(ex.Message); Logger.Log($"Beatmap submission failed on upload: {ex}"); - backButton.Enabled.Value = true; + allowExit(); }; uploadRequest.Progressed += (current, total) => uploadStep.SetInProgress((float)current / Math.Max(total, 1)); @@ -354,8 +346,6 @@ namespace osu.Game.Screens.Edit.Submission updateStep.SetInProgress(); - Live? importedSet; - try { importedSet = await beatmaps.ImportAsUpdate( @@ -367,28 +357,13 @@ namespace osu.Game.Screens.Edit.Submission { updateStep.SetFailed(ex.Message); Logger.Log($"Beatmap submission failed on local update: {ex}"); - Schedule(() => backButton.Enabled.Value = true); + allowExit(); return; } updateStep.SetCompleted(); - backButton.Enabled.Value = true; - backButton.Action = () => - { - game?.PerformFromScreen(s => - { - if (s is OsuScreen osuScreen) - { - Debug.Assert(importedSet != null); - var targetBeatmap = importedSet.Value.Beatmaps.FirstOrDefault(b => b.DifficultyName == Beatmap.Value.BeatmapInfo.DifficultyName) - ?? importedSet.Value.Beatmaps.First(); - osuScreen.Beatmap.Value = beatmaps.GetWorkingBeatmap(targetBeatmap); - } - - s.Push(new EditorLoader()); - }, [typeof(MainMenu)]); - }; showBeatmapCard(); + allowExit(); } private void showBeatmapCard() @@ -408,6 +383,11 @@ namespace osu.Game.Screens.Edit.Submission api.Queue(getBeatmapSetRequest); } + private void allowExit() + { + BackButtonVisibility.Value = true; + } + protected override void Update() { base.Update(); @@ -419,6 +399,33 @@ namespace osu.Game.Screens.Edit.Submission updateStep.SetInProgress(updateProgressNotification.Progress); } + public override bool OnExiting(ScreenExitEvent e) + { + // We probably want a method of cancelling in the future… + if (!BackButtonVisibility.Value) + return true; + + if (importedSet != null) + { + game?.PerformFromScreen(s => + { + if (s is OsuScreen osuScreen) + { + Debug.Assert(importedSet != null); + var targetBeatmap = importedSet.Value.Beatmaps.FirstOrDefault(b => b.DifficultyName == Beatmap.Value.BeatmapInfo.DifficultyName) + ?? importedSet.Value.Beatmaps.First(); + osuScreen.Beatmap.Value = beatmaps.GetWorkingBeatmap(targetBeatmap); + } + + s.Push(new EditorLoader()); + }, [typeof(MainMenu)]); + + return true; + } + + return base.OnExiting(e); + } + public override void OnEntering(ScreenTransitionEvent e) { base.OnEntering(e); From ce88ecfb3cbfb2df90663b6f7ac1d3b8021da22e Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 7 Feb 2025 18:39:01 +0900 Subject: [PATCH 0959/1275] Adjust timeouts to be much higher for upload requests It seems that right now these timeouts do not check for actual data movement, which is to say if a user with a very slow connection is uploading and it takes more than `Timeout`, their upload will fail. For now let's set these values high enough that most users will not be affected. --- osu.Game/Online/API/Requests/PatchBeatmapPackageRequest.cs | 2 +- osu.Game/Online/API/Requests/ReplaceBeatmapPackageRequest.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Online/API/Requests/PatchBeatmapPackageRequest.cs b/osu.Game/Online/API/Requests/PatchBeatmapPackageRequest.cs index 5728dbe3fa..df3c9d071c 100644 --- a/osu.Game/Online/API/Requests/PatchBeatmapPackageRequest.cs +++ b/osu.Game/Online/API/Requests/PatchBeatmapPackageRequest.cs @@ -46,7 +46,7 @@ namespace osu.Game.Online.API.Requests foreach (string filename in FilesDeleted) request.AddParameter(@"filesDeleted", filename, RequestParameterType.Form); - request.Timeout = 60_000; + request.Timeout = 600_000; return request; } } diff --git a/osu.Game/Online/API/Requests/ReplaceBeatmapPackageRequest.cs b/osu.Game/Online/API/Requests/ReplaceBeatmapPackageRequest.cs index 2e224ce602..de8af6a623 100644 --- a/osu.Game/Online/API/Requests/ReplaceBeatmapPackageRequest.cs +++ b/osu.Game/Online/API/Requests/ReplaceBeatmapPackageRequest.cs @@ -38,7 +38,7 @@ namespace osu.Game.Online.API.Requests var request = base.CreateWebRequest(); request.AddFile(@"beatmapArchive", oszPackage); request.Method = HttpMethod.Put; - request.Timeout = 60_000; + request.Timeout = 600_000; return request; } } From 753eae426d7c33978621025424b8dd43081a31fc Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 7 Feb 2025 18:42:36 +0900 Subject: [PATCH 0960/1275] Update strings --- .../Localisation/BeatmapSubmissionStrings.cs | 20 +++++++++---------- .../Submission/BeatmapSubmissionScreen.cs | 8 ++++---- 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/osu.Game/Localisation/BeatmapSubmissionStrings.cs b/osu.Game/Localisation/BeatmapSubmissionStrings.cs index 50b65ab572..3abe8cc515 100644 --- a/osu.Game/Localisation/BeatmapSubmissionStrings.cs +++ b/osu.Game/Localisation/BeatmapSubmissionStrings.cs @@ -45,24 +45,24 @@ namespace osu.Game.Localisation public static LocalisableString ConfirmSubmission => new TranslatableString(getKey(@"confirm_submission"), @"Submit beatmap!"); /// - /// "Exporting beatmap set in compatibility mode..." + /// "Exporting beatmap for compatibility..." /// - public static LocalisableString ExportingBeatmapSet => new TranslatableString(getKey(@"exporting_beatmap_set"), @"Exporting beatmap set in compatibility mode..."); + public static LocalisableString Exporting => new TranslatableString(getKey(@"exporting"), @"Exporting beatmap for compatibility..."); /// - /// "Preparing beatmap set online..." + /// "Preparing for upload..." /// - public static LocalisableString PreparingBeatmapSet => new TranslatableString(getKey(@"preparing_beatmap_set"), @"Preparing beatmap set online..."); + public static LocalisableString Preparing => new TranslatableString(getKey(@"preparing"), @"Preparing for upload..."); /// - /// "Uploading beatmap set contents..." + /// "Uploading beatmap contents..." /// - public static LocalisableString UploadingBeatmapSetContents => new TranslatableString(getKey(@"uploading_beatmap_set_contents"), @"Uploading beatmap set contents..."); + public static LocalisableString Uploading => new TranslatableString(getKey(@"uploading"), @"Uploading beatmap contents..."); /// - /// "Updating local beatmap with relevant changes..." + /// "Finishing up..." /// - public static LocalisableString UpdatingLocalBeatmap => new TranslatableString(getKey(@"updating_local_beatmap"), @"Updating local beatmap with relevant changes..."); + public static LocalisableString Finishing => new TranslatableString(getKey(@"finishing"), @"Finishing up..."); /// /// "Before you continue, we ask you to check whether the content you are uploading has been cleared for upload. Please understand that you are responsible for the content you upload to the platform and if in doubt, should ask permission from the creators before uploading!" @@ -140,9 +140,9 @@ namespace osu.Game.Localisation public static LocalisableString LoadInBrowserAfterSubmission => new TranslatableString(getKey(@"load_in_browser_after_submission"), @"Load in browser after submission"); /// - /// "Note: In order to make it possible for users of all osu! versions to enjoy your beatmap, it will be exported in a backwards-compatible format. While we have made efforts to ensure that that process keeps the beatmap playable in its intended form, some data related to features that previous versions of osu! do not support may be lost." + /// "Note: In order to make it possible for users of all osu! versions to enjoy your beatmap, it will be exported in a backwards-compatible format. While we have made efforts to ensure that process keeps the beatmap playable in its intended form, some data related to features that previous versions of osu! do not support may be lost." /// - public static LocalisableString LegacyExportDisclaimer => new TranslatableString(getKey(@"legacy_export_disclaimer"), @"Note: In order to make it possible for users of all osu! versions to enjoy your beatmap, it will be exported in a backwards-compatible format. While we have made efforts to ensure that that process keeps the beatmap playable in its intended form, some data related to features that previous versions of osu! do not support may be lost."); + public static LocalisableString LegacyExportDisclaimer => new TranslatableString(getKey(@"legacy_export_disclaimer"), @"Note: In order to make it possible for users of all osu! versions to enjoy your beatmap, it will be exported in a backwards-compatible format. While we have made efforts to ensure that process keeps the beatmap playable in its intended form, some data related to features that previous versions of osu! do not support may be lost."); /// /// "Empty beatmaps cannot be submitted." diff --git a/osu.Game/Screens/Edit/Submission/BeatmapSubmissionScreen.cs b/osu.Game/Screens/Edit/Submission/BeatmapSubmissionScreen.cs index 8536ba5f02..41c875ac1f 100644 --- a/osu.Game/Screens/Edit/Submission/BeatmapSubmissionScreen.cs +++ b/osu.Game/Screens/Edit/Submission/BeatmapSubmissionScreen.cs @@ -114,25 +114,25 @@ namespace osu.Game.Screens.Edit.Submission { createSetStep = new SubmissionStageProgress { - StageDescription = BeatmapSubmissionStrings.PreparingBeatmapSet, + StageDescription = BeatmapSubmissionStrings.Preparing, Anchor = Anchor.TopCentre, Origin = Anchor.TopCentre, }, exportStep = new SubmissionStageProgress { - StageDescription = BeatmapSubmissionStrings.ExportingBeatmapSet, + StageDescription = BeatmapSubmissionStrings.Exporting, Anchor = Anchor.TopCentre, Origin = Anchor.TopCentre, }, uploadStep = new SubmissionStageProgress { - StageDescription = BeatmapSubmissionStrings.UploadingBeatmapSetContents, + StageDescription = BeatmapSubmissionStrings.Uploading, Anchor = Anchor.TopCentre, Origin = Anchor.TopCentre, }, updateStep = new SubmissionStageProgress { - StageDescription = BeatmapSubmissionStrings.UpdatingLocalBeatmap, + StageDescription = BeatmapSubmissionStrings.Finishing, Anchor = Anchor.TopCentre, Origin = Anchor.TopCentre, }, From fab5cfd275bb827ab9c81c7d1e1be2a298a403d4 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 7 Feb 2025 18:57:26 +0900 Subject: [PATCH 0961/1275] Fix slider ball rotation not being updated when rewinding to a slider --- .../Objects/Drawables/DrawableSliderBall.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderBall.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderBall.cs index 24c0d0fcf0..9b8b197804 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderBall.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderBall.cs @@ -66,8 +66,9 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables Slider slider = drawableSlider.HitObject; Position = slider.CurvePositionAt(completionProgress); - //0.1 / slider.Path.Distance is the additional progress needed to ensure the diff length is 0.1 - var diff = slider.CurvePositionAt(completionProgress) - slider.CurvePositionAt(Math.Min(1, completionProgress + 0.1 / slider.Path.Distance)); + // 0.1 / slider.Path.Distance is the additional progress needed to ensure the diff length is 0.1 + double checkDistance = 0.1 / slider.Path.Distance; + var diff = slider.CurvePositionAt(Math.Min(1 - checkDistance, completionProgress)) - slider.CurvePositionAt(Math.Min(1, completionProgress + checkDistance)); // Ensure the value is substantially high enough to allow for Atan2 to get a valid angle. // Needed for when near completion, or in case of a very short slider. From aad12024b0db512c32b77cd2b48dd50a64cb7d05 Mon Sep 17 00:00:00 2001 From: Layendan Date: Fri, 7 Feb 2025 03:13:51 -0700 Subject: [PATCH 0962/1275] remove using cache, improve tests, and revert loading --- .../TestSceneAddPlaylistToCollectionButton.cs | 37 ++++++++--- .../AddPlaylistToCollectionButton.cs | 62 +++++++------------ 2 files changed, 50 insertions(+), 49 deletions(-) diff --git a/osu.Game.Tests/Visual/Playlists/TestSceneAddPlaylistToCollectionButton.cs b/osu.Game.Tests/Visual/Playlists/TestSceneAddPlaylistToCollectionButton.cs index acf2c4b3f9..f18488170d 100644 --- a/osu.Game.Tests/Visual/Playlists/TestSceneAddPlaylistToCollectionButton.cs +++ b/osu.Game.Tests/Visual/Playlists/TestSceneAddPlaylistToCollectionButton.cs @@ -4,12 +4,14 @@ using System; using System.Diagnostics; using System.Linq; +using NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Audio; using osu.Framework.Graphics; using osu.Framework.Platform; using osu.Framework.Testing; using osu.Game.Beatmaps; +using osu.Game.Collections; using osu.Game.Database; using osu.Game.Online.Rooms; using osu.Game.Overlays; @@ -17,14 +19,17 @@ using osu.Game.Rulesets; using osu.Game.Rulesets.Osu; using osu.Game.Screens.OnlinePlay.Playlists; using osuTK; +using osuTK.Input; +using SharpCompress; namespace osu.Game.Tests.Visual.Playlists { - public partial class TestSceneAddPlaylistToCollectionButton : OsuTestScene + public partial class TestSceneAddPlaylistToCollectionButton : OsuManualInputManagerTestScene { private BeatmapManager manager = null!; private BeatmapSetInfo importedBeatmap = null!; private Room room = null!; + private AddPlaylistToCollectionButton button = null!; [BackgroundDependencyLoader] private void load(GameHost host, AudioManager audio) @@ -32,6 +37,8 @@ namespace osu.Game.Tests.Visual.Playlists Dependencies.Cache(new RealmRulesetStore(Realm)); Dependencies.Cache(manager = new BeatmapManager(LocalStorage, Realm, API, audio, Resources, host, Beatmap.Default)); Dependencies.Cache(Realm); + + Add(notificationOverlay); } [Cached(typeof(INotificationOverlay))] @@ -44,25 +51,37 @@ namespace osu.Game.Tests.Visual.Playlists [SetUpSteps] public void SetUpSteps() { + AddStep("clear realm", () => Realm.Realm.Write(() => Realm.Realm.RemoveAll())); + + AddStep("clear notifications", () => notificationOverlay.AllNotifications.Empty()); + importBeatmap(); setupRoom(); AddStep("create button", () => { - AddRange(new Drawable[] + Add(button = new AddPlaylistToCollectionButton(room) { - notificationOverlay, - new AddPlaylistToCollectionButton(room) - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Size = new Vector2(300, 40), - } + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Size = new Vector2(300, 40), }); }); } + [Test] + public void TestButtonFlow() + { + AddStep("move mouse to button", () => InputManager.MoveMouseTo(button)); + + AddStep("click button", () => InputManager.Click(MouseButton.Left)); + + AddAssert("notification shown", () => notificationOverlay.AllNotifications.FirstOrDefault(n => n.Text.ToString().StartsWith("Created", StringComparison.Ordinal)) != null); + + AddAssert("realm is updated", () => Realm.Realm.All().FirstOrDefault(c => c.Name == room.Name) != null); + } + private void importBeatmap() => AddStep("import beatmap", () => { var beatmap = CreateBeatmap(new OsuRuleset().RulesetInfo); diff --git a/osu.Game/Screens/OnlinePlay/Playlists/AddPlaylistToCollectionButton.cs b/osu.Game/Screens/OnlinePlay/Playlists/AddPlaylistToCollectionButton.cs index ab3e481f9f..8801d73e9e 100644 --- a/osu.Game/Screens/OnlinePlay/Playlists/AddPlaylistToCollectionButton.cs +++ b/osu.Game/Screens/OnlinePlay/Playlists/AddPlaylistToCollectionButton.cs @@ -2,17 +2,15 @@ // See the LICENCE file in the repository root for full licence text. using System.Linq; -using System.Threading.Tasks; using osu.Framework.Allocation; -using osu.Framework.Extensions; +using osu.Game.Beatmaps; using osu.Game.Collections; using osu.Game.Database; -using osu.Game.Graphics; -using osu.Game.Graphics.UserInterface; using osu.Game.Graphics.UserInterfaceV2; using osu.Game.Online.Rooms; using osu.Game.Overlays; using osu.Game.Overlays.Notifications; +using Realms; namespace osu.Game.Screens.OnlinePlay.Playlists { @@ -20,14 +18,9 @@ namespace osu.Game.Screens.OnlinePlay.Playlists { private readonly Room room; - private LoadingLayer loading = null!; - [Resolved] private RealmAccess realmAccess { get; set; } = null!; - [Resolved] - private BeatmapLookupCache beatmapLookupCache { get; set; } = null!; - [Resolved(canBeNull: true)] private INotificationOverlay? notifications { get; set; } @@ -38,12 +31,8 @@ namespace osu.Game.Screens.OnlinePlay.Playlists } [BackgroundDependencyLoader] - private void load(OsuColour colours) + private void load() { - BackgroundColour = colours.Gray5; - - Add(loading = new LoadingLayer(true, false)); - Action = () => { int[] ids = room.Playlist.Select(item => item.Beatmap.OnlineID).Where(onlineId => onlineId > 0).ToArray(); @@ -54,34 +43,27 @@ namespace osu.Game.Screens.OnlinePlay.Playlists return; } - Enabled.Value = false; - loading.Show(); - beatmapLookupCache.GetBeatmapsAsync(ids).ContinueWith(task => Schedule(() => + string filter = string.Join(" OR ", ids.Select(id => $"(OnlineID == {id})")); + var beatmaps = realmAccess.Realm.All().Filter(filter).ToList(); + + var collection = realmAccess.Realm.All().FirstOrDefault(c => c.Name == room.Name); + + if (collection == null) { - var beatmaps = task.GetResultSafely().Where(item => item?.BeatmapSet != null).ToList(); - - var collection = realmAccess.Realm.All().FirstOrDefault(c => c.Name == room.Name); - - if (collection == null) + collection = new BeatmapCollection(room.Name, beatmaps.Select(i => i.MD5Hash).Distinct().ToList()); + realmAccess.Realm.Write(() => realmAccess.Realm.Add(collection)); + notifications?.Post(new SimpleNotification { Text = $"Created new collection: {room.Name}" }); + } + else + { + collection.ToLive(realmAccess).PerformWrite(c => { - collection = new BeatmapCollection(room.Name, beatmaps.Select(i => i!.MD5Hash).Distinct().ToList()); - realmAccess.Realm.Write(() => realmAccess.Realm.Add(collection)); - notifications?.Post(new SimpleNotification { Text = $"Created new collection: {room.Name}" }); - } - else - { - collection.ToLive(realmAccess).PerformWrite(c => - { - beatmaps = beatmaps.Where(i => !c.BeatmapMD5Hashes.Contains(i!.MD5Hash)).ToList(); - foreach (var item in beatmaps) - c.BeatmapMD5Hashes.Add(item!.MD5Hash); - notifications?.Post(new SimpleNotification { Text = $"Updated collection: {room.Name}" }); - }); - } - - loading.Hide(); - Enabled.Value = true; - }), TaskContinuationOptions.OnlyOnRanToCompletion); + beatmaps = beatmaps.Where(i => !c.BeatmapMD5Hashes.Contains(i.MD5Hash)).ToList(); + foreach (var item in beatmaps) + c.BeatmapMD5Hashes.Add(item.MD5Hash); + notifications?.Post(new SimpleNotification { Text = $"Updated collection: {room.Name}" }); + }); + } }; } } From 9f90ebb2f774bd023befdc73a849fe087cca9550 Mon Sep 17 00:00:00 2001 From: James Wilson Date: Fri, 7 Feb 2025 10:21:12 +0000 Subject: [PATCH 0963/1275] Calculate hit windows in performance calculator instead of databased difficulty attributes (#31735) * Calculate hit windows in performance calculator instead of databased difficulty attributes * Apply mods to beatmap difficulty in osu! performance calculator * Remove `GreatHitWindow` difficulty attribute for osu!mania * Remove use of approach rate and overall difficulty attributes for osu! * Remove use of hit window difficulty attributes in osu!taiko * Remove use of approach rate attribute in osu!catch * Remove unused attribute IDs * Code quality * Fix `computeDeviationUpperBound` being called before `greatHitWindow` is set --- .../Difficulty/CatchDifficultyAttributes.cs | 12 --- .../Difficulty/CatchDifficultyCalculator.cs | 4 - .../Difficulty/CatchPerformanceCalculator.cs | 17 ++++- .../Difficulty/ManiaDifficultyAttributes.cs | 12 --- .../Difficulty/ManiaDifficultyCalculator.cs | 29 -------- .../Difficulty/OsuDifficultyAttributes.cs | 41 ---------- .../Difficulty/OsuDifficultyCalculator.cs | 15 ---- .../Difficulty/OsuPerformanceCalculator.cs | 74 +++++++++++++------ .../Difficulty/TaikoDifficultyAttributes.cs | 22 ------ .../Difficulty/TaikoDifficultyCalculator.cs | 5 -- .../Difficulty/TaikoPerformanceCalculator.cs | 30 ++++++-- .../Difficulty/DifficultyAttributes.cs | 6 -- 12 files changed, 92 insertions(+), 175 deletions(-) diff --git a/osu.Game.Rulesets.Catch/Difficulty/CatchDifficultyAttributes.cs b/osu.Game.Rulesets.Catch/Difficulty/CatchDifficultyAttributes.cs index 5c64643fd4..82c3cfe735 100644 --- a/osu.Game.Rulesets.Catch/Difficulty/CatchDifficultyAttributes.cs +++ b/osu.Game.Rulesets.Catch/Difficulty/CatchDifficultyAttributes.cs @@ -2,7 +2,6 @@ // See the LICENCE file in the repository root for full licence text. using System.Collections.Generic; -using Newtonsoft.Json; using osu.Game.Beatmaps; using osu.Game.Rulesets.Difficulty; @@ -10,15 +9,6 @@ namespace osu.Game.Rulesets.Catch.Difficulty { public class CatchDifficultyAttributes : DifficultyAttributes { - /// - /// The perceived approach rate inclusive of rate-adjusting mods (DT/HT/etc). - /// - /// - /// Rate-adjusting mods don't directly affect the approach rate difficulty value, but have a perceived effect as a result of adjusting audio timing. - /// - [JsonProperty("approach_rate")] - public double ApproachRate { get; set; } - public override IEnumerable<(int attributeId, object value)> ToDatabaseAttributes() { foreach (var v in base.ToDatabaseAttributes()) @@ -26,7 +16,6 @@ namespace osu.Game.Rulesets.Catch.Difficulty // Todo: osu!catch should not output star rating in the 'aim' attribute. yield return (ATTRIB_ID_AIM, StarRating); - yield return (ATTRIB_ID_APPROACH_RATE, ApproachRate); } public override void FromDatabaseAttributes(IReadOnlyDictionary values, IBeatmapOnlineInfo onlineInfo) @@ -34,7 +23,6 @@ namespace osu.Game.Rulesets.Catch.Difficulty base.FromDatabaseAttributes(values, onlineInfo); StarRating = values[ATTRIB_ID_AIM]; - ApproachRate = values[ATTRIB_ID_APPROACH_RATE]; } } } diff --git a/osu.Game.Rulesets.Catch/Difficulty/CatchDifficultyCalculator.cs b/osu.Game.Rulesets.Catch/Difficulty/CatchDifficultyCalculator.cs index 99df2731ff..6434adb63c 100644 --- a/osu.Game.Rulesets.Catch/Difficulty/CatchDifficultyCalculator.cs +++ b/osu.Game.Rulesets.Catch/Difficulty/CatchDifficultyCalculator.cs @@ -36,14 +36,10 @@ namespace osu.Game.Rulesets.Catch.Difficulty if (beatmap.HitObjects.Count == 0) return new CatchDifficultyAttributes { Mods = mods }; - // this is the same as osu!, so there's potential to share the implementation... maybe - double preempt = IBeatmapDifficultyInfo.DifficultyRange(beatmap.Difficulty.ApproachRate, 1800, 1200, 450) / clockRate; - CatchDifficultyAttributes attributes = new CatchDifficultyAttributes { StarRating = Math.Sqrt(skills.OfType().Single().DifficultyValue()) * difficulty_multiplier, Mods = mods, - ApproachRate = preempt > 1200.0 ? -(preempt - 1800.0) / 120.0 : -(preempt - 1200.0) / 150.0 + 5.0, MaxCombo = beatmap.GetMaxCombo(), }; diff --git a/osu.Game.Rulesets.Catch/Difficulty/CatchPerformanceCalculator.cs b/osu.Game.Rulesets.Catch/Difficulty/CatchPerformanceCalculator.cs index 55232a9598..62a9fe250e 100644 --- a/osu.Game.Rulesets.Catch/Difficulty/CatchPerformanceCalculator.cs +++ b/osu.Game.Rulesets.Catch/Difficulty/CatchPerformanceCalculator.cs @@ -3,6 +3,9 @@ using System; using System.Linq; +using osu.Framework.Audio.Track; +using osu.Framework.Extensions.IEnumerableExtensions; +using osu.Game.Beatmaps; using osu.Game.Rulesets.Difficulty; using osu.Game.Rulesets.Mods; using osu.Game.Scoring; @@ -50,7 +53,19 @@ namespace osu.Game.Rulesets.Catch.Difficulty if (catchAttributes.MaxCombo > 0) value *= Math.Min(Math.Pow(score.MaxCombo, 0.8) / Math.Pow(catchAttributes.MaxCombo, 0.8), 1.0); - double approachRate = catchAttributes.ApproachRate; + var difficulty = score.BeatmapInfo!.Difficulty.Clone(); + + score.Mods.OfType().ForEach(m => m.ApplyToDifficulty(difficulty)); + + var track = new TrackVirtual(10000); + score.Mods.OfType().ForEach(m => m.ApplyToTrack(track)); + double clockRate = track.Rate; + + // this is the same as osu!, so there's potential to share the implementation... maybe + double preempt = IBeatmapDifficultyInfo.DifficultyRange(difficulty.ApproachRate, 1800, 1200, 450) / clockRate; + + double approachRate = preempt > 1200.0 ? -(preempt - 1800.0) / 120.0 : -(preempt - 1200.0) / 150.0 + 5.0; + double approachRateFactor = 1.0; if (approachRate > 9.0) approachRateFactor += 0.1 * (approachRate - 9.0); // 10% for each AR above 9 diff --git a/osu.Game.Rulesets.Mania/Difficulty/ManiaDifficultyAttributes.cs b/osu.Game.Rulesets.Mania/Difficulty/ManiaDifficultyAttributes.cs index db60e757e1..512d98f713 100644 --- a/osu.Game.Rulesets.Mania/Difficulty/ManiaDifficultyAttributes.cs +++ b/osu.Game.Rulesets.Mania/Difficulty/ManiaDifficultyAttributes.cs @@ -2,7 +2,6 @@ // See the LICENCE file in the repository root for full licence text. using System.Collections.Generic; -using Newtonsoft.Json; using osu.Game.Beatmaps; using osu.Game.Rulesets.Difficulty; @@ -10,22 +9,12 @@ namespace osu.Game.Rulesets.Mania.Difficulty { public class ManiaDifficultyAttributes : DifficultyAttributes { - /// - /// The hit window for a GREAT hit inclusive of rate-adjusting mods (DT/HT/etc). - /// - /// - /// Rate-adjusting mods do not affect the hit window at all in osu-stable. - /// - [JsonProperty("great_hit_window")] - public double GreatHitWindow { get; set; } - public override IEnumerable<(int attributeId, object value)> ToDatabaseAttributes() { foreach (var v in base.ToDatabaseAttributes()) yield return v; yield return (ATTRIB_ID_DIFFICULTY, StarRating); - yield return (ATTRIB_ID_GREAT_HIT_WINDOW, GreatHitWindow); } public override void FromDatabaseAttributes(IReadOnlyDictionary values, IBeatmapOnlineInfo onlineInfo) @@ -33,7 +22,6 @@ namespace osu.Game.Rulesets.Mania.Difficulty base.FromDatabaseAttributes(values, onlineInfo); StarRating = values[ATTRIB_ID_DIFFICULTY]; - GreatHitWindow = values[ATTRIB_ID_GREAT_HIT_WINDOW]; } } } diff --git a/osu.Game.Rulesets.Mania/Difficulty/ManiaDifficultyCalculator.cs b/osu.Game.Rulesets.Mania/Difficulty/ManiaDifficultyCalculator.cs index 1efa7cb42f..06b8018f2b 100644 --- a/osu.Game.Rulesets.Mania/Difficulty/ManiaDifficultyCalculator.cs +++ b/osu.Game.Rulesets.Mania/Difficulty/ManiaDifficultyCalculator.cs @@ -27,7 +27,6 @@ namespace osu.Game.Rulesets.Mania.Difficulty private const double difficulty_multiplier = 0.018; private readonly bool isForCurrentRuleset; - private readonly double originalOverallDifficulty; public override int Version => 20241007; @@ -35,7 +34,6 @@ namespace osu.Game.Rulesets.Mania.Difficulty : base(ruleset, beatmap) { isForCurrentRuleset = beatmap.BeatmapInfo.Ruleset.MatchesOnlineID(ruleset); - originalOverallDifficulty = beatmap.BeatmapInfo.Difficulty.OverallDifficulty; } protected override DifficultyAttributes CreateDifficultyAttributes(IBeatmap beatmap, Mod[] mods, Skill[] skills, double clockRate) @@ -50,9 +48,6 @@ namespace osu.Game.Rulesets.Mania.Difficulty { StarRating = skills.OfType().Single().DifficultyValue() * difficulty_multiplier, Mods = mods, - // In osu-stable mania, rate-adjustment mods don't affect the hit window. - // This is done the way it is to introduce fractional differences in order to match osu-stable for the time being. - GreatHitWindow = Math.Ceiling((int)(getHitWindow300(mods) * clockRate) / clockRate), MaxCombo = beatmap.HitObjects.Sum(maxComboForObject), }; @@ -124,29 +119,5 @@ namespace osu.Game.Rulesets.Mania.Difficulty }).ToArray(); } } - - private double getHitWindow300(Mod[] mods) - { - if (isForCurrentRuleset) - { - double od = Math.Min(10.0, Math.Max(0, 10.0 - originalOverallDifficulty)); - return applyModAdjustments(34 + 3 * od, mods); - } - - if (Math.Round(originalOverallDifficulty) > 4) - return applyModAdjustments(34, mods); - - return applyModAdjustments(47, mods); - - static double applyModAdjustments(double value, Mod[] mods) - { - if (mods.Any(m => m is ManiaModHardRock)) - value /= 1.4; - else if (mods.Any(m => m is ManiaModEasy)) - value *= 1.4; - - return value; - } - } } } diff --git a/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyAttributes.cs b/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyAttributes.cs index 395f581b65..f7d8c649c1 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyAttributes.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyAttributes.cs @@ -59,36 +59,6 @@ namespace osu.Game.Rulesets.Osu.Difficulty [JsonProperty("speed_difficult_strain_count")] public double SpeedDifficultStrainCount { get; set; } - /// - /// The perceived approach rate inclusive of rate-adjusting mods (DT/HT/etc). - /// - [JsonProperty("approach_rate")] - public double ApproachRate { get; set; } - - /// - /// The perceived overall difficulty inclusive of rate-adjusting mods (DT/HT/etc). - /// - [JsonProperty("overall_difficulty")] - public double OverallDifficulty { get; set; } - - /// - /// The perceived hit window for a GREAT hit inclusive of rate-adjusting mods (DT/HT/etc). - /// - [JsonProperty("great_hit_window")] - public double GreatHitWindow { get; set; } - - /// - /// The perceived hit window for an OK hit inclusive of rate-adjusting mods (DT/HT/etc). - /// - [JsonProperty("ok_hit_window")] - public double OkHitWindow { get; set; } - - /// - /// The perceived hit window for a MEH hit inclusive of rate-adjusting mods (DT/HT/etc). - /// - [JsonProperty("meh_hit_window")] - public double MehHitWindow { get; set; } - /// /// The beatmap's drain rate. This doesn't scale with rate-adjusting mods. /// @@ -116,10 +86,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty yield return (ATTRIB_ID_AIM, AimDifficulty); yield return (ATTRIB_ID_SPEED, SpeedDifficulty); - yield return (ATTRIB_ID_OVERALL_DIFFICULTY, OverallDifficulty); - yield return (ATTRIB_ID_APPROACH_RATE, ApproachRate); yield return (ATTRIB_ID_DIFFICULTY, StarRating); - yield return (ATTRIB_ID_GREAT_HIT_WINDOW, GreatHitWindow); if (ShouldSerializeFlashlightDifficulty()) yield return (ATTRIB_ID_FLASHLIGHT, FlashlightDifficulty); @@ -130,9 +97,6 @@ namespace osu.Game.Rulesets.Osu.Difficulty yield return (ATTRIB_ID_SPEED_DIFFICULT_STRAIN_COUNT, SpeedDifficultStrainCount); yield return (ATTRIB_ID_SPEED_NOTE_COUNT, SpeedNoteCount); yield return (ATTRIB_ID_AIM_DIFFICULT_SLIDER_COUNT, AimDifficultSliderCount); - - yield return (ATTRIB_ID_OK_HIT_WINDOW, OkHitWindow); - yield return (ATTRIB_ID_MEH_HIT_WINDOW, MehHitWindow); } public override void FromDatabaseAttributes(IReadOnlyDictionary values, IBeatmapOnlineInfo onlineInfo) @@ -141,18 +105,13 @@ namespace osu.Game.Rulesets.Osu.Difficulty AimDifficulty = values[ATTRIB_ID_AIM]; SpeedDifficulty = values[ATTRIB_ID_SPEED]; - OverallDifficulty = values[ATTRIB_ID_OVERALL_DIFFICULTY]; - ApproachRate = values[ATTRIB_ID_APPROACH_RATE]; StarRating = values[ATTRIB_ID_DIFFICULTY]; - GreatHitWindow = values[ATTRIB_ID_GREAT_HIT_WINDOW]; FlashlightDifficulty = values.GetValueOrDefault(ATTRIB_ID_FLASHLIGHT); SliderFactor = values[ATTRIB_ID_SLIDER_FACTOR]; AimDifficultStrainCount = values[ATTRIB_ID_AIM_DIFFICULT_STRAIN_COUNT]; SpeedDifficultStrainCount = values[ATTRIB_ID_SPEED_DIFFICULT_STRAIN_COUNT]; SpeedNoteCount = values[ATTRIB_ID_SPEED_NOTE_COUNT]; AimDifficultSliderCount = values[ATTRIB_ID_AIM_DIFFICULT_SLIDER_COUNT]; - OkHitWindow = values[ATTRIB_ID_OK_HIT_WINDOW]; - MehHitWindow = values[ATTRIB_ID_MEH_HIT_WINDOW]; DrainRate = onlineInfo.DrainRate; HitCircleCount = onlineInfo.CircleCount; SliderCount = onlineInfo.SliderCount; diff --git a/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs b/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs index 1505c51592..30339fbaa7 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs @@ -15,8 +15,6 @@ using osu.Game.Rulesets.Osu.Difficulty.Preprocessing; using osu.Game.Rulesets.Osu.Difficulty.Skills; using osu.Game.Rulesets.Osu.Mods; using osu.Game.Rulesets.Osu.Objects; -using osu.Game.Rulesets.Osu.Scoring; -using osu.Game.Rulesets.Scoring; namespace osu.Game.Rulesets.Osu.Difficulty { @@ -90,20 +88,12 @@ namespace osu.Game.Rulesets.Osu.Difficulty ? Math.Cbrt(OsuPerformanceCalculator.PERFORMANCE_BASE_MULTIPLIER) * 0.027 * (Math.Cbrt(100000 / Math.Pow(2, 1 / 1.1) * basePerformance) + 4) : 0; - double preempt = IBeatmapDifficultyInfo.DifficultyRange(beatmap.Difficulty.ApproachRate, 1800, 1200, 450) / clockRate; double drainRate = beatmap.Difficulty.DrainRate; int hitCirclesCount = beatmap.HitObjects.Count(h => h is HitCircle); int sliderCount = beatmap.HitObjects.Count(h => h is Slider); int spinnerCount = beatmap.HitObjects.Count(h => h is Spinner); - HitWindows hitWindows = new OsuHitWindows(); - hitWindows.SetDifficulty(beatmap.Difficulty.OverallDifficulty); - - double hitWindowGreat = hitWindows.WindowFor(HitResult.Great) / clockRate; - double hitWindowOk = hitWindows.WindowFor(HitResult.Ok) / clockRate; - double hitWindowMeh = hitWindows.WindowFor(HitResult.Meh) / clockRate; - OsuDifficultyAttributes attributes = new OsuDifficultyAttributes { StarRating = starRating, @@ -116,11 +106,6 @@ namespace osu.Game.Rulesets.Osu.Difficulty SliderFactor = sliderFactor, AimDifficultStrainCount = aimDifficultyStrainCount, SpeedDifficultStrainCount = speedDifficultyStrainCount, - ApproachRate = preempt > 1200 ? (1800 - preempt) / 120 : (1200 - preempt) / 150 + 5, - OverallDifficulty = (80 - hitWindowGreat) / 6, - GreatHitWindow = hitWindowGreat, - OkHitWindow = hitWindowOk, - MehHitWindow = hitWindowMeh, DrainRate = drainRate, MaxCombo = beatmap.GetMaxCombo(), HitCircleCount = hitCirclesCount, diff --git a/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs b/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs index dc2df39cdb..09ec890926 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs @@ -4,10 +4,15 @@ using System; using System.Collections.Generic; using System.Linq; +using osu.Framework.Audio.Track; +using osu.Framework.Extensions.IEnumerableExtensions; +using osu.Game.Beatmaps; using osu.Game.Rulesets.Difficulty; using osu.Game.Rulesets.Difficulty.Utils; +using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Osu.Difficulty.Skills; using osu.Game.Rulesets.Osu.Mods; +using osu.Game.Rulesets.Osu.Scoring; using osu.Game.Rulesets.Scoring; using osu.Game.Scoring; @@ -41,6 +46,13 @@ namespace osu.Game.Rulesets.Osu.Difficulty /// private double effectiveMissCount; + private double clockRate; + private double greatHitWindow; + private double okHitWindow; + private double mehHitWindow; + private double overallDifficulty; + private double approachRate; + private double? speedDeviation; public OsuPerformanceCalculator() @@ -64,6 +76,26 @@ namespace osu.Game.Rulesets.Osu.Difficulty countSliderTickMiss = score.Statistics.GetValueOrDefault(HitResult.LargeTickMiss); effectiveMissCount = countMiss; + var difficulty = score.BeatmapInfo!.Difficulty.Clone(); + + score.Mods.OfType().ForEach(m => m.ApplyToDifficulty(difficulty)); + + var track = new TrackVirtual(10000); + score.Mods.OfType().ForEach(m => m.ApplyToTrack(track)); + clockRate = track.Rate; + + HitWindows hitWindows = new OsuHitWindows(); + hitWindows.SetDifficulty(difficulty.OverallDifficulty); + + greatHitWindow = hitWindows.WindowFor(HitResult.Great) / clockRate; + okHitWindow = hitWindows.WindowFor(HitResult.Ok) / clockRate; + mehHitWindow = hitWindows.WindowFor(HitResult.Meh) / clockRate; + + double preempt = IBeatmapDifficultyInfo.DifficultyRange(difficulty.ApproachRate, 1800, 1200, 450) / clockRate; + + overallDifficulty = (80 - greatHitWindow) / 6; + approachRate = preempt > 1200 ? (1800 - preempt) / 120 : (1200 - preempt) / 150 + 5; + if (osuAttributes.SliderCount > 0) { if (usingClassicSliderAccuracy) @@ -106,8 +138,8 @@ namespace osu.Game.Rulesets.Osu.Difficulty // https://www.desmos.com/calculator/bc9eybdthb // we use OD13.3 as maximum since it's the value at which great hitwidow becomes 0 // this is well beyond currently maximum achievable OD which is 12.17 (DTx2 + DA with OD11) - double okMultiplier = Math.Max(0.0, osuAttributes.OverallDifficulty > 0.0 ? 1 - Math.Pow(osuAttributes.OverallDifficulty / 13.33, 1.8) : 1.0); - double mehMultiplier = Math.Max(0.0, osuAttributes.OverallDifficulty > 0.0 ? 1 - Math.Pow(osuAttributes.OverallDifficulty / 13.33, 5) : 1.0); + double okMultiplier = Math.Max(0.0, overallDifficulty > 0.0 ? 1 - Math.Pow(overallDifficulty / 13.33, 1.8) : 1.0); + double mehMultiplier = Math.Max(0.0, overallDifficulty > 0.0 ? 1 - Math.Pow(overallDifficulty / 13.33, 5) : 1.0); // As we're adding Oks and Mehs to an approximated number of combo breaks the result can be higher than total hits in specific scenarios (which breaks some calculations) so we need to clamp it. effectiveMissCount = Math.Min(effectiveMissCount + countOk * okMultiplier + countMeh * mehMultiplier, totalHits); @@ -178,10 +210,10 @@ namespace osu.Game.Rulesets.Osu.Difficulty aimValue *= calculateMissPenalty(effectiveMissCount, attributes.AimDifficultStrainCount); double approachRateFactor = 0.0; - if (attributes.ApproachRate > 10.33) - approachRateFactor = 0.3 * (attributes.ApproachRate - 10.33); - else if (attributes.ApproachRate < 8.0) - approachRateFactor = 0.05 * (8.0 - attributes.ApproachRate); + if (approachRate > 10.33) + approachRateFactor = 0.3 * (approachRate - 10.33); + else if (approachRate < 8.0) + approachRateFactor = 0.05 * (8.0 - approachRate); if (score.Mods.Any(h => h is OsuModRelax)) approachRateFactor = 0.0; @@ -193,12 +225,12 @@ namespace osu.Game.Rulesets.Osu.Difficulty else if (score.Mods.Any(m => m is OsuModHidden || m is OsuModTraceable)) { // We want to give more reward for lower AR when it comes to aim and HD. This nerfs high AR and buffs lower AR. - aimValue *= 1.0 + 0.04 * (12.0 - attributes.ApproachRate); + aimValue *= 1.0 + 0.04 * (12.0 - approachRate); } aimValue *= accuracy; // It is important to consider accuracy difficulty when scaling with accuracy. - aimValue *= 0.98 + Math.Pow(Math.Max(0, attributes.OverallDifficulty), 2) / 2500; + aimValue *= 0.98 + Math.Pow(Math.Max(0, overallDifficulty), 2) / 2500; return aimValue; } @@ -218,8 +250,8 @@ namespace osu.Game.Rulesets.Osu.Difficulty speedValue *= calculateMissPenalty(effectiveMissCount, attributes.SpeedDifficultStrainCount); double approachRateFactor = 0.0; - if (attributes.ApproachRate > 10.33) - approachRateFactor = 0.3 * (attributes.ApproachRate - 10.33); + if (approachRate > 10.33) + approachRateFactor = 0.3 * (approachRate - 10.33); if (score.Mods.Any(h => h is OsuModAutopilot)) approachRateFactor = 0.0; @@ -234,7 +266,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty else if (score.Mods.Any(m => m is OsuModHidden || m is OsuModTraceable)) { // We want to give more reward for lower AR when it comes to aim and HD. This nerfs high AR and buffs lower AR. - speedValue *= 1.0 + 0.04 * (12.0 - attributes.ApproachRate); + speedValue *= 1.0 + 0.04 * (12.0 - approachRate); } double speedHighDeviationMultiplier = calculateSpeedHighDeviationNerf(attributes); @@ -248,7 +280,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty double relevantAccuracy = attributes.SpeedNoteCount == 0 ? 0 : (relevantCountGreat * 6.0 + relevantCountOk * 2.0 + relevantCountMeh) / (attributes.SpeedNoteCount * 6.0); // Scale the speed value with accuracy and OD. - speedValue *= (0.95 + Math.Pow(Math.Max(0, attributes.OverallDifficulty), 2) / 750) * Math.Pow((accuracy + relevantAccuracy) / 2.0, (14.5 - attributes.OverallDifficulty) / 2); + speedValue *= (0.95 + Math.Pow(Math.Max(0, overallDifficulty), 2) / 750) * Math.Pow((accuracy + relevantAccuracy) / 2.0, (14.5 - overallDifficulty) / 2); return speedValue; } @@ -275,7 +307,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty // Lots of arbitrary values from testing. // Considering to use derivation from perfect accuracy in a probabilistic manner - assume normal distribution. - double accuracyValue = Math.Pow(1.52163, attributes.OverallDifficulty) * Math.Pow(betterAccuracyPercentage, 24) * 2.83; + double accuracyValue = Math.Pow(1.52163, overallDifficulty) * Math.Pow(betterAccuracyPercentage, 24) * 2.83; // Bonus for many hitcircles - it's harder to keep good accuracy up for longer. accuracyValue *= Math.Min(1.15, Math.Pow(amountHitObjectsWithAccuracy / 1000.0, 0.3)); @@ -312,7 +344,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty // Scale the flashlight value with accuracy _slightly_. flashlightValue *= 0.5 + accuracy / 2.0; // It is important to also consider accuracy difficulty when doing that. - flashlightValue *= 0.98 + Math.Pow(Math.Max(0, attributes.OverallDifficulty), 2) / 2500; + flashlightValue *= 0.98 + Math.Pow(Math.Max(0, overallDifficulty), 2) / 2500; return flashlightValue; } @@ -352,10 +384,6 @@ namespace osu.Game.Rulesets.Osu.Difficulty double objectCount = relevantCountGreat + relevantCountOk + relevantCountMeh + relevantCountMiss; - double hitWindowGreat = attributes.GreatHitWindow; - double hitWindowOk = attributes.OkHitWindow; - double hitWindowMeh = attributes.MehHitWindow; - // The probability that a player hits a circle is unknown, but we can estimate it to be // the number of greats on circles divided by the number of circles, and then add one // to the number of circles as a bias correction. @@ -370,22 +398,22 @@ namespace osu.Game.Rulesets.Osu.Difficulty // Compute the deviation assuming greats and oks are normally distributed, and mehs are uniformly distributed. // Begin with greats and oks first. Ignoring mehs, we can be 99% confident that the deviation is not higher than: - double deviation = hitWindowGreat / (Math.Sqrt(2) * DifficultyCalculationUtils.ErfInv(pLowerBound)); + double deviation = greatHitWindow / (Math.Sqrt(2) * DifficultyCalculationUtils.ErfInv(pLowerBound)); - double randomValue = Math.Sqrt(2 / Math.PI) * hitWindowOk * Math.Exp(-0.5 * Math.Pow(hitWindowOk / deviation, 2)) - / (deviation * DifficultyCalculationUtils.Erf(hitWindowOk / (Math.Sqrt(2) * deviation))); + double randomValue = Math.Sqrt(2 / Math.PI) * okHitWindow * Math.Exp(-0.5 * Math.Pow(okHitWindow / deviation, 2)) + / (deviation * DifficultyCalculationUtils.Erf(okHitWindow / (Math.Sqrt(2) * deviation))); deviation *= Math.Sqrt(1 - randomValue); // Value deviation approach as greatCount approaches 0 - double limitValue = hitWindowOk / Math.Sqrt(3); + double limitValue = okHitWindow / Math.Sqrt(3); // If precision is not enough to compute true deviation - use limit value if (pLowerBound == 0 || randomValue >= 1 || deviation > limitValue) deviation = limitValue; // Then compute the variance for mehs. - double mehVariance = (hitWindowMeh * hitWindowMeh + hitWindowOk * hitWindowMeh + hitWindowOk * hitWindowOk) / 3; + double mehVariance = (mehHitWindow * mehHitWindow + okHitWindow * mehHitWindow + okHitWindow * okHitWindow) / 3; // Find the total deviation. deviation = Math.Sqrt(((relevantCountGreat + relevantCountOk) * Math.Pow(deviation, 2) + relevantCountMeh * mehVariance) / (relevantCountGreat + relevantCountOk + relevantCountMeh)); diff --git a/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyAttributes.cs b/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyAttributes.cs index 37e6996e5a..b43468ab18 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyAttributes.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyAttributes.cs @@ -49,32 +49,12 @@ namespace osu.Game.Rulesets.Taiko.Difficulty [JsonProperty("stamina_difficult_strains")] public double StaminaTopStrains { get; set; } - /// - /// The perceived hit window for a GREAT hit inclusive of rate-adjusting mods (DT/HT/etc). - /// - /// - /// Rate-adjusting mods don't directly affect the hit window, but have a perceived effect as a result of adjusting audio timing. - /// - [JsonProperty("great_hit_window")] - public double GreatHitWindow { get; set; } - - /// - /// The perceived hit window for an OK hit inclusive of rate-adjusting mods (DT/HT/etc). - /// - /// - /// Rate-adjusting mods don't directly affect the hit window, but have a perceived effect as a result of adjusting audio timing. - /// - [JsonProperty("ok_hit_window")] - public double OkHitWindow { get; set; } - public override IEnumerable<(int attributeId, object value)> ToDatabaseAttributes() { foreach (var v in base.ToDatabaseAttributes()) yield return v; yield return (ATTRIB_ID_DIFFICULTY, StarRating); - yield return (ATTRIB_ID_GREAT_HIT_WINDOW, GreatHitWindow); - yield return (ATTRIB_ID_OK_HIT_WINDOW, OkHitWindow); yield return (ATTRIB_ID_MONO_STAMINA_FACTOR, MonoStaminaFactor); } @@ -83,8 +63,6 @@ namespace osu.Game.Rulesets.Taiko.Difficulty base.FromDatabaseAttributes(values, onlineInfo); StarRating = values[ATTRIB_ID_DIFFICULTY]; - GreatHitWindow = values[ATTRIB_ID_GREAT_HIT_WINDOW]; - OkHitWindow = values[ATTRIB_ID_OK_HIT_WINDOW]; MonoStaminaFactor = values[ATTRIB_ID_MONO_STAMINA_FACTOR]; } } diff --git a/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs b/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs index 6b9986bd68..7bc050d2df 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs @@ -129,9 +129,6 @@ namespace osu.Game.Rulesets.Taiko.Difficulty double combinedRating = combinedDifficultyValue(rhythm, reading, colour, stamina, isRelax, isConvert); double starRating = rescale(combinedRating * 1.4); - HitWindows hitWindows = new TaikoHitWindows(); - hitWindows.SetDifficulty(beatmap.Difficulty.OverallDifficulty); - TaikoDifficultyAttributes attributes = new TaikoDifficultyAttributes { StarRating = starRating, @@ -144,8 +141,6 @@ namespace osu.Game.Rulesets.Taiko.Difficulty RhythmTopStrains = rhythmDifficultStrains, ColourTopStrains = colourDifficultStrains, StaminaTopStrains = staminaDifficultStrains, - GreatHitWindow = hitWindows.WindowFor(HitResult.Great) / clockRate, - OkHitWindow = hitWindows.WindowFor(HitResult.Ok) / clockRate, MaxCombo = beatmap.GetMaxCombo(), }; diff --git a/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceCalculator.cs b/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceCalculator.cs index bcd3693119..9e049df87c 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceCalculator.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceCalculator.cs @@ -4,11 +4,14 @@ using System; using System.Collections.Generic; using System.Linq; +using osu.Framework.Audio.Track; +using osu.Framework.Extensions.IEnumerableExtensions; using osu.Game.Rulesets.Difficulty; using osu.Game.Rulesets.Difficulty.Utils; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.Taiko.Objects; +using osu.Game.Rulesets.Taiko.Scoring; using osu.Game.Scoring; namespace osu.Game.Rulesets.Taiko.Difficulty @@ -21,6 +24,9 @@ namespace osu.Game.Rulesets.Taiko.Difficulty private int countMiss; private double? estimatedUnstableRate; + private double clockRate; + private double greatHitWindow; + private double effectiveMissCount; public TaikoPerformanceCalculator() @@ -36,7 +42,21 @@ namespace osu.Game.Rulesets.Taiko.Difficulty countOk = score.Statistics.GetValueOrDefault(HitResult.Ok); countMeh = score.Statistics.GetValueOrDefault(HitResult.Meh); countMiss = score.Statistics.GetValueOrDefault(HitResult.Miss); - estimatedUnstableRate = computeDeviationUpperBound(taikoAttributes) * 10; + + var track = new TrackVirtual(10000); + score.Mods.OfType().ForEach(m => m.ApplyToTrack(track)); + clockRate = track.Rate; + + var difficulty = score.BeatmapInfo!.Difficulty.Clone(); + + score.Mods.OfType().ForEach(m => m.ApplyToDifficulty(difficulty)); + + HitWindows hitWindows = new TaikoHitWindows(); + hitWindows.SetDifficulty(difficulty.OverallDifficulty); + + greatHitWindow = hitWindows.WindowFor(HitResult.Great) / clockRate; + + estimatedUnstableRate = computeDeviationUpperBound() * 10; // The effectiveMissCount is calculated by gaining a ratio for totalSuccessfulHits and increasing the miss penalty for shorter object counts lower than 1000. if (totalSuccessfulHits > 0) @@ -104,7 +124,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty private double computeAccuracyValue(ScoreInfo score, TaikoDifficultyAttributes attributes, bool isConvert) { - if (attributes.GreatHitWindow <= 0 || estimatedUnstableRate == null) + if (greatHitWindow <= 0 || estimatedUnstableRate == null) return 0; double accuracyValue = Math.Pow(70 / estimatedUnstableRate.Value, 1.1) * Math.Pow(attributes.StarRating, 0.4) * 100.0; @@ -123,9 +143,9 @@ namespace osu.Game.Rulesets.Taiko.Difficulty /// and the hit judgements, assuming the player's mean hit error is 0. The estimation is consistent in that /// two SS scores on the same map with the same settings will always return the same deviation. /// - private double? computeDeviationUpperBound(TaikoDifficultyAttributes attributes) + private double? computeDeviationUpperBound() { - if (countGreat == 0 || attributes.GreatHitWindow <= 0) + if (countGreat == 0 || greatHitWindow <= 0) return null; const double z = 2.32634787404; // 99% critical value for the normal distribution (one-tailed). @@ -139,7 +159,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty double pLowerBound = (n * p + z * z / 2) / (n + z * z) - z / (n + z * z) * Math.Sqrt(n * p * (1 - p) + z * z / 4); // We can be 99% confident that the deviation is not higher than: - return attributes.GreatHitWindow / (Math.Sqrt(2) * DifficultyCalculationUtils.ErfInv(pLowerBound)); + return greatHitWindow / (Math.Sqrt(2) * DifficultyCalculationUtils.ErfInv(pLowerBound)); } private int totalHits => countGreat + countOk + countMeh + countMiss; diff --git a/osu.Game/Rulesets/Difficulty/DifficultyAttributes.cs b/osu.Game/Rulesets/Difficulty/DifficultyAttributes.cs index 1d6cee043b..59511973f7 100644 --- a/osu.Game/Rulesets/Difficulty/DifficultyAttributes.cs +++ b/osu.Game/Rulesets/Difficulty/DifficultyAttributes.cs @@ -17,21 +17,15 @@ namespace osu.Game.Rulesets.Difficulty { protected const int ATTRIB_ID_AIM = 1; protected const int ATTRIB_ID_SPEED = 3; - protected const int ATTRIB_ID_OVERALL_DIFFICULTY = 5; - protected const int ATTRIB_ID_APPROACH_RATE = 7; protected const int ATTRIB_ID_MAX_COMBO = 9; protected const int ATTRIB_ID_DIFFICULTY = 11; - protected const int ATTRIB_ID_GREAT_HIT_WINDOW = 13; - protected const int ATTRIB_ID_SCORE_MULTIPLIER = 15; protected const int ATTRIB_ID_FLASHLIGHT = 17; protected const int ATTRIB_ID_SLIDER_FACTOR = 19; protected const int ATTRIB_ID_SPEED_NOTE_COUNT = 21; protected const int ATTRIB_ID_SPEED_DIFFICULT_STRAIN_COUNT = 23; protected const int ATTRIB_ID_AIM_DIFFICULT_STRAIN_COUNT = 25; - protected const int ATTRIB_ID_OK_HIT_WINDOW = 27; protected const int ATTRIB_ID_MONO_STAMINA_FACTOR = 29; protected const int ATTRIB_ID_AIM_DIFFICULT_SLIDER_COUNT = 31; - protected const int ATTRIB_ID_MEH_HIT_WINDOW = 33; /// /// The mods which were applied to the beatmap. From d4c69f0c9063c7c4d56f75ecc37a1819b616e4dc Mon Sep 17 00:00:00 2001 From: Layendan Date: Fri, 7 Feb 2025 04:04:29 -0700 Subject: [PATCH 0964/1275] Assume room is setup correctly and remove duplicate maps before querying realm --- .../OnlinePlay/Playlists/AddPlaylistToCollectionButton.cs | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/Playlists/AddPlaylistToCollectionButton.cs b/osu.Game/Screens/OnlinePlay/Playlists/AddPlaylistToCollectionButton.cs index 8801d73e9e..c24c7d834d 100644 --- a/osu.Game/Screens/OnlinePlay/Playlists/AddPlaylistToCollectionButton.cs +++ b/osu.Game/Screens/OnlinePlay/Playlists/AddPlaylistToCollectionButton.cs @@ -35,15 +35,13 @@ namespace osu.Game.Screens.OnlinePlay.Playlists { Action = () => { - int[] ids = room.Playlist.Select(item => item.Beatmap.OnlineID).Where(onlineId => onlineId > 0).ToArray(); - - if (ids.Length == 0) + if (room.Playlist.Count == 0) { notifications?.Post(new SimpleErrorNotification { Text = "Cannot add local beatmaps" }); return; } - string filter = string.Join(" OR ", ids.Select(id => $"(OnlineID == {id})")); + string filter = string.Join(" OR ", room.Playlist.Select(item => $"(OnlineID == {item.Beatmap.OnlineID})").Distinct()); var beatmaps = realmAccess.Realm.All().Filter(filter).ToList(); var collection = realmAccess.Realm.All().FirstOrDefault(c => c.Name == room.Name); From 5ace8e911bde4a5f9c9318f57a98519995b5b55f Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 7 Feb 2025 21:45:31 +0900 Subject: [PATCH 0965/1275] Fix failing test --- .../SongSelect/TestSceneBeatmapCarouselV2DifficultyGrouping.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2DifficultyGrouping.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2DifficultyGrouping.cs index c043fd87a9..a0c56020ab 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2DifficultyGrouping.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2DifficultyGrouping.cs @@ -75,7 +75,7 @@ namespace osu.Game.Tests.Visual.SongSelect RemoveAllBeatmaps(); AddUntilStep("no drawable selection", GetSelectedPanel, () => Is.Null); - AddBeatmaps(5); + AddBeatmaps(3); WaitForDrawablePanels(); CheckHasSelection(); From de0aabbfc59963923637bc08edcc3c205a3e1f8c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 7 Feb 2025 15:34:52 +0100 Subject: [PATCH 0966/1275] Add staging submission service URL to development endpoint config --- osu.Game/Online/DevelopmentEndpointConfiguration.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/osu.Game/Online/DevelopmentEndpointConfiguration.cs b/osu.Game/Online/DevelopmentEndpointConfiguration.cs index f4e1b257ee..e36e36ee9f 100644 --- a/osu.Game/Online/DevelopmentEndpointConfiguration.cs +++ b/osu.Game/Online/DevelopmentEndpointConfiguration.cs @@ -13,6 +13,7 @@ namespace osu.Game.Online SpectatorUrl = $@"{APIUrl}/signalr/spectator"; MultiplayerUrl = $@"{APIUrl}/signalr/multiplayer"; MetadataUrl = $@"{APIUrl}/signalr/metadata"; + BeatmapSubmissionServiceUrl = $@"{APIUrl}/beatmap-submission"; } } } From 64f0d234d84222b00363397b43c9cda55c772a04 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 7 Feb 2025 15:37:27 +0100 Subject: [PATCH 0967/1275] Fix exiting being eternally blocked after successful beatmap submission --- osu.Game/Screens/Edit/Submission/BeatmapSubmissionScreen.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/Edit/Submission/BeatmapSubmissionScreen.cs b/osu.Game/Screens/Edit/Submission/BeatmapSubmissionScreen.cs index 41c875ac1f..9dfe998138 100644 --- a/osu.Game/Screens/Edit/Submission/BeatmapSubmissionScreen.cs +++ b/osu.Game/Screens/Edit/Submission/BeatmapSubmissionScreen.cs @@ -420,7 +420,7 @@ namespace osu.Game.Screens.Edit.Submission s.Push(new EditorLoader()); }, [typeof(MainMenu)]); - return true; + return false; } return base.OnExiting(e); From bcd4fcbeed3a2a4057849f3e01defdcea17b849e Mon Sep 17 00:00:00 2001 From: SebastianPeP Date: Sun, 9 Feb 2025 01:29:22 -0300 Subject: [PATCH 0968/1275] Changed the Currently Playing Text when no track is selected Changed the currently playing text for when the track isnt selected/loaded --- osu.Game/Beatmaps/DummyWorkingBeatmap.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Beatmaps/DummyWorkingBeatmap.cs b/osu.Game/Beatmaps/DummyWorkingBeatmap.cs index 35067f4055..5dc73d8679 100644 --- a/osu.Game/Beatmaps/DummyWorkingBeatmap.cs +++ b/osu.Game/Beatmaps/DummyWorkingBeatmap.cs @@ -30,8 +30,8 @@ namespace osu.Game.Beatmaps { Metadata = new BeatmapMetadata { - Artist = "please load a beatmap!", - Title = "no beatmaps available!" + Artist = "please select or load a beatmap!", + Title = "no beatmap selected!" }, BeatmapSet = new BeatmapSetInfo(), Difficulty = new BeatmapDifficulty From f9bda0524ada81a9bbc440b88195af3d8ec9786e Mon Sep 17 00:00:00 2001 From: Layendan Date: Sun, 9 Feb 2025 18:45:13 -0700 Subject: [PATCH 0969/1275] Update button text to include downloaded beatmaps and collection status --- .../AddPlaylistToCollectionButton.cs | 37 +++++++++++++++++-- 1 file changed, 34 insertions(+), 3 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/Playlists/AddPlaylistToCollectionButton.cs b/osu.Game/Screens/OnlinePlay/Playlists/AddPlaylistToCollectionButton.cs index c24c7d834d..cc875b707d 100644 --- a/osu.Game/Screens/OnlinePlay/Playlists/AddPlaylistToCollectionButton.cs +++ b/osu.Game/Screens/OnlinePlay/Playlists/AddPlaylistToCollectionButton.cs @@ -1,8 +1,11 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System; +using System.Collections.Generic; using System.Linq; using osu.Framework.Allocation; +using osu.Framework.Bindables; using osu.Game.Beatmaps; using osu.Game.Collections; using osu.Game.Database; @@ -17,6 +20,10 @@ namespace osu.Game.Screens.OnlinePlay.Playlists public partial class AddPlaylistToCollectionButton : RoundedButton { private readonly Room room; + private readonly Bindable downloadedBeatmapsCount = new Bindable(0); + private readonly Bindable collectionExists = new Bindable(false); + private IDisposable? beatmapSubscription; + private IDisposable? collectionSubscription; [Resolved] private RealmAccess realmAccess { get; set; } = null!; @@ -27,7 +34,7 @@ namespace osu.Game.Screens.OnlinePlay.Playlists public AddPlaylistToCollectionButton(Room room) { this.room = room; - Text = "Add Maps to Collection"; + Text = formatButtonText(downloadedBeatmapsCount.Value, collectionExists.Value); } [BackgroundDependencyLoader] @@ -41,8 +48,7 @@ namespace osu.Game.Screens.OnlinePlay.Playlists return; } - string filter = string.Join(" OR ", room.Playlist.Select(item => $"(OnlineID == {item.Beatmap.OnlineID})").Distinct()); - var beatmaps = realmAccess.Realm.All().Filter(filter).ToList(); + var beatmaps = realmAccess.Realm.All().Filter(formatFilterQuery(room.Playlist)).ToList(); var collection = realmAccess.Realm.All().FirstOrDefault(c => c.Name == room.Name); @@ -64,5 +70,30 @@ namespace osu.Game.Screens.OnlinePlay.Playlists } }; } + + protected override void LoadComplete() + { + base.LoadComplete(); + + beatmapSubscription = realmAccess.RegisterForNotifications(r => r.All().Filter(formatFilterQuery(room.Playlist)), (sender, _) => downloadedBeatmapsCount.Value = sender.Count); + + collectionSubscription = realmAccess.RegisterForNotifications(r => r.All().Where(c => c.Name == room.Name), (sender, _) => collectionExists.Value = sender.Count > 0); + + downloadedBeatmapsCount.BindValueChanged(_ => Text = formatButtonText(downloadedBeatmapsCount.Value, collectionExists.Value)); + + collectionExists.BindValueChanged(_ => Text = formatButtonText(downloadedBeatmapsCount.Value, collectionExists.Value), true); + } + + protected override void Dispose(bool isDisposing) + { + base.Dispose(isDisposing); + + beatmapSubscription?.Dispose(); + collectionSubscription?.Dispose(); + } + + private string formatFilterQuery(IReadOnlyList playlistItems) => string.Join(" OR ", playlistItems.Select(item => $"(OnlineID == {item.Beatmap.OnlineID})").Distinct()); + + private string formatButtonText(int count, bool collectionExists) => $"Add {count} {(count == 1 ? "beatmap" : "beatmaps")} to {(collectionExists ? "collection" : "new collection")}"; } } From 274b4221398ba232edfd101ebaab862cbd12c6c2 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 10 Feb 2025 14:51:48 +0900 Subject: [PATCH 0970/1275] Add percent progress display to editor footer --- .../Edit/Components/TimeInfoContainer.cs | 21 ++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/osu.Game/Screens/Edit/Components/TimeInfoContainer.cs b/osu.Game/Screens/Edit/Components/TimeInfoContainer.cs index 8f2a3d49ca..d17f9011f4 100644 --- a/osu.Game/Screens/Edit/Components/TimeInfoContainer.cs +++ b/osu.Game/Screens/Edit/Components/TimeInfoContainer.cs @@ -18,6 +18,7 @@ namespace osu.Game.Screens.Edit.Components public partial class TimeInfoContainer : BottomBarContainer { private OsuSpriteText bpm = null!; + private OsuSpriteText progress = null!; [Resolved] private EditorBeatmap editorBeatmap { get; set; } = null!; @@ -36,26 +37,44 @@ namespace osu.Game.Screens.Edit.Components bpm = new OsuSpriteText { Colour = colours.Orange1, + Font = OsuFont.Torus.With(size: 14, weight: FontWeight.SemiBold, fixedWidth: true), + Spacing = new Vector2(-1, 0), + Position = new Vector2(0, 4), + Anchor = Anchor.CentreRight, + Origin = Anchor.TopRight, + }, + progress = new OsuSpriteText + { + Colour = colours.Purple1, + Font = OsuFont.Torus.With(size: 14, weight: FontWeight.SemiBold, fixedWidth: true), + Spacing = new Vector2(-1, 0), Anchor = Anchor.CentreLeft, - Font = OsuFont.Torus.With(size: 14, weight: FontWeight.SemiBold), Position = new Vector2(2, 4), } }; } private double? lastBPM; + private double? lastProgress; protected override void Update() { base.Update(); double newBPM = editorBeatmap.ControlPointInfo.TimingPointAt(editorClock.CurrentTime).BPM; + double newProgress = (int)(editorClock.CurrentTime / editorClock.TrackLength * 100); if (lastBPM != newBPM) { lastBPM = newBPM; bpm.Text = @$"{newBPM:0} BPM"; } + + if (lastProgress != newProgress) + { + lastProgress = newProgress; + progress.Text = @$"{newProgress:0}%"; + } } private partial class TimestampControl : OsuClickableContainer From 7853456c06abf8c7e46d233580b50cdf070f2efe Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 10 Feb 2025 15:12:59 +0900 Subject: [PATCH 0971/1275] Add delay before browser displays beatmap --- .../Submission/BeatmapSubmissionScreen.cs | 32 ++++++++----------- 1 file changed, 14 insertions(+), 18 deletions(-) diff --git a/osu.Game/Screens/Edit/Submission/BeatmapSubmissionScreen.cs b/osu.Game/Screens/Edit/Submission/BeatmapSubmissionScreen.cs index 9dfe998138..039c919ed6 100644 --- a/osu.Game/Screens/Edit/Submission/BeatmapSubmissionScreen.cs +++ b/osu.Game/Screens/Edit/Submission/BeatmapSubmissionScreen.cs @@ -290,15 +290,7 @@ namespace osu.Game.Screens.Edit.Submission var patchRequest = new PatchBeatmapPackageRequest(beatmapSetId.Value); patchRequest.FilesChanged.AddRange(changedFiles); patchRequest.FilesDeleted.AddRange(onlineFilesByFilename.Keys); - patchRequest.Success += async () => - { - uploadStep.SetCompleted(); - - if (configManager.Get(OsuSetting.EditorSubmissionLoadInBrowserAfterSubmission)) - game?.OpenUrlExternally($"{api.Endpoints.WebsiteUrl}/beatmapsets/{beatmapSetId}"); - - await updateLocalBeatmap().ConfigureAwait(true); - }; + patchRequest.Success += uploadCompleted; patchRequest.Failure += ex => { uploadStep.SetFailed(ex.Message); @@ -318,15 +310,7 @@ namespace osu.Game.Screens.Edit.Submission var uploadRequest = new ReplaceBeatmapPackageRequest(beatmapSetId.Value, beatmapPackageStream.ToArray()); - uploadRequest.Success += async () => - { - uploadStep.SetCompleted(); - - if (configManager.Get(OsuSetting.EditorSubmissionLoadInBrowserAfterSubmission)) - game?.OpenUrlExternally($"{api.Endpoints.WebsiteUrl}/beatmapsets/{beatmapSetId}"); - - await updateLocalBeatmap().ConfigureAwait(true); - }; + uploadRequest.Success += uploadCompleted; uploadRequest.Failure += ex => { uploadStep.SetFailed(ex.Message); @@ -339,6 +323,12 @@ namespace osu.Game.Screens.Edit.Submission uploadStep.SetInProgress(); } + private void uploadCompleted() + { + uploadStep.SetCompleted(); + updateLocalBeatmap().ConfigureAwait(true); + } + private async Task updateLocalBeatmap() { Debug.Assert(beatmapSetId != null); @@ -364,6 +354,12 @@ namespace osu.Game.Screens.Edit.Submission updateStep.SetCompleted(); showBeatmapCard(); allowExit(); + + if (configManager.Get(OsuSetting.EditorSubmissionLoadInBrowserAfterSubmission)) + { + await Task.Delay(1000).ConfigureAwait(true); + game?.OpenUrlExternally($"{api.Endpoints.WebsiteUrl}/beatmapsets/{beatmapSetId}"); + } } private void showBeatmapCard() From 930aaecd7fc39a9455f3e56fe7baffe97b9dc360 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 10 Feb 2025 15:22:31 +0900 Subject: [PATCH 0972/1275] Fix back button displaying before it should --- osu.Game/Screens/Edit/Submission/BeatmapSubmissionScreen.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/osu.Game/Screens/Edit/Submission/BeatmapSubmissionScreen.cs b/osu.Game/Screens/Edit/Submission/BeatmapSubmissionScreen.cs index 039c919ed6..0967bcfc65 100644 --- a/osu.Game/Screens/Edit/Submission/BeatmapSubmissionScreen.cs +++ b/osu.Game/Screens/Edit/Submission/BeatmapSubmissionScreen.cs @@ -39,6 +39,8 @@ namespace osu.Game.Screens.Edit.Submission public override bool DisallowExternalBeatmapRulesetChanges => true; + protected override bool InitialBackButtonVisibility => false; + [Cached] private readonly OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Aquamarine); From eae1ea7e32484c03cd24b656c68c3138f4197b82 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 10 Feb 2025 15:23:25 +0900 Subject: [PATCH 0973/1275] Adjust animations and induce some short delays to make things more graceful --- .../Screens/Edit/Submission/BeatmapSubmissionScreen.cs | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/osu.Game/Screens/Edit/Submission/BeatmapSubmissionScreen.cs b/osu.Game/Screens/Edit/Submission/BeatmapSubmissionScreen.cs index 0967bcfc65..121e25d8b7 100644 --- a/osu.Game/Screens/Edit/Submission/BeatmapSubmissionScreen.cs +++ b/osu.Game/Screens/Edit/Submission/BeatmapSubmissionScreen.cs @@ -92,6 +92,8 @@ namespace osu.Game.Screens.Edit.Submission { RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y, + AutoSizeDuration = 400, + AutoSizeEasing = Easing.OutQuint, Alpha = 0, Anchor = Anchor.Centre, Origin = Anchor.Centre, @@ -144,9 +146,6 @@ namespace osu.Game.Screens.Edit.Submission Anchor = Anchor.TopCentre, Origin = Anchor.TopCentre, AutoSizeAxes = Axes.Both, - AutoSizeDuration = 500, - AutoSizeEasing = Easing.OutQuint, - Masking = true, CornerRadius = BeatmapCard.CORNER_RADIUS, Child = flashLayer = new Container { @@ -252,6 +251,8 @@ namespace osu.Game.Screens.Edit.Submission exportStep.SetCompleted(); exportProgressNotification = null; + await Task.Delay(200).ConfigureAwait(true); + if (onlineFiles.Count > 0) await patchBeatmapSet(onlineFiles).ConfigureAwait(true); else @@ -337,6 +338,7 @@ namespace osu.Game.Screens.Edit.Submission Debug.Assert(beatmapPackageStream != null); updateStep.SetInProgress(); + await Task.Delay(200).ConfigureAwait(true); try { From 5e9f195117307feb555e663fe8544c9a2527bc51 Mon Sep 17 00:00:00 2001 From: Layendan Date: Sun, 9 Feb 2025 23:27:28 -0700 Subject: [PATCH 0974/1275] Fix tests failing if playlist was empty --- .../OnlinePlay/Playlists/AddPlaylistToCollectionButton.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/osu.Game/Screens/OnlinePlay/Playlists/AddPlaylistToCollectionButton.cs b/osu.Game/Screens/OnlinePlay/Playlists/AddPlaylistToCollectionButton.cs index cc875b707d..8b5d5c752c 100644 --- a/osu.Game/Screens/OnlinePlay/Playlists/AddPlaylistToCollectionButton.cs +++ b/osu.Game/Screens/OnlinePlay/Playlists/AddPlaylistToCollectionButton.cs @@ -75,7 +75,8 @@ namespace osu.Game.Screens.OnlinePlay.Playlists { base.LoadComplete(); - beatmapSubscription = realmAccess.RegisterForNotifications(r => r.All().Filter(formatFilterQuery(room.Playlist)), (sender, _) => downloadedBeatmapsCount.Value = sender.Count); + if (room.Playlist.Count > 0) + beatmapSubscription = realmAccess.RegisterForNotifications(r => r.All().Filter(formatFilterQuery(room.Playlist)), (sender, _) => downloadedBeatmapsCount.Value = sender.Count); collectionSubscription = realmAccess.RegisterForNotifications(r => r.All().Where(c => c.Name == room.Name), (sender, _) => collectionExists.Value = sender.Count > 0); From a3cd62ec7295b3dd5f16350ae6439a623acab111 Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Sat, 25 Jan 2025 20:17:21 -0500 Subject: [PATCH 0975/1275] Flip mania judgement anchor on flipped scroll direction --- .../UI/DrawableManiaJudgement.cs | 65 +++++-------------- osu.Game.Rulesets.Mania/UI/Stage.cs | 8 +-- 2 files changed, 17 insertions(+), 56 deletions(-) diff --git a/osu.Game.Rulesets.Mania/UI/DrawableManiaJudgement.cs b/osu.Game.Rulesets.Mania/UI/DrawableManiaJudgement.cs index 75f56bffa4..40fef1a56a 100644 --- a/osu.Game.Rulesets.Mania/UI/DrawableManiaJudgement.cs +++ b/osu.Game.Rulesets.Mania/UI/DrawableManiaJudgement.cs @@ -3,65 +3,32 @@ #nullable disable +using osu.Framework.Allocation; +using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Scoring; -using osuTK; +using osu.Game.Rulesets.UI.Scrolling; namespace osu.Game.Rulesets.Mania.UI { public partial class DrawableManiaJudgement : DrawableJudgement { - protected override Drawable CreateDefaultJudgement(HitResult result) => new DefaultManiaJudgementPiece(result); + private IBindable direction; - private partial class DefaultManiaJudgementPiece : DefaultJudgementPiece + [BackgroundDependencyLoader] + private void load(IScrollingInfo scrollingInfo) { - private const float judgement_y_position = -180f; - - public DefaultManiaJudgementPiece(HitResult result) - : base(result) - { - Y = judgement_y_position; - } - - protected override void LoadComplete() - { - base.LoadComplete(); - - JudgementText.Font = JudgementText.Font.With(size: 25); - } - - public override void PlayAnimation() - { - switch (Result) - { - case HitResult.None: - this.FadeOutFromOne(800); - break; - - case HitResult.Miss: - this.ScaleTo(1.6f); - this.ScaleTo(1, 100, Easing.In); - - this.MoveToY(judgement_y_position); - this.MoveToOffset(new Vector2(0, 100), 800, Easing.InQuint); - - this.RotateTo(0); - this.RotateTo(40, 800, Easing.InQuint); - - this.FadeOutFromOne(800); - break; - - default: - this.ScaleTo(0.8f); - this.ScaleTo(1, 250, Easing.OutElastic); - - this.Delay(50) - .ScaleTo(0.75f, 250) - .FadeOut(200); - break; - } - } + direction = scrollingInfo.Direction.GetBoundCopy(); + direction.BindValueChanged(_ => onDirectionChanged(), true); } + + private void onDirectionChanged() + { + Anchor = direction.Value == ScrollingDirection.Up ? Anchor.TopCentre : Anchor.BottomCentre; + Origin = Anchor.Centre; + } + + protected override Drawable CreateDefaultJudgement(HitResult result) => new DefaultManiaJudgementPiece(result); } } diff --git a/osu.Game.Rulesets.Mania/UI/Stage.cs b/osu.Game.Rulesets.Mania/UI/Stage.cs index fb9671c14d..faa9fc318c 100644 --- a/osu.Game.Rulesets.Mania/UI/Stage.cs +++ b/osu.Game.Rulesets.Mania/UI/Stage.cs @@ -216,13 +216,7 @@ namespace osu.Game.Rulesets.Mania.UI return; judgements.Clear(false); - judgements.Add(judgementPooler.Get(result.Type, j => - { - j.Apply(result, judgedObject); - - j.Anchor = Anchor.BottomCentre; - j.Origin = Anchor.Centre; - })!); + judgements.Add(judgementPooler.Get(result.Type, j => j.Apply(result, judgedObject))!); } protected override void Update() From 1e06c5cc4ac823f773101899b0ea55e23e82e291 Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Sat, 25 Jan 2025 20:17:36 -0500 Subject: [PATCH 0976/1275] Flip the Y offset of skin judgement pieces on flipped scroll direction --- .../Skinning/Argon/ArgonJudgementPiece.cs | 14 +++- .../Legacy/LegacyManiaJudgementPiece.cs | 29 +++++-- .../UI/DefaultManiaJudgementPiece.cs | 75 +++++++++++++++++++ 3 files changed, 108 insertions(+), 10 deletions(-) create mode 100644 osu.Game.Rulesets.Mania/UI/DefaultManiaJudgementPiece.cs diff --git a/osu.Game.Rulesets.Mania/Skinning/Argon/ArgonJudgementPiece.cs b/osu.Game.Rulesets.Mania/Skinning/Argon/ArgonJudgementPiece.cs index a1c81d3a6a..6098459f6b 100644 --- a/osu.Game.Rulesets.Mania/Skinning/Argon/ArgonJudgementPiece.cs +++ b/osu.Game.Rulesets.Mania/Skinning/Argon/ArgonJudgementPiece.cs @@ -3,6 +3,7 @@ using System; using osu.Framework.Allocation; +using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; @@ -12,6 +13,7 @@ using osu.Game.Graphics; using osu.Game.Graphics.Sprites; using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Scoring; +using osu.Game.Rulesets.UI.Scrolling; using osuTK; using osuTK.Graphics; @@ -26,18 +28,22 @@ namespace osu.Game.Rulesets.Mania.Skinning.Argon [Resolved] private OsuColour colours { get; set; } = null!; + private IBindable direction = null!; + public ArgonJudgementPiece(HitResult result) : base(result) { AutoSizeAxes = Axes.Both; Origin = Anchor.Centre; - Y = judgement_y_position; } [BackgroundDependencyLoader] - private void load() + private void load(IScrollingInfo scrollingInfo) { + direction = scrollingInfo.Direction.GetBoundCopy(); + direction.BindValueChanged(_ => onDirectionChanged(), true); + if (Result.IsHit()) { AddInternal(ringExplosion = new RingExplosion(Result) @@ -47,6 +53,8 @@ namespace osu.Game.Rulesets.Mania.Skinning.Argon } } + private void onDirectionChanged() => Y = direction.Value == ScrollingDirection.Up ? -judgement_y_position : judgement_y_position; + protected override SpriteText CreateJudgementText() => new OsuSpriteText { @@ -78,7 +86,7 @@ namespace osu.Game.Rulesets.Mania.Skinning.Argon this.ScaleTo(1.6f); this.ScaleTo(1, 100, Easing.In); - this.MoveToY(judgement_y_position); + this.MoveToY(direction.Value == ScrollingDirection.Up ? -judgement_y_position : judgement_y_position); this.MoveToOffset(new Vector2(0, 100), 800, Easing.InQuint); this.RotateTo(0); diff --git a/osu.Game.Rulesets.Mania/Skinning/Legacy/LegacyManiaJudgementPiece.cs b/osu.Game.Rulesets.Mania/Skinning/Legacy/LegacyManiaJudgementPiece.cs index 4b0cc482d9..3752c5f27a 100644 --- a/osu.Game.Rulesets.Mania/Skinning/Legacy/LegacyManiaJudgementPiece.cs +++ b/osu.Game.Rulesets.Mania/Skinning/Legacy/LegacyManiaJudgementPiece.cs @@ -2,12 +2,14 @@ // See the LICENCE file in the repository root for full licence text. using osu.Framework.Allocation; +using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Animations; using osu.Framework.Graphics.Containers; using osu.Framework.Utils; using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Scoring; +using osu.Game.Rulesets.UI.Scrolling; using osu.Game.Skinning; namespace osu.Game.Rulesets.Mania.Skinning.Legacy @@ -28,14 +30,16 @@ namespace osu.Game.Rulesets.Mania.Skinning.Legacy AutoSizeAxes = Axes.Both; } - [BackgroundDependencyLoader] - private void load(ISkinSource skin) - { - float hitPosition = skin.GetManiaSkinConfig(LegacyManiaSkinConfigurationLookups.HitPosition)?.Value ?? 0; - float scorePosition = skin.GetManiaSkinConfig(LegacyManiaSkinConfigurationLookups.ScorePosition)?.Value ?? 0; + private IBindable direction = null!; - float absoluteHitPosition = 480f * LegacyManiaSkinConfiguration.POSITION_SCALE_FACTOR - hitPosition; - Y = scorePosition - absoluteHitPosition; + [Resolved] + private ISkinSource skin { get; set; } = null!; + + [BackgroundDependencyLoader] + private void load(IScrollingInfo scrollingInfo) + { + direction = scrollingInfo.Direction.GetBoundCopy(); + direction.BindValueChanged(_ => onDirectionChanged(), true); InternalChild = animation.With(d => { @@ -44,6 +48,17 @@ namespace osu.Game.Rulesets.Mania.Skinning.Legacy }); } + private void onDirectionChanged() + { + float hitPosition = skin.GetManiaSkinConfig(LegacyManiaSkinConfigurationLookups.HitPosition)?.Value ?? 0; + float scorePosition = skin.GetManiaSkinConfig(LegacyManiaSkinConfigurationLookups.ScorePosition)?.Value ?? 0; + + float absoluteHitPosition = 480f * LegacyManiaSkinConfiguration.POSITION_SCALE_FACTOR - hitPosition; + float finalPosition = scorePosition - absoluteHitPosition; + + Y = direction.Value == ScrollingDirection.Up ? -finalPosition : finalPosition; + } + public void PlayAnimation() { (animation as IFramedAnimation)?.GotoFrame(0); diff --git a/osu.Game.Rulesets.Mania/UI/DefaultManiaJudgementPiece.cs b/osu.Game.Rulesets.Mania/UI/DefaultManiaJudgementPiece.cs new file mode 100644 index 0000000000..f0af6085d0 --- /dev/null +++ b/osu.Game.Rulesets.Mania/UI/DefaultManiaJudgementPiece.cs @@ -0,0 +1,75 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Allocation; +using osu.Framework.Bindables; +using osu.Framework.Graphics; +using osu.Game.Rulesets.Judgements; +using osu.Game.Rulesets.Scoring; +using osu.Game.Rulesets.UI.Scrolling; +using osuTK; + +namespace osu.Game.Rulesets.Mania.UI +{ + public partial class DefaultManiaJudgementPiece : DefaultJudgementPiece + { + private const float judgement_y_position = -180f; + + private IBindable direction = null!; + + public DefaultManiaJudgementPiece(HitResult result) + : base(result) + { + } + + [BackgroundDependencyLoader] + private void load(IScrollingInfo scrollingInfo) + { + direction = scrollingInfo.Direction.GetBoundCopy(); + direction.BindValueChanged(_ => onDirectionChanged(), true); + } + + private void onDirectionChanged() => Y = direction.Value == ScrollingDirection.Up ? -judgement_y_position : judgement_y_position; + + protected override void LoadComplete() + { + base.LoadComplete(); + + JudgementText.Font = JudgementText.Font.With(size: 25); + } + + public override void PlayAnimation() + { + switch (Result) + { + case HitResult.None: + this.FadeOutFromOne(800); + break; + + case HitResult.Miss: + this.ScaleTo(1.6f); + this.ScaleTo(1, 100, Easing.In); + + this.MoveToY(direction.Value == ScrollingDirection.Up ? -judgement_y_position : judgement_y_position); + this.MoveToOffset(new Vector2(0, 100), 800, Easing.InQuint); + + this.RotateTo(0); + this.RotateTo(40, 800, Easing.InQuint); + + this.FadeOutFromOne(800); + break; + + default: + this.ScaleTo(0.8f); + this.ScaleTo(1, 250, Easing.OutElastic); + + this.Delay(50) + .ScaleTo(0.75f, 250) + .FadeOut(200); + + // osu!mania uses a custom fade length, so the base call is intentionally omitted. + break; + } + } + } +} From 895493877cd0f04699099a4228657b05365c7b85 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 10 Feb 2025 09:02:47 +0100 Subject: [PATCH 0977/1275] Allow performing beatmap reload after submission from song select --- osu.Game/Screens/Edit/Submission/BeatmapSubmissionScreen.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/osu.Game/Screens/Edit/Submission/BeatmapSubmissionScreen.cs b/osu.Game/Screens/Edit/Submission/BeatmapSubmissionScreen.cs index 121e25d8b7..f53d10d23b 100644 --- a/osu.Game/Screens/Edit/Submission/BeatmapSubmissionScreen.cs +++ b/osu.Game/Screens/Edit/Submission/BeatmapSubmissionScreen.cs @@ -29,6 +29,7 @@ using osu.Game.Online.API.Requests.Responses; using osu.Game.Overlays; using osu.Game.Overlays.Notifications; using osu.Game.Screens.Menu; +using osu.Game.Screens.Select; using osuTK; namespace osu.Game.Screens.Edit.Submission @@ -418,7 +419,7 @@ namespace osu.Game.Screens.Edit.Submission } s.Push(new EditorLoader()); - }, [typeof(MainMenu)]); + }, [typeof(SongSelect)]); return false; } From 45259b374a2fdd6626e06a7ed9c526cf28cd5fb9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 10 Feb 2025 09:09:43 +0100 Subject: [PATCH 0978/1275] Remove unused using --- osu.Game/Screens/Edit/Submission/BeatmapSubmissionScreen.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/osu.Game/Screens/Edit/Submission/BeatmapSubmissionScreen.cs b/osu.Game/Screens/Edit/Submission/BeatmapSubmissionScreen.cs index f53d10d23b..9672e4360a 100644 --- a/osu.Game/Screens/Edit/Submission/BeatmapSubmissionScreen.cs +++ b/osu.Game/Screens/Edit/Submission/BeatmapSubmissionScreen.cs @@ -28,7 +28,6 @@ using osu.Game.Online.API.Requests; using osu.Game.Online.API.Requests.Responses; using osu.Game.Overlays; using osu.Game.Overlays.Notifications; -using osu.Game.Screens.Menu; using osu.Game.Screens.Select; using osuTK; From b8e33a28d25c8590cf4d0b93e59deeaa21daa1d2 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 10 Feb 2025 17:40:00 +0900 Subject: [PATCH 0979/1275] Minor code refactors --- .../Submission/BeatmapSubmissionScreen.cs | 19 ++++++++++------- .../Submission/SubmissionBeatmapExporter.cs | 21 +++++++------------ 2 files changed, 19 insertions(+), 21 deletions(-) diff --git a/osu.Game/Screens/Edit/Submission/BeatmapSubmissionScreen.cs b/osu.Game/Screens/Edit/Submission/BeatmapSubmissionScreen.cs index 9672e4360a..201888e078 100644 --- a/osu.Game/Screens/Edit/Submission/BeatmapSubmissionScreen.cs +++ b/osu.Game/Screens/Edit/Submission/BeatmapSubmissionScreen.cs @@ -76,7 +76,6 @@ namespace osu.Game.Screens.Edit.Submission private uint? beatmapSetId; private MemoryStream? beatmapPackageStream; - private SubmissionBeatmapExporter legacyBeatmapExporter = null!; private ProgressNotification? exportProgressNotification; private ProgressNotification? updateProgressNotification; @@ -214,8 +213,7 @@ namespace osu.Game.Screens.Edit.Submission }).ConfigureAwait(true); } - legacyBeatmapExporter = new SubmissionBeatmapExporter(storage, response); - await createBeatmapPackage(response.Files).ConfigureAwait(true); + await createBeatmapPackage(response).ConfigureAwait(true); }; createRequest.Failure += ex => { @@ -228,7 +226,7 @@ namespace osu.Game.Screens.Edit.Submission api.Queue(createRequest); } - private async Task createBeatmapPackage(ICollection onlineFiles) + private async Task createBeatmapPackage(PutBeatmapSetResponse response) { Debug.Assert(ThreadSafety.IsUpdateThread); @@ -237,8 +235,13 @@ namespace osu.Game.Screens.Edit.Submission try { beatmapPackageStream = new MemoryStream(); - await legacyBeatmapExporter.ExportToStreamAsync(Beatmap.Value.BeatmapSetInfo.ToLive(realmAccess), beatmapPackageStream, exportProgressNotification = new ProgressNotification()) - .ConfigureAwait(true); + exportProgressNotification = new ProgressNotification(); + + var legacyBeatmapExporter = new SubmissionBeatmapExporter(storage, response); + + await legacyBeatmapExporter + .ExportToStreamAsync(Beatmap.Value.BeatmapSetInfo.ToLive(realmAccess), beatmapPackageStream, exportProgressNotification) + .ConfigureAwait(true); } catch (Exception ex) { @@ -253,8 +256,8 @@ namespace osu.Game.Screens.Edit.Submission await Task.Delay(200).ConfigureAwait(true); - if (onlineFiles.Count > 0) - await patchBeatmapSet(onlineFiles).ConfigureAwait(true); + if (response.Files.Count > 0) + await patchBeatmapSet(response.Files).ConfigureAwait(true); else replaceBeatmapSet(); } diff --git a/osu.Game/Screens/Edit/Submission/SubmissionBeatmapExporter.cs b/osu.Game/Screens/Edit/Submission/SubmissionBeatmapExporter.cs index 3c50a1bf80..fab080cdba 100644 --- a/osu.Game/Screens/Edit/Submission/SubmissionBeatmapExporter.cs +++ b/osu.Game/Screens/Edit/Submission/SubmissionBeatmapExporter.cs @@ -14,43 +14,38 @@ namespace osu.Game.Screens.Edit.Submission public class SubmissionBeatmapExporter : LegacyBeatmapExporter { private readonly uint? beatmapSetId; - private readonly HashSet? beatmapIds; - - public SubmissionBeatmapExporter(Storage storage) - : base(storage) - { - } + private readonly HashSet? allocatedBeatmapIds; public SubmissionBeatmapExporter(Storage storage, PutBeatmapSetResponse putBeatmapSetResponse) : base(storage) { beatmapSetId = putBeatmapSetResponse.BeatmapSetId; - beatmapIds = putBeatmapSetResponse.BeatmapIds.Select(id => (int)id).ToHashSet(); + allocatedBeatmapIds = putBeatmapSetResponse.BeatmapIds.Select(id => (int)id).ToHashSet(); } protected override void MutateBeatmap(BeatmapSetInfo beatmapSet, IBeatmap playableBeatmap) { base.MutateBeatmap(beatmapSet, playableBeatmap); - if (beatmapSetId != null && beatmapIds != null) + if (beatmapSetId != null && allocatedBeatmapIds != null) { playableBeatmap.BeatmapInfo.BeatmapSet = beatmapSet; playableBeatmap.BeatmapInfo.BeatmapSet!.OnlineID = (int)beatmapSetId; - if (beatmapIds.Contains(playableBeatmap.BeatmapInfo.OnlineID)) + if (allocatedBeatmapIds.Contains(playableBeatmap.BeatmapInfo.OnlineID)) { - beatmapIds.Remove(playableBeatmap.BeatmapInfo.OnlineID); + allocatedBeatmapIds.Remove(playableBeatmap.BeatmapInfo.OnlineID); return; } if (playableBeatmap.BeatmapInfo.OnlineID > 0) throw new InvalidOperationException(@"Encountered beatmap with ID that has not been assigned to it by the server!"); - if (beatmapIds.Count == 0) + if (allocatedBeatmapIds.Count == 0) throw new InvalidOperationException(@"Ran out of new beatmap IDs to assign to unsubmitted beatmaps!"); - int newId = beatmapIds.First(); - beatmapIds.Remove(newId); + int newId = allocatedBeatmapIds.First(); + allocatedBeatmapIds.Remove(newId); playableBeatmap.BeatmapInfo.OnlineID = newId; } } From d4ce71267256590ff170281b17ef471fdb497653 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 10 Feb 2025 17:57:01 +0900 Subject: [PATCH 0980/1275] Add note about weird taiko iteration --- .../Difficulty/Utils/IntervalGroupingUtils.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Utils/IntervalGroupingUtils.cs b/osu.Game.Rulesets.Taiko/Difficulty/Utils/IntervalGroupingUtils.cs index 7bd7aa7677..5ab58ad4f3 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Utils/IntervalGroupingUtils.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Utils/IntervalGroupingUtils.cs @@ -23,6 +23,8 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Utils { const double margin_of_error = 5; + // This never compares the first two elements in the group. + // This sounds wrong but is apparently "as intended" (https://github.com/ppy/osu/pull/31636#discussion_r1942673329) var groupedObjects = new List { objects[i] }; i++; From 340e081965355fc1f36acdd1acce1d7c6b773780 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 10 Feb 2025 18:05:08 +0900 Subject: [PATCH 0981/1275] Rename buzz variable per review --- osu.Game.Rulesets.Catch/Difficulty/Skills/Movement.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/osu.Game.Rulesets.Catch/Difficulty/Skills/Movement.cs b/osu.Game.Rulesets.Catch/Difficulty/Skills/Movement.cs index 2d1adbd056..559e9dafa0 100644 --- a/osu.Game.Rulesets.Catch/Difficulty/Skills/Movement.cs +++ b/osu.Game.Rulesets.Catch/Difficulty/Skills/Movement.cs @@ -28,7 +28,7 @@ namespace osu.Game.Rulesets.Catch.Difficulty.Skills private float lastDistanceMoved; private float lastExactDistanceMoved; private double lastStrainTime; - private bool isBuzzSliderTriggered; + private bool isInBuzzSection; /// /// The speed multiplier applied to the player's catcher. @@ -107,14 +107,14 @@ namespace osu.Game.Rulesets.Catch.Difficulty.Skills // To achieve that, we need to store the exact distances (distance ignoring absolute_player_positioning_error and normalized_hitobject_radius) if (Math.Abs(exactDistanceMoved) <= HalfCatcherWidth * 2 && exactDistanceMoved == -lastExactDistanceMoved && catchCurrent.StrainTime == lastStrainTime) { - if (isBuzzSliderTriggered) + if (isInBuzzSection) distanceAddition = 0; else - isBuzzSliderTriggered = true; + isInBuzzSection = true; } else { - isBuzzSliderTriggered = false; + isInBuzzSection = false; } lastPlayerPosition = playerPosition; From 3ba56e009e347942089dbfe8533020ad7a4b63e5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 10 Feb 2025 10:41:10 +0100 Subject: [PATCH 0982/1275] Privatise a few members --- osu.Game/Screens/Play/HUD/SpectatorList.cs | 35 +++++++++++----------- 1 file changed, 17 insertions(+), 18 deletions(-) diff --git a/osu.Game/Screens/Play/HUD/SpectatorList.cs b/osu.Game/Screens/Play/HUD/SpectatorList.cs index c784fc298a..72e866cb24 100644 --- a/osu.Game/Screens/Play/HUD/SpectatorList.cs +++ b/osu.Game/Screens/Play/HUD/SpectatorList.cs @@ -28,17 +28,16 @@ namespace osu.Game.Screens.Play.HUD { private const int max_spectators_displayed = 10; - public BindableList Spectators { get; } = new BindableList(); - public Bindable UserPlayingState { get; } = new Bindable(); - [SettingSource(typeof(SkinnableComponentStrings), nameof(SkinnableComponentStrings.Font), nameof(SkinnableComponentStrings.FontDescription))] public Bindable Font { get; } = new Bindable(Typeface.Torus); [SettingSource(typeof(SkinnableComponentStrings), nameof(SkinnableComponentStrings.TextColour), nameof(SkinnableComponentStrings.TextColourDescription))] public BindableColour4 HeaderColour { get; } = new BindableColour4(Colour4.White); - protected OsuSpriteText Header { get; private set; } = null!; + private BindableList spectators { get; } = new BindableList(); + private Bindable userPlayingState { get; } = new Bindable(); + private OsuSpriteText header = null!; private FillFlowContainer mainFlow = null!; private FillFlowContainer spectatorsFlow = null!; private DrawablePool pool = null!; @@ -63,7 +62,7 @@ namespace osu.Game.Screens.Play.HUD Direction = FillDirection.Vertical, Children = new Drawable[] { - Header = new OsuSpriteText + header = new OsuSpriteText { Colour = colours.Blue0, Font = OsuFont.GetFont(size: 12, weight: FontWeight.Bold), @@ -78,18 +77,18 @@ namespace osu.Game.Screens.Play.HUD pool = new DrawablePool(max_spectators_displayed), }; - HeaderColour.Value = Header.Colour; + HeaderColour.Value = header.Colour; } protected override void LoadComplete() { base.LoadComplete(); - ((IBindableList)Spectators).BindTo(client.WatchingUsers); - ((IBindable)UserPlayingState).BindTo(gameplayState.PlayingState); + ((IBindableList)spectators).BindTo(client.WatchingUsers); + ((IBindable)userPlayingState).BindTo(gameplayState.PlayingState); - Spectators.BindCollectionChanged(onSpectatorsChanged, true); - UserPlayingState.BindValueChanged(_ => updateVisibility()); + spectators.BindCollectionChanged(onSpectatorsChanged, true); + userPlayingState.BindValueChanged(_ => updateVisibility()); Font.BindValueChanged(_ => updateAppearance()); HeaderColour.BindValueChanged(_ => updateAppearance(), true); @@ -125,10 +124,10 @@ namespace osu.Game.Screens.Play.HUD for (int i = 0; i < spectatorsFlow.Count; i++) spectatorsFlow.SetLayoutPosition(spectatorsFlow[i], i); - if (Spectators.Count >= max_spectators_displayed && spectatorsFlow.Count < max_spectators_displayed) + if (spectators.Count >= max_spectators_displayed && spectatorsFlow.Count < max_spectators_displayed) { for (int i = spectatorsFlow.Count; i < max_spectators_displayed; i++) - addNewSpectatorToList(i, Spectators[i]); + addNewSpectatorToList(i, spectators[i]); } break; @@ -144,7 +143,7 @@ namespace osu.Game.Screens.Play.HUD throw new NotSupportedException(); } - Header.Text = SpectatorListStrings.SpectatorCount(Spectators.Count).ToUpper(); + header.Text = SpectatorListStrings.SpectatorCount(spectators.Count).ToUpper(); updateVisibility(); for (int i = 0; i < spectatorsFlow.Count; i++) @@ -160,7 +159,7 @@ namespace osu.Game.Screens.Play.HUD var entry = pool.Get(entry => { entry.Current.Value = spectator; - entry.UserPlayingState = UserPlayingState; + entry.UserPlayingState = userPlayingState; }); spectatorsFlow.Insert(i, entry); @@ -169,15 +168,15 @@ namespace osu.Game.Screens.Play.HUD private void updateVisibility() { // We don't want to show spectators when we are watching a replay. - mainFlow.FadeTo(Spectators.Count > 0 && UserPlayingState.Value != LocalUserPlayingState.NotPlaying ? 1 : 0, 250, Easing.OutQuint); + mainFlow.FadeTo(spectators.Count > 0 && userPlayingState.Value != LocalUserPlayingState.NotPlaying ? 1 : 0, 250, Easing.OutQuint); } private void updateAppearance() { - Header.Font = OsuFont.GetFont(Font.Value, 12, FontWeight.Bold); - Header.Colour = HeaderColour.Value; + header.Font = OsuFont.GetFont(Font.Value, 12, FontWeight.Bold); + header.Colour = HeaderColour.Value; - Width = Header.DrawWidth; + Width = header.DrawWidth; } private partial class SpectatorListEntry : PoolableDrawable From ad642b84258497b0140ae3d45680f52988a1429f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 10 Feb 2025 11:17:17 +0100 Subject: [PATCH 0983/1275] Fix spectator list showing other users in multiplayer room even if they're not spectating --- .../Visual/Gameplay/TestSceneSpectatorList.cs | 17 ++++++--- osu.Game/Screens/Play/HUD/SpectatorList.cs | 37 +++++++++++++++---- 2 files changed, 42 insertions(+), 12 deletions(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSpectatorList.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSpectatorList.cs index 66a87c0715..66c465cbed 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneSpectatorList.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSpectatorList.cs @@ -8,11 +8,14 @@ using osu.Framework.Graphics; using osu.Framework.Utils; using osu.Game.Beatmaps; using osu.Game.Graphics; +using osu.Game.Online.Multiplayer; using osu.Game.Online.Spectator; using osu.Game.Rulesets.Osu; using osu.Game.Rulesets.Osu.Scoring; using osu.Game.Screens.Play; using osu.Game.Screens.Play.HUD; +using osu.Game.Tests.Visual.Multiplayer; +using osu.Game.Tests.Visual.OnlinePlay; using osu.Game.Tests.Visual.Spectator; namespace osu.Game.Tests.Visual.Gameplay @@ -28,20 +31,23 @@ namespace osu.Game.Tests.Visual.Gameplay SpectatorList list = null!; Bindable playingState = new Bindable(); GameplayState gameplayState = new GameplayState(new Beatmap(), new OsuRuleset(), healthProcessor: new OsuHealthProcessor(0), localUserPlayingState: playingState); - TestSpectatorClient client = new TestSpectatorClient(); + TestSpectatorClient spectatorClient = new TestSpectatorClient(); + TestMultiplayerClient multiplayerClient = new TestMultiplayerClient(new TestMultiplayerRoomManager(new TestRoomRequestsHandler())); AddStep("create spectator list", () => { Children = new Drawable[] { - client, + spectatorClient, + multiplayerClient, new DependencyProvidingContainer { RelativeSizeAxes = Axes.Both, CachedDependencies = [ (typeof(GameplayState), gameplayState), - (typeof(SpectatorClient), client) + (typeof(SpectatorClient), spectatorClient), + (typeof(MultiplayerClient), multiplayerClient), ], Child = list = new SpectatorList { @@ -57,7 +63,7 @@ namespace osu.Game.Tests.Visual.Gameplay AddRepeatStep("add a user", () => { int id = Interlocked.Increment(ref counter); - ((ISpectatorClient)client).UserStartedWatching([ + ((ISpectatorClient)spectatorClient).UserStartedWatching([ new SpectatorUser { OnlineID = id, @@ -66,7 +72,8 @@ namespace osu.Game.Tests.Visual.Gameplay ]); }, 10); - AddRepeatStep("remove random user", () => ((ISpectatorClient)client).UserEndedWatching(client.WatchingUsers[RNG.Next(client.WatchingUsers.Count)].OnlineID), 5); + AddRepeatStep("remove random user", () => ((ISpectatorClient)spectatorClient).UserEndedWatching( + spectatorClient.WatchingUsers[RNG.Next(spectatorClient.WatchingUsers.Count)].OnlineID), 5); AddStep("change font to venera", () => list.Font.Value = Typeface.Venera); AddStep("change font to torus", () => list.Font.Value = Typeface.Torus); diff --git a/osu.Game/Screens/Play/HUD/SpectatorList.cs b/osu.Game/Screens/Play/HUD/SpectatorList.cs index 72e866cb24..9f97121a92 100644 --- a/osu.Game/Screens/Play/HUD/SpectatorList.cs +++ b/osu.Game/Screens/Play/HUD/SpectatorList.cs @@ -2,7 +2,9 @@ // See the LICENCE file in the repository root for full licence text. using System; +using System.Collections.Generic; using System.Collections.Specialized; +using System.Linq; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Extensions.Color4Extensions; @@ -17,6 +19,7 @@ using osu.Game.Graphics.Sprites; using osu.Game.Online.Chat; using osu.Game.Localisation.HUD; using osu.Game.Localisation.SkinComponents; +using osu.Game.Online.Multiplayer; using osu.Game.Online.Spectator; using osu.Game.Skinning; using osuTK; @@ -34,8 +37,9 @@ namespace osu.Game.Screens.Play.HUD [SettingSource(typeof(SkinnableComponentStrings), nameof(SkinnableComponentStrings.TextColour), nameof(SkinnableComponentStrings.TextColourDescription))] public BindableColour4 HeaderColour { get; } = new BindableColour4(Colour4.White); - private BindableList spectators { get; } = new BindableList(); + private BindableList watchingUsers { get; } = new BindableList(); private Bindable userPlayingState { get; } = new Bindable(); + private int displayedSpectatorCount; private OsuSpriteText header = null!; private FillFlowContainer mainFlow = null!; @@ -48,6 +52,9 @@ namespace osu.Game.Screens.Play.HUD [Resolved] private GameplayState gameplayState { get; set; } = null!; + [Resolved] + private MultiplayerClient multiplayerClient { get; set; } = null!; + [BackgroundDependencyLoader] private void load(OsuColour colours) { @@ -84,10 +91,10 @@ namespace osu.Game.Screens.Play.HUD { base.LoadComplete(); - ((IBindableList)spectators).BindTo(client.WatchingUsers); + ((IBindableList)watchingUsers).BindTo(client.WatchingUsers); ((IBindable)userPlayingState).BindTo(gameplayState.PlayingState); - spectators.BindCollectionChanged(onSpectatorsChanged, true); + watchingUsers.BindCollectionChanged(onSpectatorsChanged, true); userPlayingState.BindValueChanged(_ => updateVisibility()); Font.BindValueChanged(_ => updateAppearance()); @@ -99,6 +106,18 @@ namespace osu.Game.Screens.Play.HUD private void onSpectatorsChanged(object? sender, NotifyCollectionChangedEventArgs e) { + // the multiplayer gameplay leaderboard relies on calling `SpectatorClient.WatchUser()` to get updates on users' total scores. + // this has an unfortunate side effect of other players showing up in `SpectatorClient.WatchingUsers`. + // we do not generally wish to display other players in the room as spectators due to that implementation detail, + // therefore this code is intended to filter out those players on the client side. + // note that the way that this is done is rather specific to the multiplayer use case and therefore carries a lot of assumptions + // (e.g. that the `MultiplayerRoomUser`s have the correct `State` at the point wherein they issue the `WatchUser()` calls). + // the more proper way to do this (which is by subscribing to `WatchingUsers` and `RoomUpdated`, and doing a proper diff to a third list on any change of either) + // is a lot more difficult to write correctly, given that we also rely on `BindableList`'s collection changed event arguments to properly animate this component. + var excludedUserIds = new HashSet(); + if (multiplayerClient.Room != null) + excludedUserIds.UnionWith(multiplayerClient.Room.Users.Where(u => u.State != MultiplayerUserState.Spectating).Select(u => u.UserID)); + switch (e.Action) { case NotifyCollectionChangedAction.Add: @@ -108,6 +127,9 @@ namespace osu.Game.Screens.Play.HUD var spectator = (SpectatorUser)e.NewItems![i]!; int index = Math.Max(e.NewStartingIndex, 0) + i; + if (excludedUserIds.Contains(spectator.OnlineID)) + continue; + if (index >= max_spectators_displayed) break; @@ -124,10 +146,10 @@ namespace osu.Game.Screens.Play.HUD for (int i = 0; i < spectatorsFlow.Count; i++) spectatorsFlow.SetLayoutPosition(spectatorsFlow[i], i); - if (spectators.Count >= max_spectators_displayed && spectatorsFlow.Count < max_spectators_displayed) + if (watchingUsers.Count >= max_spectators_displayed && spectatorsFlow.Count < max_spectators_displayed) { for (int i = spectatorsFlow.Count; i < max_spectators_displayed; i++) - addNewSpectatorToList(i, spectators[i]); + addNewSpectatorToList(i, watchingUsers[i]); } break; @@ -143,7 +165,8 @@ namespace osu.Game.Screens.Play.HUD throw new NotSupportedException(); } - header.Text = SpectatorListStrings.SpectatorCount(spectators.Count).ToUpper(); + displayedSpectatorCount = watchingUsers.Count(s => !excludedUserIds.Contains(s.OnlineID)); + header.Text = SpectatorListStrings.SpectatorCount(displayedSpectatorCount).ToUpper(); updateVisibility(); for (int i = 0; i < spectatorsFlow.Count; i++) @@ -168,7 +191,7 @@ namespace osu.Game.Screens.Play.HUD private void updateVisibility() { // We don't want to show spectators when we are watching a replay. - mainFlow.FadeTo(spectators.Count > 0 && userPlayingState.Value != LocalUserPlayingState.NotPlaying ? 1 : 0, 250, Easing.OutQuint); + mainFlow.FadeTo(displayedSpectatorCount > 0 && userPlayingState.Value != LocalUserPlayingState.NotPlaying ? 1 : 0, 250, Easing.OutQuint); } private void updateAppearance() From 4b8890ef0c86e7ccdd415181b89ca132331a7024 Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Mon, 10 Feb 2025 05:55:21 -0500 Subject: [PATCH 0984/1275] Fix incorrect thread access in recent iOS orientation changes --- osu.iOS/OsuGameIOS.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.iOS/OsuGameIOS.cs b/osu.iOS/OsuGameIOS.cs index a5a42c1e66..a0132de966 100644 --- a/osu.iOS/OsuGameIOS.cs +++ b/osu.iOS/OsuGameIOS.cs @@ -41,7 +41,7 @@ namespace osu.iOS updateOrientation(); } - private void updateOrientation() + private void updateOrientation() => UIApplication.SharedApplication.InvokeOnMainThread(() => { bool iPad = UIDevice.CurrentDevice.UserInterfaceIdiom == UIUserInterfaceIdiom.Pad; var orientation = MobileUtils.GetOrientation(this, (IOsuScreen)ScreenStack.CurrentScreen, iPad); @@ -60,7 +60,7 @@ namespace osu.iOS appDelegate.Orientations = null; break; } - } + }); protected override UpdateManager CreateUpdateManager() => new MobileUpdateNotifier(); From 288851c606682165d7bb9f0cc8604eefa2b1604b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 10 Feb 2025 12:13:46 +0100 Subject: [PATCH 0985/1275] Fix score position not being displayed in solo results screen Closes https://github.com/ppy/osu/issues/31842. To be honest, I recall this working too, but I don't recall when it might have broken, nor do I want to go look for the point of breakage because it might be borderline impossible to find it now. So I'm just fixing as if it was just a straight omission. Opting for a client-side fix because server-side inclusion of the score position for an entire leaderboard has been previously rejected as too expensive: https://github.com/ppy/osu-web/pull/11354#discussion_r1689217450 --- osu.Game/Screens/Ranking/SoloResultsScreen.cs | 28 +++++++++++++++++-- 1 file changed, 26 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/Ranking/SoloResultsScreen.cs b/osu.Game/Screens/Ranking/SoloResultsScreen.cs index 33b4bf976b..9f7604aa82 100644 --- a/osu.Game/Screens/Ranking/SoloResultsScreen.cs +++ b/osu.Game/Screens/Ranking/SoloResultsScreen.cs @@ -4,7 +4,6 @@ using System; using System.Collections.Generic; using System.Diagnostics; -using System.Linq; using osu.Framework.Allocation; using osu.Game.Beatmaps; using osu.Game.Extensions; @@ -35,7 +34,32 @@ namespace osu.Game.Screens.Ranking return null; getScoreRequest = new GetScoresRequest(Score.BeatmapInfo, Score.Ruleset); - getScoreRequest.Success += r => scoresCallback.Invoke(r.Scores.Where(s => !s.MatchesOnlineID(Score)).Select(s => s.ToScoreInfo(rulesets, Beatmap.Value.BeatmapInfo))); + getScoreRequest.Success += r => + { + var toDisplay = new List(); + + for (int i = 0; i < r.Scores.Count; ++i) + { + var score = r.Scores[i]; + int position = i + 1; + + if (score.MatchesOnlineID(Score)) + { + // we don't want to add the same score twice, but also setting any properties of `Score` this late will have no visible effect, + // so we have to fish out the actual drawable panel and set the position to it directly. + var panel = ScorePanelList.GetPanelForScore(Score); + Score.Position = panel.ScorePosition.Value = position; + } + else + { + var converted = score.ToScoreInfo(rulesets, Beatmap.Value.BeatmapInfo); + converted.Position = position; + toDisplay.Add(converted); + } + } + + scoresCallback.Invoke(toDisplay); + }; return getScoreRequest; } From 38e2f793cae38148dc5c2eae7af7c20dd46c98b2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 10 Feb 2025 12:47:38 +0100 Subject: [PATCH 0986/1275] Add menu items to open beatmap info & discussion pages in browser from editor --- osu.Game/Localisation/EditorStrings.cs | 12 +++++++++++- osu.Game/Screens/Edit/Editor.cs | 9 +++++++++ 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/osu.Game/Localisation/EditorStrings.cs b/osu.Game/Localisation/EditorStrings.cs index 3b4026be11..1681e541fc 100644 --- a/osu.Game/Localisation/EditorStrings.cs +++ b/osu.Game/Localisation/EditorStrings.cs @@ -184,6 +184,16 @@ namespace osu.Game.Localisation /// public static LocalisableString ResetBookmarks => new TranslatableString(getKey(@"reset_bookmarks"), @"Reset bookmarks"); + /// + /// "Open beatmap info page in browser" + /// + public static LocalisableString OpenInfoPageInBrowser => new TranslatableString(getKey(@"open_info_page_in_browser"), @"Open beatmap info page in browser"); + + /// + /// "Open beatmap discussion page in browser" + /// + public static LocalisableString OpenDiscussionPageInBrowser => new TranslatableString(getKey(@"open_discussion_page_in_browser"), @"Open beatmap discussion page in browser"); + private static string getKey(string key) => $@"{prefix}:{key}"; } -} \ No newline at end of file +} diff --git a/osu.Game/Screens/Edit/Editor.cs b/osu.Game/Screens/Edit/Editor.cs index a5dfda9c95..ecb0731c16 100644 --- a/osu.Game/Screens/Edit/Editor.cs +++ b/osu.Game/Screens/Edit/Editor.cs @@ -1256,6 +1256,15 @@ namespace osu.Game.Screens.Edit yield return externalEdit; } + if (editorBeatmap.BeatmapInfo.OnlineID > 0) + { + yield return new OsuMenuItemSpacer(); + yield return new EditorMenuItem(EditorStrings.OpenInfoPageInBrowser, MenuItemType.Standard, + () => (Game as OsuGame)?.OpenUrlExternally(editorBeatmap.BeatmapInfo.GetOnlineURL(api, editorBeatmap.BeatmapInfo.Ruleset))); + yield return new EditorMenuItem(EditorStrings.OpenDiscussionPageInBrowser, MenuItemType.Standard, + () => (Game as OsuGame)?.OpenUrlExternally($@"{api.Endpoints.WebsiteUrl}/beatmapsets/{editorBeatmap.BeatmapInfo.BeatmapSet!.OnlineID}/discussion/{editorBeatmap.BeatmapInfo.OnlineID}")); + } + yield return new OsuMenuItemSpacer(); yield return new EditorMenuItem(CommonStrings.Exit, MenuItemType.Standard, this.Exit); } From 310700b4e7a4d3570605195babc78826751f0de6 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 10 Feb 2025 21:48:27 +0900 Subject: [PATCH 0987/1275] Space out comment --- osu.Game/Screens/Play/HUD/SpectatorList.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/osu.Game/Screens/Play/HUD/SpectatorList.cs b/osu.Game/Screens/Play/HUD/SpectatorList.cs index 9f97121a92..4297c62712 100644 --- a/osu.Game/Screens/Play/HUD/SpectatorList.cs +++ b/osu.Game/Screens/Play/HUD/SpectatorList.cs @@ -108,8 +108,10 @@ namespace osu.Game.Screens.Play.HUD { // the multiplayer gameplay leaderboard relies on calling `SpectatorClient.WatchUser()` to get updates on users' total scores. // this has an unfortunate side effect of other players showing up in `SpectatorClient.WatchingUsers`. + // // we do not generally wish to display other players in the room as spectators due to that implementation detail, // therefore this code is intended to filter out those players on the client side. + // // note that the way that this is done is rather specific to the multiplayer use case and therefore carries a lot of assumptions // (e.g. that the `MultiplayerRoomUser`s have the correct `State` at the point wherein they issue the `WatchUser()` calls). // the more proper way to do this (which is by subscribing to `WatchingUsers` and `RoomUpdated`, and doing a proper diff to a third list on any change of either) From 78e5e0eddd1e20e480b3e49b59c2f1c3f5319e8e Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Tue, 11 Feb 2025 12:17:00 +0900 Subject: [PATCH 0988/1275] Refactor with a bit more null safety In particular I don't like the non-null assert around `GetCurrentItem()`, because there's no reason why it _couldn't_ be `null`. Consider, for example, if these panels are used in matchmaking where there are no items initially present in the playlist. The ruleset nullability part is debatable, but I've chosen to restore the original code here. --- .../Participants/ParticipantPanel.cs | 39 +++++++++---------- 1 file changed, 19 insertions(+), 20 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Participants/ParticipantPanel.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Participants/ParticipantPanel.cs index 51ff52c63e..230245e926 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Participants/ParticipantPanel.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Participants/ParticipantPanel.cs @@ -3,7 +3,6 @@ using System; using System.Collections.Generic; -using System.Diagnostics; using System.Linq; using System.Threading; using System.Threading.Tasks; @@ -28,6 +27,7 @@ using osu.Game.Online.API; using osu.Game.Online.Multiplayer; using osu.Game.Online.Rooms; using osu.Game.Rulesets; +using osu.Game.Rulesets.Mods; using osu.Game.Screens.Play.HUD; using osu.Game.Users; using osu.Game.Users.Drawables; @@ -216,20 +216,28 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Participants const double fade_time = 50; - MultiplayerPlaylistItem? currentItem = client.Room.GetCurrentItem(); - Debug.Assert(currentItem != null); + if (client.Room.GetCurrentItem() is MultiplayerPlaylistItem currentItem) + { + int userBeatmapId = User.BeatmapId ?? currentItem.BeatmapID; + int userRulesetId = User.RulesetId ?? currentItem.RulesetID; + Ruleset? userRuleset = rulesets.GetRuleset(userRulesetId)?.CreateInstance(); - int userBeatmapId = User.BeatmapId ?? currentItem.BeatmapID; - int userRulesetId = User.RulesetId ?? currentItem.RulesetID; - Ruleset? userRuleset = rulesets.GetRuleset(userRulesetId)?.CreateInstance(); - Debug.Assert(userRuleset != null); + int? currentModeRank = User.User?.RulesetsStatistics?.GetValueOrDefault(userRuleset?.ShortName)?.GlobalRank; + userRankText.Text = currentModeRank != null ? $"#{currentModeRank.Value:N0}" : string.Empty; + + if (userBeatmapId == currentItem.BeatmapID && userRulesetId == currentItem.RulesetID) + userStyleDisplay.Style = null; + else + userStyleDisplay.Style = (userBeatmapId, userRulesetId); + + // If the mods are updated at the end of the frame, the flow container will skip a reflow cycle: https://github.com/ppy/osu-framework/issues/4187 + // This looks particularly jarring here, so re-schedule the update to that start of our frame as a fix. + Schedule(() => userModsDisplay.Current.Value = userRuleset == null ? Array.Empty() : User.Mods.Select(m => m.ToMod(userRuleset)).ToList()); + } userStateDisplay.UpdateStatus(User.State, User.BeatmapAvailability); - int? currentModeRank = User.User?.RulesetsStatistics?.GetValueOrDefault(userRuleset.ShortName)?.GlobalRank; - userRankText.Text = currentModeRank != null ? $"#{currentModeRank.Value:N0}" : string.Empty; - - if ((User.BeatmapAvailability.State == DownloadState.LocallyAvailable) && (User.State != MultiplayerUserState.Spectating)) + if (User.BeatmapAvailability.State == DownloadState.LocallyAvailable && User.State != MultiplayerUserState.Spectating) { userModsDisplay.FadeIn(fade_time); userStyleDisplay.FadeIn(fade_time); @@ -240,17 +248,8 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Participants userStyleDisplay.FadeOut(fade_time); } - if (userBeatmapId == currentItem.BeatmapID && userRulesetId == currentItem.RulesetID) - userStyleDisplay.Style = null; - else - userStyleDisplay.Style = (userBeatmapId, userRulesetId); - kickButton.Alpha = client.IsHost && !User.Equals(client.LocalUser) ? 1 : 0; crown.Alpha = client.Room.Host?.Equals(User) == true ? 1 : 0; - - // If the mods are updated at the end of the frame, the flow container will skip a reflow cycle: https://github.com/ppy/osu-framework/issues/4187 - // This looks particularly jarring here, so re-schedule the update to that start of our frame as a fix. - Schedule(() => userModsDisplay.Current.Value = User.Mods.Select(m => m.ToMod(userRuleset)).ToList()); } public MenuItem[]? ContextMenuItems From 748c2eb3904bdd23ab60bd2e1dbb5a2c772aecb8 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Tue, 11 Feb 2025 12:43:51 +0900 Subject: [PATCH 0989/1275] Refactor `RoomSubScreen` update --- .../Screens/OnlinePlay/Match/RoomSubScreen.cs | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs b/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs index 312253774f..59acd3c17f 100644 --- a/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs @@ -439,13 +439,14 @@ namespace osu.Game.Screens.OnlinePlay.Match var rulesetInstance = GetGameplayRuleset().CreateInstance(); - // Remove any user mods that are no longer allowed. Mod[] allowedMods = item.Freestyle - ? rulesetInstance.CreateAllMods().Where(m => ModUtils.IsValidFreeModForMatchType(m, Room.Type)).ToArray() + ? rulesetInstance.AllMods.OfType().Where(m => ModUtils.IsValidFreeModForMatchType(m, Room.Type)).ToArray() : item.AllowedMods.Select(m => m.ToMod(rulesetInstance)).ToArray(); + + // Remove any user mods that are no longer allowed. Mod[] newUserMods = UserMods.Value.Where(m => allowedMods.Any(a => m.GetType() == a.GetType())).ToArray(); if (!newUserMods.SequenceEqual(UserMods.Value)) - UserMods.Value = UserMods.Value.Where(m => allowedMods.Any(a => m.GetType() == a.GetType())).ToList(); + UserMods.Value = newUserMods; // Retrieve the corresponding local beatmap, since we can't directly use the playlist's beatmap info int beatmapId = GetGameplayBeatmap().OnlineID; @@ -456,10 +457,7 @@ namespace osu.Game.Screens.OnlinePlay.Match Mods.Value = GetGameplayMods().Select(m => m.ToMod(rulesetInstance)).ToArray(); Ruleset.Value = GetGameplayRuleset(); - bool freestyle = item.Freestyle; - bool freeMod = freestyle || item.AllowedMods.Any(); - - if (freeMod) + if (allowedMods.Length > 0) { UserModsSection.Show(); UserModsSelectOverlay.IsValidMod = m => allowedMods.Any(a => a.GetType() == m.GetType()); @@ -471,7 +469,7 @@ namespace osu.Game.Screens.OnlinePlay.Match UserModsSelectOverlay.IsValidMod = _ => false; } - if (freestyle) + if (item.Freestyle) { UserStyleSection.Show(); @@ -484,7 +482,7 @@ namespace osu.Game.Screens.OnlinePlay.Match UserStyleDisplayContainer.Child = new DrawableRoomPlaylistItem(gameplayItem, true) { AllowReordering = false, - AllowEditing = freestyle, + AllowEditing = true, RequestEdit = _ => OpenStyleSelection() }; } From e51c09ec3d94823ea6707b3541da6d74a738344a Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Tue, 11 Feb 2025 14:23:51 +0900 Subject: [PATCH 0990/1275] Fix inspection Interestingly, this is not a compiler error nor does R# warn about it. No problem, because this is just restoring the original code anyway. --- .../OnlinePlay/Multiplayer/Participants/ParticipantPanel.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Participants/ParticipantPanel.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Participants/ParticipantPanel.cs index 230245e926..0fa2be44f3 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Participants/ParticipantPanel.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Participants/ParticipantPanel.cs @@ -222,7 +222,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Participants int userRulesetId = User.RulesetId ?? currentItem.RulesetID; Ruleset? userRuleset = rulesets.GetRuleset(userRulesetId)?.CreateInstance(); - int? currentModeRank = User.User?.RulesetsStatistics?.GetValueOrDefault(userRuleset?.ShortName)?.GlobalRank; + int? currentModeRank = userRuleset == null ? null : User.User?.RulesetsStatistics?.GetValueOrDefault(userRuleset.ShortName)?.GlobalRank; userRankText.Text = currentModeRank != null ? $"#{currentModeRank.Value:N0}" : string.Empty; if (userBeatmapId == currentItem.BeatmapID && userRulesetId == currentItem.RulesetID) From daf0130b2307e435641a9485fb026f1071aaff6a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 11 Feb 2025 08:06:12 +0100 Subject: [PATCH 0991/1275] Reword copy to be less verbose --- osu.Game/Localisation/EditorStrings.cs | 4 ++-- osu.Game/Screens/Edit/Editor.cs | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/osu.Game/Localisation/EditorStrings.cs b/osu.Game/Localisation/EditorStrings.cs index 1681e541fc..0a15752961 100644 --- a/osu.Game/Localisation/EditorStrings.cs +++ b/osu.Game/Localisation/EditorStrings.cs @@ -187,12 +187,12 @@ namespace osu.Game.Localisation /// /// "Open beatmap info page in browser" /// - public static LocalisableString OpenInfoPageInBrowser => new TranslatableString(getKey(@"open_info_page_in_browser"), @"Open beatmap info page in browser"); + public static LocalisableString OpenInfoPage => new TranslatableString(getKey(@"open_info_page"), @"Open beatmap info page"); /// /// "Open beatmap discussion page in browser" /// - public static LocalisableString OpenDiscussionPageInBrowser => new TranslatableString(getKey(@"open_discussion_page_in_browser"), @"Open beatmap discussion page in browser"); + public static LocalisableString OpenDiscussionPage => new TranslatableString(getKey(@"open_discussion_page"), @"Open beatmap discussion page"); private static string getKey(string key) => $@"{prefix}:{key}"; } diff --git a/osu.Game/Screens/Edit/Editor.cs b/osu.Game/Screens/Edit/Editor.cs index ecb0731c16..d73384af7f 100644 --- a/osu.Game/Screens/Edit/Editor.cs +++ b/osu.Game/Screens/Edit/Editor.cs @@ -1259,9 +1259,9 @@ namespace osu.Game.Screens.Edit if (editorBeatmap.BeatmapInfo.OnlineID > 0) { yield return new OsuMenuItemSpacer(); - yield return new EditorMenuItem(EditorStrings.OpenInfoPageInBrowser, MenuItemType.Standard, + yield return new EditorMenuItem(EditorStrings.OpenInfoPage, MenuItemType.Standard, () => (Game as OsuGame)?.OpenUrlExternally(editorBeatmap.BeatmapInfo.GetOnlineURL(api, editorBeatmap.BeatmapInfo.Ruleset))); - yield return new EditorMenuItem(EditorStrings.OpenDiscussionPageInBrowser, MenuItemType.Standard, + yield return new EditorMenuItem(EditorStrings.OpenDiscussionPage, MenuItemType.Standard, () => (Game as OsuGame)?.OpenUrlExternally($@"{api.Endpoints.WebsiteUrl}/beatmapsets/{editorBeatmap.BeatmapInfo.BeatmapSet!.OnlineID}/discussion/{editorBeatmap.BeatmapInfo.OnlineID}")); } From 7db0a6f81775248bbcb41a57f363b2d6a73b8875 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 11 Feb 2025 08:06:12 +0100 Subject: [PATCH 0992/1275] Update xmldoc --- osu.Game/Localisation/EditorStrings.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Localisation/EditorStrings.cs b/osu.Game/Localisation/EditorStrings.cs index 0a15752961..b74a546eca 100644 --- a/osu.Game/Localisation/EditorStrings.cs +++ b/osu.Game/Localisation/EditorStrings.cs @@ -185,12 +185,12 @@ namespace osu.Game.Localisation public static LocalisableString ResetBookmarks => new TranslatableString(getKey(@"reset_bookmarks"), @"Reset bookmarks"); /// - /// "Open beatmap info page in browser" + /// "Open beatmap info page" /// public static LocalisableString OpenInfoPage => new TranslatableString(getKey(@"open_info_page"), @"Open beatmap info page"); /// - /// "Open beatmap discussion page in browser" + /// "Open beatmap discussion page" /// public static LocalisableString OpenDiscussionPage => new TranslatableString(getKey(@"open_discussion_page"), @"Open beatmap discussion page"); From 1fa8d53232931e0edc37ddba22cde7aacb48e799 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 11 Feb 2025 17:11:20 +0900 Subject: [PATCH 0993/1275] Disable scale animation when holding editor "test" button --- .../Timelines/Summary/TestGameplayButton.cs | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/osu.Game/Screens/Edit/Components/Timelines/Summary/TestGameplayButton.cs b/osu.Game/Screens/Edit/Components/Timelines/Summary/TestGameplayButton.cs index 169e72fe3f..065f52b929 100644 --- a/osu.Game/Screens/Edit/Components/Timelines/Summary/TestGameplayButton.cs +++ b/osu.Game/Screens/Edit/Components/Timelines/Summary/TestGameplayButton.cs @@ -4,6 +4,7 @@ using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Sprites; +using osu.Framework.Input.Events; using osu.Game.Graphics; using osu.Game.Graphics.Sprites; using osu.Game.Graphics.UserInterface; @@ -33,5 +34,16 @@ namespace osu.Game.Screens.Edit.Components.Timelines.Summary Text = EditorStrings.TestBeatmap; } + + protected override bool OnMouseDown(MouseDownEvent e) + { + // block scale animation + return false; + } + + protected override void OnMouseUp(MouseUpEvent e) + { + // block scale animation + } } } From 884fa20b286264482f6e965f946369e42d9fe356 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 11 Feb 2025 09:13:08 +0100 Subject: [PATCH 0994/1275] Remove completely unnecessary subscriptions per collection --- .../Collections/DrawableCollectionList.cs | 1 - .../Collections/DrawableCollectionListItem.cs | 32 +++++++------------ 2 files changed, 11 insertions(+), 22 deletions(-) diff --git a/osu.Game/Collections/DrawableCollectionList.cs b/osu.Game/Collections/DrawableCollectionList.cs index 85af1d383d..c494b830d1 100644 --- a/osu.Game/Collections/DrawableCollectionList.cs +++ b/osu.Game/Collections/DrawableCollectionList.cs @@ -96,7 +96,6 @@ namespace osu.Game.Collections lastCreated = collections[changes.InsertedIndices[0]].ID; foreach (int i in changes.NewModifiedIndices) - { var updatedItem = collections[i]; diff --git a/osu.Game/Collections/DrawableCollectionListItem.cs b/osu.Game/Collections/DrawableCollectionListItem.cs index 0060dacc01..703def9546 100644 --- a/osu.Game/Collections/DrawableCollectionListItem.cs +++ b/osu.Game/Collections/DrawableCollectionListItem.cs @@ -126,15 +126,10 @@ namespace osu.Game.Collections private const float count_text_size = 12; - [Resolved] - private RealmAccess realm { get; set; } = null!; - private readonly Live collection; private OsuSpriteText countText = null!; - private IDisposable? itemCountSubscription; - public ItemTextBox(Live collection) { this.collection = collection; @@ -163,29 +158,24 @@ namespace osu.Game.Collections Colour = colours.Yellow }); - itemCountSubscription = realm.SubscribeToPropertyChanged(r => r.Find(collection.ID), c => c.BeatmapMD5Hashes, _ => - Scheduler.AddOnce(() => - { - int count = collection.PerformRead(c => c.BeatmapMD5Hashes.Count); + // interestingly, it is not required to subscribe to change notifications on this collection at all for this to work correctly. + // the reasoning for this is that `DrawableCollectionList` already takes out a subscription on the set of all `BeatmapCollection`s - + // but that subscription does not only cover *changes to the set of collections* (i.e. addition/removal/rearrangement of collections), + // but also covers *changes to the properties of collections*, which `BeatmapMD5Hashes` is one. + // when a collection item changes due to `BeatmapMD5Hashes` changing, the list item is deleted and re-inserted, thus guaranteeing this to work correctly. + int count = collection.PerformRead(c => c.BeatmapMD5Hashes.Count); - countText.Text = count == 1 - // Intentionally not localised until we have proper support for this (see https://github.com/ppy/osu-framework/pull/4918 - // but also in this case we want support for formatting a number within a string). - ? $"{count:#,0} beatmap" - : $"{count:#,0} beatmaps"; - })); + countText.Text = count == 1 + // Intentionally not localised until we have proper support for this (see https://github.com/ppy/osu-framework/pull/4918 + // but also in this case we want support for formatting a number within a string). + ? $"{count:#,0} beatmap" + : $"{count:#,0} beatmaps"; } else { PlaceholderText = "Create a new collection"; } } - - protected override void Dispose(bool isDisposing) - { - base.Dispose(isDisposing); - itemCountSubscription?.Dispose(); - } } public partial class DeleteButton : OsuClickableContainer From b9ed217308f3ebe7405274d8fbd257835bc259dd Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 11 Feb 2025 17:13:34 +0900 Subject: [PATCH 0995/1275] Add basic brighten animation instead --- .../Timelines/Summary/TestGameplayButton.cs | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/osu.Game/Screens/Edit/Components/Timelines/Summary/TestGameplayButton.cs b/osu.Game/Screens/Edit/Components/Timelines/Summary/TestGameplayButton.cs index 065f52b929..f5c0ed2382 100644 --- a/osu.Game/Screens/Edit/Components/Timelines/Summary/TestGameplayButton.cs +++ b/osu.Game/Screens/Edit/Components/Timelines/Summary/TestGameplayButton.cs @@ -15,6 +15,9 @@ namespace osu.Game.Screens.Edit.Components.Timelines.Summary { public partial class TestGameplayButton : OsuButton { + [Resolved] + private OsuColour colours { get; set; } = null!; + protected override SpriteText CreateText() => new OsuSpriteText { Depth = -1, @@ -25,7 +28,7 @@ namespace osu.Game.Screens.Edit.Components.Timelines.Summary }; [BackgroundDependencyLoader] - private void load(OsuColour colours, OverlayColourProvider colourProvider) + private void load(OverlayColourProvider colourProvider) { BackgroundColour = colours.Orange1; SpriteText.Colour = colourProvider.Background6; @@ -37,13 +40,15 @@ namespace osu.Game.Screens.Edit.Components.Timelines.Summary protected override bool OnMouseDown(MouseDownEvent e) { - // block scale animation + Background.FadeColour(colours.Orange0, 500, Easing.OutQuint); + // don't call base in order to block scale animation return false; } protected override void OnMouseUp(MouseUpEvent e) { - // block scale animation + Background.FadeColour(colours.Orange1, 300, Easing.OutQuint); + // don't call base in order to block scale animation } } } From d8b3c28c2e5cb3c666ae937d4cd13feb7d5475d4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 11 Feb 2025 09:17:11 +0100 Subject: [PATCH 0996/1275] Use more neutral terminology to avoid contentious 'beatmap' term --- osu.Game/Collections/DrawableCollectionListItem.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Collections/DrawableCollectionListItem.cs b/osu.Game/Collections/DrawableCollectionListItem.cs index 703def9546..f2b00004e2 100644 --- a/osu.Game/Collections/DrawableCollectionListItem.cs +++ b/osu.Game/Collections/DrawableCollectionListItem.cs @@ -168,8 +168,8 @@ namespace osu.Game.Collections countText.Text = count == 1 // Intentionally not localised until we have proper support for this (see https://github.com/ppy/osu-framework/pull/4918 // but also in this case we want support for formatting a number within a string). - ? $"{count:#,0} beatmap" - : $"{count:#,0} beatmaps"; + ? $"{count:#,0} item" + : $"{count:#,0} items"; } else { From b9c4e235958796bb4f85b9734b5f685541ea13d3 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 11 Feb 2025 20:05:48 +0900 Subject: [PATCH 0997/1275] Fix potential bad realm access to collection name --- osu.Game/Collections/DrawableCollectionListItem.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Collections/DrawableCollectionListItem.cs b/osu.Game/Collections/DrawableCollectionListItem.cs index f2b00004e2..b0dd70227c 100644 --- a/osu.Game/Collections/DrawableCollectionListItem.cs +++ b/osu.Game/Collections/DrawableCollectionListItem.cs @@ -255,7 +255,7 @@ namespace osu.Game.Collections private void deleteCollection() => collection.PerformWrite(c => c.Realm!.Remove(c)); } - public IEnumerable FilterTerms => [(LocalisableString)Model.Value.Name]; + public IEnumerable FilterTerms => Model.PerformRead(m => m.IsValid ? new[] { (LocalisableString)m.Name } : []); private bool matchingFilter = true; From 8c85616d1c8677a859bf007291997b092786f94c Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Tue, 11 Feb 2025 21:28:21 +0900 Subject: [PATCH 0998/1275] Fix test --- osu.Game.Tests/Visual/Gameplay/TestSceneSpectatorList.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSpectatorList.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSpectatorList.cs index 66c465cbed..bd1e15d06d 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneSpectatorList.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSpectatorList.cs @@ -32,7 +32,7 @@ namespace osu.Game.Tests.Visual.Gameplay Bindable playingState = new Bindable(); GameplayState gameplayState = new GameplayState(new Beatmap(), new OsuRuleset(), healthProcessor: new OsuHealthProcessor(0), localUserPlayingState: playingState); TestSpectatorClient spectatorClient = new TestSpectatorClient(); - TestMultiplayerClient multiplayerClient = new TestMultiplayerClient(new TestMultiplayerRoomManager(new TestRoomRequestsHandler())); + TestMultiplayerClient multiplayerClient = new TestMultiplayerClient(new TestRoomRequestsHandler()); AddStep("create spectator list", () => { From be035538c241f29ef609c9f73c670b0056278222 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 11 Feb 2025 14:01:32 +0100 Subject: [PATCH 0999/1275] Fix remaining hit counter scaling in the incorrect direction --- osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacySwell.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacySwell.cs b/osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacySwell.cs index 0eb80d333f..c819cb7937 100644 --- a/osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacySwell.cs +++ b/osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacySwell.cs @@ -108,8 +108,9 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy private void animateSwellProgress(int numHits, int requiredHits) { - remainingHitsText.Text = $"{requiredHits - numHits}"; - remainingHitsText.ScaleTo(1.6f - (0.6f * ((float)numHits / requiredHits)), 60, Easing.OutQuad); + int remainingHits = requiredHits - numHits; + remainingHitsText.Text = remainingHits.ToString(); + remainingHitsText.ScaleTo(1.6f - (0.6f * ((float)remainingHits / requiredHits)), 60, Easing.OutQuad); spinnerCircle.ClearTransforms(); spinnerCircle From 231988bc9de21a5e7cdc0fbd838e6cb20c75990a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 11 Feb 2025 15:20:36 +0100 Subject: [PATCH 1000/1275] Adjust things to be closer to stable (but not close enough yet) --- .../Skinning/Legacy/LegacySwell.cs | 22 +++++++++---------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacySwell.cs b/osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacySwell.cs index c819cb7937..d3b5d54828 100644 --- a/osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacySwell.cs +++ b/osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacySwell.cs @@ -14,6 +14,7 @@ using osuTK; using osu.Game.Rulesets.Objects.Drawables; using osu.Framework.Extensions.ObjectExtensions; using System; +using System.Globalization; namespace osu.Game.Rulesets.Taiko.Skinning.Legacy { @@ -39,7 +40,7 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy } [BackgroundDependencyLoader] - private void load(DrawableHitObject hitObject, ISkinSource skin, SkinManager skinManager) + private void load(DrawableHitObject hitObject, ISkinSource skin) { Children = new Drawable[] { @@ -54,7 +55,7 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy { Anchor = Anchor.Centre, Origin = Anchor.Centre, - Position = new Vector2(200f, 100f), + Position = new Vector2(250f, 100f), // ballparked to be horizontally centred on 4:3 resolution Children = new Drawable[] { @@ -109,14 +110,14 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy private void animateSwellProgress(int numHits, int requiredHits) { int remainingHits = requiredHits - numHits; - remainingHitsText.Text = remainingHits.ToString(); - remainingHitsText.ScaleTo(1.6f - (0.6f * ((float)remainingHits / requiredHits)), 60, Easing.OutQuad); + remainingHitsText.Text = remainingHits.ToString(CultureInfo.InvariantCulture); + remainingHitsText.ScaleTo(1.6f - (0.6f * ((float)remainingHits / requiredHits)), 60, Easing.Out); spinnerCircle.ClearTransforms(); spinnerCircle .RotateTo(180f * numHits, 1000, Easing.OutQuint) .ScaleTo(Math.Min(0.94f, spinnerCircle.Scale.X + 0.02f)) - .ScaleTo(0.8f, 400, Easing.OutQuad); + .ScaleTo(0.8f, 400, Easing.Out); } private void updateStateTransforms(DrawableHitObject drawableHitObject, ArmedState state) @@ -134,7 +135,7 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy samplePlayed = false; } - const double body_transition_duration = 100; + const double body_transition_duration = 200; warning.FadeOut(body_transition_duration); bodyContainer.FadeIn(body_transition_duration); @@ -146,9 +147,8 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy const double clear_transition_duration = 300; const double clear_fade_in = 120; - bodyContainer - .FadeOut(clear_transition_duration, Easing.OutQuad) - .ScaleTo(1.05f, clear_transition_duration, Easing.OutQuad); + bodyContainer.FadeOut(clear_transition_duration, Easing.OutQuad); + spinnerCircle.ScaleTo(spinnerCircle.Scale.X + 0.05f, clear_transition_duration, Easing.OutQuad); if (state == ArmedState.Hit) { @@ -159,11 +159,11 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy } clearAnimation - .FadeIn(clear_fade_in) .MoveTo(new Vector2(0, 0)) + .MoveTo(new Vector2(0, -90), clear_fade_in * 2, Easing.Out) .ScaleTo(0.4f) - .MoveTo(new Vector2(0, -90), clear_fade_in * 2, Easing.OutQuad) .ScaleTo(1f, clear_fade_in * 2, Easing.Out) + .FadeIn(clear_fade_in) .Delay(clear_fade_in * 3) .FadeOut(clear_fade_in * 2.5); } From a8f07ae7b1ebce7579cc97a14264b7132b017f1a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marvin=20Sch=C3=BCrz?= Date: Tue, 11 Feb 2025 18:04:23 +0100 Subject: [PATCH 1001/1275] Add comment warning about enum entry order in `GlobalAction` --- osu.Game/Input/Bindings/GlobalActionContainer.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/osu.Game/Input/Bindings/GlobalActionContainer.cs b/osu.Game/Input/Bindings/GlobalActionContainer.cs index 599ca6d6c1..e4dc2d503b 100644 --- a/osu.Game/Input/Bindings/GlobalActionContainer.cs +++ b/osu.Game/Input/Bindings/GlobalActionContainer.cs @@ -227,6 +227,10 @@ namespace osu.Game.Input.Bindings }; } + /// + /// IMPORTANT: New entries should always be added at the end of the enum, as key bindings are stored using the enum's numeric value and + /// changes in order would cause key bindings to get associated with the wrong action. + /// public enum GlobalAction { [LocalisableDescription(typeof(GlobalActionKeyBindingStrings), nameof(GlobalActionKeyBindingStrings.ToggleChat))] From ffd8bd7bf4dd4d238986c90e598ad11580667d01 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 12 Feb 2025 16:14:12 +0900 Subject: [PATCH 1002/1275] Rename `ParentObject` to `DrawableObject` It's not a parent. The follow circle is directly part of the slider itself. --- .../Skinning/FollowCircle.cs | 51 ++++++++++--------- .../Skinning/Legacy/LegacyFollowCircle.cs | 4 +- 2 files changed, 28 insertions(+), 27 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Skinning/FollowCircle.cs b/osu.Game.Rulesets.Osu/Skinning/FollowCircle.cs index 4fadb09948..d1836010fb 100644 --- a/osu.Game.Rulesets.Osu/Skinning/FollowCircle.cs +++ b/osu.Game.Rulesets.Osu/Skinning/FollowCircle.cs @@ -13,8 +13,7 @@ namespace osu.Game.Rulesets.Osu.Skinning { public abstract partial class FollowCircle : CompositeDrawable { - [Resolved] - protected DrawableHitObject? ParentObject { get; private set; } + protected DrawableSlider? DrawableObject { get; private set; } protected FollowCircle() { @@ -22,16 +21,18 @@ namespace osu.Game.Rulesets.Osu.Skinning } [BackgroundDependencyLoader] - private void load() + private void load(DrawableHitObject? hitObject) { - ((DrawableSlider?)ParentObject)?.Tracking.BindValueChanged(tracking => - { - Debug.Assert(ParentObject != null); + DrawableObject = hitObject as DrawableSlider; - if (ParentObject.Judged) + DrawableObject?.Tracking.BindValueChanged(tracking => + { + Debug.Assert(DrawableObject != null); + + if (DrawableObject.Judged) return; - using (BeginAbsoluteSequence(Math.Max(Time.Current, ParentObject.HitObject?.StartTime ?? 0))) + using (BeginAbsoluteSequence(Math.Max(Time.Current, DrawableObject.HitObject?.StartTime ?? 0))) { if (tracking.NewValue) OnSliderPress(); @@ -45,13 +46,13 @@ namespace osu.Game.Rulesets.Osu.Skinning { base.LoadComplete(); - if (ParentObject != null) + if (DrawableObject != null) { - ParentObject.HitObjectApplied += onHitObjectApplied; - onHitObjectApplied(ParentObject); + DrawableObject.HitObjectApplied += onHitObjectApplied; + onHitObjectApplied(DrawableObject); - ParentObject.ApplyCustomUpdateState += updateStateTransforms; - updateStateTransforms(ParentObject, ParentObject.State.Value); + DrawableObject.ApplyCustomUpdateState += updateStateTransforms; + updateStateTransforms(DrawableObject, DrawableObject.State.Value); } } @@ -61,26 +62,26 @@ namespace osu.Game.Rulesets.Osu.Skinning .FadeOut(); } - private void updateStateTransforms(DrawableHitObject drawableObject, ArmedState state) + private void updateStateTransforms(DrawableHitObject d, ArmedState state) { - Debug.Assert(ParentObject != null); + Debug.Assert(DrawableObject != null); switch (state) { case ArmedState.Hit: - switch (drawableObject) + switch (d) { case DrawableSliderTail: - // Use ParentObject instead of drawableObject because slider tail's + // Use DrawableObject instead of local object because slider tail's // HitStateUpdateTime is ~36ms before the actual slider end (aka slider // tail leniency) - using (BeginAbsoluteSequence(ParentObject.HitStateUpdateTime)) + using (BeginAbsoluteSequence(DrawableObject.HitStateUpdateTime)) OnSliderEnd(); break; case DrawableSliderTick: case DrawableSliderRepeat: - using (BeginAbsoluteSequence(drawableObject.HitStateUpdateTime)) + using (BeginAbsoluteSequence(d.HitStateUpdateTime)) OnSliderTick(); break; } @@ -88,15 +89,15 @@ namespace osu.Game.Rulesets.Osu.Skinning break; case ArmedState.Miss: - switch (drawableObject) + switch (d) { case DrawableSliderTail: case DrawableSliderTick: case DrawableSliderRepeat: - // Despite above comment, ok to use drawableObject.HitStateUpdateTime + // Despite above comment, ok to use d.HitStateUpdateTime // here, since on stable, the break anim plays right when the tail is // missed, not when the slider ends - using (BeginAbsoluteSequence(drawableObject.HitStateUpdateTime)) + using (BeginAbsoluteSequence(d.HitStateUpdateTime)) OnSliderBreak(); break; } @@ -109,10 +110,10 @@ namespace osu.Game.Rulesets.Osu.Skinning { base.Dispose(isDisposing); - if (ParentObject != null) + if (DrawableObject != null) { - ParentObject.HitObjectApplied -= onHitObjectApplied; - ParentObject.ApplyCustomUpdateState -= updateStateTransforms; + DrawableObject.HitObjectApplied -= onHitObjectApplied; + DrawableObject.ApplyCustomUpdateState -= updateStateTransforms; } } diff --git a/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyFollowCircle.cs b/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyFollowCircle.cs index 4a8b737206..f60b5cfe12 100644 --- a/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyFollowCircle.cs +++ b/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyFollowCircle.cs @@ -22,9 +22,9 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy protected override void OnSliderPress() { - Debug.Assert(ParentObject != null); + Debug.Assert(DrawableObject != null); - double remainingTime = Math.Max(0, ParentObject.HitStateUpdateTime - Time.Current); + double remainingTime = Math.Max(0, DrawableObject.HitStateUpdateTime - Time.Current); // Note that the scale adjust here is 2 instead of DrawableSliderBall.FOLLOW_AREA to match legacy behaviour. // This means the actual tracking area for gameplay purposes is larger than the sprite (but skins may be accounting for this). From f97708e6b3bd4bc516e7837e43599b5f1c88c6f7 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 12 Feb 2025 16:28:14 +0900 Subject: [PATCH 1003/1275] Avoid binding directly to DHO's bindable --- .../Skinning/FollowCircle.cs | 31 +++++++++++-------- 1 file changed, 18 insertions(+), 13 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Skinning/FollowCircle.cs b/osu.Game.Rulesets.Osu/Skinning/FollowCircle.cs index d1836010fb..903ba08010 100644 --- a/osu.Game.Rulesets.Osu/Skinning/FollowCircle.cs +++ b/osu.Game.Rulesets.Osu/Skinning/FollowCircle.cs @@ -4,6 +4,7 @@ using System; using System.Diagnostics; using osu.Framework.Allocation; +using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Game.Rulesets.Objects.Drawables; @@ -15,6 +16,8 @@ namespace osu.Game.Rulesets.Osu.Skinning { protected DrawableSlider? DrawableObject { get; private set; } + private readonly IBindable tracking = new Bindable(); + protected FollowCircle() { RelativeSizeAxes = Axes.Both; @@ -25,21 +28,23 @@ namespace osu.Game.Rulesets.Osu.Skinning { DrawableObject = hitObject as DrawableSlider; - DrawableObject?.Tracking.BindValueChanged(tracking => + if (DrawableObject != null) { - Debug.Assert(DrawableObject != null); - - if (DrawableObject.Judged) - return; - - using (BeginAbsoluteSequence(Math.Max(Time.Current, DrawableObject.HitObject?.StartTime ?? 0))) + tracking.BindTo(DrawableObject.Tracking); + tracking.BindValueChanged(tracking => { - if (tracking.NewValue) - OnSliderPress(); - else - OnSliderRelease(); - } - }, true); + if (DrawableObject.Judged) + return; + + using (BeginAbsoluteSequence(Math.Max(Time.Current, DrawableObject.HitObject?.StartTime ?? 0))) + { + if (tracking.NewValue) + OnSliderPress(); + else + OnSliderRelease(); + } + }, true); + } } protected override void LoadComplete() From 84b5ea3dbf6ab7b6209820468d3369e477f9d1b1 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 12 Feb 2025 16:33:23 +0900 Subject: [PATCH 1004/1275] Fix weird follow circle display when rewinding through sliders in editor Closes https://github.com/ppy/osu/issues/31812. --- osu.Game.Rulesets.Osu/Skinning/FollowCircle.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/osu.Game.Rulesets.Osu/Skinning/FollowCircle.cs b/osu.Game.Rulesets.Osu/Skinning/FollowCircle.cs index 903ba08010..db789166c6 100644 --- a/osu.Game.Rulesets.Osu/Skinning/FollowCircle.cs +++ b/osu.Game.Rulesets.Osu/Skinning/FollowCircle.cs @@ -63,8 +63,12 @@ namespace osu.Game.Rulesets.Osu.Skinning private void onHitObjectApplied(DrawableHitObject drawableObject) { + // Sane defaults when a new hitobject is applied to the drawable slider. this.ScaleTo(1f) .FadeOut(); + + // Immediately play out any pending transforms from press/release + FinishTransforms(true); } private void updateStateTransforms(DrawableHitObject d, ArmedState state) From b92e9f515bd291a19546538355aeb48001933829 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 12 Feb 2025 17:31:55 +0900 Subject: [PATCH 1005/1275] Fix layout of user setting areas when aspect ratio is vertically tall --- .../Multiplayer/MultiplayerMatchSubScreen.cs | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs index a16c5c9442..ff4c8c2fd9 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs @@ -121,9 +121,18 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer new GridContainer { RelativeSizeAxes = Axes.Both, + RowDimensions = new[] + { + new Dimension(GridSizeMode.AutoSize), + new Dimension(GridSizeMode.AutoSize), + new Dimension(GridSizeMode.Absolute, 5), + new Dimension(), + new Dimension(GridSizeMode.AutoSize), + new Dimension(GridSizeMode.AutoSize), + }, Content = new[] { - new Drawable[] { new OverlinedHeader("Beatmap") }, + new Drawable[] { new OverlinedHeader("Beatmap queue") }, new Drawable[] { addItemButton = new AddItemButton @@ -202,14 +211,6 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer }, }, }, - RowDimensions = new[] - { - new Dimension(GridSizeMode.AutoSize), - new Dimension(GridSizeMode.AutoSize), - new Dimension(GridSizeMode.Absolute, 5), - new Dimension(), - new Dimension(GridSizeMode.AutoSize), - } }, null, new GridContainer From 9aef95c38127ae72b2538326e561a28db5d3acda Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 12 Feb 2025 17:43:49 +0900 Subject: [PATCH 1006/1275] Adjust some paddings and text MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Mostly trying to give more space to the queue as we add more vertical elements to the middle area of multiplayer / playerlists. This whole UI will likely change – this is just a stop-gap fix. --- osu.Game/Screens/OnlinePlay/Components/OverlinedHeader.cs | 2 -- .../Multiplayer/Match/Playlist/MultiplayerPlaylistTabControl.cs | 2 +- .../Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs | 1 + 3 files changed, 2 insertions(+), 3 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/Components/OverlinedHeader.cs b/osu.Game/Screens/OnlinePlay/Components/OverlinedHeader.cs index d9cdcac7d7..6dfde183f0 100644 --- a/osu.Game/Screens/OnlinePlay/Components/OverlinedHeader.cs +++ b/osu.Game/Screens/OnlinePlay/Components/OverlinedHeader.cs @@ -53,13 +53,11 @@ namespace osu.Game.Screens.OnlinePlay.Components { RelativeSizeAxes = Axes.X, Height = 2, - Margin = new MarginPadding { Bottom = 2 } }, new FillFlowContainer { AutoSizeAxes = Axes.Both, Direction = FillDirection.Horizontal, - Margin = new MarginPadding { Top = 5 }, Spacing = new Vector2(10, 0), Children = new Drawable[] { diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/Playlist/MultiplayerPlaylistTabControl.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/Playlist/MultiplayerPlaylistTabControl.cs index e5d94c5358..a7f3e17efa 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/Playlist/MultiplayerPlaylistTabControl.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/Playlist/MultiplayerPlaylistTabControl.cs @@ -32,7 +32,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match.Playlist protected override void LoadComplete() { base.LoadComplete(); - QueueItems.BindCollectionChanged((_, _) => Text.Text = QueueItems.Count > 0 ? $"Queue ({QueueItems.Count})" : "Queue", true); + QueueItems.BindCollectionChanged((_, _) => Text.Text = QueueItems.Count > 0 ? $"Up next ({QueueItems.Count})" : "Up next", true); } } } diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs index ff4c8c2fd9..083c8e070e 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs @@ -176,6 +176,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer Anchor = Anchor.CentreLeft, Origin = Anchor.CentreLeft, Width = 90, + Height = 30, Text = "Select", Action = ShowUserModSelect, }, From 9c3e9e7c55b8aad452151c2c1b13a00660b3f52d Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 12 Feb 2025 17:56:15 +0900 Subject: [PATCH 1007/1275] Change free mods button to show "all" when freestyle is enabled --- .../TestSceneFreeModSelectOverlay.cs | 2 +- .../OnlinePlay/FooterButtonFreeMods.cs | 28 ++++++------------- .../OnlinePlay/FooterButtonFreestyle.cs | 15 ++++------ .../OnlinePlay/OnlinePlaySongSelect.cs | 20 +++++++++---- 4 files changed, 29 insertions(+), 36 deletions(-) diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneFreeModSelectOverlay.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneFreeModSelectOverlay.cs index fb54b89a4b..fd589e928a 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneFreeModSelectOverlay.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneFreeModSelectOverlay.cs @@ -166,7 +166,7 @@ namespace osu.Game.Tests.Visual.Multiplayer Anchor = Anchor.BottomRight, Origin = Anchor.BottomRight, Y = -ScreenFooter.HEIGHT, - Current = { BindTarget = freeModSelectOverlay.SelectedMods }, + FreeMods = { BindTarget = freeModSelectOverlay.SelectedMods }, }, footer = new ScreenFooter(), }, diff --git a/osu.Game/Screens/OnlinePlay/FooterButtonFreeMods.cs b/osu.Game/Screens/OnlinePlay/FooterButtonFreeMods.cs index 402f538716..695ed74ab9 100644 --- a/osu.Game/Screens/OnlinePlay/FooterButtonFreeMods.cs +++ b/osu.Game/Screens/OnlinePlay/FooterButtonFreeMods.cs @@ -11,31 +11,20 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Sprites; -using osu.Framework.Graphics.UserInterface; using osu.Game.Graphics; using osu.Game.Graphics.Sprites; using osu.Game.Graphics.UserInterface; +using osu.Game.Localisation; using osu.Game.Rulesets.Mods; using osu.Game.Screens.Select; using osuTK; -using osu.Game.Localisation; namespace osu.Game.Screens.OnlinePlay { - public partial class FooterButtonFreeMods : FooterButton, IHasCurrentValue> + public partial class FooterButtonFreeMods : FooterButton { - private readonly BindableWithCurrent> current = new BindableWithCurrent>(Array.Empty()); - - public Bindable> Current - { - get => current.Current; - set - { - ArgumentNullException.ThrowIfNull(value); - - current.Current = value; - } - } + public readonly Bindable> FreeMods = new Bindable>(); + public readonly IBindable Freestyle = new Bindable(); public new Action Action { set => throw new NotSupportedException("The click action is handled by the button itself."); } @@ -104,7 +93,8 @@ namespace osu.Game.Screens.OnlinePlay { base.LoadComplete(); - Current.BindValueChanged(_ => updateModDisplay(), true); + Freestyle.BindValueChanged(_ => updateModDisplay()); + FreeMods.BindValueChanged(_ => updateModDisplay(), true); } /// @@ -114,16 +104,16 @@ namespace osu.Game.Screens.OnlinePlay { var availableMods = allAvailableAndValidMods.ToArray(); - Current.Value = Current.Value.Count == availableMods.Length + FreeMods.Value = FreeMods.Value.Count == availableMods.Length ? Array.Empty() : availableMods; } private void updateModDisplay() { - int currentCount = Current.Value.Count; + int currentCount = FreeMods.Value.Count; - if (currentCount == allAvailableAndValidMods.Count()) + if (currentCount == allAvailableAndValidMods.Count() || Freestyle.Value) { count.Text = "all"; count.FadeColour(colours.Gray2, 200, Easing.OutQuint); diff --git a/osu.Game/Screens/OnlinePlay/FooterButtonFreestyle.cs b/osu.Game/Screens/OnlinePlay/FooterButtonFreestyle.cs index 157f90d078..d907fec489 100644 --- a/osu.Game/Screens/OnlinePlay/FooterButtonFreestyle.cs +++ b/osu.Game/Screens/OnlinePlay/FooterButtonFreestyle.cs @@ -16,15 +16,10 @@ using osu.Game.Localisation; namespace osu.Game.Screens.OnlinePlay { - public partial class FooterButtonFreestyle : FooterButton, IHasCurrentValue + public partial class FooterButtonFreestyle : FooterButton { - private readonly BindableWithCurrent current = new BindableWithCurrent(); + public readonly Bindable Freestyle = new Bindable(); - public Bindable Current - { - get => current.Current; - set => current.Current = value; - } public new Action Action { set => throw new NotSupportedException("The click action is handled by the button itself."); } @@ -37,7 +32,7 @@ namespace osu.Game.Screens.OnlinePlay public FooterButtonFreestyle() { // Overwrite any external behaviour as we delegate the main toggle action to a sub-button. - base.Action = () => current.Value = !current.Value; + base.Action = () => Freestyle.Value = !Freestyle.Value; } [BackgroundDependencyLoader] @@ -81,12 +76,12 @@ namespace osu.Game.Screens.OnlinePlay { base.LoadComplete(); - Current.BindValueChanged(_ => updateDisplay(), true); + Freestyle.BindValueChanged(_ => updateDisplay(), true); } private void updateDisplay() { - if (current.Value) + if (Freestyle.Value) { text.Text = "on"; text.FadeColour(colours.Gray2, 200, Easing.OutQuint); diff --git a/osu.Game/Screens/OnlinePlay/OnlinePlaySongSelect.cs b/osu.Game/Screens/OnlinePlay/OnlinePlaySongSelect.cs index 1164c4c0fc..cf351b31bf 100644 --- a/osu.Game/Screens/OnlinePlay/OnlinePlaySongSelect.cs +++ b/osu.Game/Screens/OnlinePlay/OnlinePlaySongSelect.cs @@ -126,6 +126,7 @@ namespace osu.Game.Screens.OnlinePlay { if (enabled.NewValue) { + freeModsFooterButton.Enabled.Value = false; freeModsFooterButton.Enabled.Value = false; ModsFooterButton.Enabled.Value = false; @@ -205,8 +206,15 @@ namespace osu.Game.Screens.OnlinePlay baseButtons.InsertRange(baseButtons.FindIndex(b => b.button is FooterButtonMods) + 1, new (FooterButton, OverlayContainer?)[] { - (freeModsFooterButton = new FooterButtonFreeMods(freeModSelect) { Current = FreeMods }, null), - (new FooterButtonFreestyle { Current = Freestyle }, null) + (freeModsFooterButton = new FooterButtonFreeMods(freeModSelect) + { + FreeMods = { BindTarget = FreeMods }, + Freestyle = { BindTarget = Freestyle } + }, null), + (new FooterButtonFreestyle + { + Freestyle = { BindTarget = Freestyle } + }, null) }); return baseButtons; @@ -225,10 +233,10 @@ namespace osu.Game.Screens.OnlinePlay /// The to check. /// Whether is a selectable free-mod. private bool isValidFreeMod(Mod mod) => ModUtils.IsValidFreeModForMatchType(mod, room.Type) - // Mod must not be contained in the required mods. - && Mods.Value.All(m => m.Acronym != mod.Acronym) - // Mod must be compatible with all the required mods. - && ModUtils.CheckCompatibleSet(Mods.Value.Append(mod).ToArray()); + // Mod must not be contained in the required mods. + && Mods.Value.All(m => m.Acronym != mod.Acronym) + // Mod must be compatible with all the required mods. + && ModUtils.CheckCompatibleSet(Mods.Value.Append(mod).ToArray()); protected override void Dispose(bool isDisposing) { From 218151bb3c7af0fe77b32e55757cc0079b40cce6 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 12 Feb 2025 18:27:53 +0900 Subject: [PATCH 1008/1275] Flash footer freemod/freestyle buttons when active --- .../Screens/OnlinePlay/FooterButtonFreeMods.cs | 2 ++ .../Screens/OnlinePlay/FooterButtonFreestyle.cs | 4 ++-- .../Screens/OnlinePlay/OnlinePlaySongSelect.cs | 2 +- osu.Game/Screens/Select/FooterButton.cs | 17 +++++++++++++++++ 4 files changed, 22 insertions(+), 3 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/FooterButtonFreeMods.cs b/osu.Game/Screens/OnlinePlay/FooterButtonFreeMods.cs index 695ed74ab9..3605412b2b 100644 --- a/osu.Game/Screens/OnlinePlay/FooterButtonFreeMods.cs +++ b/osu.Game/Screens/OnlinePlay/FooterButtonFreeMods.cs @@ -26,6 +26,8 @@ namespace osu.Game.Screens.OnlinePlay public readonly Bindable> FreeMods = new Bindable>(); public readonly IBindable Freestyle = new Bindable(); + protected override bool IsActive => FreeMods.Value.Count > 0; + public new Action Action { set => throw new NotSupportedException("The click action is handled by the button itself."); } private OsuSpriteText count = null!; diff --git a/osu.Game/Screens/OnlinePlay/FooterButtonFreestyle.cs b/osu.Game/Screens/OnlinePlay/FooterButtonFreestyle.cs index d907fec489..6ee983af20 100644 --- a/osu.Game/Screens/OnlinePlay/FooterButtonFreestyle.cs +++ b/osu.Game/Screens/OnlinePlay/FooterButtonFreestyle.cs @@ -8,11 +8,10 @@ using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; -using osu.Framework.Graphics.UserInterface; using osu.Game.Graphics; using osu.Game.Graphics.Sprites; -using osu.Game.Screens.Select; using osu.Game.Localisation; +using osu.Game.Screens.Select; namespace osu.Game.Screens.OnlinePlay { @@ -20,6 +19,7 @@ namespace osu.Game.Screens.OnlinePlay { public readonly Bindable Freestyle = new Bindable(); + protected override bool IsActive => Freestyle.Value; public new Action Action { set => throw new NotSupportedException("The click action is handled by the button itself."); } diff --git a/osu.Game/Screens/OnlinePlay/OnlinePlaySongSelect.cs b/osu.Game/Screens/OnlinePlay/OnlinePlaySongSelect.cs index cf351b31bf..9bedecc221 100644 --- a/osu.Game/Screens/OnlinePlay/OnlinePlaySongSelect.cs +++ b/osu.Game/Screens/OnlinePlay/OnlinePlaySongSelect.cs @@ -42,7 +42,7 @@ namespace osu.Game.Screens.OnlinePlay protected override UserActivity InitialActivity => new UserActivity.InLobby(room); protected readonly Bindable> FreeMods = new Bindable>(Array.Empty()); - protected readonly Bindable Freestyle = new Bindable(); + protected readonly Bindable Freestyle = new Bindable(true); private readonly Room room; private readonly PlaylistItem? initialItem; diff --git a/osu.Game/Screens/Select/FooterButton.cs b/osu.Game/Screens/Select/FooterButton.cs index 128e750dca..dafa0b0c1c 100644 --- a/osu.Game/Screens/Select/FooterButton.cs +++ b/osu.Game/Screens/Select/FooterButton.cs @@ -25,6 +25,11 @@ namespace osu.Game.Screens.Select protected static readonly Vector2 SHEAR = new Vector2(SHEAR_WIDTH / Footer.HEIGHT, 0); + /// + /// Used to show an initial animation hinting at the enabled state. + /// + protected virtual bool IsActive => false; + public LocalisableString Text { get => SpriteText?.Text ?? default; @@ -124,6 +129,18 @@ namespace osu.Game.Screens.Select { base.LoadComplete(); Enabled.BindValueChanged(_ => updateDisplay(), true); + + if (IsActive) + { + box.ClearTransforms(); + + using (box.BeginDelayedSequence(200)) + { + box.FadeIn(200) + .Then() + .FadeOut(1500, Easing.OutQuint); + } + } } public Action Hovered; From c049ae69370629f8c8c888705b6cb6feb7ad2ef4 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 12 Feb 2025 18:45:00 +0900 Subject: [PATCH 1009/1275] Update height specification for playlist screen too --- osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs index 957a51c467..7f2255e482 100644 --- a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs @@ -204,6 +204,7 @@ namespace osu.Game.Screens.OnlinePlay.Playlists Anchor = Anchor.CentreLeft, Origin = Anchor.CentreLeft, Width = 90, + Height = 30, Text = "Select", Action = ShowUserModSelect, }, From 3a0464299af5bde7527d48bcdb8f3a1a85d67d85 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 12 Feb 2025 19:22:57 +0900 Subject: [PATCH 1010/1275] Remove unnecessary V2 suffixes --- .../SongSelectV2/TestSceneUpdateBeatmapSetButtonV2.cs | 4 ++-- osu.Game/Screens/SelectV2/BeatmapPanel.cs | 4 ++-- osu.Game/Screens/SelectV2/BeatmapSetPanel.cs | 4 ++-- osu.Game/Screens/SelectV2/BeatmapStandalonePanel.cs | 8 ++++---- .../SelectV2/{TopLocalRankV2.cs => TopLocalRank.cs} | 6 ++---- ...ateBeatmapSetButtonV2.cs => UpdateBeatmapSetButton.cs} | 4 ++-- 6 files changed, 14 insertions(+), 16 deletions(-) rename osu.Game/Screens/SelectV2/{TopLocalRankV2.cs => TopLocalRank.cs} (94%) rename osu.Game/Screens/SelectV2/{UpdateBeatmapSetButtonV2.cs => UpdateBeatmapSetButton.cs} (98%) diff --git a/osu.Game.Tests/Visual/SongSelectV2/TestSceneUpdateBeatmapSetButtonV2.cs b/osu.Game.Tests/Visual/SongSelectV2/TestSceneUpdateBeatmapSetButtonV2.cs index 6e5d731453..ba3f2635b0 100644 --- a/osu.Game.Tests/Visual/SongSelectV2/TestSceneUpdateBeatmapSetButtonV2.cs +++ b/osu.Game.Tests/Visual/SongSelectV2/TestSceneUpdateBeatmapSetButtonV2.cs @@ -11,12 +11,12 @@ namespace osu.Game.Tests.Visual.SongSelectV2 { public partial class TestSceneUpdateBeatmapSetButtonV2 : OsuTestScene { - private UpdateBeatmapSetButtonV2 button = null!; + private UpdateBeatmapSetButton button = null!; [SetUp] public void SetUp() => Schedule(() => { - Child = button = new UpdateBeatmapSetButtonV2 + Child = button = new UpdateBeatmapSetButton { Anchor = Anchor.Centre, Origin = Anchor.Centre, diff --git a/osu.Game/Screens/SelectV2/BeatmapPanel.cs b/osu.Game/Screens/SelectV2/BeatmapPanel.cs index a888c0331f..3db60876a1 100644 --- a/osu.Game/Screens/SelectV2/BeatmapPanel.cs +++ b/osu.Game/Screens/SelectV2/BeatmapPanel.cs @@ -38,7 +38,7 @@ namespace osu.Game.Screens.SelectV2 private ConstrainedIconContainer difficultyIcon = null!; private OsuSpriteText keyCountText = null!; private StarRatingDisplay starRatingDisplay = null!; - private TopLocalRankV2 difficultyRank = null!; + private TopLocalRank difficultyRank = null!; private OsuSpriteText difficultyText = null!; private OsuSpriteText authorText = null!; @@ -118,7 +118,7 @@ namespace osu.Game.Screens.SelectV2 Anchor = Anchor.CentreLeft, Origin = Anchor.CentreLeft, }, - difficultyRank = new TopLocalRankV2 + difficultyRank = new TopLocalRank { Anchor = Anchor.CentreLeft, Origin = Anchor.CentreLeft, diff --git a/osu.Game/Screens/SelectV2/BeatmapSetPanel.cs b/osu.Game/Screens/SelectV2/BeatmapSetPanel.cs index 85e97a8464..6caabb79c3 100644 --- a/osu.Game/Screens/SelectV2/BeatmapSetPanel.cs +++ b/osu.Game/Screens/SelectV2/BeatmapSetPanel.cs @@ -35,7 +35,7 @@ namespace osu.Game.Screens.SelectV2 private OsuSpriteText titleText = null!; private OsuSpriteText artistText = null!; private Drawable chevronIcon = null!; - private UpdateBeatmapSetButtonV2 updateButton = null!; + private UpdateBeatmapSetButton updateButton = null!; private BeatmapSetOnlineStatusPill statusPill = null!; private DifficultySpectrumDisplay difficultiesDisplay = null!; @@ -98,7 +98,7 @@ namespace osu.Game.Screens.SelectV2 Margin = new MarginPadding { Top = 5f }, Children = new Drawable[] { - updateButton = new UpdateBeatmapSetButtonV2 + updateButton = new UpdateBeatmapSetButton { Anchor = Anchor.CentreLeft, Origin = Anchor.CentreLeft, diff --git a/osu.Game/Screens/SelectV2/BeatmapStandalonePanel.cs b/osu.Game/Screens/SelectV2/BeatmapStandalonePanel.cs index 32a729c95d..e8628d5b78 100644 --- a/osu.Game/Screens/SelectV2/BeatmapStandalonePanel.cs +++ b/osu.Game/Screens/SelectV2/BeatmapStandalonePanel.cs @@ -64,13 +64,13 @@ namespace osu.Game.Screens.SelectV2 private OsuSpriteText titleText = null!; private OsuSpriteText artistText = null!; - private UpdateBeatmapSetButtonV2 updateButton = null!; + private UpdateBeatmapSetButton updateButton = null!; private BeatmapSetOnlineStatusPill statusPill = null!; private ConstrainedIconContainer difficultyIcon = null!; private FillFlowContainer difficultyLine = null!; private StarRatingDisplay difficultyStarRating = null!; - private TopLocalRankV2 difficultyRank = null!; + private TopLocalRank difficultyRank = null!; private OsuSpriteText difficultyKeyCountText = null!; private OsuSpriteText difficultyName = null!; private OsuSpriteText difficultyAuthor = null!; @@ -121,7 +121,7 @@ namespace osu.Game.Screens.SelectV2 Margin = new MarginPadding { Top = 5f }, Children = new Drawable[] { - updateButton = new UpdateBeatmapSetButtonV2 + updateButton = new UpdateBeatmapSetButton { Anchor = Anchor.CentreLeft, Origin = Anchor.CentreLeft, @@ -149,7 +149,7 @@ namespace osu.Game.Screens.SelectV2 Scale = new Vector2(8f / 9f), Margin = new MarginPadding { Right = 5f }, }, - difficultyRank = new TopLocalRankV2 + difficultyRank = new TopLocalRank { Scale = new Vector2(8f / 11), Origin = Anchor.CentreLeft, diff --git a/osu.Game/Screens/SelectV2/TopLocalRankV2.cs b/osu.Game/Screens/SelectV2/TopLocalRank.cs similarity index 94% rename from osu.Game/Screens/SelectV2/TopLocalRankV2.cs rename to osu.Game/Screens/SelectV2/TopLocalRank.cs index 241e92a67d..2a72a05db7 100644 --- a/osu.Game/Screens/SelectV2/TopLocalRankV2.cs +++ b/osu.Game/Screens/SelectV2/TopLocalRank.cs @@ -19,7 +19,7 @@ using Realms; namespace osu.Game.Screens.SelectV2 { - public partial class TopLocalRankV2 : CompositeDrawable + public partial class TopLocalRank : CompositeDrawable { private BeatmapInfo? beatmap; @@ -48,9 +48,7 @@ namespace osu.Game.Screens.SelectV2 private readonly UpdateableRank updateable; - public ScoreRank? DisplayedRank => updateable.Rank; - - public TopLocalRankV2(BeatmapInfo? beatmap = null) + public TopLocalRank(BeatmapInfo? beatmap = null) { AutoSizeAxes = Axes.Both; diff --git a/osu.Game/Screens/SelectV2/UpdateBeatmapSetButtonV2.cs b/osu.Game/Screens/SelectV2/UpdateBeatmapSetButton.cs similarity index 98% rename from osu.Game/Screens/SelectV2/UpdateBeatmapSetButtonV2.cs rename to osu.Game/Screens/SelectV2/UpdateBeatmapSetButton.cs index 2d1ce4ba48..e2c841f88a 100644 --- a/osu.Game/Screens/SelectV2/UpdateBeatmapSetButtonV2.cs +++ b/osu.Game/Screens/SelectV2/UpdateBeatmapSetButton.cs @@ -22,7 +22,7 @@ using osuTK.Graphics; namespace osu.Game.Screens.SelectV2 { - public partial class UpdateBeatmapSetButtonV2 : OsuAnimatedButton + public partial class UpdateBeatmapSetButton : OsuAnimatedButton { private BeatmapSetInfo? beatmapSet; @@ -53,7 +53,7 @@ namespace osu.Game.Screens.SelectV2 [Resolved] private IDialogOverlay? dialogOverlay { get; set; } - public UpdateBeatmapSetButtonV2() + public UpdateBeatmapSetButton() { Size = new Vector2(75f, 22f); } From 151101be7031c8b87716bbb24411f44658567482 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 12 Feb 2025 19:24:30 +0900 Subject: [PATCH 1011/1275] Mark `Action` as `init` only --- osu.Game/Screens/SelectV2/CarouselPanelPiece.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/SelectV2/CarouselPanelPiece.cs b/osu.Game/Screens/SelectV2/CarouselPanelPiece.cs index 4b533e362a..5aefa57bb5 100644 --- a/osu.Game/Screens/SelectV2/CarouselPanelPiece.cs +++ b/osu.Game/Screens/SelectV2/CarouselPanelPiece.cs @@ -70,7 +70,7 @@ namespace osu.Game.Screens.SelectV2 public readonly BindableBool Active = new BindableBool(); public readonly BindableBool KeyboardActive = new BindableBool(); - public Action? Action; + public Action? Action { get; init; } public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) { From 554884710cd4bb9749e337ed25297304cfdb3541 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 12 Feb 2025 19:30:27 +0900 Subject: [PATCH 1012/1275] Rename classes for better discoverability / grouping --- ...estSceneBeatmapCarouselV2ArtistGrouping.cs | 34 ++++++------- ...ceneBeatmapCarouselV2DifficultyGrouping.cs | 50 +++++++++---------- .../TestSceneBeatmapCarouselV2NoGrouping.cs | 16 +++--- .../TestSceneBeatmapCarouselV2Scrolling.cs | 10 ++-- ...stSceneBeatmapCarouselV2DifficultyPanel.cs | 8 +-- .../TestSceneBeatmapCarouselV2GroupPanel.cs | 20 ++++---- .../TestSceneBeatmapCarouselV2SetPanel.cs | 8 +-- ...stSceneBeatmapCarouselV2StandalonePanel.cs | 8 +-- osu.Game/Screens/SelectV2/BeatmapCarousel.cs | 6 +-- .../SelectV2/BeatmapCarouselFilterGrouping.cs | 4 +- .../{BeatmapPanel.cs => PanelBeatmap.cs} | 4 +- ...{BeatmapSetPanel.cs => PanelBeatmapSet.cs} | 4 +- ...lonePanel.cs => PanelBeatmapStandalone.cs} | 4 +- .../SelectV2/{GroupPanel.cs => PanelGroup.cs} | 2 +- ...oupPanel.cs => PanelGroupStarDificulty.cs} | 2 +- 15 files changed, 90 insertions(+), 90 deletions(-) rename osu.Game/Screens/SelectV2/{BeatmapPanel.cs => PanelBeatmap.cs} (98%) rename osu.Game/Screens/SelectV2/{BeatmapSetPanel.cs => PanelBeatmapSet.cs} (98%) rename osu.Game/Screens/SelectV2/{BeatmapStandalonePanel.cs => PanelBeatmapStandalone.cs} (99%) rename osu.Game/Screens/SelectV2/{GroupPanel.cs => PanelGroup.cs} (98%) rename osu.Game/Screens/SelectV2/{StarsGroupPanel.cs => PanelGroupStarDificulty.cs} (98%) diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2ArtistGrouping.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2ArtistGrouping.cs index d3eeee151a..c378871eac 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2ArtistGrouping.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2ArtistGrouping.cs @@ -28,42 +28,42 @@ namespace osu.Game.Tests.Visual.SongSelect [Test] public void TestOpenCloseGroupWithNoSelectionMouse() { - AddAssert("no beatmaps visible", () => Carousel.ChildrenOfType().Count(p => p.Alpha > 0), () => Is.Zero); - AddUntilStep("no sets visible", () => Carousel.ChildrenOfType().Count(p => p.Alpha > 0), () => Is.Zero); + AddAssert("no beatmaps visible", () => Carousel.ChildrenOfType().Count(p => p.Alpha > 0), () => Is.Zero); + AddUntilStep("no sets visible", () => Carousel.ChildrenOfType().Count(p => p.Alpha > 0), () => Is.Zero); CheckNoSelection(); - ClickVisiblePanel(0); + ClickVisiblePanel(0); - AddUntilStep("some sets visible", () => Carousel.ChildrenOfType().Count(p => p.Alpha > 0), () => Is.GreaterThan(0)); - AddUntilStep("no beatmaps visible", () => Carousel.ChildrenOfType().Count(p => p.Alpha > 0), () => Is.Zero); + AddUntilStep("some sets visible", () => Carousel.ChildrenOfType().Count(p => p.Alpha > 0), () => Is.GreaterThan(0)); + AddUntilStep("no beatmaps visible", () => Carousel.ChildrenOfType().Count(p => p.Alpha > 0), () => Is.Zero); CheckNoSelection(); - ClickVisiblePanel(0); + ClickVisiblePanel(0); - AddUntilStep("no sets visible", () => Carousel.ChildrenOfType().Count(p => p.Alpha > 0), () => Is.Zero); - AddUntilStep("no beatmaps visible", () => Carousel.ChildrenOfType().Count(p => p.Alpha > 0), () => Is.Zero); + AddUntilStep("no sets visible", () => Carousel.ChildrenOfType().Count(p => p.Alpha > 0), () => Is.Zero); + AddUntilStep("no beatmaps visible", () => Carousel.ChildrenOfType().Count(p => p.Alpha > 0), () => Is.Zero); CheckNoSelection(); } [Test] public void TestOpenCloseGroupWithNoSelectionKeyboard() { - AddAssert("no beatmaps visible", () => Carousel.ChildrenOfType().Count(p => p.Alpha > 0), () => Is.Zero); - AddUntilStep("no sets visible", () => Carousel.ChildrenOfType().Count(p => p.Alpha > 0), () => Is.Zero); + AddAssert("no beatmaps visible", () => Carousel.ChildrenOfType().Count(p => p.Alpha > 0), () => Is.Zero); + AddUntilStep("no sets visible", () => Carousel.ChildrenOfType().Count(p => p.Alpha > 0), () => Is.Zero); CheckNoSelection(); SelectNextPanel(); Select(); - AddUntilStep("some sets visible", () => Carousel.ChildrenOfType().Count(p => p.Alpha > 0), () => Is.GreaterThan(0)); - AddUntilStep("no beatmaps visible", () => Carousel.ChildrenOfType().Count(p => p.Alpha > 0), () => Is.Zero); + AddUntilStep("some sets visible", () => Carousel.ChildrenOfType().Count(p => p.Alpha > 0), () => Is.GreaterThan(0)); + AddUntilStep("no beatmaps visible", () => Carousel.ChildrenOfType().Count(p => p.Alpha > 0), () => Is.Zero); AddAssert("keyboard selected is expanded", () => GetKeyboardSelectedPanel()?.Expanded.Value, () => Is.True); CheckNoSelection(); Select(); - AddUntilStep("no sets visible", () => Carousel.ChildrenOfType().Count(p => p.Alpha > 0), () => Is.Zero); - AddUntilStep("no beatmaps visible", () => Carousel.ChildrenOfType().Count(p => p.Alpha > 0), () => Is.Zero); + AddUntilStep("no sets visible", () => Carousel.ChildrenOfType().Count(p => p.Alpha > 0), () => Is.Zero); + AddUntilStep("no beatmaps visible", () => Carousel.ChildrenOfType().Count(p => p.Alpha > 0), () => Is.Zero); AddAssert("keyboard selected is collapsed", () => GetKeyboardSelectedPanel()?.Expanded.Value, () => Is.False); CheckNoSelection(); } @@ -96,10 +96,10 @@ namespace osu.Game.Tests.Visual.SongSelect AddUntilStep("drawable selection restored", () => GetSelectedPanel()?.Item?.Model, () => Is.EqualTo(selection)); AddAssert("carousel item is visible", () => GetSelectedPanel()?.Item?.IsVisible, () => Is.True); - ClickVisiblePanel(0); + ClickVisiblePanel(0); AddUntilStep("carousel item not visible", GetSelectedPanel, () => Is.Null); - ClickVisiblePanel(0); + ClickVisiblePanel(0); AddUntilStep("carousel item is visible", () => GetSelectedPanel()?.Item?.IsVisible, () => Is.True); } @@ -137,7 +137,7 @@ namespace osu.Game.Tests.Visual.SongSelect // open first group Select(); CheckNoSelection(); - AddUntilStep("some beatmaps visible", () => Carousel.ChildrenOfType().Count(p => p.Alpha > 0), () => Is.GreaterThan(0)); + AddUntilStep("some beatmaps visible", () => Carousel.ChildrenOfType().Count(p => p.Alpha > 0), () => Is.GreaterThan(0)); SelectNextPanel(); Select(); diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2DifficultyGrouping.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2DifficultyGrouping.cs index c043fd87a9..f3c1634cb2 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2DifficultyGrouping.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2DifficultyGrouping.cs @@ -29,32 +29,32 @@ namespace osu.Game.Tests.Visual.SongSelect [Test] public void TestOpenCloseGroupWithNoSelectionMouse() { - AddAssert("no beatmaps visible", () => Carousel.ChildrenOfType().Count(p => p.Alpha > 0), () => Is.Zero); + AddAssert("no beatmaps visible", () => Carousel.ChildrenOfType().Count(p => p.Alpha > 0), () => Is.Zero); CheckNoSelection(); - ClickVisiblePanel(0); - AddUntilStep("some beatmaps visible", () => Carousel.ChildrenOfType().Count(p => p.Alpha > 0), () => Is.GreaterThan(0)); + ClickVisiblePanel(0); + AddUntilStep("some beatmaps visible", () => Carousel.ChildrenOfType().Count(p => p.Alpha > 0), () => Is.GreaterThan(0)); CheckNoSelection(); - ClickVisiblePanel(0); - AddUntilStep("no beatmaps visible", () => Carousel.ChildrenOfType().Count(p => p.Alpha > 0), () => Is.Zero); + ClickVisiblePanel(0); + AddUntilStep("no beatmaps visible", () => Carousel.ChildrenOfType().Count(p => p.Alpha > 0), () => Is.Zero); CheckNoSelection(); } [Test] public void TestOpenCloseGroupWithNoSelectionKeyboard() { - AddAssert("no beatmaps visible", () => Carousel.ChildrenOfType().Count(p => p.Alpha > 0), () => Is.Zero); + AddAssert("no beatmaps visible", () => Carousel.ChildrenOfType().Count(p => p.Alpha > 0), () => Is.Zero); CheckNoSelection(); SelectNextPanel(); Select(); - AddUntilStep("some beatmaps visible", () => Carousel.ChildrenOfType().Count(p => p.Alpha > 0), () => Is.GreaterThan(0)); + AddUntilStep("some beatmaps visible", () => Carousel.ChildrenOfType().Count(p => p.Alpha > 0), () => Is.GreaterThan(0)); AddAssert("keyboard selected is expanded", () => GetKeyboardSelectedPanel()?.Expanded.Value, () => Is.True); CheckNoSelection(); Select(); - AddUntilStep("no beatmaps visible", () => Carousel.ChildrenOfType().Count(p => p.Alpha > 0), () => Is.Zero); + AddUntilStep("no beatmaps visible", () => Carousel.ChildrenOfType().Count(p => p.Alpha > 0), () => Is.Zero); AddAssert("keyboard selected is collapsed", () => GetKeyboardSelectedPanel()?.Expanded.Value, () => Is.False); CheckNoSelection(); } @@ -87,10 +87,10 @@ namespace osu.Game.Tests.Visual.SongSelect AddUntilStep("drawable selection restored", () => GetSelectedPanel()?.Item?.Model, () => Is.EqualTo(selection)); AddAssert("carousel item is visible", () => GetSelectedPanel()?.Item?.IsVisible, () => Is.True); - ClickVisiblePanel(0); + ClickVisiblePanel(0); AddUntilStep("carousel item not visible", GetSelectedPanel, () => Is.Null); - ClickVisiblePanel(0); + ClickVisiblePanel(0); AddUntilStep("carousel item is visible", () => GetSelectedPanel()?.Item?.IsVisible, () => Is.True); } @@ -120,18 +120,18 @@ namespace osu.Game.Tests.Visual.SongSelect SelectNextGroup(); WaitForGroupSelection(0, 0); - AddAssert("keyboard selected panel is beatmap", GetKeyboardSelectedPanel, Is.TypeOf); - AddAssert("selected panel is beatmap", GetSelectedPanel, Is.TypeOf); + AddAssert("keyboard selected panel is beatmap", GetKeyboardSelectedPanel, Is.TypeOf); + AddAssert("selected panel is beatmap", GetSelectedPanel, Is.TypeOf); - ClickVisiblePanel(0); - AddAssert("keyboard selected panel is group", GetKeyboardSelectedPanel, Is.TypeOf); + ClickVisiblePanel(0); + AddAssert("keyboard selected panel is group", GetKeyboardSelectedPanel, Is.TypeOf); AddAssert("keyboard selected panel is contracted", () => GetKeyboardSelectedPanel()?.Expanded.Value, () => Is.False); - ClickVisiblePanel(0); - AddAssert("keyboard selected panel is group", GetKeyboardSelectedPanel, Is.TypeOf); + ClickVisiblePanel(0); + AddAssert("keyboard selected panel is group", GetKeyboardSelectedPanel, Is.TypeOf); AddAssert("keyboard selected panel is expanded", () => GetKeyboardSelectedPanel()?.Expanded.Value, () => Is.True); - AddAssert("selected panel is still beatmap", GetSelectedPanel, Is.TypeOf); + AddAssert("selected panel is still beatmap", GetSelectedPanel, Is.TypeOf); } [Test] @@ -146,7 +146,7 @@ namespace osu.Game.Tests.Visual.SongSelect // open first group Select(); CheckNoSelection(); - AddUntilStep("some beatmaps visible", () => Carousel.ChildrenOfType().Count(p => p.Alpha > 0), () => Is.GreaterThan(0)); + AddUntilStep("some beatmaps visible", () => Carousel.ChildrenOfType().Count(p => p.Alpha > 0), () => Is.GreaterThan(0)); SelectNextPanel(); Select(); @@ -171,23 +171,23 @@ namespace osu.Game.Tests.Visual.SongSelect [Test] public void TestInputHandlingWithinGaps() { - AddAssert("no beatmaps visible", () => !GetVisiblePanels().Any()); + AddAssert("no beatmaps visible", () => !GetVisiblePanels().Any()); // Clicks just above the first group panel should not actuate any action. - ClickVisiblePanelWithOffset(0, new Vector2(0, -(GroupPanel.HEIGHT / 2 + 1))); + ClickVisiblePanelWithOffset(0, new Vector2(0, -(PanelGroup.HEIGHT / 2 + 1))); - AddAssert("no beatmaps visible", () => !GetVisiblePanels().Any()); + AddAssert("no beatmaps visible", () => !GetVisiblePanels().Any()); - ClickVisiblePanelWithOffset(0, new Vector2(0, -(GroupPanel.HEIGHT / 2))); + ClickVisiblePanelWithOffset(0, new Vector2(0, -(PanelGroup.HEIGHT / 2))); - AddUntilStep("wait for beatmaps visible", () => GetVisiblePanels().Any()); + AddUntilStep("wait for beatmaps visible", () => GetVisiblePanels().Any()); CheckNoSelection(); // Beatmap panels expand their selection area to cover holes from spacing. - ClickVisiblePanelWithOffset(0, new Vector2(0, -(CarouselItem.DEFAULT_HEIGHT / 2 + 1))); + ClickVisiblePanelWithOffset(0, new Vector2(0, -(CarouselItem.DEFAULT_HEIGHT / 2 + 1))); WaitForGroupSelection(0, 0); - ClickVisiblePanelWithOffset(1, new Vector2(0, (CarouselItem.DEFAULT_HEIGHT / 2 + 1))); + ClickVisiblePanelWithOffset(1, new Vector2(0, (CarouselItem.DEFAULT_HEIGHT / 2 + 1))); WaitForGroupSelection(0, 1); } } diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2NoGrouping.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2NoGrouping.cs index 09ded342c3..b4048a5355 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2NoGrouping.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2NoGrouping.cs @@ -213,27 +213,27 @@ namespace osu.Game.Tests.Visual.SongSelect AddBeatmaps(2, 5); WaitForDrawablePanels(); - AddAssert("no beatmaps visible", () => !GetVisiblePanels().Any()); + AddAssert("no beatmaps visible", () => !GetVisiblePanels().Any()); // Clicks just above the first group panel should not actuate any action. - ClickVisiblePanelWithOffset(0, new Vector2(0, -(BeatmapSetPanel.HEIGHT / 2 + 1))); + ClickVisiblePanelWithOffset(0, new Vector2(0, -(PanelBeatmapSet.HEIGHT / 2 + 1))); - AddAssert("no beatmaps visible", () => !GetVisiblePanels().Any()); + AddAssert("no beatmaps visible", () => !GetVisiblePanels().Any()); - ClickVisiblePanelWithOffset(0, new Vector2(0, -(BeatmapSetPanel.HEIGHT / 2))); + ClickVisiblePanelWithOffset(0, new Vector2(0, -(PanelBeatmapSet.HEIGHT / 2))); - AddUntilStep("wait for beatmaps visible", () => GetVisiblePanels().Any()); + AddUntilStep("wait for beatmaps visible", () => GetVisiblePanels().Any()); WaitForSelection(0, 0); // Beatmap panels expand their selection area to cover holes from spacing. - ClickVisiblePanelWithOffset(1, new Vector2(0, -(CarouselItem.DEFAULT_HEIGHT / 2 + 1))); + ClickVisiblePanelWithOffset(1, new Vector2(0, -(CarouselItem.DEFAULT_HEIGHT / 2 + 1))); WaitForSelection(0, 0); // Panels with higher depth will handle clicks in the gutters for simplicity. - ClickVisiblePanelWithOffset(2, new Vector2(0, (CarouselItem.DEFAULT_HEIGHT / 2 + 1))); + ClickVisiblePanelWithOffset(2, new Vector2(0, (CarouselItem.DEFAULT_HEIGHT / 2 + 1))); WaitForSelection(0, 2); - ClickVisiblePanelWithOffset(3, new Vector2(0, (CarouselItem.DEFAULT_HEIGHT / 2 + 1))); + ClickVisiblePanelWithOffset(3, new Vector2(0, (CarouselItem.DEFAULT_HEIGHT / 2 + 1))); WaitForSelection(0, 3); } diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2Scrolling.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2Scrolling.cs index ee6c11595a..890e1dd6e3 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2Scrolling.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2Scrolling.cs @@ -30,16 +30,16 @@ namespace osu.Game.Tests.Visual.SongSelect Quad positionBefore = default; AddStep("select middle beatmap", () => Carousel.CurrentSelection = BeatmapSets.ElementAt(BeatmapSets.Count - 2).Beatmaps.First()); - AddStep("scroll to selected item", () => Scroll.ScrollTo(Scroll.ChildrenOfType().Single(p => p.Selected.Value))); + AddStep("scroll to selected item", () => Scroll.ScrollTo(Scroll.ChildrenOfType().Single(p => p.Selected.Value))); WaitForScrolling(); - AddStep("save selected screen position", () => positionBefore = Carousel.ChildrenOfType().FirstOrDefault(p => p.Selected.Value)!.ScreenSpaceDrawQuad); + AddStep("save selected screen position", () => positionBefore = Carousel.ChildrenOfType().FirstOrDefault(p => p.Selected.Value)!.ScreenSpaceDrawQuad); RemoveFirstBeatmap(); WaitForSorting(); - AddAssert("select screen position unchanged", () => Carousel.ChildrenOfType().Single(p => p.Selected.Value).ScreenSpaceDrawQuad, + AddAssert("select screen position unchanged", () => Carousel.ChildrenOfType().Single(p => p.Selected.Value).ScreenSpaceDrawQuad, () => Is.EqualTo(positionBefore)); } @@ -54,11 +54,11 @@ namespace osu.Game.Tests.Visual.SongSelect WaitForScrolling(); - AddStep("save selected screen position", () => positionBefore = Carousel.ChildrenOfType().FirstOrDefault(p => p.Selected.Value)!.ScreenSpaceDrawQuad); + AddStep("save selected screen position", () => positionBefore = Carousel.ChildrenOfType().FirstOrDefault(p => p.Selected.Value)!.ScreenSpaceDrawQuad); RemoveFirstBeatmap(); WaitForSorting(); - AddAssert("select screen position unchanged", () => Carousel.ChildrenOfType().Single(p => p.Selected.Value).ScreenSpaceDrawQuad, + AddAssert("select screen position unchanged", () => Carousel.ChildrenOfType().Single(p => p.Selected.Value).ScreenSpaceDrawQuad, () => Is.EqualTo(positionBefore)); } } diff --git a/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselV2DifficultyPanel.cs b/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselV2DifficultyPanel.cs index f843c2cded..1947721d5d 100644 --- a/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselV2DifficultyPanel.cs +++ b/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselV2DifficultyPanel.cs @@ -78,21 +78,21 @@ namespace osu.Game.Tests.Visual.SongSelectV2 Spacing = new Vector2(0f, 5f), Children = new Drawable[] { - new BeatmapPanel + new PanelBeatmap { Item = new CarouselItem(beatmap) }, - new BeatmapPanel + new PanelBeatmap { Item = new CarouselItem(beatmap), KeyboardSelected = { Value = true } }, - new BeatmapPanel + new PanelBeatmap { Item = new CarouselItem(beatmap), Selected = { Value = true } }, - new BeatmapPanel + new PanelBeatmap { Item = new CarouselItem(beatmap), KeyboardSelected = { Value = true }, diff --git a/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselV2GroupPanel.cs b/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselV2GroupPanel.cs index 5c94addc74..711a3b881d 100644 --- a/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselV2GroupPanel.cs +++ b/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselV2GroupPanel.cs @@ -29,49 +29,49 @@ namespace osu.Game.Tests.Visual.SongSelectV2 Spacing = new Vector2(0f, 5f), Children = new Drawable[] { - new GroupPanel + new PanelGroup { Item = new CarouselItem(new GroupDefinition('A', "Group A")) }, - new GroupPanel + new PanelGroup { Item = new CarouselItem(new GroupDefinition('A', "Group A")), KeyboardSelected = { Value = true } }, - new GroupPanel + new PanelGroup { Item = new CarouselItem(new GroupDefinition('A', "Group A")), Expanded = { Value = true } }, - new GroupPanel + new PanelGroup { Item = new CarouselItem(new GroupDefinition('A', "Group A")), KeyboardSelected = { Value = true }, Expanded = { Value = true } }, - new StarsGroupPanel + new PanelGroupStarDificulty { Item = new CarouselItem(new GroupDefinition(1, "1")) }, - new StarsGroupPanel + new PanelGroupStarDificulty { Item = new CarouselItem(new GroupDefinition(3, "3")), Expanded = { Value = true } }, - new StarsGroupPanel + new PanelGroupStarDificulty { Item = new CarouselItem(new GroupDefinition(5, "5")), }, - new StarsGroupPanel + new PanelGroupStarDificulty { Item = new CarouselItem(new GroupDefinition(7, "7")), Expanded = { Value = true } }, - new StarsGroupPanel + new PanelGroupStarDificulty { Item = new CarouselItem(new GroupDefinition(8, "8")), }, - new StarsGroupPanel + new PanelGroupStarDificulty { Item = new CarouselItem(new GroupDefinition(9, "9")), Expanded = { Value = true } diff --git a/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselV2SetPanel.cs b/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselV2SetPanel.cs index 382357b67e..ef34394e12 100644 --- a/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselV2SetPanel.cs +++ b/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselV2SetPanel.cs @@ -68,21 +68,21 @@ namespace osu.Game.Tests.Visual.SongSelectV2 Spacing = new Vector2(0f, 5f), Children = new Drawable[] { - new BeatmapSetPanel + new PanelBeatmapSet { Item = new CarouselItem(beatmapSet) }, - new BeatmapSetPanel + new PanelBeatmapSet { Item = new CarouselItem(beatmapSet), KeyboardSelected = { Value = true } }, - new BeatmapSetPanel + new PanelBeatmapSet { Item = new CarouselItem(beatmapSet), Expanded = { Value = true } }, - new BeatmapSetPanel + new PanelBeatmapSet { Item = new CarouselItem(beatmapSet), KeyboardSelected = { Value = true }, diff --git a/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselV2StandalonePanel.cs b/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselV2StandalonePanel.cs index 41eb5c3683..2dbe9e6cd1 100644 --- a/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselV2StandalonePanel.cs +++ b/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselV2StandalonePanel.cs @@ -78,21 +78,21 @@ namespace osu.Game.Tests.Visual.SongSelectV2 Spacing = new Vector2(0f, 5f), Children = new Drawable[] { - new BeatmapStandalonePanel + new PanelBeatmapStandalone { Item = new CarouselItem(beatmap) }, - new BeatmapStandalonePanel + new PanelBeatmapStandalone { Item = new CarouselItem(beatmap), KeyboardSelected = { Value = true } }, - new BeatmapStandalonePanel + new PanelBeatmapStandalone { Item = new CarouselItem(beatmap), Selected = { Value = true } }, - new BeatmapStandalonePanel + new PanelBeatmapStandalone { Item = new CarouselItem(beatmap), KeyboardSelected = { Value = true }, diff --git a/osu.Game/Screens/SelectV2/BeatmapCarousel.cs b/osu.Game/Screens/SelectV2/BeatmapCarousel.cs index 5ae227f86c..c6bce228dc 100644 --- a/osu.Game/Screens/SelectV2/BeatmapCarousel.cs +++ b/osu.Game/Screens/SelectV2/BeatmapCarousel.cs @@ -267,9 +267,9 @@ namespace osu.Game.Screens.SelectV2 #region Drawable pooling - private readonly DrawablePool beatmapPanelPool = new DrawablePool(100); - private readonly DrawablePool setPanelPool = new DrawablePool(100); - private readonly DrawablePool groupPanelPool = new DrawablePool(100); + private readonly DrawablePool beatmapPanelPool = new DrawablePool(100); + private readonly DrawablePool setPanelPool = new DrawablePool(100); + private readonly DrawablePool groupPanelPool = new DrawablePool(100); private void setupPools() { diff --git a/osu.Game/Screens/SelectV2/BeatmapCarouselFilterGrouping.cs b/osu.Game/Screens/SelectV2/BeatmapCarouselFilterGrouping.cs index cb5a40918c..8f9d5cc31b 100644 --- a/osu.Game/Screens/SelectV2/BeatmapCarouselFilterGrouping.cs +++ b/osu.Game/Screens/SelectV2/BeatmapCarouselFilterGrouping.cs @@ -70,7 +70,7 @@ namespace osu.Game.Screens.SelectV2 addItem(new CarouselItem(newGroup) { - DrawHeight = GroupPanel.HEIGHT, + DrawHeight = PanelGroup.HEIGHT, DepthLayer = -2, }); } @@ -85,7 +85,7 @@ namespace osu.Game.Screens.SelectV2 addItem(new CarouselItem(beatmap.BeatmapSet!) { - DrawHeight = BeatmapSetPanel.HEIGHT, + DrawHeight = PanelBeatmapSet.HEIGHT, DepthLayer = -1 }); } diff --git a/osu.Game/Screens/SelectV2/BeatmapPanel.cs b/osu.Game/Screens/SelectV2/PanelBeatmap.cs similarity index 98% rename from osu.Game/Screens/SelectV2/BeatmapPanel.cs rename to osu.Game/Screens/SelectV2/PanelBeatmap.cs index 3db60876a1..93ef814f2e 100644 --- a/osu.Game/Screens/SelectV2/BeatmapPanel.cs +++ b/osu.Game/Screens/SelectV2/PanelBeatmap.cs @@ -23,11 +23,11 @@ using osuTK; namespace osu.Game.Screens.SelectV2 { - public partial class BeatmapPanel : PoolableDrawable, ICarouselPanel + public partial class PanelBeatmap : PoolableDrawable, ICarouselPanel { public const float HEIGHT = CarouselItem.DEFAULT_HEIGHT; - // todo: this should be replaced with information from CarouselItem about how deep is BeatmapPanel in the carousel + // todo: this should be replaced with information from CarouselItem about how deep is PanelBeatmap in the carousel // (i.e. whether it's under a beatmap set that's under a group, or just under a top-level beatmap set). private const float difficulty_x_offset = 100f; // constant X offset for beatmap difficulty panels specifically. diff --git a/osu.Game/Screens/SelectV2/BeatmapSetPanel.cs b/osu.Game/Screens/SelectV2/PanelBeatmapSet.cs similarity index 98% rename from osu.Game/Screens/SelectV2/BeatmapSetPanel.cs rename to osu.Game/Screens/SelectV2/PanelBeatmapSet.cs index 6caabb79c3..2904cda9de 100644 --- a/osu.Game/Screens/SelectV2/BeatmapSetPanel.cs +++ b/osu.Game/Screens/SelectV2/PanelBeatmapSet.cs @@ -19,11 +19,11 @@ using osuTK; namespace osu.Game.Screens.SelectV2 { - public partial class BeatmapSetPanel : PoolableDrawable, ICarouselPanel + public partial class PanelBeatmapSet : PoolableDrawable, ICarouselPanel { public const float HEIGHT = CarouselItem.DEFAULT_HEIGHT * 1.6f; - // todo: this should be replaced with information from CarouselItem about how deep is BeatmapPanel in the carousel + // todo: this should be replaced with information from CarouselItem about how deep is PanelBeatmap in the carousel // (i.e. whether it's under a beatmap set that's under a group, or just under a top-level beatmap set). private const float set_x_offset = 20f; // constant X offset for beatmap set/standalone panels specifically. diff --git a/osu.Game/Screens/SelectV2/BeatmapStandalonePanel.cs b/osu.Game/Screens/SelectV2/PanelBeatmapStandalone.cs similarity index 99% rename from osu.Game/Screens/SelectV2/BeatmapStandalonePanel.cs rename to osu.Game/Screens/SelectV2/PanelBeatmapStandalone.cs index e8628d5b78..c858e039ec 100644 --- a/osu.Game/Screens/SelectV2/BeatmapStandalonePanel.cs +++ b/osu.Game/Screens/SelectV2/PanelBeatmapStandalone.cs @@ -25,11 +25,11 @@ using osuTK; namespace osu.Game.Screens.SelectV2 { - public partial class BeatmapStandalonePanel : PoolableDrawable, ICarouselPanel + public partial class PanelBeatmapStandalone : PoolableDrawable, ICarouselPanel { public const float HEIGHT = CarouselItem.DEFAULT_HEIGHT * 1.6f; - // todo: this should be replaced with information from CarouselItem about how deep is BeatmapPanel in the carousel + // todo: this should be replaced with information from CarouselItem about how deep is PanelBeatmap in the carousel // (i.e. whether it's under a beatmap set that's under a group, or just under a top-level beatmap set). private const float standalone_x_offset = 20f; // constant X offset for beatmap set/standalone panels specifically. diff --git a/osu.Game/Screens/SelectV2/GroupPanel.cs b/osu.Game/Screens/SelectV2/PanelGroup.cs similarity index 98% rename from osu.Game/Screens/SelectV2/GroupPanel.cs rename to osu.Game/Screens/SelectV2/PanelGroup.cs index 506a230cb4..cdd0695147 100644 --- a/osu.Game/Screens/SelectV2/GroupPanel.cs +++ b/osu.Game/Screens/SelectV2/PanelGroup.cs @@ -18,7 +18,7 @@ using osuTK.Graphics; namespace osu.Game.Screens.SelectV2 { - public partial class GroupPanel : PoolableDrawable, ICarouselPanel + public partial class PanelGroup : PoolableDrawable, ICarouselPanel { public const float HEIGHT = CarouselItem.DEFAULT_HEIGHT; diff --git a/osu.Game/Screens/SelectV2/StarsGroupPanel.cs b/osu.Game/Screens/SelectV2/PanelGroupStarDificulty.cs similarity index 98% rename from osu.Game/Screens/SelectV2/StarsGroupPanel.cs rename to osu.Game/Screens/SelectV2/PanelGroupStarDificulty.cs index 7e2647ccbf..2215e643bd 100644 --- a/osu.Game/Screens/SelectV2/StarsGroupPanel.cs +++ b/osu.Game/Screens/SelectV2/PanelGroupStarDificulty.cs @@ -22,7 +22,7 @@ using osuTK.Graphics; namespace osu.Game.Screens.SelectV2 { - public partial class StarsGroupPanel : PoolableDrawable, ICarouselPanel + public partial class PanelGroupStarDificulty : PoolableDrawable, ICarouselPanel { public const float HEIGHT = CarouselItem.DEFAULT_HEIGHT; From 068a66e7d4c9bef92ea39e5237b37bc628e9e14f Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Wed, 12 Feb 2025 18:35:35 +0900 Subject: [PATCH 1013/1275] Move room tracking to lounge subscreen --- .../TestSceneLoungeRoomsContainer.cs | 28 ++++----- .../TestScenePlaylistsLoungeSubScreen.cs | 30 ++++----- .../Components/ListingPollingComponent.cs | 38 ++---------- .../Components/SelectionPollingComponent.cs | 5 +- .../Lounge/Components/RoomsContainer.cs | 45 ++++---------- .../OnlinePlay/Lounge/LoungeSubScreen.cs | 62 ++++++++++++++----- .../Multiplayer/MultiplayerLoungeSubScreen.cs | 34 ---------- .../Playlists/PlaylistsLoungeSubScreen.cs | 3 - 8 files changed, 95 insertions(+), 150 deletions(-) diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneLoungeRoomsContainer.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneLoungeRoomsContainer.cs index 797b69ec72..10df77f88c 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneLoungeRoomsContainer.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneLoungeRoomsContainer.cs @@ -50,17 +50,17 @@ namespace osu.Game.Tests.Visual.Multiplayer { AddStep("add rooms", () => RoomManager.AddRooms(5, withSpotlightRooms: true)); - AddAssert("has 5 rooms", () => container.Rooms.Count == 5); + AddAssert("has 5 rooms", () => container.DrawableRooms.Count == 5); - AddAssert("all spotlights at top", () => container.Rooms + AddAssert("all spotlights at top", () => container.DrawableRooms .SkipWhile(r => r.Room.Category == RoomCategory.Spotlight) .All(r => r.Room.Category == RoomCategory.Normal)); AddStep("remove first room", () => RoomManager.RemoveRoom(RoomManager.Rooms.First(r => r.RoomID == 0))); - AddAssert("has 4 rooms", () => container.Rooms.Count == 4); - AddAssert("first room removed", () => container.Rooms.All(r => r.Room.RoomID != 0)); + AddAssert("has 4 rooms", () => container.DrawableRooms.Count == 4); + AddAssert("first room removed", () => container.DrawableRooms.All(r => r.Room.RoomID != 0)); - AddStep("select first room", () => container.Rooms.First().TriggerClick()); + AddStep("select first room", () => container.DrawableRooms.First().TriggerClick()); AddAssert("first spotlight selected", () => checkRoomSelected(RoomManager.Rooms.First(r => r.Category == RoomCategory.Spotlight))); AddStep("remove last room", () => RoomManager.RemoveRoom(RoomManager.Rooms.MinBy(r => r.RoomID)!)); @@ -137,15 +137,15 @@ namespace osu.Game.Tests.Visual.Multiplayer { AddStep("add rooms", () => RoomManager.AddRooms(4)); - AddUntilStep("4 rooms visible", () => container.Rooms.Count(r => r.IsPresent) == 4); + AddUntilStep("4 rooms visible", () => container.DrawableRooms.Count(r => r.IsPresent) == 4); AddStep("filter one room", () => container.Filter.Value = new FilterCriteria { SearchString = "1" }); - AddUntilStep("1 rooms visible", () => container.Rooms.Count(r => r.IsPresent) == 1); + AddUntilStep("1 rooms visible", () => container.DrawableRooms.Count(r => r.IsPresent) == 1); AddStep("remove filter", () => container.Filter.Value = null); - AddUntilStep("4 rooms visible", () => container.Rooms.Count(r => r.IsPresent) == 4); + AddUntilStep("4 rooms visible", () => container.DrawableRooms.Count(r => r.IsPresent) == 4); } [Test] @@ -156,13 +156,13 @@ namespace osu.Game.Tests.Visual.Multiplayer // Todo: What even is this case...? AddStep("set empty filter criteria", () => container.Filter.Value = new FilterCriteria()); - AddUntilStep("5 rooms visible", () => container.Rooms.Count(r => r.IsPresent) == 5); + AddUntilStep("5 rooms visible", () => container.DrawableRooms.Count(r => r.IsPresent) == 5); AddStep("filter osu! rooms", () => container.Filter.Value = new FilterCriteria { Ruleset = new OsuRuleset().RulesetInfo }); - AddUntilStep("2 rooms visible", () => container.Rooms.Count(r => r.IsPresent) == 2); + AddUntilStep("2 rooms visible", () => container.DrawableRooms.Count(r => r.IsPresent) == 2); AddStep("filter catch rooms", () => container.Filter.Value = new FilterCriteria { Ruleset = new CatchRuleset().RulesetInfo }); - AddUntilStep("3 rooms visible", () => container.Rooms.Count(r => r.IsPresent) == 3); + AddUntilStep("3 rooms visible", () => container.DrawableRooms.Count(r => r.IsPresent) == 3); } [Test] @@ -176,15 +176,15 @@ namespace osu.Game.Tests.Visual.Multiplayer AddStep("apply default filter", () => container.Filter.SetDefault()); - AddUntilStep("both rooms visible", () => container.Rooms.Count(r => r.IsPresent) == 2); + AddUntilStep("both rooms visible", () => container.DrawableRooms.Count(r => r.IsPresent) == 2); AddStep("filter public rooms", () => container.Filter.Value = new FilterCriteria { Permissions = RoomPermissionsFilter.Public }); - AddUntilStep("private room hidden", () => container.Rooms.All(r => !r.Room.HasPassword)); + AddUntilStep("private room hidden", () => container.DrawableRooms.All(r => !r.Room.HasPassword)); AddStep("filter private rooms", () => container.Filter.Value = new FilterCriteria { Permissions = RoomPermissionsFilter.Private }); - AddUntilStep("public room hidden", () => container.Rooms.All(r => r.Room.HasPassword)); + AddUntilStep("public room hidden", () => container.DrawableRooms.All(r => r.Room.HasPassword)); } [Test] diff --git a/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsLoungeSubScreen.cs b/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsLoungeSubScreen.cs index 53c7873de5..9d65be2a19 100644 --- a/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsLoungeSubScreen.cs +++ b/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsLoungeSubScreen.cs @@ -36,7 +36,7 @@ namespace osu.Game.Tests.Visual.Playlists public void TestManyRooms() { AddStep("add rooms", () => RoomManager.AddRooms(500)); - AddUntilStep("wait for rooms", () => roomsContainer.Rooms.Count == 500); + AddUntilStep("wait for rooms", () => roomsContainer.DrawableRooms.Count == 500); } [Test] @@ -45,45 +45,45 @@ namespace osu.Game.Tests.Visual.Playlists AddStep("reset mouse", () => InputManager.ReleaseButton(MouseButton.Left)); AddStep("add rooms", () => RoomManager.AddRooms(30)); - AddUntilStep("wait for rooms", () => roomsContainer.Rooms.Count == 30); + AddUntilStep("wait for rooms", () => roomsContainer.DrawableRooms.Count == 30); - AddUntilStep("first room is not masked", () => checkRoomVisible(roomsContainer.Rooms[0])); + AddUntilStep("first room is not masked", () => checkRoomVisible(roomsContainer.DrawableRooms[0])); - AddStep("move mouse to third room", () => InputManager.MoveMouseTo(roomsContainer.Rooms[2])); + AddStep("move mouse to third room", () => InputManager.MoveMouseTo(roomsContainer.DrawableRooms[2])); AddStep("hold down", () => InputManager.PressButton(MouseButton.Left)); - AddStep("drag to top", () => InputManager.MoveMouseTo(roomsContainer.Rooms[0])); + AddStep("drag to top", () => InputManager.MoveMouseTo(roomsContainer.DrawableRooms[0])); AddAssert("first and second room masked", () - => !checkRoomVisible(roomsContainer.Rooms[0]) && - !checkRoomVisible(roomsContainer.Rooms[1])); + => !checkRoomVisible(roomsContainer.DrawableRooms[0]) && + !checkRoomVisible(roomsContainer.DrawableRooms[1])); } [Test] public void TestScrollSelectedIntoView() { AddStep("add rooms", () => RoomManager.AddRooms(30)); - AddUntilStep("wait for rooms", () => roomsContainer.Rooms.Count == 30); + AddUntilStep("wait for rooms", () => roomsContainer.DrawableRooms.Count == 30); - AddUntilStep("first room is not masked", () => checkRoomVisible(roomsContainer.Rooms[0])); + AddUntilStep("first room is not masked", () => checkRoomVisible(roomsContainer.DrawableRooms[0])); - AddStep("select last room", () => roomsContainer.Rooms[^1].TriggerClick()); + AddStep("select last room", () => roomsContainer.DrawableRooms[^1].TriggerClick()); - AddUntilStep("first room is masked", () => !checkRoomVisible(roomsContainer.Rooms[0])); - AddUntilStep("last room is not masked", () => checkRoomVisible(roomsContainer.Rooms[^1])); + AddUntilStep("first room is masked", () => !checkRoomVisible(roomsContainer.DrawableRooms[0])); + AddUntilStep("last room is not masked", () => checkRoomVisible(roomsContainer.DrawableRooms[^1])); } [Test] public void TestEnteringRoomTakesLeaseOnSelection() { AddStep("add rooms", () => RoomManager.AddRooms(1)); - AddUntilStep("wait for rooms", () => roomsContainer.Rooms.Count == 1); + AddUntilStep("wait for rooms", () => roomsContainer.DrawableRooms.Count == 1); AddAssert("selected room is not disabled", () => !loungeScreen.SelectedRoom.Disabled); - AddStep("select room", () => roomsContainer.Rooms[0].TriggerClick()); + AddStep("select room", () => roomsContainer.DrawableRooms[0].TriggerClick()); AddAssert("selected room is non-null", () => loungeScreen.SelectedRoom.Value != null); - AddStep("enter room", () => roomsContainer.Rooms[0].TriggerClick()); + AddStep("enter room", () => roomsContainer.DrawableRooms[0].TriggerClick()); AddUntilStep("wait for match load", () => Stack.CurrentScreen is PlaylistsRoomSubScreen); diff --git a/osu.Game/Screens/OnlinePlay/Components/ListingPollingComponent.cs b/osu.Game/Screens/OnlinePlay/Components/ListingPollingComponent.cs index 21452727b8..5cb4c9420a 100644 --- a/osu.Game/Screens/OnlinePlay/Components/ListingPollingComponent.cs +++ b/osu.Game/Screens/OnlinePlay/Components/ListingPollingComponent.cs @@ -1,9 +1,9 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System; using System.Linq; using System.Threading.Tasks; -using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Game.Online.Rooms; using osu.Game.Screens.OnlinePlay.Lounge.Components; @@ -15,23 +15,8 @@ namespace osu.Game.Screens.OnlinePlay.Components /// public partial class ListingPollingComponent : RoomPollingComponent { - public IBindable InitialRoomsReceived => initialRoomsReceived; - private readonly Bindable initialRoomsReceived = new Bindable(); - - public readonly Bindable Filter = new Bindable(); - - [BackgroundDependencyLoader] - private void load() - { - Filter.BindValueChanged(_ => - { - RoomManager.ClearRooms(); - initialRoomsReceived.Value = false; - - if (IsLoaded) - PollImmediately(); - }); - } + public required Action RoomsReceived { get; init; } + public readonly IBindable Filter = new Bindable(); private GetRoomsRequest? lastPollRequest; @@ -43,26 +28,14 @@ namespace osu.Game.Screens.OnlinePlay.Components if (Filter.Value == null) return base.Poll(); - var tcs = new TaskCompletionSource(); - lastPollRequest?.Cancel(); + var tcs = new TaskCompletionSource(); var req = new GetRoomsRequest(Filter.Value); req.Success += result => { - result = result.Where(r => r.Category != RoomCategory.DailyChallenge).ToList(); - - foreach (var existing in RoomManager.Rooms.ToArray()) - { - if (result.All(r => r.RoomID != existing.RoomID)) - RoomManager.RemoveRoom(existing); - } - - foreach (var incoming in result) - RoomManager.AddOrUpdateRoom(incoming); - - initialRoomsReceived.Value = true; + RoomsReceived(result.Where(r => r.Category != RoomCategory.DailyChallenge).ToArray()); tcs.SetResult(true); }; @@ -71,6 +44,7 @@ namespace osu.Game.Screens.OnlinePlay.Components API.Queue(req); lastPollRequest = req; + return tcs.Task; } } diff --git a/osu.Game/Screens/OnlinePlay/Components/SelectionPollingComponent.cs b/osu.Game/Screens/OnlinePlay/Components/SelectionPollingComponent.cs index 7cee8b3546..f04fd6a096 100644 --- a/osu.Game/Screens/OnlinePlay/Components/SelectionPollingComponent.cs +++ b/osu.Game/Screens/OnlinePlay/Components/SelectionPollingComponent.cs @@ -28,15 +28,14 @@ namespace osu.Game.Screens.OnlinePlay.Components if (room.RoomID == null) return base.Poll(); - var tcs = new TaskCompletionSource(); - lastPollRequest?.Cancel(); + var tcs = new TaskCompletionSource(); var req = new GetRoomRequest(room.RoomID.Value); req.Success += result => { - RoomManager.AddOrUpdateRoom(result); + room.CopyFrom(result); tcs.SetResult(true); }; diff --git a/osu.Game/Screens/OnlinePlay/Lounge/Components/RoomsContainer.cs b/osu.Game/Screens/OnlinePlay/Lounge/Components/RoomsContainer.cs index 6eda993f94..6681cbe720 100644 --- a/osu.Game/Screens/OnlinePlay/Lounge/Components/RoomsContainer.cs +++ b/osu.Game/Screens/OnlinePlay/Lounge/Components/RoomsContainer.cs @@ -7,10 +7,8 @@ using System.Collections.Specialized; using System.Diagnostics; using System.Globalization; using System.Linq; -using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Extensions.IEnumerableExtensions; -using osu.Framework.Extensions.ObjectExtensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Input.Bindings; @@ -24,17 +22,14 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components { public partial class RoomsContainer : CompositeDrawable, IKeyBindingHandler { + public readonly BindableList Rooms = new BindableList(); public readonly Bindable SelectedRoom = new Bindable(); public readonly Bindable Filter = new Bindable(); - public IReadOnlyList Rooms => roomFlow.FlowingChildren.Cast().ToArray(); + public IReadOnlyList DrawableRooms => roomFlow.FlowingChildren.Cast().ToArray(); - private readonly IBindableList rooms = new BindableList(); private readonly FillFlowContainer roomFlow; - [Resolved] - private IRoomManager roomManager { get; set; } = null!; - // handle deselection public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => true; @@ -62,11 +57,7 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components protected override void LoadComplete() { - rooms.CollectionChanged += roomsChanged; - roomManager.RoomsUpdated += updateSorting; - - rooms.BindTo(roomManager.Rooms); - + Rooms.BindCollectionChanged(roomsChanged, true); Filter.BindValueChanged(criteria => applyFilterCriteria(criteria.NewValue), true); } @@ -155,7 +146,14 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components private void addRooms(IEnumerable rooms) { foreach (var room in rooms) - roomFlow.Add(new DrawableLoungeRoom(room) { SelectedRoom = SelectedRoom }); + { + var drawableRoom = new DrawableLoungeRoom(room) { SelectedRoom = SelectedRoom }; + + roomFlow.Add(drawableRoom); + + // Always show spotlight playlists at the top of the listing. + roomFlow.SetLayoutPosition(drawableRoom, room.Category > RoomCategory.Normal ? float.MinValue : -(room.RoomID ?? 0)); + } applyFilterCriteria(Filter.Value); } @@ -181,17 +179,6 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components SelectedRoom.Value = null; } - private void updateSorting() - { - foreach (var room in roomFlow) - { - roomFlow.SetLayoutPosition(room, room.Room.Category > RoomCategory.Normal - // Always show spotlight playlists at the top of the listing. - ? float.MinValue - : -(room.Room.RoomID ?? 0)); - } - } - protected override bool OnClick(ClickEvent e) { if (!SelectedRoom.Disabled) @@ -226,7 +213,7 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components if (SelectedRoom.Disabled) return; - var visibleRooms = Rooms.AsEnumerable().Where(r => r.IsPresent); + var visibleRooms = DrawableRooms.AsEnumerable().Where(r => r.IsPresent); Room? room; @@ -246,13 +233,5 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components } #endregion - - protected override void Dispose(bool isDisposing) - { - base.Dispose(isDisposing); - - if (roomManager.IsNotNull()) - roomManager.RoomsUpdated -= updateSorting; - } } } diff --git a/osu.Game/Screens/OnlinePlay/Lounge/LoungeSubScreen.cs b/osu.Game/Screens/OnlinePlay/Lounge/LoungeSubScreen.cs index 0e08e398a4..78501a56d7 100644 --- a/osu.Game/Screens/OnlinePlay/Lounge/LoungeSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Lounge/LoungeSubScreen.cs @@ -53,8 +53,6 @@ namespace osu.Game.Screens.OnlinePlay.Lounge AutoSizeAxes = Axes.Both }; - protected ListingPollingComponent ListingPollingComponent { get; private set; } = null!; - protected readonly Bindable SelectedRoom = new Bindable(); [Resolved] @@ -75,12 +73,15 @@ namespace osu.Game.Screens.OnlinePlay.Lounge [Resolved] protected OsuConfigManager Config { get; private set; } = null!; - private IDisposable? joiningRoomOperation { get; set; } + private IDisposable? joiningRoomOperation; private LeasedBindable? selectionLease; + private readonly BindableList rooms = new BindableList(); private readonly Bindable filter = new Bindable(); + private readonly Bindable hasListingResults = new Bindable(); private readonly IBindable operationInProgress = new Bindable(); private readonly IBindable isIdle = new BindableBool(); + private ListingPollingComponent listingPollingComponent = null!; private PopoverContainer popoverContainer = null!; private LoadingLayer loadingLayer = null!; private RoomsContainer roomsContainer = null!; @@ -100,7 +101,11 @@ namespace osu.Game.Screens.OnlinePlay.Lounge InternalChildren = new Drawable[] { - ListingPollingComponent = CreatePollingComponent().With(c => c.Filter.BindTarget = filter), + listingPollingComponent = new ListingPollingComponent + { + RoomsReceived = onListingReceived, + Filter = { BindTarget = filter } + }, popoverContainer = new PopoverContainer { Name = @"Rooms area", @@ -116,8 +121,9 @@ namespace osu.Game.Screens.OnlinePlay.Lounge ScrollbarOverlapsContent = false, Child = roomsContainer = new RoomsContainer { + Rooms = { BindTarget = rooms }, + SelectedRoom = { BindTarget = SelectedRoom }, Filter = { BindTarget = filter }, - SelectedRoom = { BindTarget = SelectedRoom } } }, }, @@ -178,7 +184,7 @@ namespace osu.Game.Screens.OnlinePlay.Lounge // scroll selected room into view on selection. SelectedRoom.BindValueChanged(val => { - var drawable = roomsContainer.Rooms.FirstOrDefault(r => r.Room == val.NewValue); + var drawable = roomsContainer.DrawableRooms.FirstOrDefault(r => r.Room == val.NewValue); if (drawable != null) scrollContainer.ScrollIntoView(drawable); }); @@ -190,7 +196,6 @@ namespace osu.Game.Screens.OnlinePlay.Lounge searchTextBox.Current.BindValueChanged(_ => updateFilterDebounced()); ruleset.BindValueChanged(_ => UpdateFilter()); - isIdle.BindValueChanged(_ => updatePollingRate(this.IsCurrentScreen()), true); if (ongoingOperationTracker != null) @@ -199,11 +204,38 @@ namespace osu.Game.Screens.OnlinePlay.Lounge operationInProgress.BindValueChanged(_ => updateLoadingLayer()); } - ListingPollingComponent.InitialRoomsReceived.BindValueChanged(_ => updateLoadingLayer(), true); + hasListingResults.BindValueChanged(_ => updateLoadingLayer()); + + filter.BindValueChanged(_ => + { + rooms.Clear(); + hasListingResults.Value = false; + listingPollingComponent.PollImmediately(); + }); updateFilter(); } + private void onListingReceived(Room[] result) + { + Dictionary localRoomsById = rooms.ToDictionary(r => r.RoomID!.Value); + Dictionary resultRoomsById = result.ToDictionary(r => r.RoomID!.Value); + + // Remove all local rooms no longer in the result set. + rooms.RemoveAll(r => !resultRoomsById.ContainsKey(r.RoomID!.Value)); + + // Add or update local rooms with the result set. + foreach (var r in result) + { + if (localRoomsById.TryGetValue(r.RoomID!.Value, out Room? existingRoom)) + existingRoom.CopyFrom(r); + else + rooms.Add(r); + } + + hasListingResults.Value = true; + } + #region Filtering public void UpdateFilter() => Scheduler.AddOnce(updateFilter); @@ -267,7 +299,7 @@ namespace osu.Game.Screens.OnlinePlay.Lounge onReturning(); // Poll for any newly-created rooms (including potentially the user's own). - ListingPollingComponent.PollImmediately(); + listingPollingComponent.PollImmediately(); } public override bool OnExiting(ScreenExitEvent e) @@ -392,11 +424,11 @@ namespace osu.Game.Screens.OnlinePlay.Lounge this.Push(CreateRoomSubScreen(room)); } - public void RefreshRooms() => ListingPollingComponent.PollImmediately(); + public void RefreshRooms() => listingPollingComponent.PollImmediately(); private void updateLoadingLayer() { - if (operationInProgress.Value || !ListingPollingComponent.InitialRoomsReceived.Value) + if (operationInProgress.Value || !hasListingResults.Value) loadingLayer.Show(); else loadingLayer.Hide(); @@ -405,11 +437,11 @@ namespace osu.Game.Screens.OnlinePlay.Lounge private void updatePollingRate(bool isCurrentScreen) { if (!isCurrentScreen) - ListingPollingComponent.TimeBetweenPolls.Value = 0; + listingPollingComponent.TimeBetweenPolls.Value = 0; else - ListingPollingComponent.TimeBetweenPolls.Value = isIdle.Value ? 120000 : 15000; + listingPollingComponent.TimeBetweenPolls.Value = isIdle.Value ? 120000 : 15000; - Logger.Log($"Polling adjusted (listing: {ListingPollingComponent.TimeBetweenPolls.Value})"); + Logger.Log($"Polling adjusted (listing: {listingPollingComponent.TimeBetweenPolls.Value})"); } protected abstract OsuButton CreateNewRoomButton(); @@ -421,7 +453,5 @@ namespace osu.Game.Screens.OnlinePlay.Lounge protected abstract Room CreateNewRoom(); protected abstract RoomSubScreen CreateRoomSubScreen(Room room); - - protected abstract ListingPollingComponent CreatePollingComponent(); } } diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerLoungeSubScreen.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerLoungeSubScreen.cs index 873a9cde88..3cf873ec78 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerLoungeSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerLoungeSubScreen.cs @@ -79,8 +79,6 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer protected override RoomSubScreen CreateRoomSubScreen(Room room) => new MultiplayerMatchSubScreen(room); - protected override ListingPollingComponent CreatePollingComponent() => new MultiplayerListingPollingComponent(); - protected override void JoinInternal(Room room, string? password, Action onSuccess, Action onFailure) { client.JoinRoom(room, password).ContinueWith(result => @@ -109,37 +107,5 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer base.OpenNewRoom(room); } - - private partial class MultiplayerListingPollingComponent : ListingPollingComponent - { - [Resolved] - private MultiplayerClient client { get; set; } = null!; - - private readonly IBindable isConnected = new Bindable(); - - [BackgroundDependencyLoader] - private void load() - { - isConnected.BindTo(client.IsConnected); - isConnected.BindValueChanged(_ => Scheduler.AddOnce(poll), true); - } - - private void poll() - { - if (isConnected.Value && IsLoaded) - PollImmediately(); - } - - protected override Task Poll() - { - if (!isConnected.Value) - return Task.CompletedTask; - - if (client.Room != null) - return Task.CompletedTask; - - return base.Poll(); - } - } } } diff --git a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsLoungeSubScreen.cs b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsLoungeSubScreen.cs index 6ed367328c..26eae50797 100644 --- a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsLoungeSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsLoungeSubScreen.cs @@ -11,7 +11,6 @@ using osu.Framework.Graphics.UserInterface; using osu.Game.Graphics.UserInterface; using osu.Game.Online.API; using osu.Game.Online.Rooms; -using osu.Game.Screens.OnlinePlay.Components; using osu.Game.Screens.OnlinePlay.Lounge; using osu.Game.Screens.OnlinePlay.Lounge.Components; using osu.Game.Screens.OnlinePlay.Match; @@ -87,8 +86,6 @@ namespace osu.Game.Screens.OnlinePlay.Playlists protected override RoomSubScreen CreateRoomSubScreen(Room room) => new PlaylistsRoomSubScreen(room); - protected override ListingPollingComponent CreatePollingComponent() => new ListingPollingComponent(); - private enum PlaylistsCategory { Any, From 96db6964df2e1045eacedebae3bfdf95eb250983 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 12 Feb 2025 10:55:57 +0100 Subject: [PATCH 1014/1275] Adjust things further --- .../Skinning/Legacy/LegacySwell.cs | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacySwell.cs b/osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacySwell.cs index d3b5d54828..5d65ac6058 100644 --- a/osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacySwell.cs +++ b/osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacySwell.cs @@ -15,11 +15,14 @@ using osu.Game.Rulesets.Objects.Drawables; using osu.Framework.Extensions.ObjectExtensions; using System; using System.Globalization; +using osu.Framework.Utils; namespace osu.Game.Rulesets.Taiko.Skinning.Legacy { public partial class LegacySwell : Container { + private const float scale_adjust = 768f / 480; + private DrawableSwell drawableSwell = null!; private Container bodyContainer = null!; @@ -80,12 +83,13 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy Anchor = Anchor.Centre, Origin = Anchor.Centre, Scale = new Vector2(1.86f * 0.8f), + Alpha = 0.8f, }, - remainingHitsText = new LegacySpriteText(LegacyFont.Combo) + remainingHitsText = new LegacySpriteText(LegacyFont.Score) { Anchor = Anchor.Centre, Origin = Anchor.Centre, - Position = new Vector2(0f, 165f), + Position = new Vector2(0f, 130f), Scale = Vector2.One, }, } @@ -96,6 +100,7 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy Anchor = Anchor.Centre, Origin = Anchor.Centre, Alpha = 0, + Y = -40, }, }, }, @@ -159,11 +164,10 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy } clearAnimation - .MoveTo(new Vector2(0, 0)) - .MoveTo(new Vector2(0, -90), clear_fade_in * 2, Easing.Out) + .MoveToOffset(new Vector2(0, -90 * scale_adjust), clear_fade_in * 2, Easing.Out) .ScaleTo(0.4f) .ScaleTo(1f, clear_fade_in * 2, Easing.Out) - .FadeIn(clear_fade_in) + .FadeIn() .Delay(clear_fade_in * 3) .FadeOut(clear_fade_in * 2.5); } From 0ac08158e33867092f76f94b1534ba3bc1ce962c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 12 Feb 2025 11:20:27 +0100 Subject: [PATCH 1015/1275] Fix transforms from swell progress being cleared on completion by not using transforms --- .../Objects/Drawables/DrawableSwell.cs | 4 +-- .../Skinning/Default/DefaultSwell.cs | 26 +++++++++++------ .../Skinning/Legacy/LegacySwell.cs | 28 +++++++++++++------ 3 files changed, 39 insertions(+), 19 deletions(-) diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableSwell.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableSwell.cs index 1dde4b6f9c..6ad14c87d1 100644 --- a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableSwell.cs +++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableSwell.cs @@ -38,7 +38,7 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables /// public bool MustAlternate { get; internal set; } = true; - public event Action UpdateHitProgress; + public event Action UpdateHitProgress; public DrawableSwell() : this(null) @@ -125,7 +125,7 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables int numHits = ticks.Count(r => r.IsHit); - UpdateHitProgress?.Invoke(numHits, HitObject.RequiredHits); + UpdateHitProgress?.Invoke(numHits); if (numHits == HitObject.RequiredHits) ApplyMaxResult(); diff --git a/osu.Game.Rulesets.Taiko/Skinning/Default/DefaultSwell.cs b/osu.Game.Rulesets.Taiko/Skinning/Default/DefaultSwell.cs index a588f866c6..ac72ba73b8 100644 --- a/osu.Game.Rulesets.Taiko/Skinning/Default/DefaultSwell.cs +++ b/osu.Game.Rulesets.Taiko/Skinning/Default/DefaultSwell.cs @@ -8,10 +8,12 @@ using osu.Framework.Extensions.ObjectExtensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; +using osu.Framework.Utils; using osu.Game.Graphics; using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Taiko.Objects; using osu.Game.Rulesets.Taiko.Objects.Drawables; +using osuTK; using osuTK.Graphics; namespace osu.Game.Rulesets.Taiko.Skinning.Default @@ -29,6 +31,7 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Default private readonly CircularContainer targetRing; private readonly CircularContainer expandingRing; private readonly Drawable centreCircle; + private int numHits; public DefaultSwell() { @@ -125,18 +128,25 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Default }; } - private void animateSwellProgress(int numHits, int requiredHits) + private void animateSwellProgress(int numHits) { - float completion = (float)numHits / requiredHits; + this.numHits = numHits; - centreCircle.RotateTo((float)(completion * drawableSwell.HitObject.Duration / 8), 4000, Easing.OutQuint); + float completion = (float)numHits / drawableSwell.HitObject.RequiredHits; + expandingRing.Alpha += Math.Clamp(completion / 16, 0.1f, 0.6f); + } - expandingRing.ScaleTo(1f + Math.Min(target_ring_scale - 1f, (target_ring_scale - 1f) * completion * 1.3f), 260, Easing.OutQuint); + protected override void Update() + { + base.Update(); - expandingRing - .FadeTo(expandingRing.Alpha + Math.Clamp(completion / 16, 0.1f, 0.6f), 50) - .Then() - .FadeTo(completion / 8, 2000, Easing.OutQuint); + float completion = (float)numHits / drawableSwell.HitObject.RequiredHits; + + centreCircle.Rotation = (float)Interpolation.DampContinuously(centreCircle.Rotation, + (float)(completion * drawableSwell.HitObject.Duration / 8), 500, Math.Abs(Time.Elapsed)); + expandingRing.Scale = new Vector2((float)Interpolation.DampContinuously(expandingRing.Scale.X, + 1f + Math.Min(target_ring_scale - 1f, (target_ring_scale - 1f) * completion * 1.3f), 35, Math.Abs(Time.Elapsed))); + expandingRing.Alpha = (float)Interpolation.DampContinuously(expandingRing.Alpha, completion / 16, 250, Math.Abs(Time.Elapsed)); } private void updateStateTransforms(DrawableHitObject drawableHitObject, ArmedState state) diff --git a/osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacySwell.cs b/osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacySwell.cs index 5d65ac6058..62ccd05a06 100644 --- a/osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacySwell.cs +++ b/osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacySwell.cs @@ -35,6 +35,8 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy private bool samplePlayed; + private int numHits; + public LegacySwell() { Anchor = Anchor.Centre; @@ -112,17 +114,25 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy clearSample = skin.GetSample(new SampleInfo("spinner-osu")); } - private void animateSwellProgress(int numHits, int requiredHits) + private void animateSwellProgress(int numHits) { - int remainingHits = requiredHits - numHits; - remainingHitsText.Text = remainingHits.ToString(CultureInfo.InvariantCulture); - remainingHitsText.ScaleTo(1.6f - (0.6f * ((float)remainingHits / requiredHits)), 60, Easing.Out); + this.numHits = numHits; + remainingHitsText.Text = (drawableSwell.HitObject.RequiredHits - numHits).ToString(CultureInfo.InvariantCulture); + spinnerCircle.Scale = new Vector2(Math.Min(0.94f, spinnerCircle.Scale.X + 0.02f)); + } - spinnerCircle.ClearTransforms(); - spinnerCircle - .RotateTo(180f * numHits, 1000, Easing.OutQuint) - .ScaleTo(Math.Min(0.94f, spinnerCircle.Scale.X + 0.02f)) - .ScaleTo(0.8f, 400, Easing.Out); + protected override void Update() + { + base.Update(); + + int requiredHits = drawableSwell.HitObject.RequiredHits; + int remainingHits = requiredHits - numHits; + remainingHitsText.Scale = new Vector2((float)Interpolation.DampContinuously( + remainingHitsText.Scale.X, 1.6f - (0.6f * ((float)remainingHits / requiredHits)), 17.5, Math.Abs(Time.Elapsed))); + + spinnerCircle.Rotation = (float)Interpolation.DampContinuously(spinnerCircle.Rotation, 180f * numHits, 130, Math.Abs(Time.Elapsed)); + spinnerCircle.Scale = new Vector2((float)Interpolation.DampContinuously( + spinnerCircle.Scale.X, 0.8f, 120, Math.Abs(Time.Elapsed))); } private void updateStateTransforms(DrawableHitObject drawableHitObject, ArmedState state) From e385848edcbfbab7eaf0618a01ffb98aeed209d9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 12 Feb 2025 11:30:30 +0100 Subject: [PATCH 1016/1275] Add missing animation of warning sprite --- osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacySwell.cs | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacySwell.cs b/osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacySwell.cs index 62ccd05a06..c9e03d3508 100644 --- a/osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacySwell.cs +++ b/osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacySwell.cs @@ -22,6 +22,7 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy public partial class LegacySwell : Container { private const float scale_adjust = 768f / 480; + private static readonly Vector2 swell_display_position = new Vector2(250f, 100f); private DrawableSwell drawableSwell = null!; @@ -60,7 +61,7 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy { Anchor = Anchor.Centre, Origin = Anchor.Centre, - Position = new Vector2(250f, 100f), // ballparked to be horizontally centred on 4:3 resolution + Position = swell_display_position, // ballparked to be horizontally centred on 4:3 resolution Children = new Drawable[] { @@ -152,7 +153,10 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy const double body_transition_duration = 200; - warning.FadeOut(body_transition_duration); + warning.MoveTo(swell_display_position, body_transition_duration) + .ScaleTo(3, body_transition_duration, Easing.Out) + .FadeOut(body_transition_duration); + bodyContainer.FadeIn(body_transition_duration); approachCircle.ResizeTo(0.1f * 0.8f, swell.Duration); } From d87a775e716801705b1de47cc4d2776770c348ea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 12 Feb 2025 13:19:55 +0100 Subject: [PATCH 1017/1275] Fix clear sample potentially playing multiple times simultaneously --- osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacySwell.cs | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacySwell.cs b/osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacySwell.cs index c9e03d3508..9f1b692984 100644 --- a/osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacySwell.cs +++ b/osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacySwell.cs @@ -8,7 +8,6 @@ using osu.Game.Skinning; using osu.Framework.Graphics.Sprites; using osu.Game.Rulesets.Taiko.Objects.Drawables; using osu.Game.Rulesets.Taiko.Objects; -using osu.Framework.Audio.Sample; using osu.Game.Audio; using osuTK; using osu.Game.Rulesets.Objects.Drawables; @@ -31,7 +30,7 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy private Sprite spinnerCircle = null!; private Sprite approachCircle = null!; private Sprite clearAnimation = null!; - private ISample? clearSample; + private SkinnableSound clearSample = null!; private LegacySpriteText remainingHitsText = null!; private bool samplePlayed; @@ -107,12 +106,12 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy }, }, }, + clearSample = new SkinnableSound(new SampleInfo("spinner-osu")), }; drawableSwell = (DrawableSwell)hitObject; drawableSwell.UpdateHitProgress += animateSwellProgress; drawableSwell.ApplyCustomUpdateState += updateStateTransforms; - clearSample = skin.GetSample(new SampleInfo("spinner-osu")); } private void animateSwellProgress(int numHits) @@ -173,7 +172,7 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy { if (!samplePlayed) { - clearSample?.Play(); + clearSample.Play(); samplePlayed = true; } From f146a7d116bbebec4880c8a3dd7124d20dc58022 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Wed, 12 Feb 2025 21:09:58 +0900 Subject: [PATCH 1018/1275] Remove `RoomManager` and related components --- .../TestSceneLoungeRoomsContainer.cs | 54 ++++++------ .../TestSceneMultiplayerLoungeSubScreen.cs | 33 +++++--- .../TestScenePlaylistsLoungeSubScreen.cs | 30 +++---- .../TestScenePlaylistsMatchSettingsOverlay.cs | 2 - .../Components/ListingPollingComponent.cs | 14 +++- .../OnlinePlay/Components/RoomManager.cs | 82 ------------------- .../Components/RoomPollingComponent.cs | 18 ---- .../Components/SelectionPollingComponent.cs | 14 +++- osu.Game/Screens/OnlinePlay/IRoomManager.cs | 42 ---------- .../OnlinePlay/Lounge/LoungeSubScreen.cs | 12 +-- .../Screens/OnlinePlay/OnlinePlayScreen.cs | 5 -- .../IOnlinePlayTestSceneDependencies.cs | 5 -- .../Visual/OnlinePlay/OnlinePlayTestScene.cs | 32 +++++++- .../OnlinePlayTestSceneDependencies.cs | 3 - .../Visual/OnlinePlay/TestRoomManager.cs | 59 ------------- .../OnlinePlay/TestRoomRequestsHandler.cs | 3 +- 16 files changed, 121 insertions(+), 287 deletions(-) delete mode 100644 osu.Game/Screens/OnlinePlay/Components/RoomManager.cs delete mode 100644 osu.Game/Screens/OnlinePlay/Components/RoomPollingComponent.cs delete mode 100644 osu.Game/Screens/OnlinePlay/IRoomManager.cs delete mode 100644 osu.Game/Tests/Visual/OnlinePlay/TestRoomManager.cs diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneLoungeRoomsContainer.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneLoungeRoomsContainer.cs index 10df77f88c..9daad960c7 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneLoungeRoomsContainer.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneLoungeRoomsContainer.cs @@ -3,6 +3,7 @@ using System.Linq; using NUnit.Framework; +using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Cursor; @@ -19,8 +20,7 @@ namespace osu.Game.Tests.Visual.Multiplayer { public partial class TestSceneLoungeRoomsContainer : OnlinePlayTestScene { - protected new TestRoomManager RoomManager => (TestRoomManager)base.RoomManager; - + private BindableList rooms = null!; private RoomsContainer container = null!; public override void SetUpSteps() @@ -29,6 +29,7 @@ namespace osu.Game.Tests.Visual.Multiplayer AddStep("create container", () => { + rooms = new BindableList(); Child = new PopoverContainer { RelativeSizeAxes = Axes.X, @@ -36,9 +37,9 @@ namespace osu.Game.Tests.Visual.Multiplayer Anchor = Anchor.Centre, Origin = Anchor.Centre, Width = 0.5f, - Child = container = new RoomsContainer { + Rooms = { BindTarget = rooms }, SelectedRoom = { BindTarget = SelectedRoom } } }; @@ -48,7 +49,7 @@ namespace osu.Game.Tests.Visual.Multiplayer [Test] public void TestBasicListChanges() { - AddStep("add rooms", () => RoomManager.AddRooms(5, withSpotlightRooms: true)); + AddStep("add rooms", () => rooms.AddRange(GenerateRooms(5, withSpotlightRooms: true))); AddAssert("has 5 rooms", () => container.DrawableRooms.Count == 5); @@ -56,49 +57,50 @@ namespace osu.Game.Tests.Visual.Multiplayer .SkipWhile(r => r.Room.Category == RoomCategory.Spotlight) .All(r => r.Room.Category == RoomCategory.Normal)); - AddStep("remove first room", () => RoomManager.RemoveRoom(RoomManager.Rooms.First(r => r.RoomID == 0))); + AddStep("remove first room", () => rooms.RemoveAt(0)); AddAssert("has 4 rooms", () => container.DrawableRooms.Count == 4); AddAssert("first room removed", () => container.DrawableRooms.All(r => r.Room.RoomID != 0)); AddStep("select first room", () => container.DrawableRooms.First().TriggerClick()); - AddAssert("first spotlight selected", () => checkRoomSelected(RoomManager.Rooms.First(r => r.Category == RoomCategory.Spotlight))); + AddAssert("first spotlight selected", () => checkRoomSelected(rooms.First(r => r.Category == RoomCategory.Spotlight))); - AddStep("remove last room", () => RoomManager.RemoveRoom(RoomManager.Rooms.MinBy(r => r.RoomID)!)); - AddAssert("first spotlight still selected", () => checkRoomSelected(RoomManager.Rooms.First(r => r.Category == RoomCategory.Spotlight))); + AddStep("remove last room", () => rooms.RemoveAt(rooms.Count - 1)); + AddAssert("first spotlight still selected", () => checkRoomSelected(rooms.First(r => r.Category == RoomCategory.Spotlight))); - AddStep("remove spotlight room", () => RoomManager.RemoveRoom(RoomManager.Rooms.Single(r => r.Category == RoomCategory.Spotlight))); + AddStep("remove spotlight room", () => rooms.RemoveAll(r => r.Category == RoomCategory.Spotlight)); AddAssert("selection vacated", () => checkRoomSelected(null)); } [Test] public void TestKeyboardNavigation() { - AddStep("add rooms", () => RoomManager.AddRooms(3)); + AddStep("add rooms", () => rooms.AddRange(GenerateRooms(3))); AddAssert("no selection", () => checkRoomSelected(null)); press(Key.Down); - AddAssert("first room selected", () => checkRoomSelected(RoomManager.Rooms.First())); + AddAssert("first room selected", () => checkRoomSelected(container.DrawableRooms.First().Room)); press(Key.Up); - AddAssert("first room selected", () => checkRoomSelected(RoomManager.Rooms.First())); + AddAssert("first room selected", () => checkRoomSelected(container.DrawableRooms.First().Room)); press(Key.Down); press(Key.Down); - AddAssert("last room selected", () => checkRoomSelected(RoomManager.Rooms.Last())); + AddAssert("last room selected", () => checkRoomSelected(container.DrawableRooms.Last().Room)); } [Test] public void TestKeyboardNavigationAfterOrderChange() { - AddStep("add rooms", () => RoomManager.AddRooms(3)); + AddStep("add rooms", () => rooms.AddRange(GenerateRooms(3))); AddStep("reorder rooms", () => { - var room = RoomManager.Rooms[1]; + var room = rooms[1]; + rooms.Remove(room); - RoomManager.RemoveRoom(room); - RoomManager.AddOrUpdateRoom(room); + room.RoomID += 3; + rooms.Add(room); }); AddAssert("no selection", () => checkRoomSelected(null)); @@ -116,12 +118,12 @@ namespace osu.Game.Tests.Visual.Multiplayer [Test] public void TestClickDeselection() { - AddStep("add room", () => RoomManager.AddRooms(1)); + AddStep("add room", () => rooms.AddRange(GenerateRooms(1))); AddAssert("no selection", () => checkRoomSelected(null)); press(Key.Down); - AddAssert("first room selected", () => checkRoomSelected(RoomManager.Rooms.First())); + AddAssert("first room selected", () => checkRoomSelected(container.DrawableRooms.First().Room)); AddStep("click away", () => InputManager.Click(MouseButton.Left)); AddAssert("no selection", () => checkRoomSelected(null)); @@ -135,11 +137,11 @@ namespace osu.Game.Tests.Visual.Multiplayer [Test] public void TestStringFiltering() { - AddStep("add rooms", () => RoomManager.AddRooms(4)); + AddStep("add rooms", () => rooms.AddRange(GenerateRooms(4))); AddUntilStep("4 rooms visible", () => container.DrawableRooms.Count(r => r.IsPresent) == 4); - AddStep("filter one room", () => container.Filter.Value = new FilterCriteria { SearchString = "1" }); + AddStep("filter one room", () => container.Filter.Value = new FilterCriteria { SearchString = rooms.First().Name }); AddUntilStep("1 rooms visible", () => container.DrawableRooms.Count(r => r.IsPresent) == 1); @@ -151,8 +153,8 @@ namespace osu.Game.Tests.Visual.Multiplayer [Test] public void TestRulesetFiltering() { - AddStep("add rooms", () => RoomManager.AddRooms(2, new OsuRuleset().RulesetInfo)); - AddStep("add rooms", () => RoomManager.AddRooms(3, new CatchRuleset().RulesetInfo)); + AddStep("add rooms", () => rooms.AddRange(GenerateRooms(2, new OsuRuleset().RulesetInfo))); + AddStep("add rooms", () => rooms.AddRange(GenerateRooms(3, new CatchRuleset().RulesetInfo))); // Todo: What even is this case...? AddStep("set empty filter criteria", () => container.Filter.Value = new FilterCriteria()); @@ -170,8 +172,8 @@ namespace osu.Game.Tests.Visual.Multiplayer { AddStep("add rooms", () => { - RoomManager.AddRooms(1, withPassword: true); - RoomManager.AddRooms(1, withPassword: false); + rooms.AddRange(GenerateRooms(1, withPassword: true)); + rooms.AddRange(GenerateRooms(1, withPassword: false)); }); AddStep("apply default filter", () => container.Filter.SetDefault()); @@ -190,7 +192,7 @@ namespace osu.Game.Tests.Visual.Multiplayer [Test] public void TestPasswordProtectedRooms() { - AddStep("add rooms", () => RoomManager.AddRooms(3, withPassword: true)); + AddStep("add rooms", () => rooms.AddRange(GenerateRooms(3, withPassword: true))); } private bool checkRoomSelected(Room? room) => SelectedRoom.Value == room; diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerLoungeSubScreen.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerLoungeSubScreen.cs index eb649acd2d..b4ec9d5858 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerLoungeSubScreen.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerLoungeSubScreen.cs @@ -8,8 +8,8 @@ using osu.Framework.Graphics.UserInterface; using osu.Framework.Screens; using osu.Framework.Testing; using osu.Game.Graphics.UserInterface; +using osu.Game.Online.Rooms; using osu.Game.Screens.OnlinePlay.Lounge; -using osu.Game.Screens.OnlinePlay.Lounge.Components; using osu.Game.Screens.OnlinePlay.Multiplayer; using osu.Game.Tests.Visual.OnlinePlay; using osuTK.Input; @@ -18,11 +18,7 @@ namespace osu.Game.Tests.Visual.Multiplayer { public partial class TestSceneMultiplayerLoungeSubScreen : MultiplayerTestScene { - protected new TestRoomManager RoomManager => (TestRoomManager)base.RoomManager; - - private LoungeSubScreen loungeScreen = null!; - - private RoomsContainer roomsContainer => loungeScreen.ChildrenOfType().First(); + private MultiplayerLoungeSubScreen loungeScreen = null!; public TestSceneMultiplayerLoungeSubScreen() : base(false) @@ -40,7 +36,7 @@ namespace osu.Game.Tests.Visual.Multiplayer [Test] public void TestJoinRoomWithoutPassword() { - AddStep("add room", () => RoomManager.AddRooms(1, withPassword: false)); + createRooms(GenerateRooms(1, withPassword: false)); AddStep("select room", () => InputManager.Key(Key.Down)); AddStep("join room", () => InputManager.Key(Key.Enter)); @@ -50,7 +46,7 @@ namespace osu.Game.Tests.Visual.Multiplayer [Test] public void TestPopoverHidesOnBackButton() { - AddStep("add room", () => RoomManager.AddRooms(1, withPassword: true)); + createRooms(GenerateRooms(1, withPassword: true)); AddStep("select room", () => InputManager.Key(Key.Down)); AddStep("attempt join room", () => InputManager.Key(Key.Enter)); @@ -70,7 +66,7 @@ namespace osu.Game.Tests.Visual.Multiplayer [Test] public void TestPopoverHidesOnLeavingScreen() { - AddStep("add room", () => RoomManager.AddRooms(1, withPassword: true)); + createRooms(GenerateRooms(1, withPassword: true)); AddStep("select room", () => InputManager.Key(Key.Down)); AddStep("attempt join room", () => InputManager.Key(Key.Enter)); @@ -86,7 +82,7 @@ namespace osu.Game.Tests.Visual.Multiplayer { DrawableLoungeRoom.PasswordEntryPopover? passwordEntryPopover = null; - AddStep("add room", () => RoomManager.AddRooms(1, withPassword: true)); + createRooms(GenerateRooms(1, withPassword: true)); AddStep("select room", () => InputManager.Key(Key.Down)); AddStep("attempt join room", () => InputManager.Key(Key.Enter)); AddUntilStep("password prompt appeared", () => (passwordEntryPopover = InputManager.ChildrenOfType().FirstOrDefault()) != null); @@ -105,7 +101,7 @@ namespace osu.Game.Tests.Visual.Multiplayer { DrawableLoungeRoom.PasswordEntryPopover? passwordEntryPopover = null; - AddStep("add room", () => RoomManager.AddRooms(1, withPassword: true)); + createRooms(GenerateRooms(1, withPassword: true)); AddStep("select room", () => InputManager.Key(Key.Down)); AddStep("attempt join room", () => InputManager.Key(Key.Enter)); AddUntilStep("password prompt appeared", () => (passwordEntryPopover = InputManager.ChildrenOfType().FirstOrDefault()) != null); @@ -124,7 +120,7 @@ namespace osu.Game.Tests.Visual.Multiplayer { DrawableLoungeRoom.PasswordEntryPopover? passwordEntryPopover = null; - AddStep("add room", () => RoomManager.AddRooms(1, withPassword: true)); + createRooms(GenerateRooms(1, withPassword: true)); AddStep("select room", () => InputManager.Key(Key.Down)); AddStep("attempt join room", () => InputManager.Key(Key.Enter)); AddUntilStep("password prompt appeared", () => (passwordEntryPopover = InputManager.ChildrenOfType().FirstOrDefault()) != null); @@ -139,7 +135,7 @@ namespace osu.Game.Tests.Visual.Multiplayer { DrawableLoungeRoom.PasswordEntryPopover? passwordEntryPopover = null; - AddStep("add room", () => RoomManager.AddRooms(1, withPassword: true)); + createRooms(GenerateRooms(1, withPassword: true)); AddStep("select room", () => InputManager.Key(Key.Down)); AddStep("attempt join room", () => InputManager.Key(Key.Enter)); AddUntilStep("password prompt appeared", () => (passwordEntryPopover = InputManager.ChildrenOfType().FirstOrDefault()) != null); @@ -149,6 +145,17 @@ namespace osu.Game.Tests.Visual.Multiplayer AddAssert("room joined", () => MultiplayerClient.RoomJoined); } + private void createRooms(params Room[] rooms) + { + AddStep("create rooms", () => + { + foreach (var room in rooms) + API.Queue(new CreateRoomRequest(room)); + }); + + AddStep("refresh lounge", () => loungeScreen.RefreshRooms()); + } + protected override OnlinePlayTestSceneDependencies CreateOnlinePlayDependencies() => new MultiplayerTestSceneDependencies(); } } diff --git a/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsLoungeSubScreen.cs b/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsLoungeSubScreen.cs index 9d65be2a19..94a81ecdc7 100644 --- a/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsLoungeSubScreen.cs +++ b/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsLoungeSubScreen.cs @@ -17,8 +17,6 @@ namespace osu.Game.Tests.Visual.Playlists { public partial class TestScenePlaylistsLoungeSubScreen : OnlinePlayTestScene { - protected new TestRoomManager RoomManager => (TestRoomManager)base.RoomManager; - private TestLoungeSubScreen loungeScreen = null!; public override void SetUpSteps() @@ -26,7 +24,6 @@ namespace osu.Game.Tests.Visual.Playlists base.SetUpSteps(); AddStep("push screen", () => LoadScreen(loungeScreen = new TestLoungeSubScreen())); - AddUntilStep("wait for present", () => loungeScreen.IsCurrentScreen()); } @@ -35,8 +32,7 @@ namespace osu.Game.Tests.Visual.Playlists [Test] public void TestManyRooms() { - AddStep("add rooms", () => RoomManager.AddRooms(500)); - AddUntilStep("wait for rooms", () => roomsContainer.DrawableRooms.Count == 500); + createRooms(GenerateRooms(500)); } [Test] @@ -44,10 +40,7 @@ namespace osu.Game.Tests.Visual.Playlists { AddStep("reset mouse", () => InputManager.ReleaseButton(MouseButton.Left)); - AddStep("add rooms", () => RoomManager.AddRooms(30)); - AddUntilStep("wait for rooms", () => roomsContainer.DrawableRooms.Count == 30); - - AddUntilStep("first room is not masked", () => checkRoomVisible(roomsContainer.DrawableRooms[0])); + createRooms(GenerateRooms(30)); AddStep("move mouse to third room", () => InputManager.MoveMouseTo(roomsContainer.DrawableRooms[2])); AddStep("hold down", () => InputManager.PressButton(MouseButton.Left)); @@ -61,10 +54,7 @@ namespace osu.Game.Tests.Visual.Playlists [Test] public void TestScrollSelectedIntoView() { - AddStep("add rooms", () => RoomManager.AddRooms(30)); - AddUntilStep("wait for rooms", () => roomsContainer.DrawableRooms.Count == 30); - - AddUntilStep("first room is not masked", () => checkRoomVisible(roomsContainer.DrawableRooms[0])); + createRooms(GenerateRooms(30)); AddStep("select last room", () => roomsContainer.DrawableRooms[^1].TriggerClick()); @@ -75,8 +65,7 @@ namespace osu.Game.Tests.Visual.Playlists [Test] public void TestEnteringRoomTakesLeaseOnSelection() { - AddStep("add rooms", () => RoomManager.AddRooms(1)); - AddUntilStep("wait for rooms", () => roomsContainer.DrawableRooms.Count == 1); + createRooms(GenerateRooms(1)); AddAssert("selected room is not disabled", () => !loungeScreen.SelectedRoom.Disabled); @@ -95,6 +84,17 @@ namespace osu.Game.Tests.Visual.Playlists loungeScreen.ChildrenOfType().First().ScreenSpaceDrawQuad .Contains(room.ScreenSpaceDrawQuad.Centre); + private void createRooms(params Room[] rooms) + { + AddStep("create rooms", () => + { + foreach (var room in rooms) + API.Queue(new CreateRoomRequest(room)); + }); + + AddStep("refresh lounge", () => loungeScreen.RefreshRooms()); + } + private partial class TestLoungeSubScreen : PlaylistsLoungeSubScreen { public new Bindable SelectedRoom => base.SelectedRoom; diff --git a/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsMatchSettingsOverlay.cs b/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsMatchSettingsOverlay.cs index 51e39e1b7f..f7b0bc0d58 100644 --- a/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsMatchSettingsOverlay.cs +++ b/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsMatchSettingsOverlay.cs @@ -17,8 +17,6 @@ namespace osu.Game.Tests.Visual.Playlists { public partial class TestScenePlaylistsMatchSettingsOverlay : OnlinePlayTestScene { - protected new TestRoomManager RoomManager => (TestRoomManager)base.RoomManager; - private TestRoomSettings settings = null!; private Func? handleRequest; diff --git a/osu.Game/Screens/OnlinePlay/Components/ListingPollingComponent.cs b/osu.Game/Screens/OnlinePlay/Components/ListingPollingComponent.cs index 5cb4c9420a..1495f97de4 100644 --- a/osu.Game/Screens/OnlinePlay/Components/ListingPollingComponent.cs +++ b/osu.Game/Screens/OnlinePlay/Components/ListingPollingComponent.cs @@ -4,17 +4,23 @@ using System; using System.Linq; using System.Threading.Tasks; +using osu.Framework.Allocation; using osu.Framework.Bindables; +using osu.Game.Online; +using osu.Game.Online.API; using osu.Game.Online.Rooms; using osu.Game.Screens.OnlinePlay.Lounge.Components; namespace osu.Game.Screens.OnlinePlay.Components { /// - /// A that polls for the lounge listing. + /// A that polls for the lounge listing. /// - public partial class ListingPollingComponent : RoomPollingComponent + public partial class ListingPollingComponent : PollingComponent { + [Resolved] + private IAPIProvider api { get; set; } = null!; + public required Action RoomsReceived { get; init; } public readonly IBindable Filter = new Bindable(); @@ -22,7 +28,7 @@ namespace osu.Game.Screens.OnlinePlay.Components protected override Task Poll() { - if (!API.IsLoggedIn) + if (!api.IsLoggedIn) return base.Poll(); if (Filter.Value == null) @@ -41,7 +47,7 @@ namespace osu.Game.Screens.OnlinePlay.Components req.Failure += _ => tcs.SetResult(false); - API.Queue(req); + api.Queue(req); lastPollRequest = req; diff --git a/osu.Game/Screens/OnlinePlay/Components/RoomManager.cs b/osu.Game/Screens/OnlinePlay/Components/RoomManager.cs deleted file mode 100644 index a1b61ea7a3..0000000000 --- a/osu.Game/Screens/OnlinePlay/Components/RoomManager.cs +++ /dev/null @@ -1,82 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -using System; -using System.Collections.Generic; -using System.Diagnostics; -using System.Linq; -using osu.Framework.Bindables; -using osu.Framework.Development; -using osu.Framework.Graphics; -using osu.Framework.Logging; -using osu.Game.Online.Rooms; - -namespace osu.Game.Screens.OnlinePlay.Components -{ - // Todo: This class should be inlined into the lounge. - public partial class RoomManager : Component, IRoomManager - { - public event Action? RoomsUpdated; - - private readonly BindableList rooms = new BindableList(); - - public IBindableList Rooms => rooms; - - public RoomManager() - { - RelativeSizeAxes = Axes.Both; - } - - private readonly HashSet ignoredRooms = new HashSet(); - - public void AddOrUpdateRoom(Room room) - { - Debug.Assert(ThreadSafety.IsUpdateThread); - Debug.Assert(room.RoomID != null); - - if (ignoredRooms.Contains(room.RoomID.Value)) - return; - - try - { - var existing = rooms.FirstOrDefault(e => e.RoomID == room.RoomID); - if (existing == null) - rooms.Add(room); - else - existing.CopyFrom(room); - } - catch (Exception ex) - { - Logger.Error(ex, $"Failed to update room: {room.Name}."); - - ignoredRooms.Add(room.RoomID.Value); - rooms.Remove(room); - } - - notifyRoomsUpdated(); - } - - public void RemoveRoom(Room room) - { - Debug.Assert(ThreadSafety.IsUpdateThread); - - rooms.Remove(room); - notifyRoomsUpdated(); - } - - public void ClearRooms() - { - Debug.Assert(ThreadSafety.IsUpdateThread); - - rooms.Clear(); - notifyRoomsUpdated(); - } - - private void notifyRoomsUpdated() - { - Scheduler.AddOnce(invokeRoomsUpdated); - - void invokeRoomsUpdated() => RoomsUpdated?.Invoke(); - } - } -} diff --git a/osu.Game/Screens/OnlinePlay/Components/RoomPollingComponent.cs b/osu.Game/Screens/OnlinePlay/Components/RoomPollingComponent.cs deleted file mode 100644 index 0ba7f20f1c..0000000000 --- a/osu.Game/Screens/OnlinePlay/Components/RoomPollingComponent.cs +++ /dev/null @@ -1,18 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -using osu.Framework.Allocation; -using osu.Game.Online; -using osu.Game.Online.API; - -namespace osu.Game.Screens.OnlinePlay.Components -{ - public abstract partial class RoomPollingComponent : PollingComponent - { - [Resolved] - protected IAPIProvider API { get; private set; } = null!; - - [Resolved] - protected IRoomManager RoomManager { get; private set; } = null!; - } -} diff --git a/osu.Game/Screens/OnlinePlay/Components/SelectionPollingComponent.cs b/osu.Game/Screens/OnlinePlay/Components/SelectionPollingComponent.cs index f04fd6a096..bfa059f72e 100644 --- a/osu.Game/Screens/OnlinePlay/Components/SelectionPollingComponent.cs +++ b/osu.Game/Screens/OnlinePlay/Components/SelectionPollingComponent.cs @@ -2,15 +2,21 @@ // See the LICENCE file in the repository root for full licence text. using System.Threading.Tasks; +using osu.Framework.Allocation; +using osu.Game.Online; +using osu.Game.Online.API; using osu.Game.Online.Rooms; namespace osu.Game.Screens.OnlinePlay.Components { /// - /// A that polls for the currently-selected room. + /// A that polls for and updates a room. /// - public partial class SelectionPollingComponent : RoomPollingComponent + public partial class SelectionPollingComponent : PollingComponent { + [Resolved] + private IAPIProvider api { get; set; } = null!; + private readonly Room room; public SelectionPollingComponent(Room room) @@ -22,7 +28,7 @@ namespace osu.Game.Screens.OnlinePlay.Components protected override Task Poll() { - if (!API.IsLoggedIn) + if (!api.IsLoggedIn) return base.Poll(); if (room.RoomID == null) @@ -41,7 +47,7 @@ namespace osu.Game.Screens.OnlinePlay.Components req.Failure += _ => tcs.SetResult(false); - API.Queue(req); + api.Queue(req); lastPollRequest = req; diff --git a/osu.Game/Screens/OnlinePlay/IRoomManager.cs b/osu.Game/Screens/OnlinePlay/IRoomManager.cs deleted file mode 100644 index 8ecb1dd7e0..0000000000 --- a/osu.Game/Screens/OnlinePlay/IRoomManager.cs +++ /dev/null @@ -1,42 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -using System; -using osu.Framework.Allocation; -using osu.Framework.Bindables; -using osu.Game.Online.Rooms; - -namespace osu.Game.Screens.OnlinePlay -{ - [Cached(typeof(IRoomManager))] - public interface IRoomManager - { - /// - /// Invoked when the s have been updated. - /// - event Action RoomsUpdated; - - /// - /// All the active s. - /// - IBindableList Rooms { get; } - - /// - /// Adds a to this . - /// If already existing, the local room will be updated with the given one. - /// - /// The incoming . - void AddOrUpdateRoom(Room room); - - /// - /// Removes a from this . - /// - /// The to remove. - void RemoveRoom(Room room); - - /// - /// Removes all s from this . - /// - void ClearRooms(); - } -} diff --git a/osu.Game/Screens/OnlinePlay/Lounge/LoungeSubScreen.cs b/osu.Game/Screens/OnlinePlay/Lounge/LoungeSubScreen.cs index 78501a56d7..6c383f1bf6 100644 --- a/osu.Game/Screens/OnlinePlay/Lounge/LoungeSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Lounge/LoungeSubScreen.cs @@ -54,6 +54,7 @@ namespace osu.Game.Screens.OnlinePlay.Lounge }; protected readonly Bindable SelectedRoom = new Bindable(); + protected readonly BindableList Rooms = new BindableList(); [Resolved] private MusicController music { get; set; } = null!; @@ -76,7 +77,6 @@ namespace osu.Game.Screens.OnlinePlay.Lounge private IDisposable? joiningRoomOperation; private LeasedBindable? selectionLease; - private readonly BindableList rooms = new BindableList(); private readonly Bindable filter = new Bindable(); private readonly Bindable hasListingResults = new Bindable(); private readonly IBindable operationInProgress = new Bindable(); @@ -121,7 +121,7 @@ namespace osu.Game.Screens.OnlinePlay.Lounge ScrollbarOverlapsContent = false, Child = roomsContainer = new RoomsContainer { - Rooms = { BindTarget = rooms }, + Rooms = { BindTarget = Rooms }, SelectedRoom = { BindTarget = SelectedRoom }, Filter = { BindTarget = filter }, } @@ -208,7 +208,7 @@ namespace osu.Game.Screens.OnlinePlay.Lounge filter.BindValueChanged(_ => { - rooms.Clear(); + Rooms.Clear(); hasListingResults.Value = false; listingPollingComponent.PollImmediately(); }); @@ -218,11 +218,11 @@ namespace osu.Game.Screens.OnlinePlay.Lounge private void onListingReceived(Room[] result) { - Dictionary localRoomsById = rooms.ToDictionary(r => r.RoomID!.Value); + Dictionary localRoomsById = Rooms.ToDictionary(r => r.RoomID!.Value); Dictionary resultRoomsById = result.ToDictionary(r => r.RoomID!.Value); // Remove all local rooms no longer in the result set. - rooms.RemoveAll(r => !resultRoomsById.ContainsKey(r.RoomID!.Value)); + Rooms.RemoveAll(r => !resultRoomsById.ContainsKey(r.RoomID!.Value)); // Add or update local rooms with the result set. foreach (var r in result) @@ -230,7 +230,7 @@ namespace osu.Game.Screens.OnlinePlay.Lounge if (localRoomsById.TryGetValue(r.RoomID!.Value, out Room? existingRoom)) existingRoom.CopyFrom(r); else - rooms.Add(r); + Rooms.Add(r); } hasListingResults.Value = true; diff --git a/osu.Game/Screens/OnlinePlay/OnlinePlayScreen.cs b/osu.Game/Screens/OnlinePlay/OnlinePlayScreen.cs index 8988c82dee..812e42479b 100644 --- a/osu.Game/Screens/OnlinePlay/OnlinePlayScreen.cs +++ b/osu.Game/Screens/OnlinePlay/OnlinePlayScreen.cs @@ -11,7 +11,6 @@ using osu.Game.Graphics.Containers; using osu.Game.Online.API; using osu.Game.Overlays; using osu.Game.Screens.Menu; -using osu.Game.Screens.OnlinePlay.Components; using osu.Game.Screens.OnlinePlay.Lounge; using osu.Game.Users; @@ -39,9 +38,6 @@ namespace osu.Game.Screens.OnlinePlay [Cached] private readonly OngoingOperationTracker ongoingOperationTracker = new OngoingOperationTracker(); - [Cached(Type = typeof(IRoomManager))] - private readonly RoomManager roomManager = new RoomManager(); - [Resolved] protected IAPIProvider API { get; private set; } = null!; @@ -65,7 +61,6 @@ namespace osu.Game.Screens.OnlinePlay { screenStack, new Header(ScreenTitle, screenStack), - roomManager, ongoingOperationTracker, } }; diff --git a/osu.Game/Tests/Visual/OnlinePlay/IOnlinePlayTestSceneDependencies.cs b/osu.Game/Tests/Visual/OnlinePlay/IOnlinePlayTestSceneDependencies.cs index 8ddc5325db..5780cf6eff 100644 --- a/osu.Game/Tests/Visual/OnlinePlay/IOnlinePlayTestSceneDependencies.cs +++ b/osu.Game/Tests/Visual/OnlinePlay/IOnlinePlayTestSceneDependencies.cs @@ -18,11 +18,6 @@ namespace osu.Game.Tests.Visual.OnlinePlay /// Bindable SelectedRoom { get; } - /// - /// The cached - /// - IRoomManager RoomManager { get; } - /// /// The cached . /// diff --git a/osu.Game/Tests/Visual/OnlinePlay/OnlinePlayTestScene.cs b/osu.Game/Tests/Visual/OnlinePlay/OnlinePlayTestScene.cs index 3f6c175fbd..c3a5e1c3ec 100644 --- a/osu.Game/Tests/Visual/OnlinePlay/OnlinePlayTestScene.cs +++ b/osu.Game/Tests/Visual/OnlinePlay/OnlinePlayTestScene.cs @@ -10,7 +10,9 @@ using osu.Framework.Logging; using osu.Game.Beatmaps; using osu.Game.Database; using osu.Game.Online.API; +using osu.Game.Online.API.Requests.Responses; using osu.Game.Online.Rooms; +using osu.Game.Rulesets; using osu.Game.Screens.OnlinePlay; namespace osu.Game.Tests.Visual.OnlinePlay @@ -21,7 +23,6 @@ namespace osu.Game.Tests.Visual.OnlinePlay public abstract partial class OnlinePlayTestScene : ScreenTestScene, IOnlinePlayTestSceneDependencies { public Bindable SelectedRoom => OnlinePlayDependencies.SelectedRoom; - public IRoomManager RoomManager => OnlinePlayDependencies.RoomManager; public OngoingOperationTracker OngoingOperationTracker => OnlinePlayDependencies.OngoingOperationTracker; public OnlinePlayBeatmapAvailabilityTracker AvailabilityTracker => OnlinePlayDependencies.AvailabilityTracker; public TestUserLookupCache UserLookupCache => OnlinePlayDependencies.UserLookupCache; @@ -34,9 +35,13 @@ namespace osu.Game.Tests.Visual.OnlinePlay protected override Container Content => content; + [Resolved] + private RulesetStore rulesets { get; set; } = null!; + private readonly Container content; private readonly Container drawableDependenciesContainer; private DelegatedDependencyContainer dependencies = null!; + private int currentRoomId; protected OnlinePlayTestScene() { @@ -93,6 +98,31 @@ namespace osu.Game.Tests.Visual.OnlinePlay /// protected virtual OnlinePlayTestSceneDependencies CreateOnlinePlayDependencies() => new OnlinePlayTestSceneDependencies(); + protected Room[] GenerateRooms(int count, RulesetInfo? ruleset = null, bool withPassword = false, bool withSpotlightRooms = false) + { + Room[] rooms = new Room[count]; + + // Can't reference Osu ruleset project here. + ruleset ??= rulesets.GetRuleset(0)!; + + for (int i = 0; i < count; i++) + { + rooms[i] = new Room + { + RoomID = currentRoomId++, + Name = $@"Room {currentRoomId}", + Host = new APIUser { Username = @"Host" }, + Duration = TimeSpan.FromSeconds(10), + Category = withSpotlightRooms && i % 2 == 0 ? RoomCategory.Spotlight : RoomCategory.Normal, + Password = withPassword ? @"password" : null, + PlaylistItemStats = new Room.RoomPlaylistItemStats { RulesetIDs = [ruleset.OnlineID] }, + Playlist = [new PlaylistItem(new BeatmapInfo { Metadata = new BeatmapMetadata() }) { RulesetID = ruleset.OnlineID }] + }; + } + + return rooms; + } + /// /// A providing a mutable lookup source for online play dependencies. /// diff --git a/osu.Game/Tests/Visual/OnlinePlay/OnlinePlayTestSceneDependencies.cs b/osu.Game/Tests/Visual/OnlinePlay/OnlinePlayTestSceneDependencies.cs index 203922c057..cc448beea0 100644 --- a/osu.Game/Tests/Visual/OnlinePlay/OnlinePlayTestSceneDependencies.cs +++ b/osu.Game/Tests/Visual/OnlinePlay/OnlinePlayTestSceneDependencies.cs @@ -19,7 +19,6 @@ namespace osu.Game.Tests.Visual.OnlinePlay public class OnlinePlayTestSceneDependencies : IReadOnlyDependencyContainer, IOnlinePlayTestSceneDependencies { public Bindable SelectedRoom { get; } - public IRoomManager RoomManager { get; } public OngoingOperationTracker OngoingOperationTracker { get; } public OnlinePlayBeatmapAvailabilityTracker AvailabilityTracker { get; } public TestRoomRequestsHandler RequestsHandler { get; } @@ -40,7 +39,6 @@ namespace osu.Game.Tests.Visual.OnlinePlay RequestsHandler = new TestRoomRequestsHandler(); OngoingOperationTracker = new OngoingOperationTracker(); AvailabilityTracker = new OnlinePlayBeatmapAvailabilityTracker(); - RoomManager = new TestRoomManager(); UserLookupCache = new TestUserLookupCache(); BeatmapLookupCache = new BeatmapLookupCache(); @@ -48,7 +46,6 @@ namespace osu.Game.Tests.Visual.OnlinePlay CacheAs(RequestsHandler); CacheAs(SelectedRoom); - CacheAs(RoomManager); CacheAs(OngoingOperationTracker); CacheAs(AvailabilityTracker); CacheAs(new OverlayColourProvider(OverlayColourScheme.Plum)); diff --git a/osu.Game/Tests/Visual/OnlinePlay/TestRoomManager.cs b/osu.Game/Tests/Visual/OnlinePlay/TestRoomManager.cs deleted file mode 100644 index bff2753929..0000000000 --- a/osu.Game/Tests/Visual/OnlinePlay/TestRoomManager.cs +++ /dev/null @@ -1,59 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -using System; -using osu.Framework.Allocation; -using osu.Game.Beatmaps; -using osu.Game.Online.API; -using osu.Game.Online.API.Requests.Responses; -using osu.Game.Online.Rooms; -using osu.Game.Rulesets; -using osu.Game.Screens.OnlinePlay.Components; - -namespace osu.Game.Tests.Visual.OnlinePlay -{ - /// - /// A very simple for use in online play test scenes. - /// - public partial class TestRoomManager : RoomManager - { - private int currentRoomId; - - [Resolved] - private IAPIProvider api { get; set; } = null!; - - [Resolved] - private RulesetStore rulesets { get; set; } = null!; - - public void AddRooms(int count, RulesetInfo? ruleset = null, bool withPassword = false, bool withSpotlightRooms = false) - { - // Can't reference Osu ruleset project here. - ruleset ??= rulesets.GetRuleset(0)!; - - for (int i = 0; i < count; i++) - { - AddRoom(new Room - { - Name = $@"Room {currentRoomId}", - Host = new APIUser { Username = @"Host" }, - Duration = TimeSpan.FromSeconds(10), - Category = withSpotlightRooms && i % 2 == 0 ? RoomCategory.Spotlight : RoomCategory.Normal, - Password = withPassword ? @"password" : null, - PlaylistItemStats = new Room.RoomPlaylistItemStats { RulesetIDs = [ruleset.OnlineID] }, - Playlist = [new PlaylistItem(new BeatmapInfo { Metadata = new BeatmapMetadata() }) { RulesetID = ruleset.OnlineID }] - }); - } - } - - public void AddRoom(Room room) - { - room.RoomID = -currentRoomId; - - var req = new CreateRoomRequest(room); - req.Success += AddOrUpdateRoom; - api.Queue(req); - - currentRoomId++; - } - } -} diff --git a/osu.Game/Tests/Visual/OnlinePlay/TestRoomRequestsHandler.cs b/osu.Game/Tests/Visual/OnlinePlay/TestRoomRequestsHandler.cs index c9149bda22..63bc9325fa 100644 --- a/osu.Game/Tests/Visual/OnlinePlay/TestRoomRequestsHandler.cs +++ b/osu.Game/Tests/Visual/OnlinePlay/TestRoomRequestsHandler.cs @@ -36,8 +36,7 @@ namespace osu.Game.Tests.Visual.OnlinePlay private int currentScoreId = 1; /// - /// Handles an API request, while also updating the local state to match - /// how the server would eventually respond and update an . + /// Handles an API request, while also updating the local state to match how the server would eventually respond. /// /// The API request to handle. /// The local user to store in responses where required. From 1b07b6d16f49fd06572c3366685a08f2a2641669 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Wed, 12 Feb 2025 21:48:59 +0900 Subject: [PATCH 1019/1275] Remove selected room leasing, make bindables private I believe once upon a time the `SelectedRoom` bindable used to be bound to `RoomManager.JoinedRoom` or similar. But now it's effectively private to the lounge subscreen and so a lease is unnecessary. --- .../TestScenePlaylistsLoungeSubScreen.cs | 28 +------------ .../OnlinePlay/Lounge/LoungeSubScreen.cs | 39 ++++++------------- 2 files changed, 13 insertions(+), 54 deletions(-) diff --git a/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsLoungeSubScreen.cs b/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsLoungeSubScreen.cs index 94a81ecdc7..35bf6dc28a 100644 --- a/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsLoungeSubScreen.cs +++ b/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsLoungeSubScreen.cs @@ -3,7 +3,6 @@ using System.Linq; using NUnit.Framework; -using osu.Framework.Bindables; using osu.Framework.Screens; using osu.Framework.Testing; using osu.Game.Graphics.Containers; @@ -17,13 +16,13 @@ namespace osu.Game.Tests.Visual.Playlists { public partial class TestScenePlaylistsLoungeSubScreen : OnlinePlayTestScene { - private TestLoungeSubScreen loungeScreen = null!; + private PlaylistsLoungeSubScreen loungeScreen = null!; public override void SetUpSteps() { base.SetUpSteps(); - AddStep("push screen", () => LoadScreen(loungeScreen = new TestLoungeSubScreen())); + AddStep("push screen", () => LoadScreen(loungeScreen = new PlaylistsLoungeSubScreen())); AddUntilStep("wait for present", () => loungeScreen.IsCurrentScreen()); } @@ -62,24 +61,6 @@ namespace osu.Game.Tests.Visual.Playlists AddUntilStep("last room is not masked", () => checkRoomVisible(roomsContainer.DrawableRooms[^1])); } - [Test] - public void TestEnteringRoomTakesLeaseOnSelection() - { - createRooms(GenerateRooms(1)); - - AddAssert("selected room is not disabled", () => !loungeScreen.SelectedRoom.Disabled); - - AddStep("select room", () => roomsContainer.DrawableRooms[0].TriggerClick()); - AddAssert("selected room is non-null", () => loungeScreen.SelectedRoom.Value != null); - - AddStep("enter room", () => roomsContainer.DrawableRooms[0].TriggerClick()); - - AddUntilStep("wait for match load", () => Stack.CurrentScreen is PlaylistsRoomSubScreen); - - AddAssert("selected room is non-null", () => loungeScreen.SelectedRoom.Value != null); - AddAssert("selected room is disabled", () => loungeScreen.SelectedRoom.Disabled); - } - private bool checkRoomVisible(DrawableRoom room) => loungeScreen.ChildrenOfType().First().ScreenSpaceDrawQuad .Contains(room.ScreenSpaceDrawQuad.Centre); @@ -94,10 +75,5 @@ namespace osu.Game.Tests.Visual.Playlists AddStep("refresh lounge", () => loungeScreen.RefreshRooms()); } - - private partial class TestLoungeSubScreen : PlaylistsLoungeSubScreen - { - public new Bindable SelectedRoom => base.SelectedRoom; - } } } diff --git a/osu.Game/Screens/OnlinePlay/Lounge/LoungeSubScreen.cs b/osu.Game/Screens/OnlinePlay/Lounge/LoungeSubScreen.cs index 6c383f1bf6..7bb0c67990 100644 --- a/osu.Game/Screens/OnlinePlay/Lounge/LoungeSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Lounge/LoungeSubScreen.cs @@ -41,7 +41,7 @@ namespace osu.Game.Screens.OnlinePlay.Lounge protected override BackgroundScreen CreateBackground() => new LoungeBackgroundScreen { - SelectedRoom = { BindTarget = SelectedRoom } + SelectedRoom = { BindTarget = selectedRoom } }; protected override UserActivity InitialActivity => new UserActivity.SearchingForLobby(); @@ -53,9 +53,6 @@ namespace osu.Game.Screens.OnlinePlay.Lounge AutoSizeAxes = Axes.Both }; - protected readonly Bindable SelectedRoom = new Bindable(); - protected readonly BindableList Rooms = new BindableList(); - [Resolved] private MusicController music { get; set; } = null!; @@ -75,8 +72,9 @@ namespace osu.Game.Screens.OnlinePlay.Lounge protected OsuConfigManager Config { get; private set; } = null!; private IDisposable? joiningRoomOperation; - private LeasedBindable? selectionLease; + private readonly Bindable selectedRoom = new Bindable(); + private readonly BindableList rooms = new BindableList(); private readonly Bindable filter = new Bindable(); private readonly Bindable hasListingResults = new Bindable(); private readonly IBindable operationInProgress = new Bindable(); @@ -121,8 +119,8 @@ namespace osu.Game.Screens.OnlinePlay.Lounge ScrollbarOverlapsContent = false, Child = roomsContainer = new RoomsContainer { - Rooms = { BindTarget = Rooms }, - SelectedRoom = { BindTarget = SelectedRoom }, + Rooms = { BindTarget = rooms }, + SelectedRoom = { BindTarget = selectedRoom }, Filter = { BindTarget = filter }, } }, @@ -182,7 +180,7 @@ namespace osu.Game.Screens.OnlinePlay.Lounge }; // scroll selected room into view on selection. - SelectedRoom.BindValueChanged(val => + selectedRoom.BindValueChanged(val => { var drawable = roomsContainer.DrawableRooms.FirstOrDefault(r => r.Room == val.NewValue); if (drawable != null) @@ -208,7 +206,7 @@ namespace osu.Game.Screens.OnlinePlay.Lounge filter.BindValueChanged(_ => { - Rooms.Clear(); + rooms.Clear(); hasListingResults.Value = false; listingPollingComponent.PollImmediately(); }); @@ -218,11 +216,11 @@ namespace osu.Game.Screens.OnlinePlay.Lounge private void onListingReceived(Room[] result) { - Dictionary localRoomsById = Rooms.ToDictionary(r => r.RoomID!.Value); + Dictionary localRoomsById = rooms.ToDictionary(r => r.RoomID!.Value); Dictionary resultRoomsById = result.ToDictionary(r => r.RoomID!.Value); // Remove all local rooms no longer in the result set. - Rooms.RemoveAll(r => !resultRoomsById.ContainsKey(r.RoomID!.Value)); + rooms.RemoveAll(r => !resultRoomsById.ContainsKey(r.RoomID!.Value)); // Add or update local rooms with the result set. foreach (var r in result) @@ -230,7 +228,7 @@ namespace osu.Game.Screens.OnlinePlay.Lounge if (localRoomsById.TryGetValue(r.RoomID!.Value, out Room? existingRoom)) existingRoom.CopyFrom(r); else - Rooms.Add(r); + rooms.Add(r); } hasListingResults.Value = true; @@ -286,14 +284,6 @@ namespace osu.Game.Screens.OnlinePlay.Lounge { base.OnResuming(e); - Debug.Assert(selectionLease != null); - - selectionLease.Return(); - selectionLease = null; - - if (SelectedRoom.Value?.RoomID == null) - SelectedRoom.Value = new Room(); - music.EnsurePlayingSomething(); onReturning(); @@ -415,14 +405,7 @@ namespace osu.Game.Screens.OnlinePlay.Lounge OpenNewRoom(room ?? CreateNewRoom()); }); - protected virtual void OpenNewRoom(Room room) - { - selectionLease = SelectedRoom.BeginLease(false); - Debug.Assert(selectionLease != null); - selectionLease.Value = room; - - this.Push(CreateRoomSubScreen(room)); - } + protected virtual void OpenNewRoom(Room room) => this.Push(CreateRoomSubScreen(room)); public void RefreshRooms() => listingPollingComponent.PollImmediately(); From 74ccac37ae665ea2a9a603316077453520a8b9de Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Wed, 12 Feb 2025 21:57:18 +0900 Subject: [PATCH 1020/1275] Encapsulate RoomsContainer scroll a bit better --- .../TestSceneLoungeRoomsContainer.cs | 4 +-- .../Lounge/Components/RoomsContainer.cs | 35 ++++++++++++------- .../OnlinePlay/Lounge/LoungeSubScreen.cs | 26 +++----------- 3 files changed, 30 insertions(+), 35 deletions(-) diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneLoungeRoomsContainer.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneLoungeRoomsContainer.cs index 9daad960c7..772eb91174 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneLoungeRoomsContainer.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneLoungeRoomsContainer.cs @@ -32,13 +32,13 @@ namespace osu.Game.Tests.Visual.Multiplayer rooms = new BindableList(); Child = new PopoverContainer { - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, + RelativeSizeAxes = Axes.Both, Anchor = Anchor.Centre, Origin = Anchor.Centre, Width = 0.5f, Child = container = new RoomsContainer { + RelativeSizeAxes = Axes.Both, Rooms = { BindTarget = rooms }, SelectedRoom = { BindTarget = SelectedRoom } } diff --git a/osu.Game/Screens/OnlinePlay/Lounge/Components/RoomsContainer.cs b/osu.Game/Screens/OnlinePlay/Lounge/Components/RoomsContainer.cs index 6681cbe720..65f969bc7b 100644 --- a/osu.Game/Screens/OnlinePlay/Lounge/Components/RoomsContainer.cs +++ b/osu.Game/Screens/OnlinePlay/Lounge/Components/RoomsContainer.cs @@ -13,6 +13,7 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Input.Bindings; using osu.Framework.Input.Events; +using osu.Game.Graphics.Containers; using osu.Game.Graphics.Cursor; using osu.Game.Input.Bindings; using osu.Game.Online.Rooms; @@ -28,6 +29,7 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components public IReadOnlyList DrawableRooms => roomFlow.FlowingChildren.Cast().ToArray(); + private readonly ScrollContainer scroll; private readonly FillFlowContainer roomFlow; // handle deselection @@ -35,28 +37,29 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components public RoomsContainer() { - RelativeSizeAxes = Axes.X; - AutoSizeAxes = Axes.Y; - - // account for the fact we are in a scroll container and want a bit of spacing from the scroll bar. - Padding = new MarginPadding { Right = 5 }; - - InternalChild = new OsuContextMenuContainer + InternalChild = scroll = new OsuScrollContainer { - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, - Child = roomFlow = new FillFlowContainer + RelativeSizeAxes = Axes.Both, + ScrollbarOverlapsContent = false, + Padding = new MarginPadding { Right = 5 }, + Child = new OsuContextMenuContainer { RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y, - Direction = FillDirection.Vertical, - Spacing = new Vector2(10), + Child = roomFlow = new FillFlowContainer + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Direction = FillDirection.Vertical, + Spacing = new Vector2(10), + } } }; } protected override void LoadComplete() { + SelectedRoom.BindValueChanged(onSelectedRoomChanged, true); Rooms.BindCollectionChanged(roomsChanged, true); Filter.BindValueChanged(criteria => applyFilterCriteria(criteria.NewValue), true); } @@ -119,6 +122,14 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components } } + private void onSelectedRoomChanged(ValueChangedEvent room) + { + // scroll selected room into view on selection. + var drawable = DrawableRooms.FirstOrDefault(r => r.Room == room.NewValue); + if (drawable != null) + scroll.ScrollIntoView(drawable); + } + private void roomsChanged(object? sender, NotifyCollectionChangedEventArgs args) { switch (args.Action) diff --git a/osu.Game/Screens/OnlinePlay/Lounge/LoungeSubScreen.cs b/osu.Game/Screens/OnlinePlay/Lounge/LoungeSubScreen.cs index 7bb0c67990..1877244c03 100644 --- a/osu.Game/Screens/OnlinePlay/Lounge/LoungeSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Lounge/LoungeSubScreen.cs @@ -17,7 +17,6 @@ using osu.Framework.Logging; using osu.Framework.Screens; using osu.Framework.Threading; using osu.Game.Configuration; -using osu.Game.Graphics.Containers; using osu.Game.Graphics.UserInterface; using osu.Game.Input; using osu.Game.Online.API; @@ -82,7 +81,6 @@ namespace osu.Game.Screens.OnlinePlay.Lounge private ListingPollingComponent listingPollingComponent = null!; private PopoverContainer popoverContainer = null!; private LoadingLayer loadingLayer = null!; - private RoomsContainer roomsContainer = null!; private SearchTextBox searchTextBox = null!; protected Dropdown StatusDropdown { get; private set; } = null!; @@ -95,8 +93,6 @@ namespace osu.Game.Screens.OnlinePlay.Lounge if (idleTracker != null) isIdle.BindTo(idleTracker.IsIdle); - OsuScrollContainer scrollContainer; - InternalChildren = new Drawable[] { listingPollingComponent = new ListingPollingComponent @@ -113,17 +109,13 @@ namespace osu.Game.Screens.OnlinePlay.Lounge Horizontal = WaveOverlayContainer.WIDTH_PADDING, Top = Header.HEIGHT + controls_area_height + 20, }, - Child = scrollContainer = new OsuScrollContainer + Child = new RoomsContainer { RelativeSizeAxes = Axes.Both, - ScrollbarOverlapsContent = false, - Child = roomsContainer = new RoomsContainer - { - Rooms = { BindTarget = rooms }, - SelectedRoom = { BindTarget = selectedRoom }, - Filter = { BindTarget = filter }, - } - }, + Rooms = { BindTarget = rooms }, + SelectedRoom = { BindTarget = selectedRoom }, + Filter = { BindTarget = filter }, + } }, loadingLayer = new LoadingLayer(true), new FillFlowContainer @@ -178,14 +170,6 @@ namespace osu.Game.Screens.OnlinePlay.Lounge }, }, }; - - // scroll selected room into view on selection. - selectedRoom.BindValueChanged(val => - { - var drawable = roomsContainer.DrawableRooms.FirstOrDefault(r => r.Room == val.NewValue); - if (drawable != null) - scrollContainer.ScrollIntoView(drawable); - }); } protected override void LoadComplete() From 43928c94db5b4695b2baab8acfb41d58198322aa Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Wed, 12 Feb 2025 22:03:22 +0900 Subject: [PATCH 1021/1275] Remove remaining bindables --- .../OnlinePlay/Lounge/LoungeSubScreen.cs | 17 +++++++---------- .../Multiplayer/MultiplayerLoungeSubScreen.cs | 3 --- .../OnlinePlay/TestRoomRequestsHandler.cs | 2 -- 3 files changed, 7 insertions(+), 15 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/Lounge/LoungeSubScreen.cs b/osu.Game/Screens/OnlinePlay/Lounge/LoungeSubScreen.cs index 1877244c03..2e78e88ccf 100644 --- a/osu.Game/Screens/OnlinePlay/Lounge/LoungeSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Lounge/LoungeSubScreen.cs @@ -40,7 +40,7 @@ namespace osu.Game.Screens.OnlinePlay.Lounge protected override BackgroundScreen CreateBackground() => new LoungeBackgroundScreen { - SelectedRoom = { BindTarget = selectedRoom } + SelectedRoom = { BindTarget = roomsContainer.SelectedRoom } }; protected override UserActivity InitialActivity => new UserActivity.SearchingForLobby(); @@ -72,12 +72,11 @@ namespace osu.Game.Screens.OnlinePlay.Lounge private IDisposable? joiningRoomOperation; - private readonly Bindable selectedRoom = new Bindable(); - private readonly BindableList rooms = new BindableList(); private readonly Bindable filter = new Bindable(); private readonly Bindable hasListingResults = new Bindable(); private readonly IBindable operationInProgress = new Bindable(); private readonly IBindable isIdle = new BindableBool(); + private RoomsContainer roomsContainer = null!; private ListingPollingComponent listingPollingComponent = null!; private PopoverContainer popoverContainer = null!; private LoadingLayer loadingLayer = null!; @@ -109,11 +108,9 @@ namespace osu.Game.Screens.OnlinePlay.Lounge Horizontal = WaveOverlayContainer.WIDTH_PADDING, Top = Header.HEIGHT + controls_area_height + 20, }, - Child = new RoomsContainer + Child = roomsContainer = new RoomsContainer { RelativeSizeAxes = Axes.Both, - Rooms = { BindTarget = rooms }, - SelectedRoom = { BindTarget = selectedRoom }, Filter = { BindTarget = filter }, } }, @@ -190,7 +187,7 @@ namespace osu.Game.Screens.OnlinePlay.Lounge filter.BindValueChanged(_ => { - rooms.Clear(); + roomsContainer.Rooms.Clear(); hasListingResults.Value = false; listingPollingComponent.PollImmediately(); }); @@ -200,11 +197,11 @@ namespace osu.Game.Screens.OnlinePlay.Lounge private void onListingReceived(Room[] result) { - Dictionary localRoomsById = rooms.ToDictionary(r => r.RoomID!.Value); + Dictionary localRoomsById = roomsContainer.Rooms.ToDictionary(r => r.RoomID!.Value); Dictionary resultRoomsById = result.ToDictionary(r => r.RoomID!.Value); // Remove all local rooms no longer in the result set. - rooms.RemoveAll(r => !resultRoomsById.ContainsKey(r.RoomID!.Value)); + roomsContainer.Rooms.RemoveAll(r => !resultRoomsById.ContainsKey(r.RoomID!.Value)); // Add or update local rooms with the result set. foreach (var r in result) @@ -212,7 +209,7 @@ namespace osu.Game.Screens.OnlinePlay.Lounge if (localRoomsById.TryGetValue(r.RoomID!.Value, out Room? existingRoom)) existingRoom.CopyFrom(r); else - rooms.Add(r); + roomsContainer.Rooms.Add(r); } hasListingResults.Value = true; diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerLoungeSubScreen.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerLoungeSubScreen.cs index 3cf873ec78..6191cfd975 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerLoungeSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerLoungeSubScreen.cs @@ -3,9 +3,7 @@ using System; using System.Collections.Generic; -using System.Threading.Tasks; using osu.Framework.Allocation; -using osu.Framework.Bindables; using osu.Framework.Extensions.ExceptionExtensions; using osu.Framework.Logging; using osu.Framework.Graphics; @@ -15,7 +13,6 @@ using osu.Game.Graphics.UserInterface; using osu.Game.Online.API; using osu.Game.Online.Multiplayer; using osu.Game.Online.Rooms; -using osu.Game.Screens.OnlinePlay.Components; using osu.Game.Screens.OnlinePlay.Lounge; using osu.Game.Screens.OnlinePlay.Lounge.Components; using osu.Game.Screens.OnlinePlay.Match; diff --git a/osu.Game/Tests/Visual/OnlinePlay/TestRoomRequestsHandler.cs b/osu.Game/Tests/Visual/OnlinePlay/TestRoomRequestsHandler.cs index 63bc9325fa..617a4cff79 100644 --- a/osu.Game/Tests/Visual/OnlinePlay/TestRoomRequestsHandler.cs +++ b/osu.Game/Tests/Visual/OnlinePlay/TestRoomRequestsHandler.cs @@ -15,7 +15,6 @@ using osu.Game.Online.Rooms; using osu.Game.Rulesets; using osu.Game.Rulesets.Scoring; using osu.Game.Scoring; -using osu.Game.Screens.OnlinePlay.Components; using osu.Game.Tests.Beatmaps; using osu.Game.Utils; @@ -28,7 +27,6 @@ namespace osu.Game.Tests.Visual.OnlinePlay public class TestRoomRequestsHandler { public IReadOnlyList ServerSideRooms => serverSideRooms; - private readonly List serverSideRooms = new List(); private int currentRoomId = 1; From ee6dcbd80899c3865803311b372c8f8623092ade Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 12 Feb 2025 14:12:43 +0100 Subject: [PATCH 1022/1275] Fix android build again Another month, another freak android build failure. From what I can tell, this time the build is broken because the second- -to-last workaround applied to fix it, namely explicitly specifying the version of workloads to install, stopped working, presumably because github pushed a new .NET SDK version to runners, and microsoft didn't actually put up a set of workload packages whose version matches the .NET SDK version 1:1. Thankfully, the fix to the *last* android build breakage (which caused the workload installation to completely and irrecoverably fail), appears to be rolling out this week, and *also* fix that same second-last issue, so both workarounds of specifying the workload version and pinning the image to `windows-2019` appear to no longer be required. Note that the newest image version, 20250209.1.0, is still not fully rolled out yet, thus rather than just fix all builds, this will fix like 20% of builds until the newest image is fully rolled out. But I guess a 20% passing build is better than a 0% passing build, in a sense...? --- .github/workflows/ci.yml | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a88f1320cd..d75f09f184 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -96,7 +96,7 @@ jobs: build-only-android: name: Build only (Android) - runs-on: windows-2019 + runs-on: windows-latest timeout-minutes: 60 steps: - name: Checkout @@ -114,10 +114,7 @@ jobs: dotnet-version: "8.0.x" - name: Install .NET workloads - # since windows image 20241113.3.0, not specifying a version here - # installs the .NET 7 version of android workload for very unknown reasons. - # revisit once we upgrade to .NET 9, it's probably fixed there. - run: dotnet workload install android --version (dotnet --version) + run: dotnet workload install android - name: Compile run: dotnet build -c Debug osu.Android.slnf From 24cc77287e5e715a0fc684999f0a9aadd1355380 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Wed, 12 Feb 2025 22:21:04 +0900 Subject: [PATCH 1023/1275] Refactor polling components (namespace/namings) --- .../Visual/Multiplayer/TestSceneMultiplayer.cs | 3 +-- .../LoungePollingComponent.cs} | 4 ++-- .../OnlinePlay/Lounge/LoungeSubScreen.cs | 17 ++++++++--------- .../Playlists/PlaylistsRoomSubScreen.cs | 8 ++++---- .../PlaylistsRoomUpdater.cs} | 6 +++--- 5 files changed, 18 insertions(+), 20 deletions(-) rename osu.Game/Screens/OnlinePlay/{Components/ListingPollingComponent.cs => Lounge/LoungePollingComponent.cs} (92%) rename osu.Game/Screens/OnlinePlay/{Components/SelectionPollingComponent.cs => Playlists/PlaylistsRoomUpdater.cs} (88%) diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs index 0966c61a3a..a87216287d 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs @@ -33,7 +33,6 @@ using osu.Game.Rulesets.Osu.Mods; using osu.Game.Rulesets.Taiko; using osu.Game.Scoring; using osu.Game.Screens.OnlinePlay; -using osu.Game.Screens.OnlinePlay.Components; using osu.Game.Screens.OnlinePlay.Lounge; using osu.Game.Screens.OnlinePlay.Lounge.Components; using osu.Game.Screens.OnlinePlay.Match; @@ -806,7 +805,7 @@ namespace osu.Game.Tests.Visual.Multiplayer AddUntilStep("wait for room", () => this.ChildrenOfType().Any()); AddStep("select room", () => InputManager.Key(Key.Down)); - AddStep("disable polling", () => this.ChildrenOfType().Single().TimeBetweenPolls.Value = 0); + AddStep("disable polling", () => this.ChildrenOfType().Single().TimeBetweenPolls.Value = 0); AddStep("change server-side settings", () => { multiplayerClient.ServerSideRooms[0].Name = "New name"; diff --git a/osu.Game/Screens/OnlinePlay/Components/ListingPollingComponent.cs b/osu.Game/Screens/OnlinePlay/Lounge/LoungePollingComponent.cs similarity index 92% rename from osu.Game/Screens/OnlinePlay/Components/ListingPollingComponent.cs rename to osu.Game/Screens/OnlinePlay/Lounge/LoungePollingComponent.cs index 1495f97de4..420a96cf8a 100644 --- a/osu.Game/Screens/OnlinePlay/Components/ListingPollingComponent.cs +++ b/osu.Game/Screens/OnlinePlay/Lounge/LoungePollingComponent.cs @@ -11,12 +11,12 @@ using osu.Game.Online.API; using osu.Game.Online.Rooms; using osu.Game.Screens.OnlinePlay.Lounge.Components; -namespace osu.Game.Screens.OnlinePlay.Components +namespace osu.Game.Screens.OnlinePlay.Lounge { /// /// A that polls for the lounge listing. /// - public partial class ListingPollingComponent : PollingComponent + public partial class LoungePollingComponent : PollingComponent { [Resolved] private IAPIProvider api { get; set; } = null!; diff --git a/osu.Game/Screens/OnlinePlay/Lounge/LoungeSubScreen.cs b/osu.Game/Screens/OnlinePlay/Lounge/LoungeSubScreen.cs index 2e78e88ccf..3a4da96ba1 100644 --- a/osu.Game/Screens/OnlinePlay/Lounge/LoungeSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Lounge/LoungeSubScreen.cs @@ -24,7 +24,6 @@ using osu.Game.Online.API.Requests; using osu.Game.Online.Rooms; using osu.Game.Overlays; using osu.Game.Rulesets; -using osu.Game.Screens.OnlinePlay.Components; using osu.Game.Screens.OnlinePlay.Lounge.Components; using osu.Game.Screens.OnlinePlay.Match; using osu.Game.Users; @@ -77,7 +76,7 @@ namespace osu.Game.Screens.OnlinePlay.Lounge private readonly IBindable operationInProgress = new Bindable(); private readonly IBindable isIdle = new BindableBool(); private RoomsContainer roomsContainer = null!; - private ListingPollingComponent listingPollingComponent = null!; + private LoungePollingComponent pollingComponent = null!; private PopoverContainer popoverContainer = null!; private LoadingLayer loadingLayer = null!; private SearchTextBox searchTextBox = null!; @@ -94,7 +93,7 @@ namespace osu.Game.Screens.OnlinePlay.Lounge InternalChildren = new Drawable[] { - listingPollingComponent = new ListingPollingComponent + pollingComponent = new LoungePollingComponent { RoomsReceived = onListingReceived, Filter = { BindTarget = filter } @@ -189,7 +188,7 @@ namespace osu.Game.Screens.OnlinePlay.Lounge { roomsContainer.Rooms.Clear(); hasListingResults.Value = false; - listingPollingComponent.PollImmediately(); + pollingComponent.PollImmediately(); }); updateFilter(); @@ -270,7 +269,7 @@ namespace osu.Game.Screens.OnlinePlay.Lounge onReturning(); // Poll for any newly-created rooms (including potentially the user's own). - listingPollingComponent.PollImmediately(); + pollingComponent.PollImmediately(); } public override bool OnExiting(ScreenExitEvent e) @@ -388,7 +387,7 @@ namespace osu.Game.Screens.OnlinePlay.Lounge protected virtual void OpenNewRoom(Room room) => this.Push(CreateRoomSubScreen(room)); - public void RefreshRooms() => listingPollingComponent.PollImmediately(); + public void RefreshRooms() => pollingComponent.PollImmediately(); private void updateLoadingLayer() { @@ -401,11 +400,11 @@ namespace osu.Game.Screens.OnlinePlay.Lounge private void updatePollingRate(bool isCurrentScreen) { if (!isCurrentScreen) - listingPollingComponent.TimeBetweenPolls.Value = 0; + pollingComponent.TimeBetweenPolls.Value = 0; else - listingPollingComponent.TimeBetweenPolls.Value = isIdle.Value ? 120000 : 15000; + pollingComponent.TimeBetweenPolls.Value = isIdle.Value ? 120000 : 15000; - Logger.Log($"Polling adjusted (listing: {listingPollingComponent.TimeBetweenPolls.Value})"); + Logger.Log($"Polling adjusted (listing: {pollingComponent.TimeBetweenPolls.Value})"); } protected abstract OsuButton CreateNewRoomButton(); diff --git a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs index bf0e428483..a74ae642fb 100644 --- a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs @@ -44,7 +44,7 @@ namespace osu.Game.Screens.OnlinePlay.Playlists private IdleTracker? idleTracker { get; set; } private MatchLeaderboard leaderboard = null!; - private SelectionPollingComponent selectionPollingComponent = null!; + private PlaylistsRoomUpdater roomUpdater = null!; private FillFlowContainer progressSection = null!; private DrawableRoomPlaylist drawablePlaylist = null!; @@ -64,7 +64,7 @@ namespace osu.Game.Screens.OnlinePlay.Playlists if (idleTracker != null) isIdle.BindTo(idleTracker.IsIdle); - AddInternal(selectionPollingComponent = new SelectionPollingComponent(Room)); + AddInternal(roomUpdater = new PlaylistsRoomUpdater(Room)); } protected override void LoadComplete() @@ -328,8 +328,8 @@ namespace osu.Game.Screens.OnlinePlay.Playlists private void updatePollingRate() { - selectionPollingComponent.TimeBetweenPolls.Value = isIdle.Value ? 30000 : 5000; - Logger.Log($"Polling adjusted (selection: {selectionPollingComponent.TimeBetweenPolls.Value})"); + roomUpdater.TimeBetweenPolls.Value = isIdle.Value ? 30000 : 5000; + Logger.Log($"Polling adjusted (selection: {roomUpdater.TimeBetweenPolls.Value})"); } private void closePlaylist() diff --git a/osu.Game/Screens/OnlinePlay/Components/SelectionPollingComponent.cs b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomUpdater.cs similarity index 88% rename from osu.Game/Screens/OnlinePlay/Components/SelectionPollingComponent.cs rename to osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomUpdater.cs index bfa059f72e..f68703750a 100644 --- a/osu.Game/Screens/OnlinePlay/Components/SelectionPollingComponent.cs +++ b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomUpdater.cs @@ -7,19 +7,19 @@ using osu.Game.Online; using osu.Game.Online.API; using osu.Game.Online.Rooms; -namespace osu.Game.Screens.OnlinePlay.Components +namespace osu.Game.Screens.OnlinePlay.Playlists { /// /// A that polls for and updates a room. /// - public partial class SelectionPollingComponent : PollingComponent + public partial class PlaylistsRoomUpdater : PollingComponent { [Resolved] private IAPIProvider api { get; set; } = null!; private readonly Room room; - public SelectionPollingComponent(Room room) + public PlaylistsRoomUpdater(Room room) { this.room = room; } From 205d6ecffbc989d75c1a32e53a29a9342b88c175 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Wed, 12 Feb 2025 22:51:25 +0900 Subject: [PATCH 1024/1275] Remove `SelectedRoom` abstraction from `OnlinePlayTestScene` --- .../StatefulMultiplayerClientTest.cs | 6 ++ .../TestSceneDrawableRoomParticipantsList.cs | 15 +++-- .../TestSceneLoungeRoomsContainer.cs | 7 +- .../TestSceneMatchBeatmapDetailArea.cs | 10 +-- .../Multiplayer/TestSceneMatchLeaderboard.cs | 4 +- .../TestSceneMultiSpectatorLeaderboard.cs | 2 + .../TestSceneMultiSpectatorScreen.cs | 6 +- .../TestSceneMultiplayerLoungeSubScreen.cs | 5 -- .../TestSceneMultiplayerMatchSongSelect.cs | 13 +++- .../TestSceneMultiplayerMatchSubScreen.cs | 28 ++++---- .../TestSceneMultiplayerParticipantsList.cs | 6 +- .../Multiplayer/TestSceneMultiplayerPlayer.cs | 6 ++ .../TestSceneMultiplayerPlaylist.cs | 5 +- .../TestSceneMultiplayerQueueList.cs | 5 +- .../TestSceneMultiplayerSpectateButton.cs | 11 +-- .../TestScenePlaylistsSongSelect.cs | 23 ++++--- .../TestScenePlaylistsMatchSettingsOverlay.cs | 29 ++++---- .../TestScenePlaylistsParticipantsList.cs | 10 +-- .../TestScenePlaylistsRoomCreation.cs | 12 ++-- .../Multiplayer/MultiplayerTestScene.cs | 67 ++++++++++--------- .../IOnlinePlayTestSceneDependencies.cs | 6 -- .../Visual/OnlinePlay/OnlinePlayTestScene.cs | 2 - .../OnlinePlayTestSceneDependencies.cs | 4 -- 23 files changed, 149 insertions(+), 133 deletions(-) diff --git a/osu.Game.Tests/NonVisual/Multiplayer/StatefulMultiplayerClientTest.cs b/osu.Game.Tests/NonVisual/Multiplayer/StatefulMultiplayerClientTest.cs index be30e06ed4..c0ca387260 100644 --- a/osu.Game.Tests/NonVisual/Multiplayer/StatefulMultiplayerClientTest.cs +++ b/osu.Game.Tests/NonVisual/Multiplayer/StatefulMultiplayerClientTest.cs @@ -15,6 +15,12 @@ namespace osu.Game.Tests.NonVisual.Multiplayer [HeadlessTest] public partial class StatefulMultiplayerClientTest : MultiplayerTestScene { + public override void SetUpSteps() + { + base.SetUpSteps(); + JoinDefaultRoom(); + } + [Test] public void TestUserAddedOnJoin() { diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneDrawableRoomParticipantsList.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneDrawableRoomParticipantsList.cs index c1662bf944..2fd1268c8a 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneDrawableRoomParticipantsList.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneDrawableRoomParticipantsList.cs @@ -15,6 +15,7 @@ namespace osu.Game.Tests.Visual.Multiplayer { public partial class TestSceneDrawableRoomParticipantsList : OnlinePlayTestScene { + private Room room = null!; private DrawableRoomParticipantsList list = null!; public override void SetUpSteps() @@ -23,7 +24,7 @@ namespace osu.Game.Tests.Visual.Multiplayer AddStep("create list", () => { - SelectedRoom.Value = new Room + room = new Room { Name = "test room", Host = new APIUser @@ -33,7 +34,7 @@ namespace osu.Game.Tests.Visual.Multiplayer } }; - Child = list = new DrawableRoomParticipantsList(SelectedRoom.Value) + Child = list = new DrawableRoomParticipantsList(room) { Anchor = Anchor.Centre, Origin = Anchor.Centre, @@ -119,7 +120,7 @@ namespace osu.Game.Tests.Visual.Multiplayer AddAssert("4 circles displayed", () => list.ChildrenOfType().Count() == 4); AddAssert("46 hidden users", () => list.ChildrenOfType().Single().Count == 46); - AddStep("remove from end", () => removeUserAt(SelectedRoom.Value!.RecentParticipants.Count - 1)); + AddStep("remove from end", () => removeUserAt(room.RecentParticipants.Count - 1)); AddAssert("4 circles displayed", () => list.ChildrenOfType().Count() == 4); AddAssert("45 hidden users", () => list.ChildrenOfType().Single().Count == 45); @@ -138,18 +139,18 @@ namespace osu.Game.Tests.Visual.Multiplayer private void addUser(int id) { - SelectedRoom.Value!.RecentParticipants = SelectedRoom.Value!.RecentParticipants.Append(new APIUser + room.RecentParticipants = room.RecentParticipants.Append(new APIUser { Id = id, Username = $"User {id}" }).ToArray(); - SelectedRoom.Value!.ParticipantCount++; + room.ParticipantCount++; } private void removeUserAt(int index) { - SelectedRoom.Value!.RecentParticipants = SelectedRoom.Value!.RecentParticipants.Where(u => !u.Equals(SelectedRoom.Value!.RecentParticipants[index])).ToArray(); - SelectedRoom.Value!.ParticipantCount--; + room.RecentParticipants = room.RecentParticipants.Where(u => !u.Equals(room.RecentParticipants[index])).ToArray(); + room.ParticipantCount--; } } } diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneLoungeRoomsContainer.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneLoungeRoomsContainer.cs index 772eb91174..e83a966144 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneLoungeRoomsContainer.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneLoungeRoomsContainer.cs @@ -21,6 +21,7 @@ namespace osu.Game.Tests.Visual.Multiplayer public partial class TestSceneLoungeRoomsContainer : OnlinePlayTestScene { private BindableList rooms = null!; + private Bindable selectedRoom = null!; private RoomsContainer container = null!; public override void SetUpSteps() @@ -30,6 +31,8 @@ namespace osu.Game.Tests.Visual.Multiplayer AddStep("create container", () => { rooms = new BindableList(); + selectedRoom = new Bindable(); + Child = new PopoverContainer { RelativeSizeAxes = Axes.Both, @@ -40,7 +43,7 @@ namespace osu.Game.Tests.Visual.Multiplayer { RelativeSizeAxes = Axes.Both, Rooms = { BindTarget = rooms }, - SelectedRoom = { BindTarget = SelectedRoom } + SelectedRoom = { BindTarget = selectedRoom } } }; }); @@ -195,7 +198,7 @@ namespace osu.Game.Tests.Visual.Multiplayer AddStep("add rooms", () => rooms.AddRange(GenerateRooms(3, withPassword: true))); } - private bool checkRoomSelected(Room? room) => SelectedRoom.Value == room; + private bool checkRoomSelected(Room? room) => selectedRoom.Value == room; private Room? getRoomInFlow(int index) => (container.ChildrenOfType>().First().FlowingChildren.ElementAt(index) as DrawableRoom)?.Room; diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchBeatmapDetailArea.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchBeatmapDetailArea.cs index 813a420cbd..e372d63fde 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchBeatmapDetailArea.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchBeatmapDetailArea.cs @@ -16,15 +16,15 @@ namespace osu.Game.Tests.Visual.Multiplayer { public partial class TestSceneMatchBeatmapDetailArea : OnlinePlayTestScene { + private Room room = null!; + public override void SetUpSteps() { base.SetUpSteps(); AddStep("create area", () => { - SelectedRoom.Value = new Room(); - - Child = new MatchBeatmapDetailArea(SelectedRoom.Value) + Child = new MatchBeatmapDetailArea(room = new Room()) { Anchor = Anchor.Centre, Origin = Anchor.Centre, @@ -36,9 +36,9 @@ namespace osu.Game.Tests.Visual.Multiplayer private void createNewItem() { - SelectedRoom.Value!.Playlist = SelectedRoom.Value.Playlist.Append(new PlaylistItem(new TestBeatmap(new OsuRuleset().RulesetInfo).BeatmapInfo) + room.Playlist = room.Playlist.Append(new PlaylistItem(new TestBeatmap(new OsuRuleset().RulesetInfo).BeatmapInfo) { - ID = SelectedRoom.Value.Playlist.Count, + ID = room.Playlist.Count, RulesetID = new OsuRuleset().RulesetInfo.OnlineID, RequiredMods = new[] { diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchLeaderboard.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchLeaderboard.cs index 38522db4d4..39ad21d0b0 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchLeaderboard.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchLeaderboard.cs @@ -61,9 +61,7 @@ namespace osu.Game.Tests.Visual.Multiplayer AddStep("create leaderboard", () => { - SelectedRoom.Value = new Room { RoomID = 3 }; - - Child = new MatchLeaderboard(SelectedRoom.Value) + Child = new MatchLeaderboard(new Room { RoomID = 3 }) { Origin = Anchor.Centre, Anchor = Anchor.Centre, diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiSpectatorLeaderboard.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiSpectatorLeaderboard.cs index 3245b3c6a9..1821c2f3bc 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiSpectatorLeaderboard.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiSpectatorLeaderboard.cs @@ -24,6 +24,8 @@ namespace osu.Game.Tests.Visual.Multiplayer { base.SetUpSteps(); + JoinDefaultRoom(); + AddStep("reset", () => { leaderboard?.RemoveAndDisposeImmediately(); diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiSpectatorScreen.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiSpectatorScreen.cs index 0a3d48828e..6cbd8a3fed 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiSpectatorScreen.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiSpectatorScreen.cs @@ -17,6 +17,7 @@ using osu.Game.Online.API; using osu.Game.Online.API.Requests.Responses; using osu.Game.Online.Multiplayer; using osu.Game.Online.Multiplayer.MatchTypes.TeamVersus; +using osu.Game.Online.Rooms; using osu.Game.Rulesets.Osu.Mods; using osu.Game.Rulesets.UI; using osu.Game.Screens.OnlinePlay.Multiplayer.Spectate; @@ -42,6 +43,7 @@ namespace osu.Game.Tests.Visual.Multiplayer private BeatmapManager beatmapManager { get; set; } = null!; private MultiSpectatorScreen spectatorScreen = null!; + private Room room = null!; private readonly List playingUsers = new List(); @@ -63,6 +65,8 @@ namespace osu.Game.Tests.Visual.Multiplayer base.SetUpSteps(); AddStep("clear playing users", () => playingUsers.Clear()); + + JoinDefaultRoom(r => room = r); } [TestCase(1)] @@ -455,7 +459,7 @@ namespace osu.Game.Tests.Visual.Multiplayer applyToBeatmap?.Invoke(Beatmap.Value); - LoadScreen(spectatorScreen = new MultiSpectatorScreen(SelectedRoom.Value!, playingUsers.ToArray())); + LoadScreen(spectatorScreen = new MultiSpectatorScreen(room, playingUsers.ToArray())); }); AddUntilStep("wait for screen load", () => spectatorScreen.LoadState == LoadState.Loaded && (!waitForPlayerLoad || spectatorScreen.AllPlayersLoaded)); diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerLoungeSubScreen.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerLoungeSubScreen.cs index b4ec9d5858..56187f8778 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerLoungeSubScreen.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerLoungeSubScreen.cs @@ -20,11 +20,6 @@ namespace osu.Game.Tests.Visual.Multiplayer { private MultiplayerLoungeSubScreen loungeScreen = null!; - public TestSceneMultiplayerLoungeSubScreen() - : base(false) - { - } - public override void SetUpSteps() { base.SetUpSteps(); diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSongSelect.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSongSelect.cs index 298e6e1b3c..287d7f5816 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSongSelect.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSongSelect.cs @@ -39,6 +39,7 @@ namespace osu.Game.Tests.Visual.Multiplayer private TestMultiplayerMatchSongSelect songSelect = null!; private Live importedBeatmapSet = null!; + private Room room = null!; [Resolved] private OsuConfigManager configManager { get; set; } = null!; @@ -58,6 +59,12 @@ namespace osu.Game.Tests.Visual.Multiplayer Add(beatmapStore); } + public override void SetUpSteps() + { + base.SetUpSteps(); + JoinDefaultRoom(r => room = r); + } + private void setUp() { AddStep("create song select", () => @@ -66,7 +73,7 @@ namespace osu.Game.Tests.Visual.Multiplayer Beatmap.SetDefault(); SelectedMods.SetDefault(); - LoadScreen(songSelect = new TestMultiplayerMatchSongSelect(SelectedRoom.Value!)); + LoadScreen(songSelect = new TestMultiplayerMatchSongSelect(room)); }); AddUntilStep("wait for present", () => songSelect.IsCurrentScreen() && songSelect.BeatmapSetsLoaded); @@ -138,8 +145,8 @@ namespace osu.Game.Tests.Visual.Multiplayer AddStep("create song select", () => { - SelectedRoom.Value!.Playlist.Single().RulesetID = 2; - songSelect = new TestMultiplayerMatchSongSelect(SelectedRoom.Value, SelectedRoom.Value.Playlist.Single()); + room.Playlist.Single().RulesetID = 2; + songSelect = new TestMultiplayerMatchSongSelect(room, room.Playlist.Single()); songSelect.OnLoadComplete += _ => Ruleset.Value = new TaikoRuleset().RulesetInfo; LoadScreen(songSelect); }); diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSubScreen.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSubScreen.cs index e95209f993..18e926ca5d 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSubScreen.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSubScreen.cs @@ -43,11 +43,7 @@ namespace osu.Game.Tests.Visual.Multiplayer private MultiplayerMatchSubScreen screen = null!; private BeatmapManager beatmaps = null!; private BeatmapSetInfo importedSet = null!; - - public TestSceneMultiplayerMatchSubScreen() - : base(false) - { - } + private Room room = null!; [BackgroundDependencyLoader] private void load(GameHost host, AudioManager audio) @@ -66,8 +62,8 @@ namespace osu.Game.Tests.Visual.Multiplayer { AddStep("load match", () => { - SelectedRoom.Value = new Room { Name = "Test Room" }; - LoadScreen(screen = new TestMultiplayerMatchSubScreen(SelectedRoom.Value!)); + room = new Room { Name = "Test Room" }; + LoadScreen(screen = new TestMultiplayerMatchSubScreen(room)); }); AddUntilStep("wait for load", () => screen.IsCurrentScreen()); @@ -78,7 +74,7 @@ namespace osu.Game.Tests.Visual.Multiplayer { AddStep("add playlist item", () => { - SelectedRoom.Value!.Playlist = + room.Playlist = [ new PlaylistItem(new TestBeatmap(new OsuRuleset().RulesetInfo).BeatmapInfo) { @@ -97,7 +93,7 @@ namespace osu.Game.Tests.Visual.Multiplayer { AddStep("add playlist item", () => { - SelectedRoom.Value!.Playlist = + room.Playlist = [ new PlaylistItem(new TestBeatmap(new TaikoRuleset().RulesetInfo).BeatmapInfo) { @@ -122,7 +118,7 @@ namespace osu.Game.Tests.Visual.Multiplayer AddStep("set playlist", () => { - SelectedRoom.Value!.Playlist = + room.Playlist = [ new PlaylistItem(new TestBeatmap(new OsuRuleset().RulesetInfo).BeatmapInfo) { @@ -139,7 +135,7 @@ namespace osu.Game.Tests.Visual.Multiplayer { AddStep("set playlist", () => { - SelectedRoom.Value!.Playlist = + room.Playlist = [ new PlaylistItem(beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First()).BeatmapInfo) { @@ -170,7 +166,7 @@ namespace osu.Game.Tests.Visual.Multiplayer { AddStep("add playlist item with allowed mod", () => { - SelectedRoom.Value!.Playlist = + room.Playlist = [ new PlaylistItem(new TestBeatmap(new OsuRuleset().RulesetInfo).BeatmapInfo) { @@ -199,7 +195,7 @@ namespace osu.Game.Tests.Visual.Multiplayer { AddStep("add playlist item with allowed mod", () => { - SelectedRoom.Value!.Playlist = + room.Playlist = [ new PlaylistItem(new TestBeatmap(new OsuRuleset().RulesetInfo).BeatmapInfo) { @@ -223,7 +219,7 @@ namespace osu.Game.Tests.Visual.Multiplayer { AddStep("add playlist item with no allowed mods", () => { - SelectedRoom.Value!.Playlist = + room.Playlist = [ new PlaylistItem(new TestBeatmap(new OsuRuleset().RulesetInfo).BeatmapInfo) { @@ -246,7 +242,7 @@ namespace osu.Game.Tests.Visual.Multiplayer { AddStep("add two playlist items", () => { - SelectedRoom.Value!.Playlist = + room.Playlist = [ new PlaylistItem(beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First()).BeatmapInfo) { @@ -285,7 +281,7 @@ namespace osu.Game.Tests.Visual.Multiplayer { AddStep("add playlist item", () => { - SelectedRoom.Value!.Playlist = + room.Playlist = [ new PlaylistItem(new TestBeatmap(new OsuRuleset().RulesetInfo).BeatmapInfo) { diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerParticipantsList.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerParticipantsList.cs index 238a716f91..e7e6112297 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerParticipantsList.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerParticipantsList.cs @@ -25,9 +25,11 @@ namespace osu.Game.Tests.Visual.Multiplayer { public partial class TestSceneMultiplayerParticipantsList : MultiplayerTestScene { - [SetUpSteps] - public void SetupSteps() + public override void SetUpSteps() { + base.SetUpSteps(); + + JoinDefaultRoom(); createNewParticipantsList(); } diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerPlayer.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerPlayer.cs index 94dd114c32..1a5be48cad 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerPlayer.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerPlayer.cs @@ -22,6 +22,12 @@ namespace osu.Game.Tests.Visual.Multiplayer { private MultiplayerPlayer player = null!; + public override void SetUpSteps() + { + base.SetUpSteps(); + JoinDefaultRoom(); + } + [Test] public void TestGameplay() { diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerPlaylist.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerPlaylist.cs index 77b75f407b..406c6cacae 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerPlaylist.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerPlaylist.cs @@ -32,6 +32,7 @@ namespace osu.Game.Tests.Visual.Multiplayer private BeatmapManager beatmaps = null!; private BeatmapSetInfo importedSet = null!; private BeatmapInfo importedBeatmap = null!; + private Room room = null!; [BackgroundDependencyLoader] private void load(GameHost host, AudioManager audio) @@ -46,9 +47,11 @@ namespace osu.Game.Tests.Visual.Multiplayer { base.SetUpSteps(); + JoinDefaultRoom(r => room = r); + AddStep("create list", () => { - Child = list = new MultiplayerPlaylist(SelectedRoom.Value!) + Child = list = new MultiplayerPlaylist(room) { Anchor = Anchor.Centre, Origin = Anchor.Centre, diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerQueueList.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerQueueList.cs index 3ef2e4ecf4..5eba67bab5 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerQueueList.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerQueueList.cs @@ -29,6 +29,7 @@ namespace osu.Game.Tests.Visual.Multiplayer private BeatmapManager beatmaps = null!; private BeatmapSetInfo importedSet = null!; private BeatmapInfo importedBeatmap = null!; + private Room room = null!; [BackgroundDependencyLoader] private void load(GameHost host, AudioManager audio) @@ -42,9 +43,11 @@ namespace osu.Game.Tests.Visual.Multiplayer { base.SetUpSteps(); + JoinDefaultRoom(r => room = r); + AddStep("create playlist", () => { - Child = playlist = new MultiplayerQueueList(SelectedRoom.Value!) + Child = playlist = new MultiplayerQueueList(room) { Anchor = Anchor.Centre, Origin = Anchor.Centre, diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerSpectateButton.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerSpectateButton.cs index 1429f86164..f92721b04b 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerSpectateButton.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerSpectateButton.cs @@ -28,6 +28,7 @@ namespace osu.Game.Tests.Visual.Multiplayer { private MultiplayerSpectateButton spectateButton = null!; private MatchStartControl startControl = null!; + private Room room = null!; private BeatmapSetInfo importedSet = null!; private BeatmapManager beatmaps = null!; @@ -46,11 +47,11 @@ namespace osu.Game.Tests.Visual.Multiplayer { base.SetUpSteps(); + JoinDefaultRoom(r => room = r); + AddStep("create button", () => { - PlaylistItem item = SelectedRoom.Value!.Playlist.First(); - - AvailabilityTracker.SelectedItem.Value = item; + AvailabilityTracker.SelectedItem.Value = room.Playlist.First(); importedSet = beatmaps.GetAllUsableBeatmapSets().First(); Beatmap.Value = beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First()); @@ -69,14 +70,14 @@ namespace osu.Game.Tests.Visual.Multiplayer Anchor = Anchor.Centre, Origin = Anchor.Centre, Size = new Vector2(200, 50), - SelectedItem = new Bindable(item) + SelectedItem = new Bindable(room.Playlist.First()) }, startControl = new MatchStartControl { Anchor = Anchor.Centre, Origin = Anchor.Centre, Size = new Vector2(200, 50), - SelectedItem = new Bindable(item) + SelectedItem = new Bindable(room.Playlist.First()) } } } diff --git a/osu.Game.Tests/Visual/Multiplayer/TestScenePlaylistsSongSelect.cs b/osu.Game.Tests/Visual/Multiplayer/TestScenePlaylistsSongSelect.cs index 726d0ac9f9..7c73fb8321 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestScenePlaylistsSongSelect.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestScenePlaylistsSongSelect.cs @@ -27,6 +27,7 @@ namespace osu.Game.Tests.Visual.Multiplayer { private BeatmapManager manager = null!; private TestPlaylistsSongSelect songSelect = null!; + private Room room = null!; [BackgroundDependencyLoader] private void load(GameHost host, AudioManager audio) @@ -51,13 +52,13 @@ namespace osu.Game.Tests.Visual.Multiplayer AddStep("reset", () => { - SelectedRoom.Value = new Room(); + room = new Room(); Ruleset.Value = new OsuRuleset().RulesetInfo; Beatmap.SetDefault(); SelectedMods.Value = Array.Empty(); }); - AddStep("create song select", () => LoadScreen(songSelect = new TestPlaylistsSongSelect(SelectedRoom.Value!))); + AddStep("create song select", () => LoadScreen(songSelect = new TestPlaylistsSongSelect(room))); AddUntilStep("wait for present", () => songSelect.IsCurrentScreen() && songSelect.BeatmapSetsLoaded); } @@ -65,14 +66,14 @@ namespace osu.Game.Tests.Visual.Multiplayer public void TestItemAddedIfEmptyOnStart() { AddStep("finalise selection", () => songSelect.FinaliseSelection()); - AddAssert("playlist has 1 item", () => SelectedRoom.Value!.Playlist.Count == 1); + AddAssert("playlist has 1 item", () => room.Playlist.Count == 1); } [Test] public void TestItemAddedWhenCreateNewItemClicked() { AddStep("create new item", () => songSelect.BeatmapDetails.CreateNewItem!()); - AddAssert("playlist has 1 item", () => SelectedRoom.Value!.Playlist.Count == 1); + AddAssert("playlist has 1 item", () => room.Playlist.Count == 1); } [Test] @@ -80,7 +81,7 @@ namespace osu.Game.Tests.Visual.Multiplayer { AddStep("create new item", () => songSelect.BeatmapDetails.CreateNewItem!()); AddStep("finalise selection", () => songSelect.FinaliseSelection()); - AddAssert("playlist has 1 item", () => SelectedRoom.Value!.Playlist.Count == 1); + AddAssert("playlist has 1 item", () => room.Playlist.Count == 1); } [Test] @@ -88,7 +89,7 @@ namespace osu.Game.Tests.Visual.Multiplayer { AddStep("create new item", () => songSelect.BeatmapDetails.CreateNewItem!()); AddStep("create new item", () => songSelect.BeatmapDetails.CreateNewItem!()); - AddAssert("playlist has 2 items", () => SelectedRoom.Value!.Playlist.Count == 2); + AddAssert("playlist has 2 items", () => room.Playlist.Count == 2); } [Test] @@ -96,10 +97,10 @@ namespace osu.Game.Tests.Visual.Multiplayer { AddStep("create new item", () => songSelect.BeatmapDetails.CreateNewItem!()); AddStep("create new item", () => songSelect.BeatmapDetails.CreateNewItem!()); - AddStep("rearrange", () => SelectedRoom.Value!.Playlist = SelectedRoom.Value!.Playlist.Skip(1).Append(SelectedRoom.Value!.Playlist[0]).ToArray()); + AddStep("rearrange", () => room.Playlist = room.Playlist.Skip(1).Append(room.Playlist[0]).ToArray()); AddStep("create new item", () => songSelect.BeatmapDetails.CreateNewItem!()); - AddAssert("new item has id 2", () => SelectedRoom.Value!.Playlist.Last().ID == 2); + AddAssert("new item has id 2", () => room.Playlist.Last().ID == 2); } /// @@ -115,13 +116,13 @@ namespace osu.Game.Tests.Visual.Multiplayer AddAssert("item 1 has rate 1.5", () => { - var mod = (OsuModDoubleTime)SelectedRoom.Value!.Playlist.First().RequiredMods[0].ToMod(new OsuRuleset()); + var mod = (OsuModDoubleTime)room.Playlist.First().RequiredMods[0].ToMod(new OsuRuleset()); return Precision.AlmostEquals(1.5, mod.SpeedChange.Value); }); AddAssert("item 2 has rate 2", () => { - var mod = (OsuModDoubleTime)SelectedRoom.Value!.Playlist.Last().RequiredMods[0].ToMod(new OsuRuleset()); + var mod = (OsuModDoubleTime)room.Playlist.Last().RequiredMods[0].ToMod(new OsuRuleset()); return Precision.AlmostEquals(2, mod.SpeedChange.Value); }); } @@ -147,7 +148,7 @@ namespace osu.Game.Tests.Visual.Multiplayer AddStep("change stored mod rate", () => mod.SpeedChange.Value = 2); AddAssert("item has rate 1.5", () => { - var m = (OsuModDoubleTime)SelectedRoom.Value!.Playlist.First().RequiredMods[0].ToMod(new OsuRuleset()); + var m = (OsuModDoubleTime)room.Playlist.First().RequiredMods[0].ToMod(new OsuRuleset()); return Precision.AlmostEquals(1.5, m.SpeedChange.Value); }); } diff --git a/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsMatchSettingsOverlay.cs b/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsMatchSettingsOverlay.cs index f7b0bc0d58..c714c39e22 100644 --- a/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsMatchSettingsOverlay.cs +++ b/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsMatchSettingsOverlay.cs @@ -18,6 +18,7 @@ namespace osu.Game.Tests.Visual.Playlists public partial class TestScenePlaylistsMatchSettingsOverlay : OnlinePlayTestScene { private TestRoomSettings settings = null!; + private Room room = null!; private Func? handleRequest; public override void SetUpSteps() @@ -47,9 +48,7 @@ namespace osu.Game.Tests.Visual.Playlists AddStep("create overlay", () => { - SelectedRoom.Value = new Room(); - - Child = settings = new TestRoomSettings(SelectedRoom.Value!) + Child = settings = new TestRoomSettings(room = new Room()) { RelativeSizeAxes = Axes.Both, State = { Value = Visibility.Visible } @@ -62,19 +61,19 @@ namespace osu.Game.Tests.Visual.Playlists { AddStep("clear name and beatmap", () => { - SelectedRoom.Value!.Name = ""; - SelectedRoom.Value!.Playlist = []; + room.Name = ""; + room.Playlist = []; }); AddAssert("button disabled", () => !settings.ApplyButton.Enabled.Value); - AddStep("set name", () => SelectedRoom.Value!.Name = "Room name"); + AddStep("set name", () => room.Name = "Room name"); AddAssert("button disabled", () => !settings.ApplyButton.Enabled.Value); - AddStep("set beatmap", () => SelectedRoom.Value!.Playlist = [new PlaylistItem(CreateBeatmap(Ruleset.Value).BeatmapInfo)]); + AddStep("set beatmap", () => room.Playlist = [new PlaylistItem(CreateBeatmap(Ruleset.Value).BeatmapInfo)]); AddAssert("button enabled", () => settings.ApplyButton.Enabled.Value); - AddStep("clear name", () => SelectedRoom.Value!.Name = ""); + AddStep("clear name", () => room.Name = ""); AddAssert("button disabled", () => !settings.ApplyButton.Enabled.Value); } @@ -90,7 +89,7 @@ namespace osu.Game.Tests.Visual.Playlists { settings.NameField.Current.Value = expected_name; settings.DurationField.Current.Value = expectedDuration; - SelectedRoom.Value!.Playlist = [new PlaylistItem(CreateBeatmap(Ruleset.Value).BeatmapInfo)]; + room.Playlist = [new PlaylistItem(CreateBeatmap(Ruleset.Value).BeatmapInfo)]; handleRequest = r => { @@ -115,8 +114,8 @@ namespace osu.Game.Tests.Visual.Playlists { var beatmap = CreateBeatmap(Ruleset.Value).BeatmapInfo; - SelectedRoom.Value!.Name = "Test Room"; - SelectedRoom.Value!.Playlist = [new PlaylistItem(beatmap)]; + room.Name = "Test Room"; + room.Playlist = [new PlaylistItem(beatmap)]; errorMessage = $"{not_found_prefix} {beatmap.OnlineID}"; @@ -124,13 +123,13 @@ namespace osu.Game.Tests.Visual.Playlists }); AddAssert("error not displayed", () => !settings.ErrorText.IsPresent); - AddAssert("playlist item valid", () => SelectedRoom.Value!.Playlist[0].Valid.Value); + AddAssert("playlist item valid", () => room.Playlist[0].Valid.Value); AddStep("create room", () => settings.ApplyButton.Action.Invoke()); AddAssert("error displayed", () => settings.ErrorText.IsPresent); AddAssert("error has custom text", () => settings.ErrorText.Text != errorMessage); - AddAssert("playlist item marked invalid", () => !SelectedRoom.Value!.Playlist[0].Valid.Value); + AddAssert("playlist item marked invalid", () => !room.Playlist[0].Valid.Value); } [Test] @@ -142,8 +141,8 @@ namespace osu.Game.Tests.Visual.Playlists AddStep("setup", () => { - SelectedRoom.Value!.Name = "Test Room"; - SelectedRoom.Value!.Playlist = [new PlaylistItem(CreateBeatmap(Ruleset.Value).BeatmapInfo)]; + room.Name = "Test Room"; + room.Playlist = [new PlaylistItem(CreateBeatmap(Ruleset.Value).BeatmapInfo)]; handleRequest = _ => failText; }); diff --git a/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsParticipantsList.cs b/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsParticipantsList.cs index c60b208ffc..e1ec30d02a 100644 --- a/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsParticipantsList.cs +++ b/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsParticipantsList.cs @@ -14,13 +14,15 @@ namespace osu.Game.Tests.Visual.Playlists { public partial class TestScenePlaylistsParticipantsList : OnlinePlayTestScene { + private Room room = null!; + public override void SetUpSteps() { base.SetUpSteps(); - AddStep("create list", () => + AddStep("create room", () => { - SelectedRoom.Value = new Room + room = new Room { RoomID = 7, RecentParticipants = Enumerable.Range(0, 50).Select(_ => new APIUser @@ -38,7 +40,7 @@ namespace osu.Game.Tests.Visual.Playlists { AddStep("create component", () => { - Child = new ParticipantsDisplay(SelectedRoom.Value!, Direction.Horizontal) + Child = new ParticipantsDisplay(room, Direction.Horizontal) { Anchor = Anchor.Centre, Origin = Anchor.Centre, @@ -52,7 +54,7 @@ namespace osu.Game.Tests.Visual.Playlists { AddStep("create component", () => { - Child = new ParticipantsDisplay(SelectedRoom.Value!, Direction.Vertical) + Child = new ParticipantsDisplay(room, Direction.Vertical) { Anchor = Anchor.Centre, Origin = Anchor.Centre, diff --git a/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsRoomCreation.cs b/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsRoomCreation.cs index 0270840597..a748d61d44 100644 --- a/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsRoomCreation.cs +++ b/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsRoomCreation.cs @@ -35,6 +35,7 @@ namespace osu.Game.Tests.Visual.Playlists private BeatmapManager manager = null!; private TestPlaylistsRoomSubScreen match = null!; private BeatmapSetInfo importedBeatmap = null!; + private Room room = null!; [BackgroundDependencyLoader] private void load(GameHost host, AudioManager audio) @@ -47,11 +48,9 @@ namespace osu.Game.Tests.Visual.Playlists [SetUpSteps] public void SetupSteps() { - AddStep("set room", () => SelectedRoom.Value = new Room()); - importBeatmap(); - AddStep("load match", () => LoadScreen(match = new TestPlaylistsRoomSubScreen(SelectedRoom.Value!))); + AddStep("load match", () => LoadScreen(match = new TestPlaylistsRoomSubScreen(room = new Room()))); AddUntilStep("wait for load", () => match.IsCurrentScreen()); } @@ -119,7 +118,7 @@ namespace osu.Game.Tests.Visual.Playlists ]; }); - AddAssert("first playlist item selected", () => match.SelectedItem.Value == SelectedRoom.Value!.Playlist[0]); + AddAssert("first playlist item selected", () => match.SelectedItem.Value == room.Playlist[0]); } [Test] @@ -197,10 +196,9 @@ namespace osu.Game.Tests.Visual.Playlists AddUntilStep("match has correct beatmap", () => realHash == match.Beatmap.Value.BeatmapInfo.MD5Hash); } - private void setupAndCreateRoom(Action room) + private void setupAndCreateRoom(Action setupFunc) { - AddStep("setup room", () => room(SelectedRoom.Value!)); - + AddStep("setup room", () => setupFunc(room)); AddStep("click create button", () => { InputManager.MoveMouseTo(this.ChildrenOfType().Single()); diff --git a/osu.Game/Tests/Visual/Multiplayer/MultiplayerTestScene.cs b/osu.Game/Tests/Visual/Multiplayer/MultiplayerTestScene.cs index d1497d5142..97c213c7b1 100644 --- a/osu.Game/Tests/Visual/Multiplayer/MultiplayerTestScene.cs +++ b/osu.Game/Tests/Visual/Multiplayer/MultiplayerTestScene.cs @@ -1,6 +1,7 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System; using osu.Game.Online.Rooms; using osu.Game.Tests.Beatmaps; using osu.Game.Tests.Visual.OnlinePlay; @@ -23,43 +24,43 @@ namespace osu.Game.Tests.Visual.Multiplayer public bool RoomJoined => MultiplayerClient.RoomJoined; - private readonly bool joinRoom; - - protected MultiplayerTestScene(bool joinRoom = true) + /// + /// Creates and joins a basic multiplayer room. + /// + /// A callback that may be used to further set up the room. + protected void JoinDefaultRoom(Action? setupFunc = null) { - this.joinRoom = joinRoom; - } - - protected virtual Room CreateRoom() - { - return new Room + AddStep("join room", () => { - Name = "test name", - Type = MatchType.HeadToHead, - Playlist = - [ - new PlaylistItem(new TestBeatmap(Ruleset.Value).BeatmapInfo) - { - RulesetID = Ruleset.Value.OnlineID - } - ] - }; - } - - public override void SetUpSteps() - { - base.SetUpSteps(); - - if (joinRoom) - { - AddStep("join room", () => + Room room = new Room { - SelectedRoom.Value = CreateRoom(); - MultiplayerClient.CreateRoom(SelectedRoom.Value).ConfigureAwait(false); - }); + Name = "test name", + Type = MatchType.HeadToHead, + Playlist = + [ + new PlaylistItem(new TestBeatmap(Ruleset.Value).BeatmapInfo) + { + RulesetID = Ruleset.Value.OnlineID + } + ] + }; - AddUntilStep("wait for room join", () => RoomJoined); - } + setupFunc?.Invoke(room); + + MultiplayerClient.CreateRoom(room).ConfigureAwait(false); + }); + + AddUntilStep("wait for room join", () => RoomJoined); + } + + /// + /// Creates and joins the given room. + /// + /// The room to create. If null, a default room will be created. + protected void JoinRoom(Room room) + { + AddStep("join room", () => MultiplayerClient.CreateRoom(room).ConfigureAwait(false)); + AddUntilStep("wait for room join", () => RoomJoined); } protected override OnlinePlayTestSceneDependencies CreateOnlinePlayDependencies() => new MultiplayerTestSceneDependencies(); diff --git a/osu.Game/Tests/Visual/OnlinePlay/IOnlinePlayTestSceneDependencies.cs b/osu.Game/Tests/Visual/OnlinePlay/IOnlinePlayTestSceneDependencies.cs index 5780cf6eff..60730ee9a4 100644 --- a/osu.Game/Tests/Visual/OnlinePlay/IOnlinePlayTestSceneDependencies.cs +++ b/osu.Game/Tests/Visual/OnlinePlay/IOnlinePlayTestSceneDependencies.cs @@ -1,7 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using osu.Framework.Bindables; using osu.Game.Database; using osu.Game.Online.Rooms; using osu.Game.Screens.OnlinePlay; @@ -13,11 +12,6 @@ namespace osu.Game.Tests.Visual.OnlinePlay /// public interface IOnlinePlayTestSceneDependencies { - /// - /// The cached . - /// - Bindable SelectedRoom { get; } - /// /// The cached . /// diff --git a/osu.Game/Tests/Visual/OnlinePlay/OnlinePlayTestScene.cs b/osu.Game/Tests/Visual/OnlinePlay/OnlinePlayTestScene.cs index c3a5e1c3ec..ce8df36590 100644 --- a/osu.Game/Tests/Visual/OnlinePlay/OnlinePlayTestScene.cs +++ b/osu.Game/Tests/Visual/OnlinePlay/OnlinePlayTestScene.cs @@ -3,7 +3,6 @@ using System; using osu.Framework.Allocation; -using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Logging; @@ -22,7 +21,6 @@ namespace osu.Game.Tests.Visual.OnlinePlay /// public abstract partial class OnlinePlayTestScene : ScreenTestScene, IOnlinePlayTestSceneDependencies { - public Bindable SelectedRoom => OnlinePlayDependencies.SelectedRoom; public OngoingOperationTracker OngoingOperationTracker => OnlinePlayDependencies.OngoingOperationTracker; public OnlinePlayBeatmapAvailabilityTracker AvailabilityTracker => OnlinePlayDependencies.AvailabilityTracker; public TestUserLookupCache UserLookupCache => OnlinePlayDependencies.UserLookupCache; diff --git a/osu.Game/Tests/Visual/OnlinePlay/OnlinePlayTestSceneDependencies.cs b/osu.Game/Tests/Visual/OnlinePlay/OnlinePlayTestSceneDependencies.cs index cc448beea0..9537c7958c 100644 --- a/osu.Game/Tests/Visual/OnlinePlay/OnlinePlayTestSceneDependencies.cs +++ b/osu.Game/Tests/Visual/OnlinePlay/OnlinePlayTestSceneDependencies.cs @@ -4,7 +4,6 @@ using System; using System.Collections.Generic; using osu.Framework.Allocation; -using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Game.Database; using osu.Game.Online.Rooms; @@ -18,7 +17,6 @@ namespace osu.Game.Tests.Visual.OnlinePlay /// public class OnlinePlayTestSceneDependencies : IReadOnlyDependencyContainer, IOnlinePlayTestSceneDependencies { - public Bindable SelectedRoom { get; } public OngoingOperationTracker OngoingOperationTracker { get; } public OnlinePlayBeatmapAvailabilityTracker AvailabilityTracker { get; } public TestRoomRequestsHandler RequestsHandler { get; } @@ -35,7 +33,6 @@ namespace osu.Game.Tests.Visual.OnlinePlay public OnlinePlayTestSceneDependencies() { - SelectedRoom = new Bindable(); RequestsHandler = new TestRoomRequestsHandler(); OngoingOperationTracker = new OngoingOperationTracker(); AvailabilityTracker = new OnlinePlayBeatmapAvailabilityTracker(); @@ -45,7 +42,6 @@ namespace osu.Game.Tests.Visual.OnlinePlay dependencies = new DependencyContainer(); CacheAs(RequestsHandler); - CacheAs(SelectedRoom); CacheAs(OngoingOperationTracker); CacheAs(AvailabilityTracker); CacheAs(new OverlayColourProvider(OverlayColourScheme.Plum)); From d923a478e9a044432cd611424ff57b5862d69865 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Thu, 13 Feb 2025 00:04:33 +0900 Subject: [PATCH 1025/1275] Remove unused method --- .../Tests/Visual/Multiplayer/MultiplayerTestScene.cs | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/osu.Game/Tests/Visual/Multiplayer/MultiplayerTestScene.cs b/osu.Game/Tests/Visual/Multiplayer/MultiplayerTestScene.cs index 97c213c7b1..8150807f4f 100644 --- a/osu.Game/Tests/Visual/Multiplayer/MultiplayerTestScene.cs +++ b/osu.Game/Tests/Visual/Multiplayer/MultiplayerTestScene.cs @@ -53,16 +53,6 @@ namespace osu.Game.Tests.Visual.Multiplayer AddUntilStep("wait for room join", () => RoomJoined); } - /// - /// Creates and joins the given room. - /// - /// The room to create. If null, a default room will be created. - protected void JoinRoom(Room room) - { - AddStep("join room", () => MultiplayerClient.CreateRoom(room).ConfigureAwait(false)); - AddUntilStep("wait for room join", () => RoomJoined); - } - protected override OnlinePlayTestSceneDependencies CreateOnlinePlayDependencies() => new MultiplayerTestSceneDependencies(); } } From 550d21df42a11202b932194e6e40bd90e384b2e3 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 13 Feb 2025 00:21:08 +0900 Subject: [PATCH 1026/1275] Fix failing tests due to text change --- .../Visual/Multiplayer/TestSceneMultiplayerPlaylist.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerPlaylist.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerPlaylist.cs index 36f5bba384..37a3cc2faf 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerPlaylist.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerPlaylist.cs @@ -266,7 +266,7 @@ namespace osu.Game.Tests.Visual.Multiplayer private void assertQueueTabCount(int count) { - string queueTabText = count > 0 ? $"Queue ({count})" : "Queue"; + string queueTabText = count > 0 ? $"Up next ({count})" : "Up next"; AddUntilStep($"Queue tab shows \"{queueTabText}\"", () => { return this.ChildrenOfType.OsuTabItem>() From 7d6701f8e9383f1a1790103f8b29d598fdc13bb7 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Thu, 13 Feb 2025 01:20:42 +0900 Subject: [PATCH 1027/1275] Attempt to fix intermittent collections test --- .../Visual/Collections/TestSceneManageCollectionsDialog.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Tests/Visual/Collections/TestSceneManageCollectionsDialog.cs b/osu.Game.Tests/Visual/Collections/TestSceneManageCollectionsDialog.cs index 0f2f716a07..60675018e9 100644 --- a/osu.Game.Tests/Visual/Collections/TestSceneManageCollectionsDialog.cs +++ b/osu.Game.Tests/Visual/Collections/TestSceneManageCollectionsDialog.cs @@ -376,6 +376,6 @@ namespace osu.Game.Tests.Visual.Collections private void assertCollectionName(int index, string name) => AddUntilStep($"item {index + 1} has correct name", - () => dialog.ChildrenOfType().Single().OrderedItems.ElementAt(index).ChildrenOfType().First().Text == name); + () => dialog.ChildrenOfType().Single().OrderedItems.ElementAtOrDefault(index)?.ChildrenOfType().First().Text == name); } } From 315a480931e256c8e79a7193c54dad451e75cd94 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 14 Feb 2025 00:03:30 +0900 Subject: [PATCH 1028/1275] Disallow focus on difficulty range slider Alternative to https://github.com/ppy/osu/pull/31749. Closes https://github.com/ppy/osu/issues/31559. --- osu.Game/Graphics/UserInterface/RangeSlider.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/osu.Game/Graphics/UserInterface/RangeSlider.cs b/osu.Game/Graphics/UserInterface/RangeSlider.cs index 422c2ca4a3..acf10ce827 100644 --- a/osu.Game/Graphics/UserInterface/RangeSlider.cs +++ b/osu.Game/Graphics/UserInterface/RangeSlider.cs @@ -162,6 +162,8 @@ namespace osu.Game.Graphics.UserInterface protected partial class BoundSlider : RoundedSliderBar { + public override bool AcceptsFocus => false; + public new Nub Nub => base.Nub; public string? DefaultString; From 965038598975043dc148bf14b14a3adf6b688eb6 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 14 Feb 2025 00:06:20 +0900 Subject: [PATCH 1029/1275] Also disable sliderbar focus when disabled --- osu.Game/Graphics/UserInterface/OsuSliderBar.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/osu.Game/Graphics/UserInterface/OsuSliderBar.cs b/osu.Game/Graphics/UserInterface/OsuSliderBar.cs index 334fe343ae..4b52ac4a3a 100644 --- a/osu.Game/Graphics/UserInterface/OsuSliderBar.cs +++ b/osu.Game/Graphics/UserInterface/OsuSliderBar.cs @@ -18,6 +18,8 @@ namespace osu.Game.Graphics.UserInterface public abstract partial class OsuSliderBar : SliderBar, IHasTooltip where T : struct, INumber, IMinMaxValue { + public override bool AcceptsFocus => !Current.Disabled; + public bool PlaySamplesOnAdjust { get; set; } = true; /// From 601e6d8a70e953b59f0066fbe6de75ed16091c09 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 14 Feb 2025 13:53:42 +0900 Subject: [PATCH 1030/1275] Refactor pass for code quality --- .../AddPlaylistToCollectionButton.cs | 57 ++++++++++++------- 1 file changed, 35 insertions(+), 22 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/Playlists/AddPlaylistToCollectionButton.cs b/osu.Game/Screens/OnlinePlay/Playlists/AddPlaylistToCollectionButton.cs index 8b5d5c752c..d4b89a5b28 100644 --- a/osu.Game/Screens/OnlinePlay/Playlists/AddPlaylistToCollectionButton.cs +++ b/osu.Game/Screens/OnlinePlay/Playlists/AddPlaylistToCollectionButton.cs @@ -2,10 +2,11 @@ // See the LICENCE file in the repository root for full licence text. using System; -using System.Collections.Generic; using System.Linq; using osu.Framework.Allocation; using osu.Framework.Bindables; +using osu.Framework.Graphics.Cursor; +using osu.Framework.Localisation; using osu.Game.Beatmaps; using osu.Game.Collections; using osu.Game.Database; @@ -17,7 +18,7 @@ using Realms; namespace osu.Game.Screens.OnlinePlay.Playlists { - public partial class AddPlaylistToCollectionButton : RoundedButton + public partial class AddPlaylistToCollectionButton : RoundedButton, IHasTooltip { private readonly Room room; private readonly Bindable downloadedBeatmapsCount = new Bindable(0); @@ -34,7 +35,6 @@ namespace osu.Game.Screens.OnlinePlay.Playlists public AddPlaylistToCollectionButton(Room room) { this.room = room; - Text = formatButtonText(downloadedBeatmapsCount.Value, collectionExists.Value); } [BackgroundDependencyLoader] @@ -43,31 +43,31 @@ namespace osu.Game.Screens.OnlinePlay.Playlists Action = () => { if (room.Playlist.Count == 0) - { - notifications?.Post(new SimpleErrorNotification { Text = "Cannot add local beatmaps" }); return; - } - var beatmaps = realmAccess.Realm.All().Filter(formatFilterQuery(room.Playlist)).ToList(); + var beatmaps = getBeatmapsForPlaylist(realmAccess.Realm).ToArray(); var collection = realmAccess.Realm.All().FirstOrDefault(c => c.Name == room.Name); if (collection == null) { - collection = new BeatmapCollection(room.Name, beatmaps.Select(i => i.MD5Hash).Distinct().ToList()); + collection = new BeatmapCollection(room.Name); realmAccess.Realm.Write(() => realmAccess.Realm.Add(collection)); notifications?.Post(new SimpleNotification { Text = $"Created new collection: {room.Name}" }); } else { - collection.ToLive(realmAccess).PerformWrite(c => - { - beatmaps = beatmaps.Where(i => !c.BeatmapMD5Hashes.Contains(i.MD5Hash)).ToList(); - foreach (var item in beatmaps) - c.BeatmapMD5Hashes.Add(item.MD5Hash); - notifications?.Post(new SimpleNotification { Text = $"Updated collection: {room.Name}" }); - }); + notifications?.Post(new SimpleNotification { Text = $"Updated collection: {room.Name}" }); } + + collection.ToLive(realmAccess).PerformWrite(c => + { + foreach (var item in beatmaps) + { + if (!c.BeatmapMD5Hashes.Contains(item.MD5Hash)) + c.BeatmapMD5Hashes.Add(item.MD5Hash); + } + }); }; } @@ -76,13 +76,28 @@ namespace osu.Game.Screens.OnlinePlay.Playlists base.LoadComplete(); if (room.Playlist.Count > 0) - beatmapSubscription = realmAccess.RegisterForNotifications(r => r.All().Filter(formatFilterQuery(room.Playlist)), (sender, _) => downloadedBeatmapsCount.Value = sender.Count); + { + beatmapSubscription = + realmAccess.RegisterForNotifications(getBeatmapsForPlaylist, (sender, _) => downloadedBeatmapsCount.Value = sender.Count); + } - collectionSubscription = realmAccess.RegisterForNotifications(r => r.All().Where(c => c.Name == room.Name), (sender, _) => collectionExists.Value = sender.Count > 0); + collectionSubscription = realmAccess.RegisterForNotifications(r => r.All().Where(c => c.Name == room.Name), (sender, _) => collectionExists.Value = sender.Any()); - downloadedBeatmapsCount.BindValueChanged(_ => Text = formatButtonText(downloadedBeatmapsCount.Value, collectionExists.Value)); + downloadedBeatmapsCount.BindValueChanged(_ => updateButtonText()); + collectionExists.BindValueChanged(_ => updateButtonText(), true); + } - collectionExists.BindValueChanged(_ => Text = formatButtonText(downloadedBeatmapsCount.Value, collectionExists.Value), true); + private IQueryable getBeatmapsForPlaylist(Realm r) + { + return r.All().Filter(string.Join(" OR ", room.Playlist.Select(item => $"(OnlineID == {item.Beatmap.OnlineID})").Distinct())); + } + + private void updateButtonText() + { + if (!collectionExists.Value) + Text = $"Create new collection with {downloadedBeatmapsCount.Value} beatmaps"; + else + Text = $"Update collection with {downloadedBeatmapsCount.Value} beatmaps"; } protected override void Dispose(bool isDisposing) @@ -93,8 +108,6 @@ namespace osu.Game.Screens.OnlinePlay.Playlists collectionSubscription?.Dispose(); } - private string formatFilterQuery(IReadOnlyList playlistItems) => string.Join(" OR ", playlistItems.Select(item => $"(OnlineID == {item.Beatmap.OnlineID})").Distinct()); - - private string formatButtonText(int count, bool collectionExists) => $"Add {count} {(count == 1 ? "beatmap" : "beatmaps")} to {(collectionExists ? "collection" : "new collection")}"; + public LocalisableString TooltipText => "Only downloaded beatmaps will be added to the collection"; } } From 8561df40c52bc60a16335e77b6024ae6d50c6984 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 14 Feb 2025 14:30:33 +0900 Subject: [PATCH 1031/1275] Add better messaging and handling of edge cases --- .../AddPlaylistToCollectionButton.cs | 110 ++++++++++++------ 1 file changed, 77 insertions(+), 33 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/Playlists/AddPlaylistToCollectionButton.cs b/osu.Game/Screens/OnlinePlay/Playlists/AddPlaylistToCollectionButton.cs index d4b89a5b28..595e9ad15c 100644 --- a/osu.Game/Screens/OnlinePlay/Playlists/AddPlaylistToCollectionButton.cs +++ b/osu.Game/Screens/OnlinePlay/Playlists/AddPlaylistToCollectionButton.cs @@ -2,9 +2,9 @@ // See the LICENCE file in the repository root for full licence text. using System; +using System.Collections.Generic; using System.Linq; using osu.Framework.Allocation; -using osu.Framework.Bindables; using osu.Framework.Graphics.Cursor; using osu.Framework.Localisation; using osu.Game.Beatmaps; @@ -21,13 +21,15 @@ namespace osu.Game.Screens.OnlinePlay.Playlists public partial class AddPlaylistToCollectionButton : RoundedButton, IHasTooltip { private readonly Room room; - private readonly Bindable downloadedBeatmapsCount = new Bindable(0); - private readonly Bindable collectionExists = new Bindable(false); + private IDisposable? beatmapSubscription; private IDisposable? collectionSubscription; + private Live? collection; + private HashSet localBeatmapHashes = new HashSet(); + [Resolved] - private RealmAccess realmAccess { get; set; } = null!; + private RealmAccess realm { get; set; } = null!; [Resolved(canBeNull: true)] private INotificationOverlay? notifications { get; set; } @@ -45,29 +47,29 @@ namespace osu.Game.Screens.OnlinePlay.Playlists if (room.Playlist.Count == 0) return; - var beatmaps = getBeatmapsForPlaylist(realmAccess.Realm).ToArray(); + var beatmaps = getBeatmapsForPlaylist(realm.Realm).ToArray(); - var collection = realmAccess.Realm.All().FirstOrDefault(c => c.Name == room.Name); + int countBefore = 0; + int countAfter = 0; - if (collection == null) + collection ??= realm.Realm.Write(() => realm.Realm.Add(new BeatmapCollection(room.Name)).ToLive(realm)); + collection.PerformWrite(c => { - collection = new BeatmapCollection(room.Name); - realmAccess.Realm.Write(() => realmAccess.Realm.Add(collection)); - notifications?.Post(new SimpleNotification { Text = $"Created new collection: {room.Name}" }); - } - else - { - notifications?.Post(new SimpleNotification { Text = $"Updated collection: {room.Name}" }); - } + countBefore = c.BeatmapMD5Hashes.Count; - collection.ToLive(realmAccess).PerformWrite(c => - { foreach (var item in beatmaps) { if (!c.BeatmapMD5Hashes.Contains(item.MD5Hash)) c.BeatmapMD5Hashes.Add(item.MD5Hash); } + + countAfter = c.BeatmapMD5Hashes.Count; }); + + if (countBefore == 0) + notifications?.Post(new SimpleNotification { Text = $"Created new collection \"{room.Name}\" with {countAfter} beatmaps." }); + else + notifications?.Post(new SimpleNotification { Text = $"Added {countAfter - countBefore} beatmaps to collection \"{room.Name}\"." }); }; } @@ -75,16 +77,53 @@ namespace osu.Game.Screens.OnlinePlay.Playlists { base.LoadComplete(); - if (room.Playlist.Count > 0) + Enabled.Value = false; + + if (room.Playlist.Count == 0) + return; + + beatmapSubscription = realm.RegisterForNotifications(getBeatmapsForPlaylist, (sender, _) => { - beatmapSubscription = - realmAccess.RegisterForNotifications(getBeatmapsForPlaylist, (sender, _) => downloadedBeatmapsCount.Value = sender.Count); - } + localBeatmapHashes = sender.Select(b => b.MD5Hash).ToHashSet(); + Schedule(updateButtonState); + }); - collectionSubscription = realmAccess.RegisterForNotifications(r => r.All().Where(c => c.Name == room.Name), (sender, _) => collectionExists.Value = sender.Any()); + collectionSubscription = realm.RegisterForNotifications(r => r.All().Where(c => c.Name == room.Name), (sender, _) => + { + collection = sender.FirstOrDefault()?.ToLive(realm); + Schedule(updateButtonState); + }); + } - downloadedBeatmapsCount.BindValueChanged(_ => updateButtonText()); - collectionExists.BindValueChanged(_ => updateButtonText(), true); + private void updateButtonState() + { + int countToAdd = getCountToBeAdded(); + + if (collection == null) + Text = $"Create new collection with {countToAdd} beatmaps"; + else + Text = $"Update collection with {countToAdd} beatmaps"; + + Enabled.Value = countToAdd > 0; + } + + private int getCountToBeAdded() + { + if (collection == null) + return localBeatmapHashes.Count; + + return collection.PerformRead(c => + { + int count = localBeatmapHashes.Count; + + foreach (string hash in localBeatmapHashes) + { + if (c.BeatmapMD5Hashes.Contains(hash)) + count--; + } + + return count; + }); } private IQueryable getBeatmapsForPlaylist(Realm r) @@ -92,14 +131,6 @@ namespace osu.Game.Screens.OnlinePlay.Playlists return r.All().Filter(string.Join(" OR ", room.Playlist.Select(item => $"(OnlineID == {item.Beatmap.OnlineID})").Distinct())); } - private void updateButtonText() - { - if (!collectionExists.Value) - Text = $"Create new collection with {downloadedBeatmapsCount.Value} beatmaps"; - else - Text = $"Update collection with {downloadedBeatmapsCount.Value} beatmaps"; - } - protected override void Dispose(bool isDisposing) { base.Dispose(isDisposing); @@ -108,6 +139,19 @@ namespace osu.Game.Screens.OnlinePlay.Playlists collectionSubscription?.Dispose(); } - public LocalisableString TooltipText => "Only downloaded beatmaps will be added to the collection"; + public LocalisableString TooltipText + { + get + { + if (Enabled.Value) + return string.Empty; + + int currentCollectionCount = collection?.PerformRead(c => c.BeatmapMD5Hashes.Count) ?? 0; + if (room.Playlist.DistinctBy(i => i.Beatmap.OnlineID).Count() == currentCollectionCount) + return "All beatmaps have been added!"; + + return "Download some beatmaps first."; + } + } } } From f9b7a8ed103e39fbd5a791699e5c99b366736766 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 14 Feb 2025 14:49:25 +0900 Subject: [PATCH 1032/1275] Make realm operation asynchronous for good measure --- .../AddPlaylistToCollectionButton.cs | 49 +++++++++++++------ 1 file changed, 35 insertions(+), 14 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/Playlists/AddPlaylistToCollectionButton.cs b/osu.Game/Screens/OnlinePlay/Playlists/AddPlaylistToCollectionButton.cs index 595e9ad15c..741173f9a3 100644 --- a/osu.Game/Screens/OnlinePlay/Playlists/AddPlaylistToCollectionButton.cs +++ b/osu.Game/Screens/OnlinePlay/Playlists/AddPlaylistToCollectionButton.cs @@ -47,14 +47,18 @@ namespace osu.Game.Screens.OnlinePlay.Playlists if (room.Playlist.Count == 0) return; - var beatmaps = getBeatmapsForPlaylist(realm.Realm).ToArray(); - int countBefore = 0; int countAfter = 0; - collection ??= realm.Realm.Write(() => realm.Realm.Add(new BeatmapCollection(room.Name)).ToLive(realm)); - collection.PerformWrite(c => + Text = "Updating collection..."; + Enabled.Value = false; + + realm.WriteAsync(r => { + var beatmaps = getBeatmapsForPlaylist(r).ToArray(); + var c = getCollectionsForPlaylist(r).FirstOrDefault() + ?? r.Add(new BeatmapCollection(room.Name)); + countBefore = c.BeatmapMD5Hashes.Count; foreach (var item in beatmaps) @@ -64,12 +68,13 @@ namespace osu.Game.Screens.OnlinePlay.Playlists } countAfter = c.BeatmapMD5Hashes.Count; - }); - - if (countBefore == 0) - notifications?.Post(new SimpleNotification { Text = $"Created new collection \"{room.Name}\" with {countAfter} beatmaps." }); - else - notifications?.Post(new SimpleNotification { Text = $"Added {countAfter - countBefore} beatmaps to collection \"{room.Name}\"." }); + }).ContinueWith(_ => Schedule(() => + { + if (countBefore == 0) + notifications?.Post(new SimpleNotification { Text = $"Created new collection \"{room.Name}\" with {countAfter} beatmaps." }); + else + notifications?.Post(new SimpleNotification { Text = $"Added {countAfter - countBefore} beatmaps to collection \"{room.Name}\"." }); + })); }; } @@ -77,6 +82,7 @@ namespace osu.Game.Screens.OnlinePlay.Playlists { base.LoadComplete(); + // will be updated via updateButtonState() when ready. Enabled.Value = false; if (room.Playlist.Count == 0) @@ -88,7 +94,7 @@ namespace osu.Game.Screens.OnlinePlay.Playlists Schedule(updateButtonState); }); - collectionSubscription = realm.RegisterForNotifications(r => r.All().Where(c => c.Name == room.Name), (sender, _) => + collectionSubscription = realm.RegisterForNotifications(getCollectionsForPlaylist, (sender, _) => { collection = sender.FirstOrDefault()?.ToLive(realm); Schedule(updateButtonState); @@ -101,8 +107,10 @@ namespace osu.Game.Screens.OnlinePlay.Playlists if (collection == null) Text = $"Create new collection with {countToAdd} beatmaps"; + else if (hasAllItemsInCollection) + Text = "Collection complete!"; else - Text = $"Update collection with {countToAdd} beatmaps"; + Text = $"Add {countToAdd} beatmaps to collection"; Enabled.Value = countToAdd > 0; } @@ -126,11 +134,25 @@ namespace osu.Game.Screens.OnlinePlay.Playlists }); } + private IQueryable getCollectionsForPlaylist(Realm r) => r.All().Where(c => c.Name == room.Name); + private IQueryable getBeatmapsForPlaylist(Realm r) { return r.All().Filter(string.Join(" OR ", room.Playlist.Select(item => $"(OnlineID == {item.Beatmap.OnlineID})").Distinct())); } + private bool hasAllItemsInCollection + { + get + { + if (collection == null) + return false; + + return room.Playlist.DistinctBy(i => i.Beatmap.OnlineID).Count() == + collection.PerformRead(c => c.BeatmapMD5Hashes.Count); + } + } + protected override void Dispose(bool isDisposing) { base.Dispose(isDisposing); @@ -146,8 +168,7 @@ namespace osu.Game.Screens.OnlinePlay.Playlists if (Enabled.Value) return string.Empty; - int currentCollectionCount = collection?.PerformRead(c => c.BeatmapMD5Hashes.Count) ?? 0; - if (room.Playlist.DistinctBy(i => i.Beatmap.OnlineID).Count() == currentCollectionCount) + if (hasAllItemsInCollection) return "All beatmaps have been added!"; return "Download some beatmaps first."; From 8ce28d56bbe245eed781e0055ea0befd72533f8e Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 14 Feb 2025 14:58:04 +0900 Subject: [PATCH 1033/1275] Fix tests not waiting enough --- .../Playlists/TestSceneAddPlaylistToCollectionButton.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game.Tests/Visual/Playlists/TestSceneAddPlaylistToCollectionButton.cs b/osu.Game.Tests/Visual/Playlists/TestSceneAddPlaylistToCollectionButton.cs index f18488170d..46c93d9ae2 100644 --- a/osu.Game.Tests/Visual/Playlists/TestSceneAddPlaylistToCollectionButton.cs +++ b/osu.Game.Tests/Visual/Playlists/TestSceneAddPlaylistToCollectionButton.cs @@ -77,9 +77,9 @@ namespace osu.Game.Tests.Visual.Playlists AddStep("click button", () => InputManager.Click(MouseButton.Left)); - AddAssert("notification shown", () => notificationOverlay.AllNotifications.FirstOrDefault(n => n.Text.ToString().StartsWith("Created", StringComparison.Ordinal)) != null); + AddUntilStep("notification shown", () => notificationOverlay.AllNotifications.Any(n => n.Text.ToString().StartsWith("Created new collection", StringComparison.Ordinal))); - AddAssert("realm is updated", () => Realm.Realm.All().FirstOrDefault(c => c.Name == room.Name) != null); + AddUntilStep("realm is updated", () => Realm.Realm.All().FirstOrDefault(c => c.Name == room.Name) != null); } private void importBeatmap() => AddStep("import beatmap", () => From 3f3cb3df2a5b12ae2fb9cfa8b3db1daa076f9c44 Mon Sep 17 00:00:00 2001 From: Rudi Herouard Date: Mon, 20 Jan 2025 16:35:21 +0100 Subject: [PATCH 1034/1275] Fix toolbox settings hiding when dragging a slider --- osu.Game/Overlays/SettingsToolboxGroup.cs | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/osu.Game/Overlays/SettingsToolboxGroup.cs b/osu.Game/Overlays/SettingsToolboxGroup.cs index f8cf218564..cf72125007 100644 --- a/osu.Game/Overlays/SettingsToolboxGroup.cs +++ b/osu.Game/Overlays/SettingsToolboxGroup.cs @@ -8,6 +8,7 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Sprites; +using osu.Framework.Input; using osu.Framework.Input.Events; using osu.Framework.Layout; using osu.Game.Graphics; @@ -54,6 +55,8 @@ namespace osu.Game.Overlays private IconButton expandButton = null!; + private InputManager inputManager = null!; + /// /// Create a new instance. /// @@ -125,6 +128,8 @@ namespace osu.Game.Overlays { base.LoadComplete(); + inputManager = GetContainingInputManager()!; + Expanded.BindValueChanged(_ => updateExpandedState(true)); updateExpandedState(false); @@ -172,7 +177,9 @@ namespace osu.Game.Overlays // potentially continuing to get processed while content has changed to autosize. content.ClearTransforms(); - if (Expanded.Value || IsHovered) + bool sliderDraggedInHimself = inputManager.DraggedDrawable.IsRootedAt(this); + + if (Expanded.Value || IsHovered || sliderDraggedInHimself) { content.AutoSizeAxes = Axes.Y; content.AutoSizeDuration = animate ? transition_duration : 0; From 9456e376f370b2ea0260a781fd6f90e1e87ad106 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 14 Feb 2025 15:15:05 +0900 Subject: [PATCH 1035/1275] Fix expanded state not updating on drag end --- osu.Game/Overlays/SettingsToolboxGroup.cs | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/osu.Game/Overlays/SettingsToolboxGroup.cs b/osu.Game/Overlays/SettingsToolboxGroup.cs index cf72125007..dd41f156f3 100644 --- a/osu.Game/Overlays/SettingsToolboxGroup.cs +++ b/osu.Game/Overlays/SettingsToolboxGroup.cs @@ -57,6 +57,8 @@ namespace osu.Game.Overlays private InputManager inputManager = null!; + private Drawable? draggedChild; + /// /// Create a new instance. /// @@ -161,6 +163,13 @@ namespace osu.Game.Overlays headerText.FadeTo(headerText.DrawWidth < DrawWidth ? 1 : 0, 150, Easing.OutQuint); headerTextVisibilityCache.Validate(); } + + // Dragged child finished its drag operation. + if (draggedChild != null && inputManager.DraggedDrawable != draggedChild) + { + draggedChild = null; + updateExpandedState(true); + } } protected override bool OnInvalidate(Invalidation invalidation, InvalidationSource source) @@ -173,13 +182,17 @@ namespace osu.Game.Overlays private void updateExpandedState(bool animate) { + // before we collapse down, let's double check the user is not dragging a UI control contained within us. + if (inputManager.DraggedDrawable.IsRootedAt(this)) + { + draggedChild = inputManager.DraggedDrawable; + } + // clearing transforms is necessary to avoid a previous height transform // potentially continuing to get processed while content has changed to autosize. content.ClearTransforms(); - bool sliderDraggedInHimself = inputManager.DraggedDrawable.IsRootedAt(this); - - if (Expanded.Value || IsHovered || sliderDraggedInHimself) + if (Expanded.Value || IsHovered || draggedChild != null) { content.AutoSizeAxes = Axes.Y; content.AutoSizeDuration = animate ? transition_duration : 0; From 88188e8fcb4b15d0214d7106810f10b1f5c66fbe Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 14 Feb 2025 16:00:19 +0900 Subject: [PATCH 1036/1275] Add API models for teams --- .../Online/API/Requests/Responses/APITeam.cs | 23 +++++++++++++++++++ .../Online/API/Requests/Responses/APIUser.cs | 4 ++++ 2 files changed, 27 insertions(+) create mode 100644 osu.Game/Online/API/Requests/Responses/APITeam.cs diff --git a/osu.Game/Online/API/Requests/Responses/APITeam.cs b/osu.Game/Online/API/Requests/Responses/APITeam.cs new file mode 100644 index 0000000000..b4fcc2d26e --- /dev/null +++ b/osu.Game/Online/API/Requests/Responses/APITeam.cs @@ -0,0 +1,23 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using Newtonsoft.Json; + +namespace osu.Game.Online.API.Requests.Responses +{ + [JsonObject(MemberSerialization.OptIn)] + public class APITeam + { + [JsonProperty(@"id")] + public int Id { get; set; } = 1; + + [JsonProperty(@"name")] + public string Name { get; set; } = string.Empty; + + [JsonProperty(@"short_name")] + public string ShortName { get; set; } = string.Empty; + + [JsonProperty(@"flag_url")] + public string FlagUrl = string.Empty; + } +} diff --git a/osu.Game/Online/API/Requests/Responses/APIUser.cs b/osu.Game/Online/API/Requests/Responses/APIUser.cs index 30fceab852..92b7d9d874 100644 --- a/osu.Game/Online/API/Requests/Responses/APIUser.cs +++ b/osu.Game/Online/API/Requests/Responses/APIUser.cs @@ -55,6 +55,10 @@ namespace osu.Game.Online.API.Requests.Responses set => countryCodeString = value.ToString(); } + [JsonProperty(@"team")] + [CanBeNull] + public APITeam Team { get; set; } + [JsonProperty(@"profile_colour")] public string Colour; From 303961d1015f2e32549680a76fa2b68112236166 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 14 Feb 2025 16:19:55 +0900 Subject: [PATCH 1037/1275] Add drawable implementations of team logo --- .../Online/Leaderboards/LeaderboardScore.cs | 6 ++ .../Overlays/BeatmapSet/Scores/ScoreTable.cs | 15 +++- .../BeatmapSet/Scores/TopScoreUserSection.cs | 27 +++++- .../Profile/Header/TopHeaderContainer.cs | 6 ++ .../Participants/ParticipantPanel.cs | 6 ++ .../Leaderboards/LeaderboardScoreV2.cs | 6 ++ .../OnlinePlay/TestRoomRequestsHandler.cs | 11 ++- .../Users/Drawables/UpdateableTeamFlag.cs | 86 +++++++++++++++++++ osu.Game/Users/UserGridPanel.cs | 3 +- osu.Game/Users/UserPanel.cs | 5 ++ osu.Game/Users/UserRankPanel.cs | 3 +- 11 files changed, 167 insertions(+), 7 deletions(-) create mode 100644 osu.Game/Users/Drawables/UpdateableTeamFlag.cs diff --git a/osu.Game/Online/Leaderboards/LeaderboardScore.cs b/osu.Game/Online/Leaderboards/LeaderboardScore.cs index 52074119b8..11e1710e75 100644 --- a/osu.Game/Online/Leaderboards/LeaderboardScore.cs +++ b/osu.Game/Online/Leaderboards/LeaderboardScore.cs @@ -199,6 +199,12 @@ namespace osu.Game.Online.Leaderboards Origin = Anchor.CentreLeft, Size = new Vector2(28, 20), }, + new UpdateableTeamFlag(user.Team) + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + Size = new Vector2(40, 20), + }, new DateLabel(Score.Date) { Anchor = Anchor.CentreLeft, diff --git a/osu.Game/Overlays/BeatmapSet/Scores/ScoreTable.cs b/osu.Game/Overlays/BeatmapSet/Scores/ScoreTable.cs index c70c41feed..be6ad49150 100644 --- a/osu.Game/Overlays/BeatmapSet/Scores/ScoreTable.cs +++ b/osu.Game/Overlays/BeatmapSet/Scores/ScoreTable.cs @@ -160,7 +160,20 @@ namespace osu.Game.Overlays.BeatmapSet.Scores { Size = new Vector2(19, 14), }, - username, + new FillFlowContainer + { + AutoSizeAxes = Axes.Both, + Direction = FillDirection.Horizontal, + Spacing = new Vector2(4), + Children = new Drawable[] + { + new UpdateableTeamFlag(score.User.Team) + { + Size = new Vector2(28, 14), + }, + username, + } + }, #pragma warning disable 618 new StatisticText(score.MaxCombo, score.BeatmapInfo!.MaxCombo, @"0\x"), #pragma warning restore 618 diff --git a/osu.Game/Overlays/BeatmapSet/Scores/TopScoreUserSection.cs b/osu.Game/Overlays/BeatmapSet/Scores/TopScoreUserSection.cs index 13ba9fb74b..14c9bedc67 100644 --- a/osu.Game/Overlays/BeatmapSet/Scores/TopScoreUserSection.cs +++ b/osu.Game/Overlays/BeatmapSet/Scores/TopScoreUserSection.cs @@ -27,7 +27,9 @@ namespace osu.Game.Overlays.BeatmapSet.Scores private readonly UpdateableAvatar avatar; private readonly LinkFlowContainer usernameText; private readonly DrawableDate achievedOn; + private readonly UpdateableFlag flag; + private readonly UpdateableTeamFlag teamFlag; public TopScoreUserSection() { @@ -112,12 +114,30 @@ namespace osu.Game.Overlays.BeatmapSet.Scores }, } }, - flag = new UpdateableFlag + new FillFlowContainer { + AutoSizeAxes = Axes.Both, Anchor = Anchor.CentreLeft, Origin = Anchor.CentreLeft, - Size = new Vector2(19, 14), - Margin = new MarginPadding { Top = 3 }, // makes spacing look more even + Direction = FillDirection.Horizontal, + Spacing = new Vector2(4), + Children = new Drawable[] + { + flag = new UpdateableFlag + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + Size = new Vector2(19, 14), + Margin = new MarginPadding { Top = 3 }, // makes spacing look more even + }, + teamFlag = new UpdateableTeamFlag + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + Size = new Vector2(28, 14), + Margin = new MarginPadding { Top = 3 }, // makes spacing look more even + }, + } }, } } @@ -139,6 +159,7 @@ namespace osu.Game.Overlays.BeatmapSet.Scores { avatar.User = value.User; flag.CountryCode = value.User.CountryCode; + teamFlag.Team = value.User.Team; achievedOn.Date = value.Date; usernameText.Clear(); diff --git a/osu.Game/Overlays/Profile/Header/TopHeaderContainer.cs b/osu.Game/Overlays/Profile/Header/TopHeaderContainer.cs index ba2cd5b705..5f404375e6 100644 --- a/osu.Game/Overlays/Profile/Header/TopHeaderContainer.cs +++ b/osu.Game/Overlays/Profile/Header/TopHeaderContainer.cs @@ -42,6 +42,7 @@ namespace osu.Game.Overlays.Profile.Header private ExternalLinkButton openUserExternally = null!; private OsuSpriteText titleText = null!; private UpdateableFlag userFlag = null!; + private UpdateableTeamFlag teamFlag = null!; private OsuHoverContainer userCountryContainer = null!; private OsuSpriteText userCountryText = null!; private GroupBadgeFlow groupBadgeFlow = null!; @@ -166,6 +167,10 @@ namespace osu.Game.Overlays.Profile.Header { Size = new Vector2(28, 20), }, + teamFlag = new UpdateableTeamFlag + { + Size = new Vector2(40, 20), + }, userCountryContainer = new OsuHoverContainer { AutoSizeAxes = Axes.Both, @@ -215,6 +220,7 @@ namespace osu.Game.Overlays.Profile.Header usernameText.Text = user?.Username ?? string.Empty; openUserExternally.Link = $@"{api.Endpoints.WebsiteUrl}/users/{user?.Id ?? 0}"; userFlag.CountryCode = user?.CountryCode ?? default; + teamFlag.Team = user?.Team; userCountryText.Text = (user?.CountryCode ?? default).GetDescription(); userCountryContainer.Action = () => rankingsOverlay?.ShowCountry(user?.CountryCode ?? default); supporterTag.SupportLevel = user?.SupportLevel ?? 0; diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Participants/ParticipantPanel.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Participants/ParticipantPanel.cs index 0fa2be44f3..0cedfb9909 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Participants/ParticipantPanel.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Participants/ParticipantPanel.cs @@ -140,6 +140,12 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Participants Size = new Vector2(28, 20), CountryCode = user?.CountryCode ?? default }, + new UpdateableTeamFlag(user?.Team) + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + Size = new Vector2(40, 20), + }, new OsuSpriteText { Anchor = Anchor.CentreLeft, diff --git a/osu.Game/Screens/SelectV2/Leaderboards/LeaderboardScoreV2.cs b/osu.Game/Screens/SelectV2/Leaderboards/LeaderboardScoreV2.cs index a2253b413c..978d6eca32 100644 --- a/osu.Game/Screens/SelectV2/Leaderboards/LeaderboardScoreV2.cs +++ b/osu.Game/Screens/SelectV2/Leaderboards/LeaderboardScoreV2.cs @@ -339,6 +339,12 @@ namespace osu.Game.Screens.SelectV2.Leaderboards Origin = Anchor.CentreLeft, Size = new Vector2(24, 16), }, + new UpdateableTeamFlag(user.Team) + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + Size = new Vector2(40, 20), + }, new DateLabel(score.Date) { Anchor = Anchor.CentreLeft, diff --git a/osu.Game/Tests/Visual/OnlinePlay/TestRoomRequestsHandler.cs b/osu.Game/Tests/Visual/OnlinePlay/TestRoomRequestsHandler.cs index c9149bda22..d73fd5ab22 100644 --- a/osu.Game/Tests/Visual/OnlinePlay/TestRoomRequestsHandler.cs +++ b/osu.Game/Tests/Visual/OnlinePlay/TestRoomRequestsHandler.cs @@ -6,6 +6,7 @@ using System.Collections.Generic; using System.Diagnostics; using System.Linq; using Newtonsoft.Json; +using osu.Framework.Utils; using osu.Game.Beatmaps; using osu.Game.Database; using osu.Game.Online.API; @@ -221,7 +222,15 @@ namespace osu.Game.Tests.Visual.OnlinePlay : new APIUser { Id = id, - Username = $"User {id}" + Username = $"User {id}", + Team = RNG.NextBool() + ? new APITeam + { + Name = "Collective Wangs", + ShortName = "WANG", + FlagUrl = "https://assets.ppy.sh/teams/logo/1/wanglogo.jpg", + } + : null, }) .Where(u => u != null).ToList(), }); diff --git a/osu.Game/Users/Drawables/UpdateableTeamFlag.cs b/osu.Game/Users/Drawables/UpdateableTeamFlag.cs new file mode 100644 index 0000000000..486cb697a1 --- /dev/null +++ b/osu.Game/Users/Drawables/UpdateableTeamFlag.cs @@ -0,0 +1,86 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Allocation; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Cursor; +using osu.Framework.Graphics.Sprites; +using osu.Framework.Graphics.Textures; +using osu.Framework.Localisation; +using osu.Game.Graphics.UserInterface; +using osu.Game.Online.API.Requests.Responses; + +namespace osu.Game.Users.Drawables +{ + /// + /// A team logo which can update to a new team when needed. + /// + public partial class UpdateableTeamFlag : ModelBackedDrawable + { + public APITeam? Team + { + get => Model; + set => Model = value; + } + + protected override double LoadDelay => 200; + + public UpdateableTeamFlag(APITeam? team = null) + { + Team = team; + + Masking = true; + } + + protected override Drawable? CreateDrawable(APITeam? team) + { + if (team == null) + return Empty(); + + return new Container + { + RelativeSizeAxes = Axes.Both, + Children = new Drawable[] + { + new TeamFlag(team) + { + RelativeSizeAxes = Axes.Both + }, + new HoverClickSounds() + } + }; + } + + // Generally we just want team flags to disappear if the user doesn't have one. + // This also handles fill flow cases and avoids spacing being added for non-displaying flags. + public override bool IsPresent => base.IsPresent && Team != null; + + protected override void Update() + { + base.Update(); + + CornerRadius = DrawHeight / 8; + } + + public partial class TeamFlag : Sprite, IHasTooltip + { + private readonly APITeam team; + + public LocalisableString TooltipText { get; } + + public TeamFlag(APITeam team) + { + this.team = team; + TooltipText = team.Name; + } + + [BackgroundDependencyLoader] + private void load(LargeTextureStore textures) + { + if (!string.IsNullOrEmpty(team.Name)) + Texture = textures.Get(team.FlagUrl); + } + } + } +} diff --git a/osu.Game/Users/UserGridPanel.cs b/osu.Game/Users/UserGridPanel.cs index fce543415d..f62c9ab4e7 100644 --- a/osu.Game/Users/UserGridPanel.cs +++ b/osu.Game/Users/UserGridPanel.cs @@ -82,9 +82,10 @@ namespace osu.Game.Users AutoSizeAxes = Axes.Both, Direction = FillDirection.Horizontal, Spacing = new Vector2(6), - Children = new Drawable[] + Children = new[] { CreateFlag(), + CreateTeamLogo(), // supporter icon is being added later } } diff --git a/osu.Game/Users/UserPanel.cs b/osu.Game/Users/UserPanel.cs index 0d3ea52611..09a5cb414f 100644 --- a/osu.Game/Users/UserPanel.cs +++ b/osu.Game/Users/UserPanel.cs @@ -130,6 +130,11 @@ namespace osu.Game.Users Action = Action, }; + protected Drawable CreateTeamLogo() => new UpdateableTeamFlag(User.Team) + { + Size = new Vector2(52, 26), + }; + public MenuItem[] ContextMenuItems { get diff --git a/osu.Game/Users/UserRankPanel.cs b/osu.Game/Users/UserRankPanel.cs index 5e3ae172be..ff8adf055c 100644 --- a/osu.Game/Users/UserRankPanel.cs +++ b/osu.Game/Users/UserRankPanel.cs @@ -147,9 +147,10 @@ namespace osu.Game.Users AutoSizeAxes = Axes.Both, Direction = FillDirection.Horizontal, Spacing = new Vector2(6), - Children = new Drawable[] + Children = new[] { CreateFlag(), + CreateTeamLogo(), // supporter icon is being added later } } From 44faabddcd79b0ada819d03cb10044b377e5fe89 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 14 Feb 2025 16:41:59 +0900 Subject: [PATCH 1038/1275] Add skinnable team flag --- osu.Game/Screens/Play/HUD/PlayerTeamFlag.cs | 56 +++++++++++++++++++++ 1 file changed, 56 insertions(+) create mode 100644 osu.Game/Screens/Play/HUD/PlayerTeamFlag.cs diff --git a/osu.Game/Screens/Play/HUD/PlayerTeamFlag.cs b/osu.Game/Screens/Play/HUD/PlayerTeamFlag.cs new file mode 100644 index 0000000000..f8ef03c58c --- /dev/null +++ b/osu.Game/Screens/Play/HUD/PlayerTeamFlag.cs @@ -0,0 +1,56 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Allocation; +using osu.Framework.Bindables; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Game.Online.API; +using osu.Game.Online.API.Requests.Responses; +using osu.Game.Skinning; +using osu.Game.Users.Drawables; +using osuTK; + +namespace osu.Game.Screens.Play.HUD +{ + public partial class PlayerTeamFlag : CompositeDrawable, ISerialisableDrawable + { + protected override bool ReceivePositionalInputAtSubTree(Vector2 screenSpacePos) => false; + + private readonly UpdateableTeamFlag flag; + + private const float default_size = 40f; + + [Resolved] + private GameplayState? gameplayState { get; set; } + + [Resolved] + private IAPIProvider api { get; set; } = null!; + + private IBindable? apiUser; + + public PlayerTeamFlag() + { + Size = new Vector2(default_size, default_size / 2f); + + InternalChild = flag = new UpdateableTeamFlag + { + RelativeSizeAxes = Axes.Both, + }; + } + + [BackgroundDependencyLoader] + private void load() + { + if (gameplayState != null) + flag.Team = gameplayState.Score.ScoreInfo.User.Team; + else + { + apiUser = api.LocalUser.GetBoundCopy(); + apiUser.BindValueChanged(u => flag.Team = u.NewValue.Team, true); + } + } + + public bool UsesFixedAnchor { get; set; } + } +} From 4184dd27180b3ae8407c8d06d86894950e8b1b67 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 14 Feb 2025 17:18:25 +0900 Subject: [PATCH 1039/1275] Give more breathing room in leaderboard scores --- .../Online/Leaderboards/LeaderboardScore.cs | 46 ++++++++++++------- 1 file changed, 29 insertions(+), 17 deletions(-) diff --git a/osu.Game/Online/Leaderboards/LeaderboardScore.cs b/osu.Game/Online/Leaderboards/LeaderboardScore.cs index 11e1710e75..0181c28218 100644 --- a/osu.Game/Online/Leaderboards/LeaderboardScore.cs +++ b/osu.Game/Online/Leaderboards/LeaderboardScore.cs @@ -189,7 +189,7 @@ namespace osu.Game.Online.Leaderboards RelativeSizeAxes = Axes.Y, Direction = FillDirection.Horizontal, Spacing = new Vector2(5f, 0f), - Width = 87f, + Width = 114f, Masking = true, Children = new Drawable[] { @@ -212,15 +212,6 @@ namespace osu.Game.Online.Leaderboards }, }, }, - new FillFlowContainer - { - Origin = Anchor.CentreLeft, - Anchor = Anchor.CentreLeft, - AutoSizeAxes = Axes.Both, - Direction = FillDirection.Horizontal, - Margin = new MarginPadding { Left = edge_margin }, - Children = statisticsLabels - }, }, }, }, @@ -240,6 +231,7 @@ namespace osu.Game.Online.Leaderboards GlowColour = Color4Extensions.FromHex(@"83ccfa"), Current = scoreManager.GetBindableTotalScoreString(Score), Font = OsuFont.Numeric.With(size: 23), + Margin = new MarginPadding { Top = 1 }, }, RankContainer = new Container { @@ -256,13 +248,32 @@ namespace osu.Game.Online.Leaderboards }, }, }, - modsContainer = new FillFlowContainer + new FillFlowContainer { + AutoSizeAxes = Axes.Both, Anchor = Anchor.BottomRight, Origin = Anchor.BottomRight, - AutoSizeAxes = Axes.Both, Direction = FillDirection.Horizontal, - ChildrenEnumerable = Score.Mods.AsOrdered().Select(mod => new ModIcon(mod) { Scale = new Vector2(0.375f) }) + Children = new Drawable[] + { + new FillFlowContainer + { + Anchor = Anchor.CentreRight, + Origin = Anchor.CentreRight, + AutoSizeAxes = Axes.Both, + Direction = FillDirection.Horizontal, + Margin = new MarginPadding { Left = edge_margin }, + Children = statisticsLabels + }, + modsContainer = new FillFlowContainer + { + Anchor = Anchor.CentreRight, + Origin = Anchor.CentreRight, + AutoSizeAxes = Axes.Both, + Direction = FillDirection.Horizontal, + ChildrenEnumerable = Score.Mods.AsOrdered().Select(mod => new ModIcon(mod) { Scale = new Vector2(0.34f) }) + }, + } }, }, }, @@ -330,7 +341,7 @@ namespace osu.Game.Online.Leaderboards private partial class ScoreComponentLabel : Container, IHasTooltip { - private const float icon_size = 20; + private const float icon_size = 16; private readonly FillFlowContainer content; public override bool Contains(Vector2 screenSpacePos) => content.Contains(screenSpacePos); @@ -346,7 +357,7 @@ namespace osu.Game.Online.Leaderboards { AutoSizeAxes = Axes.Both, Direction = FillDirection.Horizontal, - Padding = new MarginPadding { Right = 10 }, + Padding = new MarginPadding { Right = 5 }, Children = new Drawable[] { new Container @@ -381,7 +392,8 @@ namespace osu.Game.Online.Leaderboards Anchor = Anchor.CentreLeft, Origin = Anchor.CentreLeft, Text = statistic.Value, - Font = OsuFont.GetFont(size: 17, weight: FontWeight.Bold, fixedWidth: true) + Spacing = new Vector2(-1, 0), + Font = OsuFont.GetFont(size: 14, weight: FontWeight.Bold, fixedWidth: true) }, }, }; @@ -412,7 +424,7 @@ namespace osu.Game.Online.Leaderboards public DateLabel(DateTimeOffset date) : base(date) { - Font = OsuFont.GetFont(size: 17, weight: FontWeight.Bold, italics: true); + Font = OsuFont.GetFont(size: 13, weight: FontWeight.Bold, italics: true); } protected override string Format() => Date.ToShortRelativeTime(TimeSpan.FromSeconds(30)); From 4e043e7cabc242b051275e84b17b88553c28844b Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 14 Feb 2025 18:35:27 +0900 Subject: [PATCH 1040/1275] Change how values are applied to (hopefully) simplify things --- osu.Game/Graphics/Containers/ScalingContainer.cs | 3 ++- osu.Game/OsuGame.cs | 6 ++++-- osu.iOS/OsuGameIOS.cs | 3 ++- 3 files changed, 8 insertions(+), 4 deletions(-) diff --git a/osu.Game/Graphics/Containers/ScalingContainer.cs b/osu.Game/Graphics/Containers/ScalingContainer.cs index ac76c0546b..2a5ce23b64 100644 --- a/osu.Game/Graphics/Containers/ScalingContainer.cs +++ b/osu.Game/Graphics/Containers/ScalingContainer.cs @@ -116,7 +116,8 @@ namespace osu.Game.Graphics.Containers protected override void Update() { - TargetDrawSize = new Vector2(1024, 1024 / (game?.BaseAspectRatio ?? 1f)); + if (game != null) + TargetDrawSize = game.ScalingContainerTargetDrawSize; Scale = new Vector2(CurrentScale); Size = new Vector2(1 / CurrentScale); diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs index ecc71822af..d379392a7d 100644 --- a/osu.Game/OsuGame.cs +++ b/osu.Game/OsuGame.cs @@ -72,6 +72,7 @@ using osu.Game.Skinning; using osu.Game.Updater; using osu.Game.Users; using osu.Game.Utils; +using osuTK; using osuTK.Graphics; using Sentry; @@ -814,9 +815,10 @@ namespace osu.Game protected virtual UpdateManager CreateUpdateManager() => new UpdateManager(); /// - /// The base aspect ratio to use in all s. + /// Adjust the globally applied in every . + /// Useful for changing how the game handles different aspect ratios. /// - protected internal virtual float BaseAspectRatio => 4f / 3f; + protected internal virtual Vector2 ScalingContainerTargetDrawSize { get; } = new Vector2(1024, 768); protected override Container CreateScalingContainer() => new ScalingContainer(ScalingMode.Everything); diff --git a/osu.iOS/OsuGameIOS.cs b/osu.iOS/OsuGameIOS.cs index 64b2292d62..883e89e38a 100644 --- a/osu.iOS/OsuGameIOS.cs +++ b/osu.iOS/OsuGameIOS.cs @@ -11,6 +11,7 @@ using osu.Game; using osu.Game.Screens; using osu.Game.Updater; using osu.Game.Utils; +using osuTK; using UIKit; namespace osu.iOS @@ -22,7 +23,7 @@ namespace osu.iOS public override bool HideUnlicensedContent => true; - protected override float BaseAspectRatio => (float)(UIScreen.MainScreen.Bounds.Width / UIScreen.MainScreen.Bounds.Height); + protected override Vector2 ScalingContainerTargetDrawSize => new Vector2(1024, 1024 * DrawHeight / DrawWidth); public OsuGameIOS(AppDelegate appDelegate) { From 248bf43ec9c84d2ea31eb0c51cf814760d79e035 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 14 Feb 2025 18:35:43 +0900 Subject: [PATCH 1041/1275] Apply nullability to `ScalingContainer` --- .../Graphics/Containers/ScalingContainer.cs | 33 +++++++++---------- 1 file changed, 15 insertions(+), 18 deletions(-) diff --git a/osu.Game/Graphics/Containers/ScalingContainer.cs b/osu.Game/Graphics/Containers/ScalingContainer.cs index 2a5ce23b64..9d2a1c16af 100644 --- a/osu.Game/Graphics/Containers/ScalingContainer.cs +++ b/osu.Game/Graphics/Containers/ScalingContainer.cs @@ -1,9 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - -using JetBrains.Annotations; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; @@ -27,17 +24,17 @@ namespace osu.Game.Graphics.Containers { internal const float TRANSITION_DURATION = 500; - private Bindable sizeX; - private Bindable sizeY; - private Bindable posX; - private Bindable posY; - private Bindable applySafeAreaPadding; + private Bindable sizeX = null!; + private Bindable sizeY = null!; + private Bindable posX = null!; + private Bindable posY = null!; + private Bindable applySafeAreaPadding = null!; - private Bindable safeAreaPadding; + private Bindable safeAreaPadding = null!; private readonly ScalingMode? targetMode; - private Bindable scalingMode; + private Bindable scalingMode = null!; private readonly Container content; protected override Container Content => content; @@ -46,9 +43,9 @@ namespace osu.Game.Graphics.Containers private readonly Container sizableContainer; - private BackgroundScreenStack backgroundStack; + private BackgroundScreenStack? backgroundStack; - private Bindable scalingMenuBackgroundDim; + private Bindable scalingMenuBackgroundDim = null!; private RectangleF? customRect; private bool customRectIsRelativePosition; @@ -89,7 +86,8 @@ namespace osu.Game.Graphics.Containers public partial class ScalingDrawSizePreservingFillContainer : DrawSizePreservingFillContainer { private readonly bool applyUIScale; - private Bindable uiScale; + + private Bindable? uiScale; protected float CurrentScale { get; private set; } = 1; @@ -101,8 +99,7 @@ namespace osu.Game.Graphics.Containers } [Resolved(canBeNull: true)] - [CanBeNull] - private OsuGame game { get; set; } + private OsuGame? game { get; set; } [BackgroundDependencyLoader] private void load(OsuConfigManager osuConfig) @@ -240,13 +237,13 @@ namespace osu.Game.Graphics.Containers private partial class SizeableAlwaysInputContainer : Container { [Resolved] - private GameHost host { get; set; } + private GameHost host { get; set; } = null!; [Resolved] - private ISafeArea safeArea { get; set; } + private ISafeArea safeArea { get; set; } = null!; [Resolved] - private OsuConfigManager config { get; set; } + private OsuConfigManager config { get; set; } = null!; private readonly bool confineHostCursor; private readonly LayoutValue cursorRectCache = new LayoutValue(Invalidation.RequiredParentSizeToFit); From 26a2d0394e5d39de630524166691d86a929a501f Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 14 Feb 2025 19:04:26 +0900 Subject: [PATCH 1042/1275] Invalidate drawable on potential presence change --- osu.Game/Users/Drawables/UpdateableTeamFlag.cs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/osu.Game/Users/Drawables/UpdateableTeamFlag.cs b/osu.Game/Users/Drawables/UpdateableTeamFlag.cs index 486cb697a1..1efde2af68 100644 --- a/osu.Game/Users/Drawables/UpdateableTeamFlag.cs +++ b/osu.Game/Users/Drawables/UpdateableTeamFlag.cs @@ -21,7 +21,11 @@ namespace osu.Game.Users.Drawables public APITeam? Team { get => Model; - set => Model = value; + set + { + Model = value; + Invalidate(Invalidation.Presence); + } } protected override double LoadDelay => 200; From 82c16dee60e1e8702d95657d654d36934c083ac2 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 14 Feb 2025 19:05:13 +0900 Subject: [PATCH 1043/1275] Add missing `LongRunningLoad` attribute --- .../Users/Drawables/UpdateableTeamFlag.cs | 27 +++++++++---------- 1 file changed, 12 insertions(+), 15 deletions(-) diff --git a/osu.Game/Users/Drawables/UpdateableTeamFlag.cs b/osu.Game/Users/Drawables/UpdateableTeamFlag.cs index 1efde2af68..9c2bbb7e3e 100644 --- a/osu.Game/Users/Drawables/UpdateableTeamFlag.cs +++ b/osu.Game/Users/Drawables/UpdateableTeamFlag.cs @@ -42,18 +42,7 @@ namespace osu.Game.Users.Drawables if (team == null) return Empty(); - return new Container - { - RelativeSizeAxes = Axes.Both, - Children = new Drawable[] - { - new TeamFlag(team) - { - RelativeSizeAxes = Axes.Both - }, - new HoverClickSounds() - } - }; + return new TeamFlag(team) { RelativeSizeAxes = Axes.Both }; } // Generally we just want team flags to disappear if the user doesn't have one. @@ -67,7 +56,8 @@ namespace osu.Game.Users.Drawables CornerRadius = DrawHeight / 8; } - public partial class TeamFlag : Sprite, IHasTooltip + [LongRunningLoad] + public partial class TeamFlag : CompositeDrawable, IHasTooltip { private readonly APITeam team; @@ -82,8 +72,15 @@ namespace osu.Game.Users.Drawables [BackgroundDependencyLoader] private void load(LargeTextureStore textures) { - if (!string.IsNullOrEmpty(team.Name)) - Texture = textures.Get(team.FlagUrl); + InternalChildren = new Drawable[] + { + new HoverClickSounds(), + new Sprite + { + RelativeSizeAxes = Axes.Both, + Texture = textures.Get(team.FlagUrl) + } + }; } } } From b86eeabef08d8eb3d45848939f2ea36a44790cc7 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 14 Feb 2025 19:07:02 +0900 Subject: [PATCH 1044/1275] Fix one more misalignment on leaderboard scores --- osu.Game/Online/Leaderboards/LeaderboardScore.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/osu.Game/Online/Leaderboards/LeaderboardScore.cs b/osu.Game/Online/Leaderboards/LeaderboardScore.cs index 0181c28218..fc30f158f0 100644 --- a/osu.Game/Online/Leaderboards/LeaderboardScore.cs +++ b/osu.Game/Online/Leaderboards/LeaderboardScore.cs @@ -180,6 +180,7 @@ namespace osu.Game.Online.Leaderboards Height = 28, Direction = FillDirection.Horizontal, Spacing = new Vector2(10f, 0f), + Margin = new MarginPadding { Bottom = -2 }, Children = new Drawable[] { flagBadgeAndDateContainer = new FillFlowContainer From 1b5101ed5e155c19c0a37894ed3c5ea374ec55a5 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 14 Feb 2025 19:30:23 +0900 Subject: [PATCH 1045/1275] Add team flag display to rankings overlays --- osu.Game/Overlays/KudosuTable.cs | 4 ++-- .../Overlays/Rankings/Tables/CountriesTable.cs | 2 +- .../Overlays/Rankings/Tables/RankingsTable.cs | 17 +++++++---------- .../Overlays/Rankings/Tables/UserBasedTable.cs | 6 ++++-- 4 files changed, 14 insertions(+), 15 deletions(-) diff --git a/osu.Game/Overlays/KudosuTable.cs b/osu.Game/Overlays/KudosuTable.cs index 93884435a4..d6eaf586b9 100644 --- a/osu.Game/Overlays/KudosuTable.cs +++ b/osu.Game/Overlays/KudosuTable.cs @@ -80,7 +80,7 @@ namespace osu.Game.Overlays protected override CountryCode GetCountryCode(APIUser item) => item.CountryCode; - protected override Drawable CreateFlagContent(APIUser item) + protected override Drawable[] CreateFlagContent(APIUser item) { var username = new LinkFlowContainer(t => t.Font = OsuFont.GetFont(size: TEXT_SIZE, italics: true)) { @@ -89,7 +89,7 @@ namespace osu.Game.Overlays TextAnchor = Anchor.CentreLeft }; username.AddUserLink(item); - return username; + return [username]; } } } diff --git a/osu.Game/Overlays/Rankings/Tables/CountriesTable.cs b/osu.Game/Overlays/Rankings/Tables/CountriesTable.cs index fb3e58d2ac..733aa7ca54 100644 --- a/osu.Game/Overlays/Rankings/Tables/CountriesTable.cs +++ b/osu.Game/Overlays/Rankings/Tables/CountriesTable.cs @@ -34,7 +34,7 @@ namespace osu.Game.Overlays.Rankings.Tables protected override CountryCode GetCountryCode(CountryStatistics item) => item.Code; - protected override Drawable CreateFlagContent(CountryStatistics item) => new CountryName(item.Code); + protected override Drawable[] CreateFlagContent(CountryStatistics item) => [new CountryName(item.Code)]; protected override Drawable[] CreateAdditionalContent(CountryStatistics item) => new Drawable[] { diff --git a/osu.Game/Overlays/Rankings/Tables/RankingsTable.cs b/osu.Game/Overlays/Rankings/Tables/RankingsTable.cs index b9f7e443ca..f4ed41800a 100644 --- a/osu.Game/Overlays/Rankings/Tables/RankingsTable.cs +++ b/osu.Game/Overlays/Rankings/Tables/RankingsTable.cs @@ -80,7 +80,7 @@ namespace osu.Game.Overlays.Rankings.Tables protected abstract CountryCode GetCountryCode(TModel item); - protected abstract Drawable CreateFlagContent(TModel item); + protected abstract Drawable[] CreateFlagContent(TModel item); private OsuSpriteText createIndexDrawable(int index) => new RowText { @@ -92,16 +92,13 @@ namespace osu.Game.Overlays.Rankings.Tables { AutoSizeAxes = Axes.Both, Direction = FillDirection.Horizontal, - Spacing = new Vector2(10, 0), + Spacing = new Vector2(5, 0), Margin = new MarginPadding { Bottom = row_spacing }, - Children = new[] - { - new UpdateableFlag(GetCountryCode(item)) - { - Size = new Vector2(28, 20), - }, - CreateFlagContent(item) - } + Children = + [ + new UpdateableFlag(GetCountryCode(item)) { Size = new Vector2(28, 20) }, + ..CreateFlagContent(item) + ] }; protected class RankingsTableColumn : TableColumn diff --git a/osu.Game/Overlays/Rankings/Tables/UserBasedTable.cs b/osu.Game/Overlays/Rankings/Tables/UserBasedTable.cs index 4d25065578..c651108ec3 100644 --- a/osu.Game/Overlays/Rankings/Tables/UserBasedTable.cs +++ b/osu.Game/Overlays/Rankings/Tables/UserBasedTable.cs @@ -14,6 +14,8 @@ using osu.Game.Users; using osu.Game.Scoring; using osu.Framework.Localisation; using osu.Game.Resources.Localisation.Web; +using osu.Game.Users.Drawables; +using osuTK; namespace osu.Game.Overlays.Rankings.Tables { @@ -61,7 +63,7 @@ namespace osu.Game.Overlays.Rankings.Tables protected sealed override CountryCode GetCountryCode(UserStatistics item) => item.User.CountryCode; - protected sealed override Drawable CreateFlagContent(UserStatistics item) + protected sealed override Drawable[] CreateFlagContent(UserStatistics item) { var username = new LinkFlowContainer(t => t.Font = OsuFont.GetFont(size: TEXT_SIZE, italics: true)) { @@ -70,7 +72,7 @@ namespace osu.Game.Overlays.Rankings.Tables TextAnchor = Anchor.CentreLeft }; username.AddUserLink(item.User); - return username; + return [new UpdateableTeamFlag(item.User.Team) { Size = new Vector2(40, 20) }, username]; } protected sealed override Drawable[] CreateAdditionalContent(UserStatistics item) => new[] From 55809f5e0d7429dcaf8a59d6c1c82323bc8055de Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Fri, 14 Feb 2025 06:15:32 -0500 Subject: [PATCH 1046/1275] Apply changes to Android --- osu.Android/OsuGameAndroid.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/osu.Android/OsuGameAndroid.cs b/osu.Android/OsuGameAndroid.cs index 0f2451f0a0..e725f9245f 100644 --- a/osu.Android/OsuGameAndroid.cs +++ b/osu.Android/OsuGameAndroid.cs @@ -12,6 +12,7 @@ using osu.Game; using osu.Game.Screens; using osu.Game.Updater; using osu.Game.Utils; +using osuTK; namespace osu.Android { @@ -20,6 +21,8 @@ namespace osu.Android [Cached] private readonly OsuGameActivity gameActivity; + protected override Vector2 ScalingContainerTargetDrawSize => new Vector2(1024, 1024 * DrawHeight / DrawWidth); + public OsuGameAndroid(OsuGameActivity activity) : base(null) { From 27b9a6b7a386fb975df780dfd78d3ce3bcf114e9 Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Fri, 14 Feb 2025 06:15:56 -0500 Subject: [PATCH 1047/1275] Reset UI scale for mobile platforms --- .../.idea/deploymentTargetSelector.xml | 10 ++++++++++ osu.Game/Configuration/OsuConfigManager.cs | 13 ++++++++----- 2 files changed, 18 insertions(+), 5 deletions(-) create mode 100644 .idea/.idea.osu.Android/.idea/deploymentTargetSelector.xml diff --git a/.idea/.idea.osu.Android/.idea/deploymentTargetSelector.xml b/.idea/.idea.osu.Android/.idea/deploymentTargetSelector.xml new file mode 100644 index 0000000000..4432459b86 --- /dev/null +++ b/.idea/.idea.osu.Android/.idea/deploymentTargetSelector.xml @@ -0,0 +1,10 @@ + + + + + + + + + \ No newline at end of file diff --git a/osu.Game/Configuration/OsuConfigManager.cs b/osu.Game/Configuration/OsuConfigManager.cs index 1244dd8cfc..76d06f3665 100644 --- a/osu.Game/Configuration/OsuConfigManager.cs +++ b/osu.Game/Configuration/OsuConfigManager.cs @@ -238,7 +238,7 @@ namespace osu.Game.Configuration public void Migrate() { - // arrives as 2020.123.0 + // arrives as 2020.123.0-lazer string rawVersion = Get(OsuSetting.Version); if (rawVersion.Length < 6) @@ -251,11 +251,14 @@ namespace osu.Game.Configuration if (!int.TryParse(pieces[0], out int year)) return; if (!int.TryParse(pieces[1], out int monthDay)) return; - // ReSharper disable once UnusedVariable - int combined = (year * 10000) + monthDay; + int combined = year * 10000 + monthDay; - // migrations can be added here using a condition like: - // if (combined < 20220103) { performMigration() } + if (combined < 20250214) + { + // UI scaling on mobile platforms has been internally adjusted such that 1x UI scale looks correctly zoomed in than before. + if (RuntimeInfo.IsMobile) + GetBindable(OsuSetting.UIScale).SetDefault(); + } } public override TrackedSettings CreateTrackedSettings() From ef2f482d041840bb4875a19b3f9a351b0415a63c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 14 Feb 2025 12:40:54 +0100 Subject: [PATCH 1048/1275] Fix skin deserialisation test --- .../Archives/modified-argon-20250214.osk | Bin 0 -> 1724 bytes osu.Game.Tests/Skins/SkinDeserialisationTest.cs | 2 ++ 2 files changed, 2 insertions(+) create mode 100644 osu.Game.Tests/Resources/Archives/modified-argon-20250214.osk diff --git a/osu.Game.Tests/Resources/Archives/modified-argon-20250214.osk b/osu.Game.Tests/Resources/Archives/modified-argon-20250214.osk new file mode 100644 index 0000000000000000000000000000000000000000..74abef25caa81004c11911a9d223909871da3299 GIT binary patch literal 1724 zcmZ{kcTm%37{}kRks%4gNm2GDEGx7^gtCHxpTQugOo0$aLJ|bQE6SEIDk>ud2OBhk ztYARqDVq|3NCgB%L8eH6AmZrhwe;l9?_bY7>z?Pidp?jozkmz?Km?5WIGiGex*8P# z0NTEJ0H6jEh`IzKK_#VfM;l5?aC4J}*A%pStsNKY9OfLLBo|0LE4dUlMLGCBST5>n zv*#nCXTrwAaa_D*hrh9aeB&6q)F(3~J(7LJ7|WB0e`}`C-b;%xFrb ze8>?icyFh+%pMMD?(jj1stp}Y9j^?@57FN&EB#UsGk_^-87pVFdH%%Ncv_vq9$9M( zN*p8HAzyEpQJ;QPwytR$#M1YGzFP#|eY+_P0FVX%kob2I0@0788$cxy?@nVOh-=@A z!Bt_QZarPxiOa{)+UWlNAbb&z z`OsrmGWcm9gl%nT4$E~Xn^{n7ip9sdEOxuzNbx;UX{WZ;jS}v|YGzjOaF5_jX89G? z+Am!#^)&b;`@pu&f%=cgDqSS}qf##-8@ik~95FL8&nPjkyT=`!7pE3e;;51=i?+xc zc4odtQNlfeWd4u2Z&J=(aIAKWD-4)w89YMRY&^6Y>)nZ&uX<;Af5B#SI5qDxg2qU^ zb7r`aCdxl3kVbefUvRO|Fk>!kiDCL)g{_RMe>rsTZ@u%mJBE0z3Cl+Y;TBc{sDw^`3Z?w9O%RsQ6$7y6la z-h*bxTaL^T4NZMX1Pnv;QSI9$oebEi@rtXD;JUQs{funF&gE ze)n{l=~~kgaiY=bBpZE;eGmq`;WKPIn=NwZ+5nl2yPnTnd>{e$JB_c^s9D6TEzn{0 zg>w@5E#^o03pgpBk0}u5g6p#~MGs8!h&juBUCzq-k$R$Skx|pj1IbaJGf7%r$+Vc+ zOtfiHQ2Z$!w~SKXN$37u@WwbL(d>HJfFvnj0a6VEU8}87=``!aw>iY?4(i+dL|?2^ zEc>igvxsUKNs;=Me%lr@kB{{p3ICIy!xXweom;58ADYVI1Pcu!$^^O;pNr<^)8U4L z^@Atmm`yL#34=8ZtyX5Z;Rkn5iPbmSrPp>P=%JrBvRx1f6RG1Xg;|r}-Co;6e#!(L zw!6PX(9%#v)^wV63onM9dvg7cTD(cqd_tH@&TQpW4tYI%!55dM+t18eYd)@b(Yzv3 zmi+c}r$V4*I>u`BE~s!UvT)m0NKO%WWbNHy`|`{)_)M+UbUT(TZ-=5PR#A4+v&BX=m6M{ iShl^#_N#0u+F5Y>jUanLp|5cPAOMyD0JVZ&v;P5v*XV5k literal 0 HcmV?d00001 diff --git a/osu.Game.Tests/Skins/SkinDeserialisationTest.cs b/osu.Game.Tests/Skins/SkinDeserialisationTest.cs index 55836302e6..5b343c80c5 100644 --- a/osu.Game.Tests/Skins/SkinDeserialisationTest.cs +++ b/osu.Game.Tests/Skins/SkinDeserialisationTest.cs @@ -73,6 +73,8 @@ namespace osu.Game.Tests.Skins "Archives/modified-default-20241207.osk", // Covers skinnable spectator list "Archives/modified-argon-20250116.osk", + // Covers player team flag + "Archives/modified-argon-20250214.osk", }; /// From 4e66536ae8a65219c971202addb6394c6744d1fc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 14 Feb 2025 15:52:05 +0100 Subject: [PATCH 1049/1275] Fix failed scores with no hits on beatmaps with ridiculous mod combinations showing hundreds of pp points awarded (#31741) See https://discord.com/channels/188630481301012481/1097318920991559880/1334716356582572074. On `master` this is actually worse and shows thousands of pp points, so I guess `pp-dev` is a comparable improvement, but still flagrantly wrong. The reason why `pp-dev` is better is the `speedDeviation == null` guard at the start of `computeSpeedValue()` which turns off the rest of the calculation, therefore not exposing the bug where `relevantTotalDiff` can go negative. I still guarded it in this commit just for safety's sake given it is clear it can do very wrong stuff. --- osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs b/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs index 09ec890926..a667d12a44 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs @@ -273,7 +273,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty speedValue *= speedHighDeviationMultiplier; // Calculate accuracy assuming the worst case scenario - double relevantTotalDiff = totalHits - attributes.SpeedNoteCount; + double relevantTotalDiff = Math.Max(0, totalHits - attributes.SpeedNoteCount); double relevantCountGreat = Math.Max(0, countGreat - relevantTotalDiff); double relevantCountOk = Math.Max(0, countOk - Math.Max(0, relevantTotalDiff - countGreat)); double relevantCountMeh = Math.Max(0, countMeh - Math.Max(0, relevantTotalDiff - countGreat - countOk)); @@ -297,7 +297,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty amountHitObjectsWithAccuracy += attributes.SliderCount; if (amountHitObjectsWithAccuracy > 0) - betterAccuracyPercentage = ((countGreat - (totalHits - amountHitObjectsWithAccuracy)) * 6 + countOk * 2 + countMeh) / (double)(amountHitObjectsWithAccuracy * 6); + betterAccuracyPercentage = ((countGreat - Math.Max(totalHits - amountHitObjectsWithAccuracy, 0)) * 6 + countOk * 2 + countMeh) / (double)(amountHitObjectsWithAccuracy * 6); else betterAccuracyPercentage = 0; From b21dd01de7263ecb6fa2817409b23e9eb16427c6 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sat, 15 Feb 2025 00:03:41 +0900 Subject: [PATCH 1050/1275] Use fixed width for digital clock display Supersedes and closes https://github.com/ppy/osu/pull/31093. --- .../Overlays/Toolbar/DigitalClockDisplay.cs | 39 ++++++++++++++----- 1 file changed, 30 insertions(+), 9 deletions(-) diff --git a/osu.Game/Overlays/Toolbar/DigitalClockDisplay.cs b/osu.Game/Overlays/Toolbar/DigitalClockDisplay.cs index ada2f6ff86..bd1c944847 100644 --- a/osu.Game/Overlays/Toolbar/DigitalClockDisplay.cs +++ b/osu.Game/Overlays/Toolbar/DigitalClockDisplay.cs @@ -7,8 +7,10 @@ using System; using osu.Framework.Allocation; using osu.Framework.Extensions.LocalisationExtensions; using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; using osu.Game.Graphics; using osu.Game.Graphics.Sprites; +using osuTK; namespace osu.Game.Overlays.Toolbar { @@ -17,6 +19,8 @@ namespace osu.Game.Overlays.Toolbar private OsuSpriteText realTime; private OsuSpriteText gameTime; + private FillFlowContainer runningText; + private bool showRuntime = true; public bool ShowRuntime @@ -52,17 +56,36 @@ namespace osu.Game.Overlays.Toolbar [BackgroundDependencyLoader] private void load(OsuColour colours) { - AutoSizeAxes = Axes.Y; + AutoSizeAxes = Axes.Both; InternalChildren = new Drawable[] { - realTime = new OsuSpriteText(), - gameTime = new OsuSpriteText + realTime = new OsuSpriteText + { + Font = OsuFont.Default.With(fixedWidth: true), + Spacing = new Vector2(-1.5f, 0), + }, + runningText = new FillFlowContainer { Y = 14, Colour = colours.PinkLight, - Font = OsuFont.Default.With(size: 10, weight: FontWeight.SemiBold), - } + AutoSizeAxes = Axes.Both, + Direction = FillDirection.Horizontal, + Spacing = new Vector2(2, 0), + Children = new Drawable[] + { + new OsuSpriteText + { + Text = "running", + Font = OsuFont.Default.With(size: 10, weight: FontWeight.SemiBold), + }, + gameTime = new OsuSpriteText + { + Font = OsuFont.Default.With(size: 10, fixedWidth: true, weight: FontWeight.SemiBold), + Spacing = new Vector2(-0.5f, 0), + } + } + }, }; updateMetrics(); @@ -71,14 +94,12 @@ namespace osu.Game.Overlays.Toolbar protected override void UpdateDisplay(DateTimeOffset now) { realTime.Text = now.ToLocalisableString(use24HourDisplay ? @"HH:mm:ss" : @"h:mm:ss tt"); - gameTime.Text = $"running {new TimeSpan(TimeSpan.TicksPerSecond * (int)(Clock.CurrentTime / 1000)):c}"; + gameTime.Text = $"{new TimeSpan(TimeSpan.TicksPerSecond * (int)(Clock.CurrentTime / 1000)):c}"; } private void updateMetrics() { - Width = showRuntime || !use24HourDisplay ? 66 : 45; // Allows for space for game time up to 99 days (in the padding area since this is quite rare). - - gameTime.FadeTo(showRuntime ? 1 : 0); + runningText.FadeTo(showRuntime ? 1 : 0); } } } From 7eb32ef35139793b5513c792eb3a5608fee3c207 Mon Sep 17 00:00:00 2001 From: Joseph Madamba Date: Sun, 16 Feb 2025 13:43:16 -0800 Subject: [PATCH 1051/1275] Fix team flag layout on user profile --- .../Profile/Header/TopHeaderContainer.cs | 60 +++++++++++++------ 1 file changed, 43 insertions(+), 17 deletions(-) diff --git a/osu.Game/Overlays/Profile/Header/TopHeaderContainer.cs b/osu.Game/Overlays/Profile/Header/TopHeaderContainer.cs index 5f404375e6..d6bc726c18 100644 --- a/osu.Game/Overlays/Profile/Header/TopHeaderContainer.cs +++ b/osu.Game/Overlays/Profile/Header/TopHeaderContainer.cs @@ -42,9 +42,10 @@ namespace osu.Game.Overlays.Profile.Header private ExternalLinkButton openUserExternally = null!; private OsuSpriteText titleText = null!; private UpdateableFlag userFlag = null!; - private UpdateableTeamFlag teamFlag = null!; private OsuHoverContainer userCountryContainer = null!; private OsuSpriteText userCountryText = null!; + private UpdateableTeamFlag teamFlag = null!; + private OsuSpriteText teamText = null!; private GroupBadgeFlow groupBadgeFlow = null!; private ToggleCoverButton coverToggle = null!; private PreviousUsernamesDisplay previousUsernamesDisplay = null!; @@ -161,27 +162,51 @@ namespace osu.Game.Overlays.Profile.Header { AutoSizeAxes = Axes.Both, Direction = FillDirection.Horizontal, + Spacing = new Vector2(10, 0), Children = new Drawable[] { - userFlag = new UpdateableFlag - { - Size = new Vector2(28, 20), - }, - teamFlag = new UpdateableTeamFlag - { - Size = new Vector2(40, 20), - }, - userCountryContainer = new OsuHoverContainer + new FillFlowContainer { AutoSizeAxes = Axes.Both, - Anchor = Anchor.CentreLeft, - Origin = Anchor.CentreLeft, - Margin = new MarginPadding { Left = 5 }, - Child = userCountryText = new OsuSpriteText + Direction = FillDirection.Horizontal, + Spacing = new Vector2(4, 0), + Children = new Drawable[] { - Font = OsuFont.GetFont(size: 14f, weight: FontWeight.Regular), - }, + userFlag = new UpdateableFlag + { + Size = new Vector2(28, 20), + }, + userCountryContainer = new OsuHoverContainer + { + AutoSizeAxes = Axes.Both, + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + Child = userCountryText = new OsuSpriteText + { + Font = OsuFont.GetFont(size: 14f, weight: FontWeight.Regular), + }, + }, + } }, + new FillFlowContainer + { + AutoSizeAxes = Axes.Both, + Direction = FillDirection.Horizontal, + Spacing = new Vector2(4, 0), + Children = new Drawable[] + { + teamFlag = new UpdateableTeamFlag + { + Size = new Vector2(40, 20), + }, + teamText = new OsuSpriteText + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + Font = OsuFont.GetFont(size: 14f, weight: FontWeight.Regular), + }, + } + } } }, } @@ -220,9 +245,10 @@ namespace osu.Game.Overlays.Profile.Header usernameText.Text = user?.Username ?? string.Empty; openUserExternally.Link = $@"{api.Endpoints.WebsiteUrl}/users/{user?.Id ?? 0}"; userFlag.CountryCode = user?.CountryCode ?? default; - teamFlag.Team = user?.Team; userCountryText.Text = (user?.CountryCode ?? default).GetDescription(); userCountryContainer.Action = () => rankingsOverlay?.ShowCountry(user?.CountryCode ?? default); + teamFlag.Team = user?.Team; + teamText.Text = user?.Team?.Name ?? string.Empty; supporterTag.SupportLevel = user?.SupportLevel ?? 0; titleText.Text = user?.Title ?? string.Empty; titleText.Colour = Color4Extensions.FromHex(user?.Colour ?? "fff"); From 1b333ad51c2147f5ab950a03f7de49a07721c01a Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Sun, 16 Feb 2025 17:53:34 -0500 Subject: [PATCH 1052/1275] Add sample team to user profile test scene --- .../Visual/Online/TestSceneUserProfileOverlay.cs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/osu.Game.Tests/Visual/Online/TestSceneUserProfileOverlay.cs b/osu.Game.Tests/Visual/Online/TestSceneUserProfileOverlay.cs index d16ed46bd2..a4a9816337 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneUserProfileOverlay.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneUserProfileOverlay.cs @@ -346,6 +346,13 @@ namespace osu.Game.Tests.Visual.Online Twitter = "test_user", Discord = "test_user", Website = "https://google.com", + Team = new APITeam + { + Id = 1, + Name = "Collective Wangs", + ShortName = "WANG", + FlagUrl = "https://assets.ppy.sh/teams/logo/1/wanglogo.jpg", + } }; } } From afc2c521955f00654c3c824bebe294afabf4d221 Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Sun, 16 Feb 2025 17:55:10 -0500 Subject: [PATCH 1053/1275] Add proper spacing between username, title, and country/team row --- osu.Game/Overlays/Profile/Header/TopHeaderContainer.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/osu.Game/Overlays/Profile/Header/TopHeaderContainer.cs b/osu.Game/Overlays/Profile/Header/TopHeaderContainer.cs index d6bc726c18..3d9539ce1f 100644 --- a/osu.Game/Overlays/Profile/Header/TopHeaderContainer.cs +++ b/osu.Game/Overlays/Profile/Header/TopHeaderContainer.cs @@ -156,10 +156,11 @@ namespace osu.Game.Overlays.Profile.Header titleText = new OsuSpriteText { Font = OsuFont.GetFont(size: 16, weight: FontWeight.Regular), - Margin = new MarginPadding { Bottom = 5 } + Margin = new MarginPadding { Bottom = 3 }, }, new FillFlowContainer { + Margin = new MarginPadding { Top = 3 }, AutoSizeAxes = Axes.Both, Direction = FillDirection.Horizontal, Spacing = new Vector2(10, 0), From d5566831d22fb170e1a21e522c84863eb788cd7e Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 17 Feb 2025 15:06:35 +0900 Subject: [PATCH 1054/1275] Stop beat divisor "slider" from accepting focus --- osu.Game/Screens/Edit/Compose/Components/BeatDivisorControl.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/osu.Game/Screens/Edit/Compose/Components/BeatDivisorControl.cs b/osu.Game/Screens/Edit/Compose/Components/BeatDivisorControl.cs index 43a2abe4c4..b8f2695259 100644 --- a/osu.Game/Screens/Edit/Compose/Components/BeatDivisorControl.cs +++ b/osu.Game/Screens/Edit/Compose/Components/BeatDivisorControl.cs @@ -398,6 +398,8 @@ namespace osu.Game.Screens.Edit.Compose.Components private readonly BindableBeatDivisor beatDivisor; + public override bool AcceptsFocus => false; + public TickSliderBar(BindableBeatDivisor beatDivisor) { CurrentNumber.BindTo(this.beatDivisor = beatDivisor); From 2738221c0b9ab579623a02d9f9b6ef7d0cd45dd6 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 17 Feb 2025 15:07:21 +0900 Subject: [PATCH 1055/1275] 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 6bbd432ee7..f4d49763ab 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 ca2604858c..0d95dfbd06 100644 --- a/osu.iOS.props +++ b/osu.iOS.props @@ -17,6 +17,6 @@ -all - + From db4a4a1723b48f64bb88c5289c143f9b13705e0a Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 17 Feb 2025 15:09:51 +0900 Subject: [PATCH 1056/1275] Minor bump some packages --- .../osu.Game.Rulesets.EmptyFreeform.Tests.csproj | 2 +- .../osu.Game.Rulesets.Pippidon.Tests.csproj | 2 +- ...osu.Game.Rulesets.EmptyScrolling.Tests.csproj | 2 +- .../osu.Game.Rulesets.Pippidon.Tests.csproj | 2 +- osu.Desktop/osu.Desktop.csproj | 2 +- .../osu.Game.Rulesets.Catch.Tests.csproj | 2 +- .../osu.Game.Rulesets.Mania.Tests.csproj | 2 +- .../osu.Game.Rulesets.Osu.Tests.csproj | 2 +- .../osu.Game.Rulesets.Taiko.Tests.csproj | 2 +- .../Navigation/TestSceneScreenNavigation.cs | 2 +- .../TestSceneAddPlaylistToCollectionButton.cs | 7 +++++-- osu.Game.Tests/osu.Game.Tests.csproj | 2 +- .../osu.Game.Tournament.Tests.csproj | 2 +- .../Profile/Header/Components/FollowersButton.cs | 2 +- osu.Game/osu.Game.csproj | 16 ++++++++-------- 15 files changed, 26 insertions(+), 23 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 1d368e9bd1..86f73a37d4 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,7 +9,7 @@ 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 d69bc78b8f..51c0233942 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,7 +9,7 @@ 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 7ac269f65f..ed4e8631ea 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,7 +9,7 @@ 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 d69bc78b8f..51c0233942 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,7 +9,7 @@ false - + diff --git a/osu.Desktop/osu.Desktop.csproj b/osu.Desktop/osu.Desktop.csproj index 21c570a7b2..05d5bb19fb 100644 --- a/osu.Desktop/osu.Desktop.csproj +++ b/osu.Desktop/osu.Desktop.csproj @@ -24,7 +24,7 @@ - + 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 56ee208670..fc1b13f3ad 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,7 +1,7 @@  - + 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 5e4bad279b..edb01b044e 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,7 +1,7 @@  - + 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 267dc98985..6510568555 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,7 +1,7 @@  - + 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 523df4c259..e498989a79 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,7 +1,7 @@  - + diff --git a/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs b/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs index 88b482ab4c..8c4fcc461c 100644 --- a/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs +++ b/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs @@ -11,6 +11,7 @@ using NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Configuration; using osu.Framework.Extensions; +using osu.Framework.Extensions.IEnumerableExtensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.UserInterface; @@ -52,7 +53,6 @@ using osu.Game.Tests.Resources; using osu.Game.Utils; using osuTK; using osuTK.Input; -using SharpCompress; namespace osu.Game.Tests.Visual.Navigation { diff --git a/osu.Game.Tests/Visual/Playlists/TestSceneAddPlaylistToCollectionButton.cs b/osu.Game.Tests/Visual/Playlists/TestSceneAddPlaylistToCollectionButton.cs index 46c93d9ae2..abfc5c4d0e 100644 --- a/osu.Game.Tests/Visual/Playlists/TestSceneAddPlaylistToCollectionButton.cs +++ b/osu.Game.Tests/Visual/Playlists/TestSceneAddPlaylistToCollectionButton.cs @@ -20,7 +20,6 @@ using osu.Game.Rulesets.Osu; using osu.Game.Screens.OnlinePlay.Playlists; using osuTK; using osuTK.Input; -using SharpCompress; namespace osu.Game.Tests.Visual.Playlists { @@ -53,7 +52,11 @@ namespace osu.Game.Tests.Visual.Playlists { AddStep("clear realm", () => Realm.Realm.Write(() => Realm.Realm.RemoveAll())); - AddStep("clear notifications", () => notificationOverlay.AllNotifications.Empty()); + AddStep("clear notifications", () => + { + foreach (var notification in notificationOverlay.AllNotifications) + notification.Close(runFlingAnimation: false); + }); importBeatmap(); diff --git a/osu.Game.Tests/osu.Game.Tests.csproj b/osu.Game.Tests/osu.Game.Tests.csproj index e78a3ea4f3..a1f43505f0 100644 --- a/osu.Game.Tests/osu.Game.Tests.csproj +++ b/osu.Game.Tests/osu.Game.Tests.csproj @@ -1,7 +1,7 @@  - + diff --git a/osu.Game.Tournament.Tests/osu.Game.Tournament.Tests.csproj b/osu.Game.Tournament.Tests/osu.Game.Tournament.Tests.csproj index 1daf5a446e..8437a1bc4e 100644 --- a/osu.Game.Tournament.Tests/osu.Game.Tournament.Tests.csproj +++ b/osu.Game.Tournament.Tests/osu.Game.Tournament.Tests.csproj @@ -4,7 +4,7 @@ osu.Game.Tournament.Tests.TournamentTestRunner - + diff --git a/osu.Game/Overlays/Profile/Header/Components/FollowersButton.cs b/osu.Game/Overlays/Profile/Header/Components/FollowersButton.cs index c4425643fd..b93f996ec2 100644 --- a/osu.Game/Overlays/Profile/Header/Components/FollowersButton.cs +++ b/osu.Game/Overlays/Profile/Header/Components/FollowersButton.cs @@ -6,6 +6,7 @@ using System.Linq; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Extensions.Color4Extensions; +using osu.Framework.Extensions.IEnumerableExtensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Sprites; using osu.Framework.Input.Events; @@ -16,7 +17,6 @@ using osu.Game.Online.API.Requests; using osu.Game.Online.API.Requests.Responses; using osu.Game.Overlays.Notifications; using osu.Game.Resources.Localisation.Web; -using SharpCompress; namespace osu.Game.Overlays.Profile.Header.Components { diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj index edf471ce8f..3793efd829 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -22,12 +22,12 @@ - - - - - - + + + + + + @@ -37,9 +37,9 @@ - + - + From eaf36796213de0c446e89115b5f71a757a06e959 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 17 Feb 2025 17:17:07 +0900 Subject: [PATCH 1057/1275] 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 3793efd829..6b5392eec6 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -36,7 +36,7 @@ - + From 8423d9de9b6447a42110ca69136c236175431439 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 17 Feb 2025 09:39:43 +0100 Subject: [PATCH 1058/1275] Fix distance snap grid colours being off-by-one in certain cases Closes https://github.com/ppy/osu/issues/31909. Previously: https://github.com/ppy/osu/pull/30062. Happening because of rounding errors - in this case the beat index pre-flooring was something like a 0.003 off of a full beat, which would get floored down rather than rounded up which created the discrepancy. But also we don't want to round *too* far, which is why this frankenstein solution has to exist I think. This is probably all exacerbated by stable not handling decimal control point start times. Would add tests if not for the fact that this is like extremely annoying to test. --- .../Edit/Compose/Components/DistanceSnapGrid.cs | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/osu.Game/Screens/Edit/Compose/Components/DistanceSnapGrid.cs b/osu.Game/Screens/Edit/Compose/Components/DistanceSnapGrid.cs index dd1671cfdd..88e28df8e3 100644 --- a/osu.Game/Screens/Edit/Compose/Components/DistanceSnapGrid.cs +++ b/osu.Game/Screens/Edit/Compose/Components/DistanceSnapGrid.cs @@ -11,6 +11,7 @@ using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Layout; +using osu.Framework.Utils; using osu.Game.Graphics; using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Objects.Types; @@ -148,7 +149,18 @@ namespace osu.Game.Screens.Edit.Compose.Components { var timingPoint = Beatmap.ControlPointInfo.TimingPointAt(StartTime); double beatLength = timingPoint.BeatLength / beatDivisor.Value; - int beatIndex = (int)Math.Floor((StartTime - timingPoint.Time) / beatLength); + double fractionalBeatIndex = (StartTime - timingPoint.Time) / beatLength; + int beatIndex = (int)Math.Round(fractionalBeatIndex); + // `fractionalBeatIndex` could differ from `beatIndex` for two reasons: + // - rounding errors (which can be exacerbated by timing point start times being truncated by/for stable), + // - `StartTime` is not snapped to the beat. + // in case 1, we want rounding to occur to prevent an off-by-one, + // as `StartTime` *is* quantised to the beat. but it just doesn't look like it because floats do float things. + // in case 2, we want *flooring* to occur, to prevent a possible off-by-one + // because of the rounding snapping forward by a chunk of time significantly too high to be considered a rounding error. + // the tolerance margin chosen here is arbitrary and can be adjusted if more cases of this are found. + if (Precision.DefinitelyBigger(beatIndex, fractionalBeatIndex, 0.005)) + beatIndex = (int)Math.Floor(fractionalBeatIndex); var colour = BindableBeatDivisor.GetColourFor(BindableBeatDivisor.GetDivisorForBeatIndex(beatIndex + placementIndex + 1, beatDivisor.Value), Colours); From 2b4b21beb6c50b12e0daf4031b1dcb4fab75b3d3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 17 Feb 2025 09:45:09 +0100 Subject: [PATCH 1059/1275] Fix distance snap grid line opacity being incorrect on non-1.0x velocities Noticed in passing. --- .../Edit/Compose/Components/CircularDistanceSnapGrid.cs | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/osu.Game/Screens/Edit/Compose/Components/CircularDistanceSnapGrid.cs b/osu.Game/Screens/Edit/Compose/Components/CircularDistanceSnapGrid.cs index 164a209958..8c7afd2aeb 100644 --- a/osu.Game/Screens/Edit/Compose/Components/CircularDistanceSnapGrid.cs +++ b/osu.Game/Screens/Edit/Compose/Components/CircularDistanceSnapGrid.cs @@ -63,7 +63,7 @@ namespace osu.Game.Screens.Edit.Compose.Components const float thickness = 4; float diameter = (offset + (i + 1) * DistanceBetweenTicks + thickness / 2) * 2; - AddInternal(new Ring(StartTime, GetColourForIndexFromPlacement(i)) + AddInternal(new Ring(StartTime, GetColourForIndexFromPlacement(i), SliderVelocitySource) { Position = StartPosition, Origin = Anchor.Centre, @@ -128,12 +128,14 @@ namespace osu.Game.Screens.Edit.Compose.Components private EditorClock? editorClock { get; set; } private readonly double startTime; + private readonly IHasSliderVelocity? sliderVelocitySource; private readonly Color4 baseColour; - public Ring(double startTime, Color4 baseColour) + public Ring(double startTime, Color4 baseColour, IHasSliderVelocity? sliderVelocitySource) { this.startTime = startTime; + this.sliderVelocitySource = sliderVelocitySource; Colour = this.baseColour = baseColour; @@ -150,7 +152,7 @@ namespace osu.Game.Screens.Edit.Compose.Components float distanceSpacingMultiplier = (float)snapProvider.DistanceSpacingMultiplier.Value; double timeFromReferencePoint = editorClock.CurrentTime - startTime; - float distanceForCurrentTime = snapProvider.DurationToDistance(timeFromReferencePoint, startTime) + float distanceForCurrentTime = snapProvider.DurationToDistance(timeFromReferencePoint, startTime, sliderVelocitySource) * distanceSpacingMultiplier; float timeBasedAlpha = 1 - Math.Clamp(Math.Abs(distanceForCurrentTime - Size.X / 2) / 30, 0, 1); From 5304ea2446b922cbfccef4bbefb058e30c224590 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 17 Feb 2025 22:42:03 +0900 Subject: [PATCH 1060/1275] Fix minor typo --- osu.Game/Localisation/BeatmapSubmissionStrings.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Localisation/BeatmapSubmissionStrings.cs b/osu.Game/Localisation/BeatmapSubmissionStrings.cs index 3abe8cc515..0cf0498daa 100644 --- a/osu.Game/Localisation/BeatmapSubmissionStrings.cs +++ b/osu.Game/Localisation/BeatmapSubmissionStrings.cs @@ -140,9 +140,9 @@ namespace osu.Game.Localisation public static LocalisableString LoadInBrowserAfterSubmission => new TranslatableString(getKey(@"load_in_browser_after_submission"), @"Load in browser after submission"); /// - /// "Note: In order to make it possible for users of all osu! versions to enjoy your beatmap, it will be exported in a backwards-compatible format. While we have made efforts to ensure that process keeps the beatmap playable in its intended form, some data related to features that previous versions of osu! do not support may be lost." + /// "Note: In order to make it possible for users of all osu! versions to enjoy your beatmap, it will be exported in a backwards-compatible format. While we have made efforts to ensure that this process keeps the beatmap playable in its intended form, some data related to features that previous versions of osu! do not support may be lost." /// - public static LocalisableString LegacyExportDisclaimer => new TranslatableString(getKey(@"legacy_export_disclaimer"), @"Note: In order to make it possible for users of all osu! versions to enjoy your beatmap, it will be exported in a backwards-compatible format. While we have made efforts to ensure that process keeps the beatmap playable in its intended form, some data related to features that previous versions of osu! do not support may be lost."); + public static LocalisableString LegacyExportDisclaimer => new TranslatableString(getKey(@"legacy_export_disclaimer"), @"Note: In order to make it possible for users of all osu! versions to enjoy your beatmap, it will be exported in a backwards-compatible format. While we have made efforts to ensure that this process keeps the beatmap playable in its intended form, some data related to features that previous versions of osu! do not support may be lost."); /// /// "Empty beatmaps cannot be submitted." From f37a56c3079ac78935069d7135c68a42d9dcc59f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 17 Feb 2025 15:01:05 +0100 Subject: [PATCH 1061/1275] Fix nudge operations incurring FP error from coordinate space conversions Closes https://github.com/ppy/osu/issues/31915. Reproduction of aforementioned issue requires 1280x720 resolution, which should also be a good way to confirm that this does anything. To me this is also equal-parts-bugfix, equal-parts-code-quality PR, because tell me: what on earth was this code ever doing at `ComposeBlueprintContainer` level? Nudging by one playfield-space-unit doesn't even *make sense* in something like taiko or mania. --- .../Edit/CatchSelectionHandler.cs | 62 +++++++++++++++++ .../Edit/OsuSelectionHandler.cs | 62 +++++++++++++++++ .../Components/ComposeBlueprintContainer.cs | 66 ------------------- 3 files changed, 124 insertions(+), 66 deletions(-) diff --git a/osu.Game.Rulesets.Catch/Edit/CatchSelectionHandler.cs b/osu.Game.Rulesets.Catch/Edit/CatchSelectionHandler.cs index a2784126eb..a7cd84aed5 100644 --- a/osu.Game.Rulesets.Catch/Edit/CatchSelectionHandler.cs +++ b/osu.Game.Rulesets.Catch/Edit/CatchSelectionHandler.cs @@ -5,6 +5,7 @@ using System; using System.Collections.Generic; using System.Linq; using osu.Framework.Allocation; +using osu.Framework.Input.Events; using osu.Game.Rulesets.Catch.Objects; using osu.Game.Rulesets.Catch.UI; using osu.Game.Rulesets.Objects; @@ -12,6 +13,7 @@ using osu.Game.Rulesets.UI; using osu.Game.Rulesets.UI.Scrolling; using osu.Game.Screens.Edit.Compose.Components; using osuTK; +using osuTK.Input; using Direction = osu.Framework.Graphics.Direction; namespace osu.Game.Rulesets.Catch.Edit @@ -38,6 +40,13 @@ namespace osu.Game.Rulesets.Catch.Edit return true; } + moveSelection(deltaX); + + return true; + } + + private void moveSelection(float deltaX) + { EditorBeatmap.PerformOnSelection(h => { if (!(h is CatchHitObject catchObject)) return; @@ -48,7 +57,60 @@ namespace osu.Game.Rulesets.Catch.Edit foreach (var nested in catchObject.NestedHitObjects.OfType()) nested.OriginalX += deltaX; }); + } + private bool nudgeMovementActive; + + protected override bool OnKeyDown(KeyDownEvent e) + { + // Until the keys below are global actions, this will prevent conflicts with "seek between sample points" + // which has a default of ctrl+shift+arrows. + if (e.ShiftPressed) + return false; + + if (e.ControlPressed) + { + switch (e.Key) + { + case Key.Left: + return nudgeSelection(-1); + + case Key.Right: + return nudgeSelection(1); + } + } + + return false; + } + + protected override void OnKeyUp(KeyUpEvent e) + { + base.OnKeyUp(e); + + if (nudgeMovementActive && !e.ControlPressed) + { + EditorBeatmap.EndChange(); + nudgeMovementActive = false; + } + } + + /// + /// Move the current selection spatially by the specified delta, in gamefield coordinates (ie. the same coordinates as the blueprints). + /// + private bool nudgeSelection(float deltaX) + { + if (!nudgeMovementActive) + { + nudgeMovementActive = true; + EditorBeatmap.BeginChange(); + } + + var firstBlueprint = SelectedBlueprints.FirstOrDefault(); + + if (firstBlueprint == null) + return false; + + moveSelection(deltaX); return true; } diff --git a/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs b/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs index bac0a5e273..3a1ff34fb9 100644 --- a/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs +++ b/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs @@ -40,6 +40,8 @@ namespace osu.Game.Rulesets.Osu.Edit SelectionBox.CanReverse = EditorBeatmap.SelectedHitObjects.Count > 1 || EditorBeatmap.SelectedHitObjects.Any(s => s is Slider); } + private bool nudgeMovementActive; + protected override bool OnKeyDown(KeyDownEvent e) { if (e.Key == Key.M && e.ControlPressed && e.ShiftPressed) @@ -48,9 +50,43 @@ namespace osu.Game.Rulesets.Osu.Edit return true; } + // Until the keys below are global actions, this will prevent conflicts with "seek between sample points" + // which has a default of ctrl+shift+arrows. + if (e.ShiftPressed) + return false; + + if (e.ControlPressed) + { + switch (e.Key) + { + case Key.Left: + return nudgeSelection(new Vector2(-1, 0)); + + case Key.Right: + return nudgeSelection(new Vector2(1, 0)); + + case Key.Up: + return nudgeSelection(new Vector2(0, -1)); + + case Key.Down: + return nudgeSelection(new Vector2(0, 1)); + } + } + return false; } + protected override void OnKeyUp(KeyUpEvent e) + { + base.OnKeyUp(e); + + if (nudgeMovementActive && !e.ControlPressed) + { + EditorBeatmap.EndChange(); + nudgeMovementActive = false; + } + } + public override bool HandleMovement(MoveSelectionEvent moveEvent) { var hitObjects = selectedMovableObjects; @@ -70,6 +106,13 @@ namespace osu.Game.Rulesets.Osu.Edit if (hitObjects.Any(h => Precision.AlmostEquals(localDelta, -h.StackOffset))) return true; + moveObjects(hitObjects, localDelta); + + return true; + } + + private void moveObjects(OsuHitObject[] hitObjects, Vector2 localDelta) + { // this will potentially move the selection out of bounds... foreach (var h in hitObjects) h.Position += localDelta; @@ -81,7 +124,26 @@ namespace osu.Game.Rulesets.Osu.Edit // this intentionally bypasses the editor `UpdateState()` / beatmap processor flow for performance reasons, // as the entire flow is too expensive to run on every movement. Scheduler.AddOnce(OsuBeatmapProcessor.ApplyStacking, EditorBeatmap); + } + /// + /// Move the current selection spatially by the specified delta, in gamefield coordinates (ie. the same coordinates as the blueprints). + /// + /// + private bool nudgeSelection(Vector2 delta) + { + if (!nudgeMovementActive) + { + nudgeMovementActive = true; + EditorBeatmap.BeginChange(); + } + + var firstBlueprint = SelectedBlueprints.FirstOrDefault(); + + if (firstBlueprint == null) + return false; + + moveObjects(selectedMovableObjects, delta); return true; } diff --git a/osu.Game/Screens/Edit/Compose/Components/ComposeBlueprintContainer.cs b/osu.Game/Screens/Edit/Compose/Components/ComposeBlueprintContainer.cs index e82f6395d0..4c57eee971 100644 --- a/osu.Game/Screens/Edit/Compose/Components/ComposeBlueprintContainer.cs +++ b/osu.Game/Screens/Edit/Compose/Components/ComposeBlueprintContainer.cs @@ -27,7 +27,6 @@ using osu.Game.Rulesets.Objects.Types; using osu.Game.Rulesets.UI; using osu.Game.Screens.Edit.Components.TernaryButtons; using osuTK; -using osuTK.Input; namespace osu.Game.Screens.Edit.Compose.Components { @@ -112,71 +111,6 @@ namespace osu.Game.Screens.Edit.Compose.Components blueprint.DrawableObject = drawableObject; } - private bool nudgeMovementActive; - - protected override bool OnKeyDown(KeyDownEvent e) - { - // Until the keys below are global actions, this will prevent conflicts with "seek between sample points" - // which has a default of ctrl+shift+arrows. - if (e.ShiftPressed) - return false; - - if (e.ControlPressed) - { - switch (e.Key) - { - case Key.Left: - return nudgeSelection(new Vector2(-1, 0)); - - case Key.Right: - return nudgeSelection(new Vector2(1, 0)); - - case Key.Up: - return nudgeSelection(new Vector2(0, -1)); - - case Key.Down: - return nudgeSelection(new Vector2(0, 1)); - } - } - - return false; - } - - protected override void OnKeyUp(KeyUpEvent e) - { - base.OnKeyUp(e); - - if (nudgeMovementActive && !e.ControlPressed) - { - Beatmap.EndChange(); - nudgeMovementActive = false; - } - } - - /// - /// Move the current selection spatially by the specified delta, in gamefield coordinates (ie. the same coordinates as the blueprints). - /// - /// - private bool nudgeSelection(Vector2 delta) - { - if (!nudgeMovementActive) - { - nudgeMovementActive = true; - Beatmap.BeginChange(); - } - - var firstBlueprint = SelectionHandler.SelectedBlueprints.FirstOrDefault(); - - if (firstBlueprint == null) - return false; - - // convert to game space coordinates - delta = firstBlueprint.ToScreenSpace(delta) - firstBlueprint.ToScreenSpace(Vector2.Zero); - - SelectionHandler.HandleMovement(new MoveSelectionEvent(firstBlueprint, delta)); - return true; - } - private void updatePlacementNewCombo() { if (CurrentHitObjectPlacement?.HitObject is IHasComboInformation c) From f5b485a44d1fb35be22c1b224837798b989248fd Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 18 Feb 2025 12:58:54 +0900 Subject: [PATCH 1062/1275] Stop "hold for HUD" key binding from blocking other key presses I don't think there's a good reason for this to be blocking. Closes https://github.com/ppy/osu/issues/31274. --- osu.Game/Screens/Play/HUDOverlay.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/Play/HUDOverlay.cs b/osu.Game/Screens/Play/HUDOverlay.cs index f670e2f628..8bfa8dd6ff 100644 --- a/osu.Game/Screens/Play/HUDOverlay.cs +++ b/osu.Game/Screens/Play/HUDOverlay.cs @@ -414,7 +414,7 @@ namespace osu.Game.Screens.Play case GlobalAction.HoldForHUD: holdingForHUD.Value = true; - return true; + return false; case GlobalAction.ToggleInGameInterface: switch (configVisibilityMode.Value) From 20dbe096e03e043143388eab62e1650a3be1ea2a Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 18 Feb 2025 13:04:38 +0900 Subject: [PATCH 1063/1275] Refactor slightly --- osu.Game.Rulesets.Osu/Mods/OsuModSuddenDeath.cs | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModSuddenDeath.cs b/osu.Game.Rulesets.Osu/Mods/OsuModSuddenDeath.cs index 73d0403e3f..d331b691d5 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModSuddenDeath.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModSuddenDeath.cs @@ -19,15 +19,18 @@ namespace osu.Game.Rulesets.Osu.Mods typeof(OsuModTargetPractice), }).ToArray(); - [SettingSource("Fail when missing on a slider tail")] - public BindableBool SliderTailMiss { get; } = new BindableBool(); + [SettingSource("Also fail when missing a slider tail")] + public BindableBool FailOnSliderTail { get; } = new BindableBool(); protected override bool FailCondition(HealthProcessor healthProcessor, JudgementResult result) { - if (SliderTailMiss.Value && result.HitObject is SliderTailCircle && result.Type == HitResult.IgnoreMiss) + if (base.FailCondition(healthProcessor, result)) return true; - return base.FailCondition(healthProcessor, result); + if (FailOnSliderTail.Value && result.HitObject is SliderTailCircle && !result.IsHit) + return true; + + return false; } } } From 2d8e35be32a923049c717b3ae5906804097d67b6 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 18 Feb 2025 13:08:33 +0900 Subject: [PATCH 1064/1275] Add test coverage --- .../Mods/TestSceneOsuModSuddenDeath.cs | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModSuddenDeath.cs b/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModSuddenDeath.cs index 688cf70f71..23dd2123c3 100644 --- a/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModSuddenDeath.cs +++ b/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModSuddenDeath.cs @@ -24,11 +24,15 @@ namespace osu.Game.Rulesets.Osu.Tests.Mods { } - [Test] - public void TestMissTail() => CreateModTest(new ModTestData + [TestCase(true)] + [TestCase(false)] + public void TestMissTail(bool tailMiss) => CreateModTest(new ModTestData { - Mod = new OsuModSuddenDeath(), - PassCondition = () => ((ModFailConditionTestPlayer)Player).CheckFailed(false), + Mod = new OsuModSuddenDeath + { + FailOnSliderTail = { Value = tailMiss } + }, + PassCondition = () => ((ModFailConditionTestPlayer)Player).CheckFailed(tailMiss), Autoplay = false, CreateBeatmap = () => new Beatmap { From 77e40140e5b5fc1c83892492e9809dc4b1b708e3 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 18 Feb 2025 13:41:30 +0900 Subject: [PATCH 1065/1275] Fix selected sliders sometimes not being clickable in editor Closes https://github.com/ppy/osu/issues/31918. Regressed with https://github.com/ppy/osu/commit/1648f2efa306f587714178f113e69d8ad8c4ac02 for obvious reasons. --- .../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 f7c25b43dd..39c0681dba 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) && DrawableObject.Body.Alpha > 0) + if (BodyPiece.ReceivePositionalInputAt(screenSpacePos) && (IsSelected || DrawableObject.Body.Alpha > 0)) return true; if (ControlPointVisualiser == null) From 8e25c9445234616f10053b5f2bba193e10444da9 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 18 Feb 2025 14:12:14 +0900 Subject: [PATCH 1066/1275] Fix kiai fountains sometimes not displaying when they should MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The previous logic was very wrong, as the check would only occur on each beat. But that's not how kiai sections work – they can be placed at any timestamp, even if that doesn't align with a beat. In addition, the rate limiting has been removed because it didn't exist on stable and causes some fountains to be missed. Overlap scenarios are already handled internally by the `StarFountain` class. Closes https://github.com/ppy/osu/issues/31855. --- .../Containers/BeatSyncedContainer.cs | 37 +++++++++++-------- osu.Game/Screens/Menu/KiaiMenuFountains.cs | 19 +++------- .../Screens/Play/KiaiGameplayFountains.cs | 21 ++++------- 3 files changed, 34 insertions(+), 43 deletions(-) diff --git a/osu.Game/Graphics/Containers/BeatSyncedContainer.cs b/osu.Game/Graphics/Containers/BeatSyncedContainer.cs index 7210371ebf..4331b91e61 100644 --- a/osu.Game/Graphics/Containers/BeatSyncedContainer.cs +++ b/osu.Game/Graphics/Containers/BeatSyncedContainer.cs @@ -73,6 +73,16 @@ namespace osu.Game.Graphics.Containers /// protected bool IsBeatSyncedWithTrack { get; private set; } + /// + /// The most valid timing point, updated every frame. + /// + protected TimingControlPoint TimingPoint { get; private set; } = TimingControlPoint.DEFAULT; + + /// + /// The most valid effect point, updated every frame. + /// + protected EffectControlPoint EffectPoint { get; private set; } = EffectControlPoint.DEFAULT; + [Resolved] protected IBeatSyncProvider BeatSyncSource { get; private set; } = null!; @@ -82,9 +92,6 @@ namespace osu.Game.Graphics.Containers protected override void Update() { - TimingControlPoint timingPoint; - EffectControlPoint effectPoint; - IsBeatSyncedWithTrack = BeatSyncSource.Clock.IsRunning; double currentTrackTime; @@ -102,8 +109,8 @@ namespace osu.Game.Graphics.Containers currentTrackTime = BeatSyncSource.Clock.CurrentTime + early; - timingPoint = BeatSyncSource.ControlPoints?.TimingPointAt(currentTrackTime) ?? TimingControlPoint.DEFAULT; - effectPoint = BeatSyncSource.ControlPoints?.EffectPointAt(currentTrackTime) ?? EffectControlPoint.DEFAULT; + TimingPoint = BeatSyncSource.ControlPoints?.TimingPointAt(currentTrackTime) ?? TimingControlPoint.DEFAULT; + EffectPoint = BeatSyncSource.ControlPoints?.EffectPointAt(currentTrackTime) ?? EffectControlPoint.DEFAULT; } else { @@ -111,28 +118,28 @@ namespace osu.Game.Graphics.Containers // we still want to show an idle animation, so use this container's time instead. currentTrackTime = Clock.CurrentTime + EarlyActivationMilliseconds; - timingPoint = TimingControlPoint.DEFAULT; - effectPoint = EffectControlPoint.DEFAULT; + TimingPoint = TimingControlPoint.DEFAULT; + EffectPoint = EffectControlPoint.DEFAULT; } - double beatLength = timingPoint.BeatLength / Divisor; + double beatLength = TimingPoint.BeatLength / Divisor; while (beatLength < MinimumBeatLength) beatLength *= 2; - int beatIndex = (int)((currentTrackTime - timingPoint.Time) / beatLength) - (timingPoint.OmitFirstBarLine ? 1 : 0); + int beatIndex = (int)((currentTrackTime - TimingPoint.Time) / beatLength) - (TimingPoint.OmitFirstBarLine ? 1 : 0); // The beats before the start of the first control point are off by 1, this should do the trick - if (currentTrackTime < timingPoint.Time) + if (currentTrackTime < TimingPoint.Time) beatIndex--; - TimeUntilNextBeat = (timingPoint.Time - currentTrackTime) % beatLength; + TimeUntilNextBeat = (TimingPoint.Time - currentTrackTime) % beatLength; if (TimeUntilNextBeat <= 0) TimeUntilNextBeat += beatLength; TimeSinceLastBeat = beatLength - TimeUntilNextBeat; - if (ReferenceEquals(timingPoint, lastTimingPoint) && beatIndex == lastBeat) + if (ReferenceEquals(TimingPoint, lastTimingPoint) && beatIndex == lastBeat) return; // as this event is sometimes used for sound triggers where `BeginDelayedSequence` has no effect, avoid firing it if too far away from the beat. @@ -140,13 +147,13 @@ namespace osu.Game.Graphics.Containers if (AllowMistimedEventFiring || Math.Abs(TimeSinceLastBeat) < MISTIMED_ALLOWANCE) { using (BeginDelayedSequence(-TimeSinceLastBeat)) - OnNewBeat(beatIndex, timingPoint, effectPoint, BeatSyncSource.CurrentAmplitudes); + OnNewBeat(beatIndex, TimingPoint, EffectPoint, BeatSyncSource.CurrentAmplitudes); } lastBeat = beatIndex; - lastTimingPoint = timingPoint; + lastTimingPoint = TimingPoint; - IsKiaiTime = effectPoint.KiaiMode; + IsKiaiTime = EffectPoint.KiaiMode; } } } diff --git a/osu.Game/Screens/Menu/KiaiMenuFountains.cs b/osu.Game/Screens/Menu/KiaiMenuFountains.cs index 07c06dcdb9..7978e9fa91 100644 --- a/osu.Game/Screens/Menu/KiaiMenuFountains.cs +++ b/osu.Game/Screens/Menu/KiaiMenuFountains.cs @@ -3,10 +3,8 @@ using System; using osu.Framework.Allocation; -using osu.Framework.Audio.Track; using osu.Framework.Graphics; using osu.Framework.Utils; -using osu.Game.Beatmaps.ControlPoints; using osu.Game.Graphics.Containers; namespace osu.Game.Screens.Menu @@ -40,27 +38,22 @@ namespace osu.Game.Screens.Menu private bool isTriggered; - private double? lastTrigger; - - protected override void OnNewBeat(int beatIndex, TimingControlPoint timingPoint, EffectControlPoint effectPoint, ChannelAmplitudes amplitudes) + protected override void Update() { - base.OnNewBeat(beatIndex, timingPoint, effectPoint, amplitudes); + base.Update(); - if (effectPoint.KiaiMode && !isTriggered) + if (EffectPoint.KiaiMode && !isTriggered) { - bool isNearEffectPoint = Math.Abs(BeatSyncSource.Clock.CurrentTime - effectPoint.Time) < 500; + bool isNearEffectPoint = Math.Abs(BeatSyncSource.Clock.CurrentTime - EffectPoint.Time) < 500; if (isNearEffectPoint) Shoot(); } - isTriggered = effectPoint.KiaiMode; + isTriggered = EffectPoint.KiaiMode; } public void Shoot() { - if (lastTrigger != null && Clock.CurrentTime - lastTrigger < 500) - return; - int direction = RNG.Next(-1, 2); switch (direction) @@ -80,8 +73,6 @@ namespace osu.Game.Screens.Menu rightFountain.Shoot(1); break; } - - lastTrigger = Clock.CurrentTime; } } } diff --git a/osu.Game/Screens/Play/KiaiGameplayFountains.cs b/osu.Game/Screens/Play/KiaiGameplayFountains.cs index fd9596c838..19a9c2b6e5 100644 --- a/osu.Game/Screens/Play/KiaiGameplayFountains.cs +++ b/osu.Game/Screens/Play/KiaiGameplayFountains.cs @@ -1,15 +1,13 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable using System; using osu.Framework.Allocation; -using osu.Framework.Audio.Track; using osu.Framework.Bindables; using osu.Framework.Graphics; +using osu.Framework.Logging; using osu.Framework.Utils; using osu.Game.Configuration; -using osu.Game.Beatmaps.ControlPoints; using osu.Game.Graphics.Containers; using osu.Game.Screens.Menu; @@ -48,33 +46,28 @@ namespace osu.Game.Screens.Play private bool isTriggered; - private double? lastTrigger; - - protected override void OnNewBeat(int beatIndex, TimingControlPoint timingPoint, EffectControlPoint effectPoint, ChannelAmplitudes amplitudes) + protected override void Update() { - base.OnNewBeat(beatIndex, timingPoint, effectPoint, amplitudes); + base.Update(); if (!kiaiStarFountains.Value) return; - if (effectPoint.KiaiMode && !isTriggered) + if (EffectPoint.KiaiMode && !isTriggered) { - bool isNearEffectPoint = Math.Abs(BeatSyncSource.Clock.CurrentTime - effectPoint.Time) < 500; + Logger.Log("shooting"); + bool isNearEffectPoint = Math.Abs(BeatSyncSource.Clock.CurrentTime - EffectPoint.Time) < 500; if (isNearEffectPoint) Shoot(); } - isTriggered = effectPoint.KiaiMode; + isTriggered = EffectPoint.KiaiMode; } public void Shoot() { - if (lastTrigger != null && Clock.CurrentTime - lastTrigger < 500) - return; - leftFountain.Shoot(1); rightFountain.Shoot(-1); - lastTrigger = Clock.CurrentTime; } public partial class GameplayStarFountain : StarFountain From 88ec204d264f17020d75e54eb2b6430361f35995 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 18 Feb 2025 16:22:57 +0900 Subject: [PATCH 1067/1275] User inheritance to avoid `Piece` structural nightmare --- .../SongSelect/BeatmapCarouselV2TestScene.cs | 2 +- .../TestSceneBeatmapCarouselV2GroupPanel.cs | 12 +- .../{CarouselPanelPiece.cs => PanelBase.cs} | 58 +++-- osu.Game/Screens/SelectV2/PanelBeatmap.cs | 158 ++++++------- osu.Game/Screens/SelectV2/PanelBeatmapSet.cs | 68 +++--- .../SelectV2/PanelBeatmapStandalone.cs | 221 ++++++++---------- osu.Game/Screens/SelectV2/PanelGroup.cs | 111 ++++----- .../SelectV2/PanelGroupStarDifficulty.cs | 149 ++++++++++++ .../SelectV2/PanelGroupStarDificulty.cs | 187 --------------- 9 files changed, 425 insertions(+), 541 deletions(-) rename osu.Game/Screens/SelectV2/{CarouselPanelPiece.cs => PanelBase.cs} (86%) create mode 100644 osu.Game/Screens/SelectV2/PanelGroupStarDifficulty.cs delete mode 100644 osu.Game/Screens/SelectV2/PanelGroupStarDificulty.cs diff --git a/osu.Game.Tests/Visual/SongSelect/BeatmapCarouselV2TestScene.cs b/osu.Game.Tests/Visual/SongSelect/BeatmapCarouselV2TestScene.cs index 2c422e0a85..2c902a466f 100644 --- a/osu.Game.Tests/Visual/SongSelect/BeatmapCarouselV2TestScene.cs +++ b/osu.Game.Tests/Visual/SongSelect/BeatmapCarouselV2TestScene.cs @@ -189,7 +189,7 @@ namespace osu.Game.Tests.Visual.SongSelect .Where(p => ((ICarouselPanel)p).Item?.IsVisible == true) .OrderBy(p => p.Y) .ElementAt(index) - .ChildrenOfType().Single() + .ChildrenOfType().Single() .TriggerClick(); }); } diff --git a/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselV2GroupPanel.cs b/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselV2GroupPanel.cs index 711a3b881d..9b07f01e52 100644 --- a/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselV2GroupPanel.cs +++ b/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselV2GroupPanel.cs @@ -49,29 +49,29 @@ namespace osu.Game.Tests.Visual.SongSelectV2 KeyboardSelected = { Value = true }, Expanded = { Value = true } }, - new PanelGroupStarDificulty + new PanelGroupStarDifficulty { Item = new CarouselItem(new GroupDefinition(1, "1")) }, - new PanelGroupStarDificulty + new PanelGroupStarDifficulty { Item = new CarouselItem(new GroupDefinition(3, "3")), Expanded = { Value = true } }, - new PanelGroupStarDificulty + new PanelGroupStarDifficulty { Item = new CarouselItem(new GroupDefinition(5, "5")), }, - new PanelGroupStarDificulty + new PanelGroupStarDifficulty { Item = new CarouselItem(new GroupDefinition(7, "7")), Expanded = { Value = true } }, - new PanelGroupStarDificulty + new PanelGroupStarDifficulty { Item = new CarouselItem(new GroupDefinition(8, "8")), }, - new PanelGroupStarDificulty + new PanelGroupStarDifficulty { Item = new CarouselItem(new GroupDefinition(9, "9")), Expanded = { Value = true } diff --git a/osu.Game/Screens/SelectV2/CarouselPanelPiece.cs b/osu.Game/Screens/SelectV2/PanelBase.cs similarity index 86% rename from osu.Game/Screens/SelectV2/CarouselPanelPiece.cs rename to osu.Game/Screens/SelectV2/PanelBase.cs index 5aefa57bb5..d5a087dbb2 100644 --- a/osu.Game/Screens/SelectV2/CarouselPanelPiece.cs +++ b/osu.Game/Screens/SelectV2/PanelBase.cs @@ -1,7 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using System; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Extensions.Color4Extensions; @@ -9,6 +8,7 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Colour; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Effects; +using osu.Framework.Graphics.Pooling; using osu.Framework.Graphics.Shapes; using osu.Framework.Input.Events; using osu.Game.Graphics; @@ -19,7 +19,7 @@ using osuTK.Graphics; namespace osu.Game.Screens.SelectV2 { - public partial class CarouselPanelPiece : Container + public abstract partial class PanelBase : PoolableDrawable, ICarouselPanel { private const float corner_radius = 10; @@ -43,7 +43,7 @@ namespace osu.Game.Screens.SelectV2 public Container TopLevelContent { get; } - protected override Container Content { get; } + protected Container Content { get; } public Drawable Background { @@ -67,11 +67,6 @@ namespace osu.Game.Screens.SelectV2 } } - public readonly BindableBool Active = new BindableBool(); - public readonly BindableBool KeyboardActive = new BindableBool(); - - public Action? Action { get; init; } - public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) { var inputRectangle = TopLevelContent.DrawRectangle; @@ -82,7 +77,7 @@ namespace osu.Game.Screens.SelectV2 return inputRectangle.Contains(TopLevelContent.ToLocalSpace(screenSpacePos)); } - public CarouselPanelPiece(float panelXOffset) + protected PanelBase(float panelXOffset = 0) { this.panelXOffset = panelXOffset; @@ -183,8 +178,17 @@ namespace osu.Game.Screens.SelectV2 { base.LoadComplete(); - Active.BindValueChanged(_ => updateDisplay()); - KeyboardActive.BindValueChanged(_ => updateDisplay(), true); + Expanded.BindValueChanged(_ => updateDisplay()); + KeyboardSelected.BindValueChanged(_ => updateDisplay(), true); + } + + [Resolved] + private BeatmapCarousel? carousel { get; set; } + + protected override bool OnClick(ClickEvent e) + { + carousel?.Activate(Item!); + return true; } public void Flash() @@ -194,7 +198,7 @@ namespace osu.Game.Screens.SelectV2 private void updateDisplay() { - backgroundLayer.TransformTo(nameof(Padding), backgroundLayer.Padding with { Vertical = Active.Value ? 2f : 0f }, duration, Easing.OutQuint); + backgroundLayer.TransformTo(nameof(Padding), backgroundLayer.Padding with { Vertical = Expanded.Value ? 2f : 0f }, duration, Easing.OutQuint); var backgroundColour = accentColour ?? Color4.White; var edgeEffectColour = accentColour ?? Color4Extensions.FromHex(@"4EBFFF"); @@ -202,7 +206,7 @@ namespace osu.Game.Screens.SelectV2 backgroundAccentGradient.FadeColour(ColourInfo.GradientHorizontal(backgroundColour.Opacity(0.25f), backgroundColour.Opacity(0f)), duration, Easing.OutQuint); backgroundBorder.FadeColour(backgroundColour, duration, Easing.OutQuint); - TopLevelContent.FadeEdgeEffectTo(Active.Value ? edgeEffectColour.Opacity(0.5f) : Color4.Black.Opacity(0.4f), duration, Easing.OutQuint); + TopLevelContent.FadeEdgeEffectTo(Expanded.Value ? edgeEffectColour.Opacity(0.5f) : Color4.Black.Opacity(0.4f), duration, Easing.OutQuint); updateXOffset(); updateHover(); @@ -212,10 +216,10 @@ namespace osu.Game.Screens.SelectV2 { float x = panelXOffset + active_x_offset + keyboard_active_x_offset + left_edge_x_offset; - if (Active.Value) + if (Expanded.Value) x -= active_x_offset; - if (KeyboardActive.Value) + if (KeyboardSelected.Value) x -= keyboard_active_x_offset; this.TransformTo(nameof(Padding), new MarginPadding { Left = x }, duration, Easing.OutQuint); @@ -223,7 +227,7 @@ namespace osu.Game.Screens.SelectV2 private void updateHover() { - bool hovered = IsHovered || KeyboardActive.Value; + bool hovered = IsHovered || KeyboardSelected.Value; if (hovered) hoverLayer.FadeIn(100, Easing.OutQuint); @@ -243,17 +247,27 @@ namespace osu.Game.Screens.SelectV2 base.OnHoverLost(e); } - protected override bool OnClick(ClickEvent e) - { - Action?.Invoke(); - return true; - } - protected override void Update() { base.Update(); Content.Padding = Content.Padding with { Left = iconContainer.DrawWidth }; backgroundLayerHorizontalPadding.Padding = new MarginPadding { Left = iconContainer.DrawWidth }; } + + #region ICarouselPanel + + public CarouselItem? Item { get; set; } + public BindableBool Selected { get; } = new BindableBool(); + public BindableBool Expanded { get; } = new BindableBool(); + public BindableBool KeyboardSelected { get; } = new BindableBool(); + + public double DrawYPosition { get; set; } + + public virtual void Activated() + { + activationFlash.FadeOutFromOne(500, Easing.OutQuint); + } + + #endregion } } diff --git a/osu.Game/Screens/SelectV2/PanelBeatmap.cs b/osu.Game/Screens/SelectV2/PanelBeatmap.cs index 93ef814f2e..48d15f6857 100644 --- a/osu.Game/Screens/SelectV2/PanelBeatmap.cs +++ b/osu.Game/Screens/SelectV2/PanelBeatmap.cs @@ -8,7 +8,6 @@ using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; -using osu.Framework.Graphics.Pooling; using osu.Game.Beatmaps; using osu.Game.Beatmaps.Drawables; using osu.Game.Graphics; @@ -23,7 +22,7 @@ using osuTK; namespace osu.Game.Screens.SelectV2 { - public partial class PanelBeatmap : PoolableDrawable, ICarouselPanel + public partial class PanelBeatmap : PanelBase { public const float HEIGHT = CarouselItem.DEFAULT_HEIGHT; @@ -33,7 +32,6 @@ namespace osu.Game.Screens.SelectV2 private const float duration = 500; - private CarouselPanelPiece panel = null!; private StarCounter starCounter = null!; private ConstrainedIconContainer difficultyIcon = null!; private OsuSpriteText keyCountText = null!; @@ -54,9 +52,6 @@ namespace osu.Game.Screens.SelectV2 [Resolved] private BeatmapDifficultyCache difficultyCache { get; set; } = null!; - [Resolved] - private BeatmapCarousel? carousel { get; set; } - public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) { var inputRectangle = DrawRectangle; @@ -86,84 +81,81 @@ namespace osu.Game.Screens.SelectV2 Width = 1f; Height = HEIGHT; - InternalChild = panel = new CarouselPanelPiece(difficulty_x_offset) + Icon = difficultyIcon = new ConstrainedIconContainer { - Action = () => carousel?.Activate(Item!), - Icon = difficultyIcon = new ConstrainedIconContainer + Size = new Vector2(20), + Margin = new MarginPadding { Horizontal = 5f }, + Colour = colourProvider.Background5, + }; + + Content.Children = new[] + { + new FillFlowContainer { - Size = new Vector2(20), - Margin = new MarginPadding { Horizontal = 5f }, - Colour = colourProvider.Background5, - }, - Children = new[] - { - new FillFlowContainer + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + Padding = new MarginPadding { Left = 10f }, + Direction = FillDirection.Vertical, + AutoSizeAxes = Axes.Both, + Children = new Drawable[] { - Anchor = Anchor.CentreLeft, - Origin = Anchor.CentreLeft, - Padding = new MarginPadding { Left = 10f }, - Direction = FillDirection.Vertical, - AutoSizeAxes = Axes.Both, - Children = new Drawable[] + new FillFlowContainer { - new FillFlowContainer + Direction = FillDirection.Horizontal, + Spacing = new Vector2(3, 0), + AutoSizeAxes = Axes.Both, + Children = new Drawable[] { - Direction = FillDirection.Horizontal, - Spacing = new Vector2(3, 0), - AutoSizeAxes = Axes.Both, - Children = new Drawable[] + starRatingDisplay = new StarRatingDisplay(default, StarRatingDisplaySize.Small) { - starRatingDisplay = new StarRatingDisplay(default, StarRatingDisplaySize.Small) - { - Anchor = Anchor.CentreLeft, - Origin = Anchor.CentreLeft, - }, - difficultyRank = new TopLocalRank - { - Anchor = Anchor.CentreLeft, - Origin = Anchor.CentreLeft, - Scale = new Vector2(0.75f) - }, - starCounter = new StarCounter - { - Anchor = Anchor.CentreLeft, - Origin = Anchor.CentreLeft, - Scale = new Vector2(0.4f) - } + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + }, + difficultyRank = new TopLocalRank + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + Scale = new Vector2(0.75f) + }, + starCounter = new StarCounter + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + Scale = new Vector2(0.4f) } - }, - new FillFlowContainer + } + }, + new FillFlowContainer + { + Direction = FillDirection.Horizontal, + AutoSizeAxes = Axes.Both, + Children = new[] { - Direction = FillDirection.Horizontal, - AutoSizeAxes = Axes.Both, - Children = new[] + keyCountText = new OsuSpriteText { - keyCountText = new OsuSpriteText - { - Font = OsuFont.GetFont(size: 18, weight: FontWeight.SemiBold), - Anchor = Anchor.BottomLeft, - Origin = Anchor.BottomLeft, - Alpha = 0, - }, - difficultyText = new OsuSpriteText - { - Font = OsuFont.GetFont(size: 18, weight: FontWeight.SemiBold), - Anchor = Anchor.BottomLeft, - Origin = Anchor.BottomLeft, - Margin = new MarginPadding { Right = 8f }, - }, - authorText = new OsuSpriteText - { - Colour = colourProvider.Content2, - Font = OsuFont.GetFont(weight: FontWeight.SemiBold), - Anchor = Anchor.BottomLeft, - Origin = Anchor.BottomLeft - } + Font = OsuFont.GetFont(size: 18, weight: FontWeight.SemiBold), + Anchor = Anchor.BottomLeft, + Origin = Anchor.BottomLeft, + Alpha = 0, + }, + difficultyText = new OsuSpriteText + { + Font = OsuFont.GetFont(size: 18, weight: FontWeight.SemiBold), + Anchor = Anchor.BottomLeft, + Origin = Anchor.BottomLeft, + Margin = new MarginPadding { Right = 8f }, + }, + authorText = new OsuSpriteText + { + Colour = colourProvider.Content2, + Font = OsuFont.GetFont(weight: FontWeight.SemiBold), + Anchor = Anchor.BottomLeft, + Origin = Anchor.BottomLeft } } } - }, - } + } + }, }; } @@ -183,8 +175,8 @@ namespace osu.Game.Screens.SelectV2 updateKeyCount(); }, true); - Selected.BindValueChanged(s => panel.Active.Value = s.NewValue, true); - KeyboardSelected.BindValueChanged(k => panel.KeyboardActive.Value = k.NewValue, true); + Selected.BindValueChanged(s => Expanded.Value = s.NewValue, true); + KeyboardSelected.BindValueChanged(k => KeyboardSelected.Value = k.NewValue, true); } protected override void PrepareForUse() @@ -261,23 +253,7 @@ namespace osu.Game.Screens.SelectV2 var starRatingColour = colours.ForStarDifficulty(starDifficulty.Stars); starCounter.FadeColour(starRatingColour, duration, Easing.OutQuint); - panel.AccentColour = starRatingColour; + AccentColour = starRatingColour; } - - #region ICarouselPanel - - public CarouselItem? Item { get; set; } - public BindableBool Selected { get; } = new BindableBool(); - public BindableBool Expanded { get; } = new BindableBool(); - public BindableBool KeyboardSelected { get; } = new BindableBool(); - - public double DrawYPosition { get; set; } - - public void Activated() - { - panel.Flash(); - } - - #endregion } } diff --git a/osu.Game/Screens/SelectV2/PanelBeatmapSet.cs b/osu.Game/Screens/SelectV2/PanelBeatmapSet.cs index 2904cda9de..742fe6b6e6 100644 --- a/osu.Game/Screens/SelectV2/PanelBeatmapSet.cs +++ b/osu.Game/Screens/SelectV2/PanelBeatmapSet.cs @@ -4,10 +4,8 @@ using System.Diagnostics; using System.Linq; using osu.Framework.Allocation; -using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; -using osu.Framework.Graphics.Pooling; using osu.Framework.Graphics.Sprites; using osu.Framework.Localisation; using osu.Game.Beatmaps; @@ -19,7 +17,7 @@ using osuTK; namespace osu.Game.Screens.SelectV2 { - public partial class PanelBeatmapSet : PoolableDrawable, ICarouselPanel + public partial class PanelBeatmapSet : PanelBase { public const float HEIGHT = CarouselItem.DEFAULT_HEIGHT * 1.6f; @@ -29,7 +27,6 @@ namespace osu.Game.Screens.SelectV2 private const float duration = 500; - private CarouselPanelPiece panel = null!; private BeatmapSetPanelBackground background = null!; private OsuSpriteText titleText = null!; @@ -39,15 +36,17 @@ namespace osu.Game.Screens.SelectV2 private BeatmapSetOnlineStatusPill statusPill = null!; private DifficultySpectrumDisplay difficultiesDisplay = null!; - [Resolved] - private BeatmapCarousel? carousel { get; set; } - [Resolved] private OverlayColourProvider colourProvider { get; set; } = null!; [Resolved] private BeatmapManager beatmaps { get; set; } = null!; + public PanelBeatmapSet() + : base(set_x_offset) + { + } + [BackgroundDependencyLoader] private void load() { @@ -56,27 +55,28 @@ namespace osu.Game.Screens.SelectV2 RelativeSizeAxes = Axes.X; Height = HEIGHT; - InternalChild = panel = new CarouselPanelPiece(set_x_offset) + Icon = chevronIcon = new Container { - Action = () => carousel?.Activate(Item!), - Icon = chevronIcon = new Container + Size = new Vector2(22), + Child = new SpriteIcon { - Size = new Vector2(22), - Child = new SpriteIcon - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Icon = FontAwesome.Solid.ChevronRight, - Size = new Vector2(12), - X = 1f, - Colour = colourProvider.Background5, - }, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Icon = FontAwesome.Solid.ChevronRight, + Size = new Vector2(12), + X = 1f, + Colour = colourProvider.Background5, }, - Background = background = new BeatmapSetPanelBackground - { - RelativeSizeAxes = Axes.Both, - }, - Child = new FillFlowContainer + }; + + Background = background = new BeatmapSetPanelBackground + { + RelativeSizeAxes = Axes.Both, + }; + + Content.Children = new[] + { + new FillFlowContainer { AutoSizeAxes = Axes.Both, Direction = FillDirection.Vertical, @@ -132,12 +132,11 @@ namespace osu.Game.Screens.SelectV2 base.LoadComplete(); Expanded.BindValueChanged(_ => onExpanded(), true); - KeyboardSelected.BindValueChanged(k => panel.KeyboardActive.Value = k.NewValue, true); + KeyboardSelected.BindValueChanged(k => KeyboardSelected.Value = k.NewValue, true); } private void onExpanded() { - panel.Active.Value = Expanded.Value; chevronIcon.ResizeWidthTo(Expanded.Value ? 22 : 0f, duration, Easing.OutQuint); chevronIcon.FadeTo(Expanded.Value ? 1f : 0f, duration, Easing.OutQuint); } @@ -171,20 +170,5 @@ namespace osu.Game.Screens.SelectV2 updateButton.BeatmapSet = null; difficultiesDisplay.BeatmapSet = null; } - - #region ICarouselPanel - - public CarouselItem? Item { get; set; } - public BindableBool Selected { get; } = new BindableBool(); - public BindableBool Expanded { get; } = new BindableBool(); - public BindableBool KeyboardSelected { get; } = new BindableBool(); - - public double DrawYPosition { get; set; } - - public void Activated() - { - } - - #endregion } } diff --git a/osu.Game/Screens/SelectV2/PanelBeatmapStandalone.cs b/osu.Game/Screens/SelectV2/PanelBeatmapStandalone.cs index c858e039ec..c94a337cd9 100644 --- a/osu.Game/Screens/SelectV2/PanelBeatmapStandalone.cs +++ b/osu.Game/Screens/SelectV2/PanelBeatmapStandalone.cs @@ -1,7 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using System; using System.Collections.Generic; using System.Diagnostics; using System.Linq; @@ -10,7 +9,6 @@ using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; -using osu.Framework.Graphics.Pooling; using osu.Framework.Localisation; using osu.Game.Beatmaps; using osu.Game.Beatmaps.Drawables; @@ -25,7 +23,7 @@ using osuTK; namespace osu.Game.Screens.SelectV2 { - public partial class PanelBeatmapStandalone : PoolableDrawable, ICarouselPanel + public partial class PanelBeatmapStandalone : PanelBase { public const float HEIGHT = CarouselItem.DEFAULT_HEIGHT * 1.6f; @@ -35,9 +33,6 @@ namespace osu.Game.Screens.SelectV2 private const float duration = 500; - [Resolved] - private BeatmapCarousel? carousel { get; set; } - [Resolved] private IBindable ruleset { get; set; } = null!; @@ -59,7 +54,6 @@ namespace osu.Game.Screens.SelectV2 private IBindable? starDifficultyBindable; private CancellationTokenSource? starDifficultyCancellationSource; - private CarouselPanelPiece panel = null!; private BeatmapSetPanelBackground background = null!; private OsuSpriteText titleText = null!; @@ -75,6 +69,11 @@ namespace osu.Game.Screens.SelectV2 private OsuSpriteText difficultyName = null!; private OsuSpriteText difficultyAuthor = null!; + public PanelBeatmapStandalone() + : base(standalone_x_offset) + { + } + [BackgroundDependencyLoader] private void load() { @@ -84,107 +83,105 @@ namespace osu.Game.Screens.SelectV2 Width = 1f; Height = HEIGHT; - InternalChild = panel = new CarouselPanelPiece(standalone_x_offset) + Icon = difficultyIcon = new ConstrainedIconContainer { - Action = () => carousel?.Activate(Item!), - Icon = difficultyIcon = new ConstrainedIconContainer + Size = new Vector2(20), + Margin = new MarginPadding { Horizontal = 5f }, + Colour = colourProvider.Background5, + }; + + Background = background = new BeatmapSetPanelBackground + { + RelativeSizeAxes = Axes.Both, + }; + + Content.Child = new FillFlowContainer + { + AutoSizeAxes = Axes.Both, + Direction = FillDirection.Vertical, + Padding = new MarginPadding { Top = 7.5f, Left = 15, Bottom = 5 }, + Children = new Drawable[] { - Size = new Vector2(20), - Margin = new MarginPadding { Horizontal = 5f }, - Colour = colourProvider.Background5, - }, - Background = background = new BeatmapSetPanelBackground - { - RelativeSizeAxes = Axes.Both, - }, - Child = new FillFlowContainer - { - AutoSizeAxes = Axes.Both, - Direction = FillDirection.Vertical, - Padding = new MarginPadding { Top = 7.5f, Left = 15, Bottom = 5 }, - Children = new Drawable[] + titleText = new OsuSpriteText { - titleText = new OsuSpriteText + Font = OsuFont.GetFont(weight: FontWeight.Bold, size: 22, italics: true), + Shadow = true, + }, + artistText = new OsuSpriteText + { + Font = OsuFont.GetFont(weight: FontWeight.SemiBold, size: 17, italics: true), + Shadow = true, + }, + new FillFlowContainer + { + Direction = FillDirection.Horizontal, + AutoSizeAxes = Axes.Both, + Margin = new MarginPadding { Top = 5f }, + Children = new Drawable[] { - Font = OsuFont.GetFont(weight: FontWeight.Bold, size: 22, italics: true), - Shadow = true, - }, - artistText = new OsuSpriteText - { - Font = OsuFont.GetFont(weight: FontWeight.SemiBold, size: 17, italics: true), - Shadow = true, - }, - new FillFlowContainer - { - Direction = FillDirection.Horizontal, - AutoSizeAxes = Axes.Both, - Margin = new MarginPadding { Top = 5f }, - Children = new Drawable[] + updateButton = new UpdateBeatmapSetButton { - updateButton = new UpdateBeatmapSetButton - { - Anchor = Anchor.CentreLeft, - Origin = Anchor.CentreLeft, - Margin = new MarginPadding { Right = 5f, Top = -2f }, - }, - statusPill = new BeatmapSetOnlineStatusPill - { - AutoSizeAxes = Axes.Both, - Origin = Anchor.CentreLeft, - Anchor = Anchor.CentreLeft, - TextSize = 11, - TextPadding = new MarginPadding { Horizontal = 8, Vertical = 2 }, - Margin = new MarginPadding { Right = 5f }, - }, - difficultyLine = new FillFlowContainer - { - Direction = FillDirection.Horizontal, - AutoSizeAxes = Axes.Both, - Children = new Drawable[] - { - difficultyStarRating = new StarRatingDisplay(default, StarRatingDisplaySize.Small) - { - Origin = Anchor.CentreLeft, - Anchor = Anchor.CentreLeft, - Scale = new Vector2(8f / 9f), - Margin = new MarginPadding { Right = 5f }, - }, - difficultyRank = new TopLocalRank - { - Scale = new Vector2(8f / 11), - Origin = Anchor.CentreLeft, - Anchor = Anchor.CentreLeft, - Margin = new MarginPadding { Right = 5f }, - }, - difficultyKeyCountText = new OsuSpriteText - { - Font = OsuFont.GetFont(size: 18, weight: FontWeight.SemiBold), - Anchor = Anchor.BottomLeft, - Origin = Anchor.BottomLeft, - Alpha = 0, - Margin = new MarginPadding { Bottom = 2f }, - }, - difficultyName = new OsuSpriteText - { - Font = OsuFont.GetFont(size: 18, weight: FontWeight.SemiBold), - Origin = Anchor.BottomLeft, - Anchor = Anchor.BottomLeft, - Margin = new MarginPadding { Right = 5f, Bottom = 2f }, - }, - difficultyAuthor = new OsuSpriteText - { - Colour = colourProvider.Content2, - Font = OsuFont.GetFont(weight: FontWeight.SemiBold), - Origin = Anchor.BottomLeft, - Anchor = Anchor.BottomLeft, - Margin = new MarginPadding { Right = 5f, Bottom = 2f }, - } - } - }, + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + Margin = new MarginPadding { Right = 5f, Top = -2f }, }, - } + statusPill = new BeatmapSetOnlineStatusPill + { + AutoSizeAxes = Axes.Both, + Origin = Anchor.CentreLeft, + Anchor = Anchor.CentreLeft, + TextSize = 11, + TextPadding = new MarginPadding { Horizontal = 8, Vertical = 2 }, + Margin = new MarginPadding { Right = 5f }, + }, + difficultyLine = new FillFlowContainer + { + Direction = FillDirection.Horizontal, + AutoSizeAxes = Axes.Both, + Children = new Drawable[] + { + difficultyStarRating = new StarRatingDisplay(default, StarRatingDisplaySize.Small) + { + Origin = Anchor.CentreLeft, + Anchor = Anchor.CentreLeft, + Scale = new Vector2(8f / 9f), + Margin = new MarginPadding { Right = 5f }, + }, + difficultyRank = new TopLocalRank + { + Scale = new Vector2(8f / 11), + Origin = Anchor.CentreLeft, + Anchor = Anchor.CentreLeft, + Margin = new MarginPadding { Right = 5f }, + }, + difficultyKeyCountText = new OsuSpriteText + { + Font = OsuFont.GetFont(size: 18, weight: FontWeight.SemiBold), + Anchor = Anchor.BottomLeft, + Origin = Anchor.BottomLeft, + Alpha = 0, + Margin = new MarginPadding { Bottom = 2f }, + }, + difficultyName = new OsuSpriteText + { + Font = OsuFont.GetFont(size: 18, weight: FontWeight.SemiBold), + Origin = Anchor.BottomLeft, + Anchor = Anchor.BottomLeft, + Margin = new MarginPadding { Right = 5f, Bottom = 2f }, + }, + difficultyAuthor = new OsuSpriteText + { + Colour = colourProvider.Content2, + Font = OsuFont.GetFont(weight: FontWeight.SemiBold), + Origin = Anchor.BottomLeft, + Anchor = Anchor.BottomLeft, + Margin = new MarginPadding { Right = 5f, Bottom = 2f }, + } + } + }, + }, } - }, + } }; } @@ -203,9 +200,6 @@ namespace osu.Game.Screens.SelectV2 computeStarRating(); updateKeyCount(); }, true); - - Selected.BindValueChanged(s => panel.Active.Value = s.NewValue, true); - KeyboardSelected.BindValueChanged(k => panel.KeyboardActive.Value = k.NewValue, true); } protected override void PrepareForUse() @@ -289,26 +283,9 @@ namespace osu.Game.Screens.SelectV2 { var starDifficulty = starDifficultyBindable?.Value ?? default; - panel.AccentColour = colours.ForStarDifficulty(starDifficulty.Stars); + AccentColour = colours.ForStarDifficulty(starDifficulty.Stars); difficultyIcon.FadeColour(starDifficulty.Stars > 6.5f ? colours.Orange1 : colourProvider.Background5, duration, Easing.OutQuint); difficultyStarRating.Current.Value = starDifficulty; } - - #region ICarouselPanel - - public CarouselItem? Item { get; set; } - public BindableBool Selected { get; } = new BindableBool(); - public BindableBool Expanded { get; } = new BindableBool(); - public BindableBool KeyboardSelected { get; } = new BindableBool(); - - public double DrawYPosition { get; set; } - - public void Activated() - { - // sets should never be activated. - throw new InvalidOperationException(); - } - - #endregion } } diff --git a/osu.Game/Screens/SelectV2/PanelGroup.cs b/osu.Game/Screens/SelectV2/PanelGroup.cs index cdd0695147..2b4fb9e4a9 100644 --- a/osu.Game/Screens/SelectV2/PanelGroup.cs +++ b/osu.Game/Screens/SelectV2/PanelGroup.cs @@ -3,11 +3,9 @@ using System.Diagnostics; 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.Pooling; using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Sprites; using osu.Game.Graphics; @@ -18,16 +16,12 @@ using osuTK.Graphics; namespace osu.Game.Screens.SelectV2 { - public partial class PanelGroup : PoolableDrawable, ICarouselPanel + public partial class PanelGroup : PanelBase { public const float HEIGHT = CarouselItem.DEFAULT_HEIGHT; private const float duration = 500; - [Resolved] - private BeatmapCarousel? carousel { get; set; } - - private CarouselPanelPiece panel = null!; private Drawable chevronIcon = null!; private OsuSpriteText titleText = null!; @@ -39,57 +33,53 @@ namespace osu.Game.Screens.SelectV2 RelativeSizeAxes = Axes.X; Height = HEIGHT; - InternalChild = panel = new CarouselPanelPiece(0) + Icon = chevronIcon = new SpriteIcon { - Action = () => carousel?.Activate(Item!), - Icon = chevronIcon = new SpriteIcon + AlwaysPresent = true, + Icon = FontAwesome.Solid.ChevronDown, + Size = new Vector2(12), + Margin = new MarginPadding { Horizontal = 5f }, + X = 2f, + Colour = colourProvider.Background3, + }; + Background = new Box + { + RelativeSizeAxes = Axes.Both, + Colour = colourProvider.Dark1, + }; + AccentColour = colourProvider.Highlight1; + Content.Children = new Drawable[] + { + titleText = new OsuSpriteText { - AlwaysPresent = true, - Icon = FontAwesome.Solid.ChevronDown, - Size = new Vector2(12), - Margin = new MarginPadding { Horizontal = 5f }, - X = 2f, - Colour = colourProvider.Background3, + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + X = 10f, }, - Background = new Box + new CircularContainer { - RelativeSizeAxes = Axes.Both, - Colour = colourProvider.Dark1, - }, - AccentColour = colourProvider.Highlight1, - Children = new Drawable[] - { - titleText = new OsuSpriteText + Anchor = Anchor.CentreRight, + Origin = Anchor.CentreRight, + Size = new Vector2(50f, 14f), + Margin = new MarginPadding { Right = 20f }, + Masking = true, + Children = new Drawable[] { - Anchor = Anchor.CentreLeft, - Origin = Anchor.CentreLeft, - X = 10f, - }, - new CircularContainer - { - Anchor = Anchor.CentreRight, - Origin = Anchor.CentreRight, - Size = new Vector2(50f, 14f), - Margin = new MarginPadding { Right = 20f }, - Masking = true, - Children = new Drawable[] + new Box { - new Box - { - RelativeSizeAxes = Axes.Both, - Colour = Color4.Black.Opacity(0.7f), - }, - new OsuSpriteText - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Font = OsuFont.Torus.With(size: 14.4f, weight: FontWeight.Bold), - // TODO: requires Carousel/CarouselItem-side implementation - Text = "43", - UseFullGlyphHeight = false, - } + RelativeSizeAxes = Axes.Both, + Colour = Color4.Black.Opacity(0.7f), }, - } + new OsuSpriteText + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Font = OsuFont.Torus.With(size: 14.4f, weight: FontWeight.Bold), + // TODO: requires Carousel/CarouselItem-side implementation + Text = "43", + UseFullGlyphHeight = false, + } + }, } }; } @@ -99,14 +89,10 @@ namespace osu.Game.Screens.SelectV2 base.LoadComplete(); Expanded.BindValueChanged(_ => onExpanded(), true); - KeyboardSelected.BindValueChanged(k => panel.KeyboardActive.Value = k.NewValue, true); } private void onExpanded() { - panel.Active.Value = Expanded.Value; - panel.Flash(); - chevronIcon.ResizeWidthTo(Expanded.Value ? 12f : 0f, duration, Easing.OutQuint); chevronIcon.FadeTo(Expanded.Value ? 1f : 0f, duration, Easing.OutQuint); } @@ -124,20 +110,5 @@ namespace osu.Game.Screens.SelectV2 FinishTransforms(true); this.FadeInFromZero(500, Easing.OutQuint); } - - #region ICarouselPanel - - public CarouselItem? Item { get; set; } - public BindableBool Selected { get; } = new BindableBool(); - public BindableBool Expanded { get; } = new BindableBool(); - public BindableBool KeyboardSelected { get; } = new BindableBool(); - - public double DrawYPosition { get; set; } - - public void Activated() - { - } - - #endregion } } diff --git a/osu.Game/Screens/SelectV2/PanelGroupStarDifficulty.cs b/osu.Game/Screens/SelectV2/PanelGroupStarDifficulty.cs new file mode 100644 index 0000000000..736a0f71dc --- /dev/null +++ b/osu.Game/Screens/SelectV2/PanelGroupStarDifficulty.cs @@ -0,0 +1,149 @@ +// 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.Extensions.Color4Extensions; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; +using osu.Framework.Graphics.Sprites; +using osu.Game.Beatmaps; +using osu.Game.Beatmaps.Drawables; +using osu.Game.Graphics; +using osu.Game.Graphics.Sprites; +using osu.Game.Graphics.UserInterface; +using osu.Game.Overlays; +using osuTK; +using osuTK.Graphics; + +namespace osu.Game.Screens.SelectV2 +{ + public partial class PanelGroupStarDifficulty : PanelBase + { + public const float HEIGHT = CarouselItem.DEFAULT_HEIGHT; + + private const float duration = 500; + + [Resolved] + private OsuColour colours { get; set; } = null!; + + [Resolved] + private OverlayColourProvider colourProvider { get; set; } = null!; + + private Drawable chevronIcon = null!; + private Box contentBackground = null!; + private StarRatingDisplay starRatingDisplay = null!; + private StarCounter starCounter = null!; + + [BackgroundDependencyLoader] + private void load() + { + Anchor = Anchor.TopRight; + Origin = Anchor.TopRight; + RelativeSizeAxes = Axes.X; + Height = HEIGHT; + + Icon = chevronIcon = new SpriteIcon + { + AlwaysPresent = true, + Icon = FontAwesome.Solid.ChevronDown, + Size = new Vector2(12), + Margin = new MarginPadding { Horizontal = 5f }, + X = 2f, + }; + Background = contentBackground = new Box + { + RelativeSizeAxes = Axes.Both, + Colour = colourProvider.Dark1, + }; + AccentColour = colourProvider.Highlight1; + Content.Children = new Drawable[] + { + new FillFlowContainer + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + AutoSizeAxes = Axes.Both, + Spacing = new Vector2(10f, 0f), + Margin = new MarginPadding { Left = 10f }, + Children = new Drawable[] + { + starRatingDisplay = new StarRatingDisplay(default, StarRatingDisplaySize.Small) + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + }, + starCounter = new StarCounter + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + Scale = new Vector2(8f / 20f), + }, + } + }, + new CircularContainer + { + Anchor = Anchor.CentreRight, + Origin = Anchor.CentreRight, + Size = new Vector2(50f, 14f), + Margin = new MarginPadding { Right = 20f }, + Masking = true, + Children = new Drawable[] + { + new Box + { + RelativeSizeAxes = Axes.Both, + Colour = Color4.Black.Opacity(0.7f), + }, + new OsuSpriteText + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Font = OsuFont.Torus.With(size: 14.4f, weight: FontWeight.Bold), + // TODO: requires Carousel/CarouselItem-side implementation + Text = "43", + UseFullGlyphHeight = false, + } + }, + } + }; + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + Expanded.BindValueChanged(_ => onExpanded(), true); + } + + private void onExpanded() + { + chevronIcon.ResizeWidthTo(Expanded.Value ? 12f : 0f, duration, Easing.OutQuint); + chevronIcon.FadeTo(Expanded.Value ? 1f : 0f, duration, Easing.OutQuint); + } + + protected override void PrepareForUse() + { + base.PrepareForUse(); + + Debug.Assert(Item != null); + + int starNumber = (int)((GroupDefinition)Item.Model).Data; + + Color4 colour = starNumber >= 9 ? OsuColour.Gray(0.2f) : colours.ForStarDifficulty(starNumber); + Color4 contentColour = starNumber >= 7 ? colours.Orange1 : colourProvider.Background5; + + AccentColour = colour; + contentBackground.Colour = colour.Darken(0.3f); + + starRatingDisplay.Current.Value = new StarDifficulty(starNumber, 0); + starCounter.Current = starNumber; + + chevronIcon.Colour = contentColour; + starCounter.Colour = contentColour; + + this.FadeInFromZero(500, Easing.OutQuint); + } + } +} diff --git a/osu.Game/Screens/SelectV2/PanelGroupStarDificulty.cs b/osu.Game/Screens/SelectV2/PanelGroupStarDificulty.cs deleted file mode 100644 index 2215e643bd..0000000000 --- a/osu.Game/Screens/SelectV2/PanelGroupStarDificulty.cs +++ /dev/null @@ -1,187 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -using System; -using System.Diagnostics; -using osu.Framework.Allocation; -using osu.Framework.Bindables; -using osu.Framework.Extensions.Color4Extensions; -using osu.Framework.Graphics; -using osu.Framework.Graphics.Containers; -using osu.Framework.Graphics.Pooling; -using osu.Framework.Graphics.Shapes; -using osu.Framework.Graphics.Sprites; -using osu.Game.Beatmaps; -using osu.Game.Beatmaps.Drawables; -using osu.Game.Graphics; -using osu.Game.Graphics.Sprites; -using osu.Game.Graphics.UserInterface; -using osu.Game.Overlays; -using osuTK; -using osuTK.Graphics; - -namespace osu.Game.Screens.SelectV2 -{ - public partial class PanelGroupStarDificulty : PoolableDrawable, ICarouselPanel - { - public const float HEIGHT = CarouselItem.DEFAULT_HEIGHT; - - private const float duration = 500; - - [Resolved] - private BeatmapCarousel? carousel { get; set; } - - [Resolved] - private OsuColour colours { get; set; } = null!; - - [Resolved] - private OverlayColourProvider colourProvider { get; set; } = null!; - - private CarouselPanelPiece panel = null!; - private Drawable chevronIcon = null!; - private Box contentBackground = null!; - private StarRatingDisplay starRatingDisplay = null!; - private StarCounter starCounter = null!; - - [BackgroundDependencyLoader] - private void load() - { - Anchor = Anchor.TopRight; - Origin = Anchor.TopRight; - RelativeSizeAxes = Axes.X; - Height = HEIGHT; - - InternalChild = panel = new CarouselPanelPiece(0) - { - Action = onAction, - Icon = chevronIcon = new SpriteIcon - { - AlwaysPresent = true, - Icon = FontAwesome.Solid.ChevronDown, - Size = new Vector2(12), - Margin = new MarginPadding { Horizontal = 5f }, - X = 2f, - }, - Background = contentBackground = new Box - { - RelativeSizeAxes = Axes.Both, - Colour = colourProvider.Dark1, - }, - AccentColour = colourProvider.Highlight1, - Children = new Drawable[] - { - new FillFlowContainer - { - Anchor = Anchor.CentreLeft, - Origin = Anchor.CentreLeft, - AutoSizeAxes = Axes.Both, - Spacing = new Vector2(10f, 0f), - Margin = new MarginPadding { Left = 10f }, - Children = new Drawable[] - { - starRatingDisplay = new StarRatingDisplay(default, StarRatingDisplaySize.Small) - { - Anchor = Anchor.CentreLeft, - Origin = Anchor.CentreLeft, - }, - starCounter = new StarCounter - { - Anchor = Anchor.CentreLeft, - Origin = Anchor.CentreLeft, - Scale = new Vector2(8f / 20f), - }, - } - }, - new CircularContainer - { - Anchor = Anchor.CentreRight, - Origin = Anchor.CentreRight, - Size = new Vector2(50f, 14f), - Margin = new MarginPadding { Right = 20f }, - Masking = true, - Children = new Drawable[] - { - new Box - { - RelativeSizeAxes = Axes.Both, - Colour = Color4.Black.Opacity(0.7f), - }, - new OsuSpriteText - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Font = OsuFont.Torus.With(size: 14.4f, weight: FontWeight.Bold), - // TODO: requires Carousel/CarouselItem-side implementation - Text = "43", - UseFullGlyphHeight = false, - } - }, - } - } - }; - } - - protected override void LoadComplete() - { - base.LoadComplete(); - - Expanded.BindValueChanged(_ => onExpanded(), true); - KeyboardSelected.BindValueChanged(k => panel.KeyboardActive.Value = k.NewValue, true); - } - - private void onExpanded() - { - panel.Active.Value = Expanded.Value; - panel.Flash(); - - chevronIcon.ResizeWidthTo(Expanded.Value ? 12f : 0f, duration, Easing.OutQuint); - chevronIcon.FadeTo(Expanded.Value ? 1f : 0f, duration, Easing.OutQuint); - } - - protected override void PrepareForUse() - { - base.PrepareForUse(); - - Debug.Assert(Item != null); - - int starNumber = (int)((GroupDefinition)Item.Model).Data; - - Color4 colour = starNumber >= 9 ? OsuColour.Gray(0.2f) : colours.ForStarDifficulty(starNumber); - Color4 contentColour = starNumber >= 7 ? colours.Orange1 : colourProvider.Background5; - - panel.AccentColour = colour; - contentBackground.Colour = colour.Darken(0.3f); - - starRatingDisplay.Current.Value = new StarDifficulty(starNumber, 0); - starCounter.Current = starNumber; - - chevronIcon.Colour = contentColour; - starCounter.Colour = contentColour; - - this.FadeInFromZero(500, Easing.OutQuint); - } - - private void onAction() - { - if (carousel != null) - carousel.CurrentSelection = Item!.Model; - } - - #region ICarouselPanel - - public CarouselItem? Item { get; set; } - public BindableBool Selected { get; } = new BindableBool(); - public BindableBool Expanded { get; } = new BindableBool(); - public BindableBool KeyboardSelected { get; } = new BindableBool(); - - public double DrawYPosition { get; set; } - - public void Activated() - { - // sets should never be activated. - throw new InvalidOperationException(); - } - - #endregion - } -} From 5de9584171cfcbc6394e5bc52c547d5ebac4573e Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 18 Feb 2025 16:24:04 +0900 Subject: [PATCH 1068/1275] Move `PanelXOffset` to `init` property rather than ctor Feels better to me. --- osu.Game/Screens/SelectV2/PanelBase.cs | 53 +++++++------------ osu.Game/Screens/SelectV2/PanelBeatmapSet.cs | 6 +-- .../SelectV2/PanelBeatmapStandalone.cs | 6 +-- 3 files changed, 21 insertions(+), 44 deletions(-) diff --git a/osu.Game/Screens/SelectV2/PanelBase.cs b/osu.Game/Screens/SelectV2/PanelBase.cs index d5a087dbb2..9773d93f45 100644 --- a/osu.Game/Screens/SelectV2/PanelBase.cs +++ b/osu.Game/Screens/SelectV2/PanelBase.cs @@ -29,31 +29,25 @@ namespace osu.Game.Screens.SelectV2 private const float duration = 500; - private readonly float panelXOffset; + protected float PanelXOffset { get; init; } - private readonly Box backgroundBorder; - private readonly Box backgroundGradient; - private readonly Box backgroundAccentGradient; - private readonly Container backgroundLayer; - private readonly Container backgroundLayerHorizontalPadding; - private readonly Container backgroundContainer; - private readonly Container iconContainer; - private readonly Box activationFlash; - private readonly Box hoverLayer; + private Box backgroundBorder = null!; + private Box backgroundGradient = null!; + private Box backgroundAccentGradient = null!; + private Container backgroundLayer = null!; + private Container backgroundLayerHorizontalPadding = null!; + private Container backgroundContainer = null!; + private Container iconContainer = null!; + private Box activationFlash = null!; + private Box hoverLayer = null!; - public Container TopLevelContent { get; } + public Container TopLevelContent { get; private set; } = null!; - protected Container Content { get; } + protected Container Content { get; private set; } = null!; - public Drawable Background - { - set => backgroundContainer.Child = value; - } + public Drawable Background { set => backgroundContainer.Child = value; } - public Drawable Icon - { - set => iconContainer.Child = value; - } + public Drawable Icon { set => iconContainer.Child = value; } private Color4? accentColour; @@ -77,10 +71,9 @@ namespace osu.Game.Screens.SelectV2 return inputRectangle.Contains(TopLevelContent.ToLocalSpace(screenSpacePos)); } - protected PanelBase(float panelXOffset = 0) + [BackgroundDependencyLoader] + private void load(OverlayColourProvider colourProvider, OsuColour colours) { - this.panelXOffset = panelXOffset; - RelativeSizeAxes = Axes.Both; InternalChild = TopLevelContent = new Container @@ -147,7 +140,7 @@ namespace osu.Game.Screens.SelectV2 Content = new Container { RelativeSizeAxes = Axes.Both, - Padding = new MarginPadding { Right = panelXOffset + corner_radius }, + Padding = new MarginPadding { Right = PanelXOffset + corner_radius }, }, hoverLayer = new Box { @@ -165,11 +158,7 @@ namespace osu.Game.Screens.SelectV2 new HoverSounds(), } }; - } - [BackgroundDependencyLoader] - private void load(OverlayColourProvider colourProvider, OsuColour colours) - { hoverLayer.Colour = colours.Blue.Opacity(0.1f); backgroundGradient.Colour = ColourInfo.GradientHorizontal(colourProvider.Background3, colourProvider.Background4); } @@ -187,15 +176,11 @@ namespace osu.Game.Screens.SelectV2 protected override bool OnClick(ClickEvent e) { + activationFlash.FadeOutFromOne(500, Easing.OutQuint); carousel?.Activate(Item!); return true; } - public void Flash() - { - activationFlash.FadeOutFromOne(500, Easing.OutQuint); - } - private void updateDisplay() { backgroundLayer.TransformTo(nameof(Padding), backgroundLayer.Padding with { Vertical = Expanded.Value ? 2f : 0f }, duration, Easing.OutQuint); @@ -214,7 +199,7 @@ namespace osu.Game.Screens.SelectV2 private void updateXOffset() { - float x = panelXOffset + active_x_offset + keyboard_active_x_offset + left_edge_x_offset; + float x = PanelXOffset + active_x_offset + keyboard_active_x_offset + left_edge_x_offset; if (Expanded.Value) x -= active_x_offset; diff --git a/osu.Game/Screens/SelectV2/PanelBeatmapSet.cs b/osu.Game/Screens/SelectV2/PanelBeatmapSet.cs index 742fe6b6e6..6ac52acac0 100644 --- a/osu.Game/Screens/SelectV2/PanelBeatmapSet.cs +++ b/osu.Game/Screens/SelectV2/PanelBeatmapSet.cs @@ -21,10 +21,6 @@ namespace osu.Game.Screens.SelectV2 { public const float HEIGHT = CarouselItem.DEFAULT_HEIGHT * 1.6f; - // todo: this should be replaced with information from CarouselItem about how deep is PanelBeatmap in the carousel - // (i.e. whether it's under a beatmap set that's under a group, or just under a top-level beatmap set). - private const float set_x_offset = 20f; // constant X offset for beatmap set/standalone panels specifically. - private const float duration = 500; private BeatmapSetPanelBackground background = null!; @@ -43,8 +39,8 @@ namespace osu.Game.Screens.SelectV2 private BeatmapManager beatmaps { get; set; } = null!; public PanelBeatmapSet() - : base(set_x_offset) { + PanelXOffset = 20f; } [BackgroundDependencyLoader] diff --git a/osu.Game/Screens/SelectV2/PanelBeatmapStandalone.cs b/osu.Game/Screens/SelectV2/PanelBeatmapStandalone.cs index c94a337cd9..89f9df332f 100644 --- a/osu.Game/Screens/SelectV2/PanelBeatmapStandalone.cs +++ b/osu.Game/Screens/SelectV2/PanelBeatmapStandalone.cs @@ -27,10 +27,6 @@ namespace osu.Game.Screens.SelectV2 { public const float HEIGHT = CarouselItem.DEFAULT_HEIGHT * 1.6f; - // todo: this should be replaced with information from CarouselItem about how deep is PanelBeatmap in the carousel - // (i.e. whether it's under a beatmap set that's under a group, or just under a top-level beatmap set). - private const float standalone_x_offset = 20f; // constant X offset for beatmap set/standalone panels specifically. - private const float duration = 500; [Resolved] @@ -70,8 +66,8 @@ namespace osu.Game.Screens.SelectV2 private OsuSpriteText difficultyAuthor = null!; public PanelBeatmapStandalone() - : base(standalone_x_offset) { + PanelXOffset = 20; } [BackgroundDependencyLoader] From 644fb29843a9c33b137cf1056dea659b561815b0 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 18 Feb 2025 16:27:19 +0900 Subject: [PATCH 1069/1275] Fix input handling not matching latest `master` logic --- osu.Game/Screens/SelectV2/PanelBase.cs | 10 ---------- osu.Game/Screens/SelectV2/PanelBeatmap.cs | 14 +++++++------- 2 files changed, 7 insertions(+), 17 deletions(-) diff --git a/osu.Game/Screens/SelectV2/PanelBase.cs b/osu.Game/Screens/SelectV2/PanelBase.cs index 9773d93f45..d0499f44cb 100644 --- a/osu.Game/Screens/SelectV2/PanelBase.cs +++ b/osu.Game/Screens/SelectV2/PanelBase.cs @@ -61,16 +61,6 @@ namespace osu.Game.Screens.SelectV2 } } - public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) - { - var inputRectangle = TopLevelContent.DrawRectangle; - - // Cover potential gaps introduced by the spacing between panels. - inputRectangle = inputRectangle.Inflate(new MarginPadding { Vertical = BeatmapCarousel.SPACING / 2f }); - - return inputRectangle.Contains(TopLevelContent.ToLocalSpace(screenSpacePos)); - } - [BackgroundDependencyLoader] private void load(OverlayColourProvider colourProvider, OsuColour colours) { diff --git a/osu.Game/Screens/SelectV2/PanelBeatmap.cs b/osu.Game/Screens/SelectV2/PanelBeatmap.cs index 48d15f6857..69e8e34c40 100644 --- a/osu.Game/Screens/SelectV2/PanelBeatmap.cs +++ b/osu.Game/Screens/SelectV2/PanelBeatmap.cs @@ -52,6 +52,12 @@ namespace osu.Game.Screens.SelectV2 [Resolved] private BeatmapDifficultyCache difficultyCache { get; set; } = null!; + [Resolved] + private IBindable ruleset { get; set; } = null!; + + [Resolved] + private IBindable> mods { get; set; } = null!; + public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) { var inputRectangle = DrawRectangle; @@ -60,17 +66,11 @@ namespace osu.Game.Screens.SelectV2 // // Caveat is that for simplicity, we are covering the full spacing, so panels with frontmost depth will have a slightly // larger hit target. - inputRectangle = inputRectangle.Inflate(new MarginPadding { Vertical = BeatmapCarousel.SPACING / 2f }); + inputRectangle = inputRectangle.Inflate(new MarginPadding { Vertical = BeatmapCarousel.SPACING }); return inputRectangle.Contains(ToLocalSpace(screenSpacePos)); } - [Resolved] - private IBindable ruleset { get; set; } = null!; - - [Resolved] - private IBindable> mods { get; set; } = null!; - [BackgroundDependencyLoader] private void load(OverlayColourProvider colourProvider) { From 7e1984452fb7601330dcf9b0b693cdb17d41ca1f Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 18 Feb 2025 16:32:12 +0900 Subject: [PATCH 1070/1275] Tidy up remaining common code --- osu.Game/Screens/SelectV2/PanelBase.cs | 12 +++++++++- osu.Game/Screens/SelectV2/PanelBeatmap.cs | 16 ++----------- osu.Game/Screens/SelectV2/PanelBeatmapSet.cs | 10 ++------ .../SelectV2/PanelBeatmapStandalone.cs | 12 ++-------- osu.Game/Screens/SelectV2/PanelGroup.cs | 10 ++------ .../SelectV2/PanelGroupStarDifficulty.cs | 23 +++++++------------ 6 files changed, 27 insertions(+), 56 deletions(-) diff --git a/osu.Game/Screens/SelectV2/PanelBase.cs b/osu.Game/Screens/SelectV2/PanelBase.cs index d0499f44cb..805cbac8eb 100644 --- a/osu.Game/Screens/SelectV2/PanelBase.cs +++ b/osu.Game/Screens/SelectV2/PanelBase.cs @@ -64,7 +64,11 @@ namespace osu.Game.Screens.SelectV2 [BackgroundDependencyLoader] private void load(OverlayColourProvider colourProvider, OsuColour colours) { - RelativeSizeAxes = Axes.Both; + Anchor = Anchor.TopRight; + Origin = Anchor.TopRight; + + RelativeSizeAxes = Axes.X; + Height = CarouselItem.DEFAULT_HEIGHT; InternalChild = TopLevelContent = new Container { @@ -161,6 +165,12 @@ namespace osu.Game.Screens.SelectV2 KeyboardSelected.BindValueChanged(_ => updateDisplay(), true); } + protected override void PrepareForUse() + { + base.PrepareForUse(); + this.FadeInFromZero(duration, Easing.OutQuint); + } + [Resolved] private BeatmapCarousel? carousel { get; set; } diff --git a/osu.Game/Screens/SelectV2/PanelBeatmap.cs b/osu.Game/Screens/SelectV2/PanelBeatmap.cs index 69e8e34c40..dcac460905 100644 --- a/osu.Game/Screens/SelectV2/PanelBeatmap.cs +++ b/osu.Game/Screens/SelectV2/PanelBeatmap.cs @@ -26,12 +26,6 @@ namespace osu.Game.Screens.SelectV2 { public const float HEIGHT = CarouselItem.DEFAULT_HEIGHT; - // todo: this should be replaced with information from CarouselItem about how deep is PanelBeatmap in the carousel - // (i.e. whether it's under a beatmap set that's under a group, or just under a top-level beatmap set). - private const float difficulty_x_offset = 100f; // constant X offset for beatmap difficulty panels specifically. - - private const float duration = 500; - private StarCounter starCounter = null!; private ConstrainedIconContainer difficultyIcon = null!; private OsuSpriteText keyCountText = null!; @@ -74,11 +68,6 @@ namespace osu.Game.Screens.SelectV2 [BackgroundDependencyLoader] private void load(OverlayColourProvider colourProvider) { - Anchor = Anchor.TopRight; - Origin = Anchor.TopRight; - - RelativeSizeAxes = Axes.X; - Width = 1f; Height = HEIGHT; Icon = difficultyIcon = new ConstrainedIconContainer @@ -194,9 +183,6 @@ namespace osu.Game.Screens.SelectV2 computeStarRating(); updateKeyCount(); - - FinishTransforms(true); - this.FadeInFromZero(duration, Easing.OutQuint); } protected override void FreeAfterUse() @@ -244,6 +230,8 @@ namespace osu.Game.Screens.SelectV2 private void updateDisplay() { + const float duration = 500; + var starDifficulty = starDifficultyBindable?.Value ?? default; starRatingDisplay.Current.Value = starDifficulty; diff --git a/osu.Game/Screens/SelectV2/PanelBeatmapSet.cs b/osu.Game/Screens/SelectV2/PanelBeatmapSet.cs index 6ac52acac0..5c38fe8e04 100644 --- a/osu.Game/Screens/SelectV2/PanelBeatmapSet.cs +++ b/osu.Game/Screens/SelectV2/PanelBeatmapSet.cs @@ -21,8 +21,6 @@ namespace osu.Game.Screens.SelectV2 { public const float HEIGHT = CarouselItem.DEFAULT_HEIGHT * 1.6f; - private const float duration = 500; - private BeatmapSetPanelBackground background = null!; private OsuSpriteText titleText = null!; @@ -46,9 +44,6 @@ namespace osu.Game.Screens.SelectV2 [BackgroundDependencyLoader] private void load() { - Anchor = Anchor.TopRight; - Origin = Anchor.TopRight; - RelativeSizeAxes = Axes.X; Height = HEIGHT; Icon = chevronIcon = new Container @@ -133,6 +128,8 @@ namespace osu.Game.Screens.SelectV2 private void onExpanded() { + const float duration = 500; + chevronIcon.ResizeWidthTo(Expanded.Value ? 22 : 0f, duration, Easing.OutQuint); chevronIcon.FadeTo(Expanded.Value ? 1f : 0f, duration, Easing.OutQuint); } @@ -153,9 +150,6 @@ namespace osu.Game.Screens.SelectV2 updateButton.BeatmapSet = beatmapSet; statusPill.Status = beatmapSet.Status; difficultiesDisplay.BeatmapSet = beatmapSet; - - FinishTransforms(true); - this.FadeInFromZero(duration, Easing.OutQuint); } protected override void FreeAfterUse() diff --git a/osu.Game/Screens/SelectV2/PanelBeatmapStandalone.cs b/osu.Game/Screens/SelectV2/PanelBeatmapStandalone.cs index 89f9df332f..231c7274be 100644 --- a/osu.Game/Screens/SelectV2/PanelBeatmapStandalone.cs +++ b/osu.Game/Screens/SelectV2/PanelBeatmapStandalone.cs @@ -27,8 +27,6 @@ namespace osu.Game.Screens.SelectV2 { public const float HEIGHT = CarouselItem.DEFAULT_HEIGHT * 1.6f; - private const float duration = 500; - [Resolved] private IBindable ruleset { get; set; } = null!; @@ -73,10 +71,6 @@ namespace osu.Game.Screens.SelectV2 [BackgroundDependencyLoader] private void load() { - Anchor = Anchor.TopRight; - Origin = Anchor.TopRight; - RelativeSizeAxes = Axes.X; - Width = 1f; Height = HEIGHT; Icon = difficultyIcon = new ConstrainedIconContainer @@ -224,10 +218,6 @@ namespace osu.Game.Screens.SelectV2 difficultyLine.Show(); computeStarRating(); - - FinishTransforms(true); - - this.FadeInFromZero(duration, Easing.OutQuint); } protected override void FreeAfterUse() @@ -277,6 +267,8 @@ namespace osu.Game.Screens.SelectV2 private void updateDisplay() { + const float duration = 500; + var starDifficulty = starDifficultyBindable?.Value ?? default; AccentColour = colours.ForStarDifficulty(starDifficulty.Stars); diff --git a/osu.Game/Screens/SelectV2/PanelGroup.cs b/osu.Game/Screens/SelectV2/PanelGroup.cs index 2b4fb9e4a9..ecb64f4797 100644 --- a/osu.Game/Screens/SelectV2/PanelGroup.cs +++ b/osu.Game/Screens/SelectV2/PanelGroup.cs @@ -20,17 +20,12 @@ namespace osu.Game.Screens.SelectV2 { public const float HEIGHT = CarouselItem.DEFAULT_HEIGHT; - private const float duration = 500; - private Drawable chevronIcon = null!; private OsuSpriteText titleText = null!; [BackgroundDependencyLoader] private void load(OverlayColourProvider colourProvider) { - Anchor = Anchor.TopRight; - Origin = Anchor.TopRight; - RelativeSizeAxes = Axes.X; Height = HEIGHT; Icon = chevronIcon = new SpriteIcon @@ -93,6 +88,8 @@ namespace osu.Game.Screens.SelectV2 private void onExpanded() { + const float duration = 500; + chevronIcon.ResizeWidthTo(Expanded.Value ? 12f : 0f, duration, Easing.OutQuint); chevronIcon.FadeTo(Expanded.Value ? 1f : 0f, duration, Easing.OutQuint); } @@ -106,9 +103,6 @@ namespace osu.Game.Screens.SelectV2 GroupDefinition group = (GroupDefinition)Item.Model; titleText.Text = group.Title; - - FinishTransforms(true); - this.FadeInFromZero(500, Easing.OutQuint); } } } diff --git a/osu.Game/Screens/SelectV2/PanelGroupStarDifficulty.cs b/osu.Game/Screens/SelectV2/PanelGroupStarDifficulty.cs index 736a0f71dc..0dc5a2f365 100644 --- a/osu.Game/Screens/SelectV2/PanelGroupStarDifficulty.cs +++ b/osu.Game/Screens/SelectV2/PanelGroupStarDifficulty.cs @@ -21,10 +21,6 @@ namespace osu.Game.Screens.SelectV2 { public partial class PanelGroupStarDifficulty : PanelBase { - public const float HEIGHT = CarouselItem.DEFAULT_HEIGHT; - - private const float duration = 500; - [Resolved] private OsuColour colours { get; set; } = null!; @@ -39,10 +35,7 @@ namespace osu.Game.Screens.SelectV2 [BackgroundDependencyLoader] private void load() { - Anchor = Anchor.TopRight; - Origin = Anchor.TopRight; - RelativeSizeAxes = Axes.X; - Height = HEIGHT; + Height = PanelGroup.HEIGHT; Icon = chevronIcon = new SpriteIcon { @@ -117,12 +110,6 @@ namespace osu.Game.Screens.SelectV2 Expanded.BindValueChanged(_ => onExpanded(), true); } - private void onExpanded() - { - chevronIcon.ResizeWidthTo(Expanded.Value ? 12f : 0f, duration, Easing.OutQuint); - chevronIcon.FadeTo(Expanded.Value ? 1f : 0f, duration, Easing.OutQuint); - } - protected override void PrepareForUse() { base.PrepareForUse(); @@ -142,8 +129,14 @@ namespace osu.Game.Screens.SelectV2 chevronIcon.Colour = contentColour; starCounter.Colour = contentColour; + } - this.FadeInFromZero(500, Easing.OutQuint); + private void onExpanded() + { + const float duration = 500; + + chevronIcon.ResizeWidthTo(Expanded.Value ? 12f : 0f, duration, Easing.OutQuint); + chevronIcon.FadeTo(Expanded.Value ? 1f : 0f, duration, Easing.OutQuint); } } } From 8299dfc6f2341f82ac1baeeb40967a5391296de8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 18 Feb 2025 10:10:51 +0100 Subject: [PATCH 1071/1275] Add local guard before scheduled placeholder user set When API is in `RequiresSecondFactorAuth` state, `attemptConnect()` is called over and over in a loop, with no sleeping, which means that the scheduler accumulates hundreds of thousands of these delegates. Sure you could add a sleep in there maybe, but it seems pretty wasteful to have the `localUser.IsDefault` guard *inside* the schedule anyway, so this is what I opted for. --- osu.Game/Online/API/APIAccess.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/osu.Game/Online/API/APIAccess.cs b/osu.Game/Online/API/APIAccess.cs index 88f9b3f242..711866b2aa 100644 --- a/osu.Game/Online/API/APIAccess.cs +++ b/osu.Game/Online/API/APIAccess.cs @@ -238,7 +238,8 @@ namespace osu.Game.Online.API /// Whether the connection attempt was successful. private void attemptConnect() { - Scheduler.Add(setPlaceholderLocalUser, false); + if (localUser.IsDefault) + 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); From d6552f00bed2359296df91050062a3786c59493e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 18 Feb 2025 10:14:00 +0100 Subject: [PATCH 1072/1275] Do not attempt to automatically reconnect if there is no login to use Because it'll fail anyway - there is either no username or no password. The reason why this is important is that the block was also setting API state to `Connecting`. --- osu.Game/Online/API/APIAccess.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Online/API/APIAccess.cs b/osu.Game/Online/API/APIAccess.cs index 711866b2aa..a90fccc1c0 100644 --- a/osu.Game/Online/API/APIAccess.cs +++ b/osu.Game/Online/API/APIAccess.cs @@ -244,7 +244,7 @@ namespace osu.Game.Online.API // 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); - if (!authentication.HasValidAccessToken) + if (!authentication.HasValidAccessToken && HasLogin) { state.Value = APIState.Connecting; LastLoginError = null; From 930e02300f200388e5398fee1e34f14108f09d78 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 18 Feb 2025 10:20:38 +0100 Subject: [PATCH 1073/1275] Do not allow flushed requests to transition API into `Failing` state Flushes are assumed to have already come from a definitive state change (read: disconnection). Allowing the exceptions that come from failing the flushed requests to trigger the `Failing` code paths makes completely incorrect behaviour possible. --- osu.Game/Online/API/APIAccess.cs | 27 ++++++++++++++++++++++++--- 1 file changed, 24 insertions(+), 3 deletions(-) diff --git a/osu.Game/Online/API/APIAccess.cs b/osu.Game/Online/API/APIAccess.cs index a90fccc1c0..479fc99805 100644 --- a/osu.Game/Online/API/APIAccess.cs +++ b/osu.Game/Online/API/APIAccess.cs @@ -253,6 +253,10 @@ namespace osu.Game.Online.API { authentication.AuthenticateWithLogin(ProvidedUsername, password); } + catch (WebRequestFlushedException) + { + return; + } catch (Exception e) { //todo: this fails even on network-related issues. we should probably handle those differently. @@ -313,7 +317,7 @@ namespace osu.Game.Online.API log.Add(@"Login no longer valid"); Logout(); } - else + else if (ex is not WebRequestFlushedException) { state.Value = APIState.Failing; } @@ -494,6 +498,11 @@ namespace osu.Game.Online.API handleWebException(we); return false; } + catch (WebRequestFlushedException wrf) + { + log.Add(wrf.Message); + return false; + } catch (Exception ex) { Logger.Error(ex, "Error occurred while handling an API request."); @@ -575,7 +584,7 @@ namespace osu.Game.Online.API if (failOldRequests) { foreach (var req in oldQueueRequests) - req.Fail(new WebException($@"Request failed from flush operation (state {state.Value})")); + req.Fail(new WebRequestFlushedException(state.Value)); } } } @@ -606,7 +615,11 @@ namespace osu.Game.Online.API return; var friendsReq = new GetFriendsRequest(); - friendsReq.Failure += _ => state.Value = APIState.Failing; + friendsReq.Failure += ex => + { + if (ex is not WebRequestFlushedException) + state.Value = APIState.Failing; + }; friendsReq.Success += res => { var existingFriends = friends.Select(f => f.TargetID).ToHashSet(); @@ -631,6 +644,14 @@ namespace osu.Game.Online.API flushQueue(); cancellationToken.Cancel(); } + + private class WebRequestFlushedException : Exception + { + public WebRequestFlushedException(APIState state) + : base($@"Request failed from flush operation (state {state})") + { + } + } } internal class GuestUser : APIUser From b3aba537b5f081978ea4d349562ec8c02289f737 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 18 Feb 2025 11:33:30 +0100 Subject: [PATCH 1074/1275] Add missing early return As spotted in testing with production. Would cause submission to proceed even if the export did, with an empty archive. --- osu.Game/Screens/Edit/Submission/BeatmapSubmissionScreen.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/osu.Game/Screens/Edit/Submission/BeatmapSubmissionScreen.cs b/osu.Game/Screens/Edit/Submission/BeatmapSubmissionScreen.cs index 201888e078..f62b793918 100644 --- a/osu.Game/Screens/Edit/Submission/BeatmapSubmissionScreen.cs +++ b/osu.Game/Screens/Edit/Submission/BeatmapSubmissionScreen.cs @@ -249,6 +249,7 @@ namespace osu.Game.Screens.Edit.Submission exportProgressNotification = null; Logger.Log($"Beatmap set submission failed on export: {ex}"); allowExit(); + return; } exportStep.SetCompleted(); From e6174f195cf2fdfedf9c9a054172136e3b5b2efb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 18 Feb 2025 12:06:42 +0100 Subject: [PATCH 1075/1275] Ensure `EditorBeatmap.PerformOnSelection()` marks objects in selection as updated Closes https://github.com/ppy/osu/issues/28791. The reason why nudging was not changing hyperdash state in catch was that `EditorBeatmap.Update()` was not being called on the objects that were being modified, therefore postprocessing was not performed, therefore hyperdash state was not being recomputed. Looking at the usage sites of `EditorBeatmap.PerformOnSelection()`, about two-thirds of callers called `Update()` themselves on the objects they mutated, and the rest didn't. I'd say that's the failure of the abstraction and it should be `PerformOnSelection()`'s responsibility to call `Update()` there. Yes in some of the cases here this will cause extraneous calls that weren't done before, but the method is already heavily disclaimed as 'expensive', so I'd say usability should come first. --- osu.Game.Rulesets.Taiko/Edit/TaikoSelectionHandler.cs | 6 ------ .../Edit/Compose/Components/EditorBlueprintContainer.cs | 8 +------- .../Edit/Compose/Components/EditorSelectionHandler.cs | 9 --------- osu.Game/Screens/Edit/EditorBeatmap.cs | 5 +++++ 4 files changed, 6 insertions(+), 22 deletions(-) diff --git a/osu.Game.Rulesets.Taiko/Edit/TaikoSelectionHandler.cs b/osu.Game.Rulesets.Taiko/Edit/TaikoSelectionHandler.cs index be2a5ac144..364324087b 100644 --- a/osu.Game.Rulesets.Taiko/Edit/TaikoSelectionHandler.cs +++ b/osu.Game.Rulesets.Taiko/Edit/TaikoSelectionHandler.cs @@ -62,10 +62,7 @@ namespace osu.Game.Rulesets.Taiko.Edit if (h is not TaikoStrongableHitObject strongable) return; if (strongable.IsStrong != state) - { strongable.IsStrong = state; - EditorBeatmap.Update(strongable); - } }); } @@ -77,10 +74,7 @@ namespace osu.Game.Rulesets.Taiko.Edit EditorBeatmap.PerformOnSelection(h => { if (h is Hit taikoHit) - { taikoHit.Type = state ? HitType.Rim : HitType.Centre; - EditorBeatmap.Update(h); - } }); } diff --git a/osu.Game/Screens/Edit/Compose/Components/EditorBlueprintContainer.cs b/osu.Game/Screens/Edit/Compose/Components/EditorBlueprintContainer.cs index e67644baaa..e8de1eaad9 100644 --- a/osu.Game/Screens/Edit/Compose/Components/EditorBlueprintContainer.cs +++ b/osu.Game/Screens/Edit/Compose/Components/EditorBlueprintContainer.cs @@ -81,13 +81,7 @@ namespace osu.Game.Screens.Edit.Compose.Components double offset = result.Time.Value - referenceTime; if (offset != 0) - { - Beatmap.PerformOnSelection(obj => - { - obj.StartTime += offset; - Beatmap.Update(obj); - }); - } + Beatmap.PerformOnSelection(obj => obj.StartTime += offset); } protected override void AddBlueprintFor(HitObject item) diff --git a/osu.Game/Screens/Edit/Compose/Components/EditorSelectionHandler.cs b/osu.Game/Screens/Edit/Compose/Components/EditorSelectionHandler.cs index cd6e25734a..f9e7ef6df8 100644 --- a/osu.Game/Screens/Edit/Compose/Components/EditorSelectionHandler.cs +++ b/osu.Game/Screens/Edit/Compose/Components/EditorSelectionHandler.cs @@ -355,8 +355,6 @@ namespace osu.Game.Screens.Edit.Compose.Components for (int i = 0; i < hasRepeats.NodeSamples.Count; ++i) hasRepeats.NodeSamples[i] = hasRepeats.NodeSamples[i].Select(s => s.Name == HitSampleInfo.HIT_NORMAL ? s.With(newBank: bankName) : s).ToList(); } - - EditorBeatmap.Update(h); }); } @@ -390,8 +388,6 @@ namespace osu.Game.Screens.Edit.Compose.Components hasRepeats.NodeSamples[i] = hasRepeats.NodeSamples[i].Select(s => s.Name != HitSampleInfo.HIT_NORMAL ? bankName == HIT_BANK_AUTO ? s.With(newBank: normalBank, newEditorAutoBank: true) : s.With(newBank: bankName, newEditorAutoBank: false) : s).ToList(); } } - - EditorBeatmap.Update(h); }); } @@ -439,8 +435,6 @@ namespace osu.Game.Screens.Edit.Compose.Components node.Add(hitSample); } } - - EditorBeatmap.Update(h); }); } @@ -462,8 +456,6 @@ namespace osu.Game.Screens.Edit.Compose.Components for (int i = 0; i < hasRepeats.NodeSamples.Count; ++i) hasRepeats.NodeSamples[i] = hasRepeats.NodeSamples[i].Where(s => s.Name != sampleName).ToList(); } - - EditorBeatmap.Update(h); }); } @@ -484,7 +476,6 @@ namespace osu.Game.Screens.Edit.Compose.Components if (comboInfo == null || comboInfo.NewCombo == state) return; comboInfo.NewCombo = state; - EditorBeatmap.Update(h); }); } diff --git a/osu.Game/Screens/Edit/EditorBeatmap.cs b/osu.Game/Screens/Edit/EditorBeatmap.cs index 44f9646889..254336e963 100644 --- a/osu.Game/Screens/Edit/EditorBeatmap.cs +++ b/osu.Game/Screens/Edit/EditorBeatmap.cs @@ -312,8 +312,13 @@ namespace osu.Game.Screens.Edit return; BeginChange(); + foreach (var h in SelectedHitObjects) + { action(h); + Update(h); + } + EndChange(); } From 7566da8663f8f9f33a87842b7eca5339a0fe43da Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 18 Feb 2025 23:52:08 +0900 Subject: [PATCH 1076/1275] Add sleep to reduce spinning when waiting on two factor auth --- osu.Game/Online/API/APIAccess.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/osu.Game/Online/API/APIAccess.cs b/osu.Game/Online/API/APIAccess.cs index 479fc99805..36712fbdaa 100644 --- a/osu.Game/Online/API/APIAccess.cs +++ b/osu.Game/Online/API/APIAccess.cs @@ -190,7 +190,10 @@ namespace osu.Game.Online.API attemptConnect(); if (state.Value != APIState.Online) + { + Thread.Sleep(50); continue; + } } // hard bail if we can't get a valid access token. From 687c9d6e174e6e339f9de9641322c7e67e245834 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 19 Feb 2025 12:45:37 +0100 Subject: [PATCH 1077/1275] Send "notify on discussion replies" setting value in beatmap creation request --- .../Online/API/Requests/PutBeatmapSetRequest.cs | 14 ++++++++++---- .../Edit/Submission/BeatmapSubmissionScreen.cs | 4 ++-- .../Edit/Submission/BeatmapSubmissionSettings.cs | 2 ++ .../Edit/Submission/ScreenSubmissionSettings.cs | 4 ++-- 4 files changed, 16 insertions(+), 8 deletions(-) diff --git a/osu.Game/Online/API/Requests/PutBeatmapSetRequest.cs b/osu.Game/Online/API/Requests/PutBeatmapSetRequest.cs index fb25749786..ec233b5df8 100644 --- a/osu.Game/Online/API/Requests/PutBeatmapSetRequest.cs +++ b/osu.Game/Online/API/Requests/PutBeatmapSetRequest.cs @@ -11,6 +11,7 @@ using osu.Framework.IO.Network; using osu.Framework.Localisation; using osu.Game.Localisation; using osu.Game.Online.API.Requests.Responses; +using osu.Game.Screens.Edit.Submission; namespace osu.Game.Online.API.Requests { @@ -42,22 +43,27 @@ namespace osu.Game.Online.API.Requests [JsonProperty("target")] public BeatmapSubmissionTarget SubmissionTarget { get; init; } + [JsonProperty("notify_on_discussion_replies")] + public bool NotifyOnDiscussionReplies { get; init; } + private PutBeatmapSetRequest() { } - public static PutBeatmapSetRequest CreateNew(uint beatmapCount, BeatmapSubmissionTarget target) => new PutBeatmapSetRequest + public static PutBeatmapSetRequest CreateNew(uint beatmapCount, BeatmapSubmissionSettings settings) => new PutBeatmapSetRequest { BeatmapsToCreate = beatmapCount, - SubmissionTarget = target, + SubmissionTarget = settings.Target.Value, + NotifyOnDiscussionReplies = settings.NotifyOnDiscussionReplies.Value, }; - public static PutBeatmapSetRequest UpdateExisting(uint beatmapSetId, IEnumerable beatmapsToKeep, uint beatmapsToCreate, BeatmapSubmissionTarget target) => new PutBeatmapSetRequest + public static PutBeatmapSetRequest UpdateExisting(uint beatmapSetId, IEnumerable beatmapsToKeep, uint beatmapsToCreate, BeatmapSubmissionSettings settings) => new PutBeatmapSetRequest { BeatmapSetID = beatmapSetId, BeatmapsToKeep = beatmapsToKeep.ToArray(), BeatmapsToCreate = beatmapsToCreate, - SubmissionTarget = target, + SubmissionTarget = settings.Target.Value, + NotifyOnDiscussionReplies = settings.NotifyOnDiscussionReplies.Value, }; protected override WebRequest CreateWebRequest() diff --git a/osu.Game/Screens/Edit/Submission/BeatmapSubmissionScreen.cs b/osu.Game/Screens/Edit/Submission/BeatmapSubmissionScreen.cs index f62b793918..66139bacec 100644 --- a/osu.Game/Screens/Edit/Submission/BeatmapSubmissionScreen.cs +++ b/osu.Game/Screens/Edit/Submission/BeatmapSubmissionScreen.cs @@ -192,8 +192,8 @@ namespace osu.Game.Screens.Edit.Submission (uint)Beatmap.Value.BeatmapSetInfo.OnlineID, Beatmap.Value.BeatmapSetInfo.Beatmaps.Where(b => b.OnlineID > 0).Select(b => (uint)b.OnlineID).ToArray(), (uint)Beatmap.Value.BeatmapSetInfo.Beatmaps.Count(b => b.OnlineID <= 0), - settings.Target.Value) - : PutBeatmapSetRequest.CreateNew((uint)Beatmap.Value.BeatmapSetInfo.Beatmaps.Count, settings.Target.Value); + settings) + : PutBeatmapSetRequest.CreateNew((uint)Beatmap.Value.BeatmapSetInfo.Beatmaps.Count, settings); createRequest.Success += async response => { diff --git a/osu.Game/Screens/Edit/Submission/BeatmapSubmissionSettings.cs b/osu.Game/Screens/Edit/Submission/BeatmapSubmissionSettings.cs index 359dc11f39..8cccc339a6 100644 --- a/osu.Game/Screens/Edit/Submission/BeatmapSubmissionSettings.cs +++ b/osu.Game/Screens/Edit/Submission/BeatmapSubmissionSettings.cs @@ -9,5 +9,7 @@ namespace osu.Game.Screens.Edit.Submission public class BeatmapSubmissionSettings { public Bindable Target { get; } = new Bindable(); + + public Bindable NotifyOnDiscussionReplies { get; } = new Bindable(); } } diff --git a/osu.Game/Screens/Edit/Submission/ScreenSubmissionSettings.cs b/osu.Game/Screens/Edit/Submission/ScreenSubmissionSettings.cs index 08b4d9f712..969105b5c6 100644 --- a/osu.Game/Screens/Edit/Submission/ScreenSubmissionSettings.cs +++ b/osu.Game/Screens/Edit/Submission/ScreenSubmissionSettings.cs @@ -28,7 +28,7 @@ namespace osu.Game.Screens.Edit.Submission [BackgroundDependencyLoader] private void load(OsuConfigManager configManager, OsuColour colours, BeatmapSubmissionSettings settings) { - configManager.BindWith(OsuSetting.EditorSubmissionNotifyOnDiscussionReplies, notifyOnDiscussionReplies); + configManager.BindWith(OsuSetting.EditorSubmissionNotifyOnDiscussionReplies, settings.NotifyOnDiscussionReplies); configManager.BindWith(OsuSetting.EditorSubmissionLoadInBrowserAfterSubmission, loadInBrowserAfterSubmission); Content.Add(new FillFlowContainer @@ -47,7 +47,7 @@ namespace osu.Game.Screens.Edit.Submission new FormCheckBox { Caption = BeatmapSubmissionStrings.NotifyOnDiscussionReplies, - Current = notifyOnDiscussionReplies, + Current = settings.NotifyOnDiscussionReplies, }, new FormCheckBox { From aa9e1ac8b4bf0154dff870269221d49e7e1c98d0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 19 Feb 2025 12:46:04 +0100 Subject: [PATCH 1078/1275] Specify endpoint for production instance of beatmap submission service --- osu.Game/Online/ProductionEndpointConfiguration.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/osu.Game/Online/ProductionEndpointConfiguration.cs b/osu.Game/Online/ProductionEndpointConfiguration.cs index 6e06abbeed..20583c8c7e 100644 --- a/osu.Game/Online/ProductionEndpointConfiguration.cs +++ b/osu.Game/Online/ProductionEndpointConfiguration.cs @@ -13,6 +13,7 @@ namespace osu.Game.Online SpectatorUrl = "https://spectator.ppy.sh/spectator"; MultiplayerUrl = "https://spectator.ppy.sh/multiplayer"; MetadataUrl = "https://spectator.ppy.sh/metadata"; + BeatmapSubmissionServiceUrl = "https://bss.ppy.sh"; } } } From f9d91431fd0e4eaf4c26d8125b078cdd2ec23c19 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 19 Feb 2025 15:13:48 +0100 Subject: [PATCH 1079/1275] Fix multiplayer spectator not working with freestyle It's no longer possible to just assume that using the ambient `WorkingBeatmap` is gonna work. Bit dodgy but seems to work and also I'd hope that `WorkingBeatmapCache` makes this not overly taxing. If there are concerns this can probably be an async load or something. --- .../Screens/OnlinePlay/Multiplayer/Spectate/PlayerArea.cs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/PlayerArea.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/PlayerArea.cs index 1b03452df7..2a40021ee0 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/PlayerArea.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/PlayerArea.cs @@ -53,7 +53,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate public Score? Score { get; private set; } [Resolved] - private IBindable beatmap { get; set; } = null!; + private BeatmapManager beatmapManager { get; set; } = null!; private readonly AudioAdjustments clockAdjustmentsFromMods = new AudioAdjustments(); private readonly BindableDouble volumeAdjustment = new BindableDouble(); @@ -89,7 +89,9 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate Score = score; - gameplayContent.Child = new PlayerIsolationContainer(beatmap.Value, Score.ScoreInfo.Ruleset, Score.ScoreInfo.Mods) + var workingBeatmap = beatmapManager.GetWorkingBeatmap(Score.ScoreInfo.BeatmapInfo); + workingBeatmap.LoadTrack(); + gameplayContent.Child = new PlayerIsolationContainer(workingBeatmap, Score.ScoreInfo.Ruleset, Score.ScoreInfo.Mods) { RelativeSizeAxes = Axes.Both, Child = stack = new OsuScreenStack From a274b9a1fd9f59200061f7fe794de3b8c112e0cd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 19 Feb 2025 15:24:58 +0100 Subject: [PATCH 1080/1275] Fix test --- .../Visual/Multiplayer/TestSceneMultiSpectatorScreen.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiSpectatorScreen.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiSpectatorScreen.cs index 0a3d48828e..bd483f0fa1 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiSpectatorScreen.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiSpectatorScreen.cs @@ -372,7 +372,8 @@ namespace osu.Game.Tests.Visual.Multiplayer sendFrames(getPlayerIds(4), 300); - AddUntilStep("wait for correct track speed", () => Beatmap.Value.Track.Rate, () => Is.EqualTo(1.5)); + AddUntilStep("wait for correct track speed", + () => this.ChildrenOfType().All(player => player.ClockAdjustmentsFromMods.AggregateTempo.Value == 1.5)); } [Test] From 092d80cf1b330b21ad7a10a09f25e9336999f523 Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Wed, 19 Feb 2025 10:20:04 -0500 Subject: [PATCH 1081/1275] Fix `PanelBeatmapStandalone` not handling selection state --- osu.Game/Screens/SelectV2/PanelBeatmap.cs | 1 - osu.Game/Screens/SelectV2/PanelBeatmapStandalone.cs | 2 ++ 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/osu.Game/Screens/SelectV2/PanelBeatmap.cs b/osu.Game/Screens/SelectV2/PanelBeatmap.cs index dcac460905..b27e5cae14 100644 --- a/osu.Game/Screens/SelectV2/PanelBeatmap.cs +++ b/osu.Game/Screens/SelectV2/PanelBeatmap.cs @@ -165,7 +165,6 @@ namespace osu.Game.Screens.SelectV2 }, true); Selected.BindValueChanged(s => Expanded.Value = s.NewValue, true); - KeyboardSelected.BindValueChanged(k => KeyboardSelected.Value = k.NewValue, true); } protected override void PrepareForUse() diff --git a/osu.Game/Screens/SelectV2/PanelBeatmapStandalone.cs b/osu.Game/Screens/SelectV2/PanelBeatmapStandalone.cs index 231c7274be..948311a86e 100644 --- a/osu.Game/Screens/SelectV2/PanelBeatmapStandalone.cs +++ b/osu.Game/Screens/SelectV2/PanelBeatmapStandalone.cs @@ -190,6 +190,8 @@ namespace osu.Game.Screens.SelectV2 computeStarRating(); updateKeyCount(); }, true); + + Selected.BindValueChanged(s => Expanded.Value = s.NewValue, true); } protected override void PrepareForUse() From e91706f41843b48020f514c42c42ed446a565f83 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 20 Feb 2025 14:26:33 +0900 Subject: [PATCH 1082/1275] 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 f4d49763ab..7dfe2f9d1f 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 0d95dfbd06..a40bc145ff 100644 --- a/osu.iOS.props +++ b/osu.iOS.props @@ -17,6 +17,6 @@ -all - + From 4da3752f956d2d68dbc263969d81478a5577536d Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 20 Feb 2025 14:42:26 +0900 Subject: [PATCH 1083/1275] Update flag test resources in line with web rename --- osu.Game.Tests/Visual/Online/TestSceneUserProfileOverlay.cs | 2 +- osu.Game/Tests/Visual/OnlinePlay/TestRoomRequestsHandler.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game.Tests/Visual/Online/TestSceneUserProfileOverlay.cs b/osu.Game.Tests/Visual/Online/TestSceneUserProfileOverlay.cs index a4a9816337..2972f69cba 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneUserProfileOverlay.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneUserProfileOverlay.cs @@ -351,7 +351,7 @@ namespace osu.Game.Tests.Visual.Online Id = 1, Name = "Collective Wangs", ShortName = "WANG", - FlagUrl = "https://assets.ppy.sh/teams/logo/1/wanglogo.jpg", + FlagUrl = "https://assets.ppy.sh/teams/flag/1/wanglogo.jpg", } }; } diff --git a/osu.Game/Tests/Visual/OnlinePlay/TestRoomRequestsHandler.cs b/osu.Game/Tests/Visual/OnlinePlay/TestRoomRequestsHandler.cs index d73fd5ab22..3e3fe03329 100644 --- a/osu.Game/Tests/Visual/OnlinePlay/TestRoomRequestsHandler.cs +++ b/osu.Game/Tests/Visual/OnlinePlay/TestRoomRequestsHandler.cs @@ -228,7 +228,7 @@ namespace osu.Game.Tests.Visual.OnlinePlay { Name = "Collective Wangs", ShortName = "WANG", - FlagUrl = "https://assets.ppy.sh/teams/logo/1/wanglogo.jpg", + FlagUrl = "https://assets.ppy.sh/teams/flag/1/wanglogo.jpg", } : null, }) From 1c53d93a8f3cf68888e74919ee75639fe70ffe05 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 20 Feb 2025 15:32:47 +0900 Subject: [PATCH 1084/1275] Add disposal and pre-check before reloading audio track --- .../OnlinePlay/Multiplayer/Spectate/PlayerArea.cs | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/PlayerArea.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/PlayerArea.cs index 2a40021ee0..31bd711ade 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/PlayerArea.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/PlayerArea.cs @@ -5,6 +5,7 @@ using System; using System.Collections.Generic; using osu.Framework.Allocation; using osu.Framework.Audio; +using osu.Framework.Audio.Track; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; @@ -60,6 +61,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate private readonly Container gameplayContent; private readonly LoadingLayer loadingLayer; private OsuScreenStack? stack; + private Track? loadedTrack; public PlayerArea(int userId, SpectatorPlayerClock clock) { @@ -90,7 +92,8 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate Score = score; var workingBeatmap = beatmapManager.GetWorkingBeatmap(Score.ScoreInfo.BeatmapInfo); - workingBeatmap.LoadTrack(); + if (!workingBeatmap.TrackLoaded) + loadedTrack = workingBeatmap.LoadTrack(); gameplayContent.Child = new PlayerIsolationContainer(workingBeatmap, Score.ScoreInfo.Ruleset, Score.ScoreInfo.Mods) { RelativeSizeAxes = Axes.Both, @@ -129,6 +132,12 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate public override bool PropagatePositionalInputSubTree => false; public override bool PropagateNonPositionalInputSubTree => false; + protected override void Dispose(bool isDisposing) + { + base.Dispose(isDisposing); + loadedTrack?.Dispose(); + } + /// /// Isolates each player instance from the game-wide ruleset/beatmap/mods (to allow for different players having different settings). /// From 81b4f0d8caf176aa070846ddf79f54346803fa2f Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 20 Feb 2025 15:49:40 +0900 Subject: [PATCH 1085/1275] Add comments regarding jank --- .../Screens/OnlinePlay/Multiplayer/Spectate/PlayerArea.cs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/PlayerArea.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/PlayerArea.cs index 31bd711ade..7e4aae99da 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/PlayerArea.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/PlayerArea.cs @@ -91,9 +91,14 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate Score = score; + // Required for freestyle, where each player may be playing a different beatmap. var workingBeatmap = beatmapManager.GetWorkingBeatmap(Score.ScoreInfo.BeatmapInfo); + + // Required to avoid crashes, but we really don't want to be doing this if we can avoid it. + // If we get to fixing this, we will want to investigate every access to `Track` in gameplay. if (!workingBeatmap.TrackLoaded) loadedTrack = workingBeatmap.LoadTrack(); + gameplayContent.Child = new PlayerIsolationContainer(workingBeatmap, Score.ScoreInfo.Ruleset, Score.ScoreInfo.Mods) { RelativeSizeAxes = Axes.Both, From d8bba16809a8f55899afc843fe0010c43b0acc87 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 20 Feb 2025 14:38:54 +0100 Subject: [PATCH 1086/1275] Update framework Pulls in fix for https://github.com/ppy/osu/issues/31956. --- 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 7dfe2f9d1f..d49acd7b27 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 a40bc145ff..5ca49e80f6 100644 --- a/osu.iOS.props +++ b/osu.iOS.props @@ -17,6 +17,6 @@ -all - + From 1e3d5d7d8150c916af4a059791f1d3c4b5a6f5a7 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 20 Feb 2025 23:05:43 +0900 Subject: [PATCH 1087/1275] Remove left-over debug code --- osu.Game/Screens/Play/KiaiGameplayFountains.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/osu.Game/Screens/Play/KiaiGameplayFountains.cs b/osu.Game/Screens/Play/KiaiGameplayFountains.cs index 19a9c2b6e5..f7d96dd10f 100644 --- a/osu.Game/Screens/Play/KiaiGameplayFountains.cs +++ b/osu.Game/Screens/Play/KiaiGameplayFountains.cs @@ -55,7 +55,6 @@ namespace osu.Game.Screens.Play if (EffectPoint.KiaiMode && !isTriggered) { - Logger.Log("shooting"); bool isNearEffectPoint = Math.Abs(BeatSyncSource.Clock.CurrentTime - EffectPoint.Time) < 500; if (isNearEffectPoint) Shoot(); From b4d270045b5fa3840c726add260933d145a78b9f Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Thu, 20 Feb 2025 09:29:18 -0500 Subject: [PATCH 1088/1275] Publicise base draw size property --- osu.Android/OsuGameAndroid.cs | 2 +- osu.Game/OsuGame.cs | 2 +- osu.iOS/OsuGameIOS.cs | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Android/OsuGameAndroid.cs b/osu.Android/OsuGameAndroid.cs index e725f9245f..932fc8454e 100644 --- a/osu.Android/OsuGameAndroid.cs +++ b/osu.Android/OsuGameAndroid.cs @@ -21,7 +21,7 @@ namespace osu.Android [Cached] private readonly OsuGameActivity gameActivity; - protected override Vector2 ScalingContainerTargetDrawSize => new Vector2(1024, 1024 * DrawHeight / DrawWidth); + public override Vector2 ScalingContainerTargetDrawSize => new Vector2(1024, 1024 * DrawHeight / DrawWidth); public OsuGameAndroid(OsuGameActivity activity) : base(null) diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs index d379392a7d..d23d27c89e 100644 --- a/osu.Game/OsuGame.cs +++ b/osu.Game/OsuGame.cs @@ -818,7 +818,7 @@ namespace osu.Game /// Adjust the globally applied in every . /// Useful for changing how the game handles different aspect ratios. /// - protected internal virtual Vector2 ScalingContainerTargetDrawSize { get; } = new Vector2(1024, 768); + public virtual Vector2 ScalingContainerTargetDrawSize { get; } = new Vector2(1024, 768); protected override Container CreateScalingContainer() => new ScalingContainer(ScalingMode.Everything); diff --git a/osu.iOS/OsuGameIOS.cs b/osu.iOS/OsuGameIOS.cs index 883e89e38a..96b8fb9804 100644 --- a/osu.iOS/OsuGameIOS.cs +++ b/osu.iOS/OsuGameIOS.cs @@ -23,7 +23,7 @@ namespace osu.iOS public override bool HideUnlicensedContent => true; - protected override Vector2 ScalingContainerTargetDrawSize => new Vector2(1024, 1024 * DrawHeight / DrawWidth); + public override Vector2 ScalingContainerTargetDrawSize => new Vector2(1024, 1024 * DrawHeight / DrawWidth); public OsuGameIOS(AppDelegate appDelegate) { From 4f4d2b3b3fdd24a6ab3d0a8388e6afd729806483 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 20 Feb 2025 23:42:32 +0900 Subject: [PATCH 1089/1275] Fix results screen applause playing too loud during multiplayer spectating --- .../Screens/OnlinePlay/Multiplayer/Spectate/PlayerArea.cs | 5 +++++ osu.Game/Screens/Ranking/ResultsScreen.cs | 2 ++ 2 files changed, 7 insertions(+) diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/PlayerArea.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/PlayerArea.cs index 7e4aae99da..393d34bc1a 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/PlayerArea.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/PlayerArea.cs @@ -9,6 +9,7 @@ using osu.Framework.Audio.Track; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; +using osu.Framework.Logging; using osu.Game.Beatmaps; using osu.Game.Graphics.UserInterface; using osu.Game.Rulesets; @@ -128,8 +129,12 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate get => mute; set { + if (mute == value) + return; + mute = value; volumeAdjustment.Value = value ? 0 : 1; + Logger.Log($"{(mute ? "muting" : "unmuting")} player {UserId}"); } } diff --git a/osu.Game/Screens/Ranking/ResultsScreen.cs b/osu.Game/Screens/Ranking/ResultsScreen.cs index b10684b22e..fe0d805cee 100644 --- a/osu.Game/Screens/Ranking/ResultsScreen.cs +++ b/osu.Game/Screens/Ranking/ResultsScreen.cs @@ -317,6 +317,8 @@ namespace osu.Game.Screens.Ranking if (!this.IsCurrentScreen() || s != rankApplauseSound) return; + AddInternal(rankApplauseSound); + rankApplauseSound.VolumeTo(applause_volume); rankApplauseSound.Play(); }); From 7dc5ad2f0e21c85a66f925abf5133d741fcdf937 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 20 Feb 2025 15:44:30 +0100 Subject: [PATCH 1090/1275] Adjust handling of team flags with non-matching aspect ratio to match web --- .../Components/DrawableTeamFlag.cs | 19 ++++++++++++++----- .../Users/Drawables/UpdateableTeamFlag.cs | 11 ++++++++++- 2 files changed, 24 insertions(+), 6 deletions(-) diff --git a/osu.Game.Tournament/Components/DrawableTeamFlag.cs b/osu.Game.Tournament/Components/DrawableTeamFlag.cs index aef854bb8d..90638a7758 100644 --- a/osu.Game.Tournament/Components/DrawableTeamFlag.cs +++ b/osu.Game.Tournament/Components/DrawableTeamFlag.cs @@ -6,6 +6,7 @@ using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.Textures; using osu.Game.Tournament.Models; @@ -35,12 +36,20 @@ namespace osu.Game.Tournament.Components Size = new Vector2(75, 54); Masking = true; CornerRadius = 5; - Child = flagSprite = new Sprite + Children = new Drawable[] { - RelativeSizeAxes = Axes.Both, - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - FillMode = FillMode.Fill + new Box + { + RelativeSizeAxes = Axes.Both, + Colour = Colour4.FromHex("333"), + }, + flagSprite = new Sprite + { + RelativeSizeAxes = Axes.Both, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + FillMode = FillMode.Fit + }, }; (flag = team.FlagName.GetBoundCopy()).BindValueChanged(_ => flagSprite.Texture = textures.Get($@"Flags/{team.FlagName}"), true); diff --git a/osu.Game/Users/Drawables/UpdateableTeamFlag.cs b/osu.Game/Users/Drawables/UpdateableTeamFlag.cs index 9c2bbb7e3e..2fcec66aa7 100644 --- a/osu.Game/Users/Drawables/UpdateableTeamFlag.cs +++ b/osu.Game/Users/Drawables/UpdateableTeamFlag.cs @@ -5,6 +5,7 @@ using osu.Framework.Allocation; 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.Textures; using osu.Framework.Localisation; @@ -75,10 +76,18 @@ namespace osu.Game.Users.Drawables InternalChildren = new Drawable[] { new HoverClickSounds(), + new Box + { + RelativeSizeAxes = Axes.Both, + Colour = Colour4.FromHex("333"), + }, new Sprite { RelativeSizeAxes = Axes.Both, - Texture = textures.Get(team.FlagUrl) + Texture = textures.Get(team.FlagUrl), + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + FillMode = FillMode.Fit, } }; } From a75ec75a8fa5e44fede74c4f1c4e275e6f2abee5 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 20 Feb 2025 23:48:21 +0900 Subject: [PATCH 1091/1275] Fix using --- osu.Game/Screens/Play/KiaiGameplayFountains.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/osu.Game/Screens/Play/KiaiGameplayFountains.cs b/osu.Game/Screens/Play/KiaiGameplayFountains.cs index f7d96dd10f..d4e61dc5a0 100644 --- a/osu.Game/Screens/Play/KiaiGameplayFountains.cs +++ b/osu.Game/Screens/Play/KiaiGameplayFountains.cs @@ -5,7 +5,6 @@ using System; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; -using osu.Framework.Logging; using osu.Framework.Utils; using osu.Game.Configuration; using osu.Game.Graphics.Containers; From 440a776bd7c9edcf935b2da3d1bc07c65fc71663 Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Thu, 20 Feb 2025 09:29:28 -0500 Subject: [PATCH 1092/1275] Scale catch down to remain playable on mobile --- .../UI/CatchPlayfieldAdjustmentContainer.cs | 54 +++++++++++++------ 1 file changed, 38 insertions(+), 16 deletions(-) diff --git a/osu.Game.Rulesets.Catch/UI/CatchPlayfieldAdjustmentContainer.cs b/osu.Game.Rulesets.Catch/UI/CatchPlayfieldAdjustmentContainer.cs index 74dfa6c1fd..3b9cca8ef0 100644 --- a/osu.Game.Rulesets.Catch/UI/CatchPlayfieldAdjustmentContainer.cs +++ b/osu.Game.Rulesets.Catch/UI/CatchPlayfieldAdjustmentContainer.cs @@ -1,6 +1,7 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Game.Rulesets.UI; @@ -15,6 +16,8 @@ namespace osu.Game.Rulesets.Catch.UI protected override Container Content => content; private readonly Container content; + private readonly Container scaleContainer; + public CatchPlayfieldAdjustmentContainer() { const float base_game_width = 1024f; @@ -26,30 +29,49 @@ namespace osu.Game.Rulesets.Catch.UI Anchor = Anchor.Centre; Origin = Anchor.Centre; - InternalChild = new Container + InternalChild = scaleContainer = new Container { - // This container limits vertical visibility of the playfield to ensure fairness between wide and tall resolutions (i.e. tall resolutions should not see more fruits). - // Note that the container still extends across the screen horizontally, so that hit explosions at the sides of the playfield do not get cut off. - Name = "Visible area", Anchor = Anchor.Centre, Origin = Anchor.Centre, - RelativeSizeAxes = Axes.X, - Height = base_game_height + extra_bottom_space, - Y = extra_bottom_space / 2, - Masking = true, + RelativeSizeAxes = Axes.Both, Child = new Container { - Name = "Playable area", - Anchor = Anchor.TopCentre, - Origin = Anchor.TopCentre, - // playfields in stable are positioned vertically at three fourths the difference between the playfield height and the window height in stable. - Y = base_game_height * ((1 - playfield_size_adjust) / 4 * 3), - Size = new Vector2(base_game_width, base_game_height) * playfield_size_adjust, - Child = content = new ScalingContainer { RelativeSizeAxes = Axes.Both } - }, + // This container limits vertical visibility of the playfield to ensure fairness between wide and tall resolutions (i.e. tall resolutions should not see more fruits). + // Note that the container still extends across the screen horizontally, so that hit explosions at the sides of the playfield do not get cut off. + Name = "Visible area", + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + RelativeSizeAxes = Axes.X, + Height = base_game_height + extra_bottom_space, + Y = extra_bottom_space / 2, + Masking = true, + Child = new Container + { + Name = "Playable area", + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre, + // playfields in stable are positioned vertically at three fourths the difference between the playfield height and the window height in stable. + Y = base_game_height * ((1 - playfield_size_adjust) / 4 * 3), + Size = new Vector2(base_game_width, base_game_height) * playfield_size_adjust, + Child = content = new ScalingContainer { RelativeSizeAxes = Axes.Both } + }, + } }; } + [BackgroundDependencyLoader] + private void load(OsuGame? osuGame) + { + if (osuGame != null) + { + // on mobile platforms where the base aspect ratio is wider, the catch playfield + // needs to be scaled down to remain playable. + const float base_aspect_ratio = 1024f / 768f; + float aspectRatio = osuGame.ScalingContainerTargetDrawSize.X / osuGame.ScalingContainerTargetDrawSize.Y; + scaleContainer.Scale = new Vector2(base_aspect_ratio / aspectRatio); + } + } + /// /// A which scales its content relative to a target width. /// From 7bd5b745e923ae3eea737c3dd13a43f975832cbb Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Thu, 20 Feb 2025 09:30:14 -0500 Subject: [PATCH 1093/1275] Scale taiko down to remain playable --- .../UI/TaikoPlayfieldAdjustmentContainer.cs | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/osu.Game.Rulesets.Taiko/UI/TaikoPlayfieldAdjustmentContainer.cs b/osu.Game.Rulesets.Taiko/UI/TaikoPlayfieldAdjustmentContainer.cs index c67f61052c..6a9e5789de 100644 --- a/osu.Game.Rulesets.Taiko/UI/TaikoPlayfieldAdjustmentContainer.cs +++ b/osu.Game.Rulesets.Taiko/UI/TaikoPlayfieldAdjustmentContainer.cs @@ -2,6 +2,8 @@ // See the LICENCE file in the repository root for full licence text. using System; +using osu.Framework; +using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Game.Rulesets.Taiko.Beatmaps; @@ -19,6 +21,9 @@ namespace osu.Game.Rulesets.Taiko.UI public readonly IBindable LockPlayfieldAspectRange = new BindableBool(true); + [Resolved] + private OsuGame? osuGame { get; set; } + public TaikoPlayfieldAdjustmentContainer() { RelativeSizeAxes = Axes.X; @@ -56,6 +61,18 @@ namespace osu.Game.Rulesets.Taiko.UI relativeHeight = Math.Min(relativeHeight, 1f / 3f); Scale = new Vector2(Math.Max((Parent!.ChildSize.Y / 768f) * (relativeHeight / base_relative_height), 1f)); + + // on mobile platforms where the base aspect ratio is wider, the taiko playfield + // needs to be scaled down to remain playable. + if (RuntimeInfo.IsMobile && osuGame != null) + { + const float base_aspect_ratio = 1024f / 768f; + float gameAspectRatio = osuGame.ScalingContainerTargetDrawSize.X / osuGame.ScalingContainerTargetDrawSize.Y; + // this magic scale is unexplainable, but required so the playfield doesn't become too zoomed out as the aspect ratio increases. + const float magic_scale = 1.25f; + Scale *= magic_scale * new Vector2(base_aspect_ratio / gameAspectRatio); + } + Width = 1 / Scale.X; } From b1112623dca15dfcac3995d3ac289be0ccb96840 Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Thu, 20 Feb 2025 09:29:53 -0500 Subject: [PATCH 1094/1275] Fix taiko touch controls sizing logic --- osu.Game.Rulesets.Taiko/UI/DrumTouchInputArea.cs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/osu.Game.Rulesets.Taiko/UI/DrumTouchInputArea.cs b/osu.Game.Rulesets.Taiko/UI/DrumTouchInputArea.cs index 0b7f6f621a..53d129e7ca 100644 --- a/osu.Game.Rulesets.Taiko/UI/DrumTouchInputArea.cs +++ b/osu.Game.Rulesets.Taiko/UI/DrumTouchInputArea.cs @@ -59,11 +59,10 @@ namespace osu.Game.Rulesets.Taiko.UI { Anchor = Anchor.BottomCentre, Origin = Anchor.BottomCentre, - RelativeSizeAxes = Axes.X, - Height = 350, + RelativeSizeAxes = Axes.Both, + Height = 0.45f, Y = 20, Masking = true, - FillMode = FillMode.Fit, Children = new Drawable[] { mainContent = new Container From 49c192b173640ddae1543a23eff4b6059f51f250 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Fri, 21 Feb 2025 16:19:05 +0900 Subject: [PATCH 1095/1275] Fix wrong beatmap attributes in multiplayer spectate --- osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/PlayerArea.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/PlayerArea.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/PlayerArea.cs index 393d34bc1a..b8f0a67a46 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/PlayerArea.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/PlayerArea.cs @@ -154,12 +154,15 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate private partial class PlayerIsolationContainer : Container { [Cached] + [Cached(typeof(IBindable))] private readonly Bindable ruleset = new Bindable(); [Cached] + [Cached(typeof(IBindable))] private readonly Bindable beatmap = new Bindable(); [Cached] + [Cached(typeof(IBindable>))] private readonly Bindable> mods = new Bindable>(); public PlayerIsolationContainer(WorkingBeatmap beatmap, RulesetInfo ruleset, IReadOnlyList mods) From f868f03e1b75556418c8cfd6576781c3324d3fd7 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Fri, 21 Feb 2025 16:38:55 +0900 Subject: [PATCH 1096/1275] Fix host change sounds playing when exiting multiplayer rooms --- .../Online/Multiplayer/MultiplayerClient.cs | 6 +++++ .../Multiplayer/MultiplayerRoomSounds.cs | 27 ++++++------------- 2 files changed, 14 insertions(+), 19 deletions(-) diff --git a/osu.Game/Online/Multiplayer/MultiplayerClient.cs b/osu.Game/Online/Multiplayer/MultiplayerClient.cs index 97161cce48..2d445ea25a 100644 --- a/osu.Game/Online/Multiplayer/MultiplayerClient.cs +++ b/osu.Game/Online/Multiplayer/MultiplayerClient.cs @@ -51,6 +51,11 @@ namespace osu.Game.Online.Multiplayer /// public event Action? UserKicked; + /// + /// Invoked when the room's host is changed. + /// + public event Action? HostChanged; + /// /// Invoked when a new item is added to the playlist. /// @@ -531,6 +536,7 @@ namespace osu.Game.Online.Multiplayer Room.Host = user; APIRoom.Host = user?.User; + HostChanged?.Invoke(user); RoomUpdated?.Invoke(); }, false); diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerRoomSounds.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerRoomSounds.cs index d53e485c86..cdf4e96bad 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerRoomSounds.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerRoomSounds.cs @@ -1,7 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using System.Collections.Generic; using osu.Framework.Allocation; using osu.Framework.Audio; using osu.Framework.Audio.Sample; @@ -20,7 +19,6 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer private Sample? userJoinedSample; private Sample? userLeftSample; private Sample? userKickedSample; - private MultiplayerRoomUser? host; [BackgroundDependencyLoader] private void load(AudioManager audio) @@ -35,25 +33,10 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer { base.LoadComplete(); - client.RoomUpdated += onRoomUpdated; client.UserJoined += onUserJoined; client.UserLeft += onUserLeft; client.UserKicked += onUserKicked; - updateState(); - } - - private void onRoomUpdated() => Scheduler.AddOnce(updateState); - - private void updateState() - { - if (EqualityComparer.Default.Equals(host, client.Room?.Host)) - return; - - // only play sound when the host changes from an already-existing host. - if (host != null) - Scheduler.AddOnce(() => hostChangedSample?.Play()); - - host = client.Room?.Host; + client.HostChanged += onHostChanged; } private void onUserJoined(MultiplayerRoomUser user) @@ -65,16 +48,22 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer private void onUserKicked(MultiplayerRoomUser user) => Scheduler.AddOnce(() => userKickedSample?.Play()); + private void onHostChanged(MultiplayerRoomUser? host) + { + if (host != null) + Scheduler.AddOnce(() => hostChangedSample?.Play()); + } + protected override void Dispose(bool isDisposing) { base.Dispose(isDisposing); if (client.IsNotNull()) { - client.RoomUpdated -= onRoomUpdated; client.UserJoined -= onUserJoined; client.UserLeft -= onUserLeft; client.UserKicked -= onUserKicked; + client.HostChanged -= onHostChanged; } } } From fa49b30b5cc077f807f60fd6964bf5416f5ec845 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 21 Feb 2025 11:30:52 +0100 Subject: [PATCH 1097/1275] Attempt to fix spectator list showing other users in multiplayer room even if they're not spectating better Maybe closes https://github.com/ppy/osu/issues/31972. Not sure. I have no reproduction scenario to work with, no solid understanding of how the issue can happen, and if this doesn't fix it, then I'm not even entirely sure how this can ever be fixed client-side. The working theory is that not watching updates to the room provoked a situation wherein the room was temporarily not in a correct state when `WatchingUsers` changed, therefore the collection change callback failed to exclude other players in the room from display. I'm only PRing this because of the `next-release` tag on the issue. --- osu.Game/Screens/Play/HUD/SpectatorList.cs | 80 ++++++++++++++++------ 1 file changed, 60 insertions(+), 20 deletions(-) diff --git a/osu.Game/Screens/Play/HUD/SpectatorList.cs b/osu.Game/Screens/Play/HUD/SpectatorList.cs index 4297c62712..98b3ede874 100644 --- a/osu.Game/Screens/Play/HUD/SpectatorList.cs +++ b/osu.Game/Screens/Play/HUD/SpectatorList.cs @@ -2,13 +2,13 @@ // See the LICENCE file in the repository root for full licence text. using System; -using System.Collections.Generic; using System.Collections.Specialized; using System.Linq; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Extensions.LocalisationExtensions; +using osu.Framework.Extensions.ObjectExtensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Colour; using osu.Framework.Graphics.Containers; @@ -38,8 +38,9 @@ namespace osu.Game.Screens.Play.HUD public BindableColour4 HeaderColour { get; } = new BindableColour4(Colour4.White); private BindableList watchingUsers { get; } = new BindableList(); + private BindableList actualSpectators { get; } = new BindableList(); + private Bindable userPlayingState { get; } = new Bindable(); - private int displayedSpectatorCount; private OsuSpriteText header = null!; private FillFlowContainer mainFlow = null!; @@ -94,7 +95,9 @@ namespace osu.Game.Screens.Play.HUD ((IBindableList)watchingUsers).BindTo(client.WatchingUsers); ((IBindable)userPlayingState).BindTo(gameplayState.PlayingState); - watchingUsers.BindCollectionChanged(onSpectatorsChanged, true); + watchingUsers.BindCollectionChanged(onWatchingUsersChanged, true); + multiplayerClient.RoomUpdated += removePlayersFromMultiplayerRoom; + actualSpectators.BindCollectionChanged(onSpectatorsChanged, true); userPlayingState.BindValueChanged(_ => updateVisibility()); Font.BindValueChanged(_ => updateAppearance()); @@ -104,22 +107,55 @@ namespace osu.Game.Screens.Play.HUD this.FadeInFromZero(200, Easing.OutQuint); } - private void onSpectatorsChanged(object? sender, NotifyCollectionChangedEventArgs e) + private void onWatchingUsersChanged(object? sender, NotifyCollectionChangedEventArgs e) { + switch (e.Action) + { + case NotifyCollectionChangedAction.Add: + { + for (int i = 0; i < e.NewItems!.Count; i++) + actualSpectators.Add((SpectatorUser)e.NewItems![i]!); + + break; + } + + case NotifyCollectionChangedAction.Remove: + { + for (int i = 0; i < e.OldItems!.Count; i++) + actualSpectators.Remove((SpectatorUser)e.OldItems![i]!); + + break; + } + + case NotifyCollectionChangedAction.Reset: + { + actualSpectators.Clear(); + break; + } + + default: + throw new NotSupportedException(); + } + + removePlayersFromMultiplayerRoom(); + } + + private void removePlayersFromMultiplayerRoom() + { + if (multiplayerClient.Room == null) + return; + // the multiplayer gameplay leaderboard relies on calling `SpectatorClient.WatchUser()` to get updates on users' total scores. // this has an unfortunate side effect of other players showing up in `SpectatorClient.WatchingUsers`. // // we do not generally wish to display other players in the room as spectators due to that implementation detail, // therefore this code is intended to filter out those players on the client side. - // - // note that the way that this is done is rather specific to the multiplayer use case and therefore carries a lot of assumptions - // (e.g. that the `MultiplayerRoomUser`s have the correct `State` at the point wherein they issue the `WatchUser()` calls). - // the more proper way to do this (which is by subscribing to `WatchingUsers` and `RoomUpdated`, and doing a proper diff to a third list on any change of either) - // is a lot more difficult to write correctly, given that we also rely on `BindableList`'s collection changed event arguments to properly animate this component. - var excludedUserIds = new HashSet(); - if (multiplayerClient.Room != null) - excludedUserIds.UnionWith(multiplayerClient.Room.Users.Where(u => u.State != MultiplayerUserState.Spectating).Select(u => u.UserID)); + var excludedUserIds = multiplayerClient.Room.Users.Where(u => u.State != MultiplayerUserState.Spectating).Select(u => u.UserID).ToHashSet(); + actualSpectators.RemoveAll(s => excludedUserIds.Contains(s.OnlineID)); + } + private void onSpectatorsChanged(object? sender, NotifyCollectionChangedEventArgs e) + { switch (e.Action) { case NotifyCollectionChangedAction.Add: @@ -129,9 +165,6 @@ namespace osu.Game.Screens.Play.HUD var spectator = (SpectatorUser)e.NewItems![i]!; int index = Math.Max(e.NewStartingIndex, 0) + i; - if (excludedUserIds.Contains(spectator.OnlineID)) - continue; - if (index >= max_spectators_displayed) break; @@ -148,10 +181,10 @@ namespace osu.Game.Screens.Play.HUD for (int i = 0; i < spectatorsFlow.Count; i++) spectatorsFlow.SetLayoutPosition(spectatorsFlow[i], i); - if (watchingUsers.Count >= max_spectators_displayed && spectatorsFlow.Count < max_spectators_displayed) + if (actualSpectators.Count >= max_spectators_displayed && spectatorsFlow.Count < max_spectators_displayed) { for (int i = spectatorsFlow.Count; i < max_spectators_displayed; i++) - addNewSpectatorToList(i, watchingUsers[i]); + addNewSpectatorToList(i, actualSpectators[i]); } break; @@ -167,8 +200,7 @@ namespace osu.Game.Screens.Play.HUD throw new NotSupportedException(); } - displayedSpectatorCount = watchingUsers.Count(s => !excludedUserIds.Contains(s.OnlineID)); - header.Text = SpectatorListStrings.SpectatorCount(displayedSpectatorCount).ToUpper(); + header.Text = SpectatorListStrings.SpectatorCount(actualSpectators.Count).ToUpper(); updateVisibility(); for (int i = 0; i < spectatorsFlow.Count; i++) @@ -193,7 +225,7 @@ namespace osu.Game.Screens.Play.HUD private void updateVisibility() { // We don't want to show spectators when we are watching a replay. - mainFlow.FadeTo(displayedSpectatorCount > 0 && userPlayingState.Value != LocalUserPlayingState.NotPlaying ? 1 : 0, 250, Easing.OutQuint); + mainFlow.FadeTo(actualSpectators.Count > 0 && userPlayingState.Value != LocalUserPlayingState.NotPlaying ? 1 : 0, 250, Easing.OutQuint); } private void updateAppearance() @@ -204,6 +236,14 @@ namespace osu.Game.Screens.Play.HUD Width = header.DrawWidth; } + protected override void Dispose(bool isDisposing) + { + base.Dispose(isDisposing); + + if (multiplayerClient.IsNotNull()) + multiplayerClient.RoomUpdated -= removePlayersFromMultiplayerRoom; + } + private partial class SpectatorListEntry : PoolableDrawable { public Bindable Current { get; } = new Bindable(); From a690b0bae993f06edc45fabc6ea2b5153219cc1b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 21 Feb 2025 12:05:23 +0100 Subject: [PATCH 1098/1275] Adjust rounding tolerance in distance snap grid ring colour logic --- osu.Game/Screens/Edit/Compose/Components/DistanceSnapGrid.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/Edit/Compose/Components/DistanceSnapGrid.cs b/osu.Game/Screens/Edit/Compose/Components/DistanceSnapGrid.cs index 88e28df8e3..8322c67def 100644 --- a/osu.Game/Screens/Edit/Compose/Components/DistanceSnapGrid.cs +++ b/osu.Game/Screens/Edit/Compose/Components/DistanceSnapGrid.cs @@ -159,7 +159,7 @@ namespace osu.Game.Screens.Edit.Compose.Components // in case 2, we want *flooring* to occur, to prevent a possible off-by-one // because of the rounding snapping forward by a chunk of time significantly too high to be considered a rounding error. // the tolerance margin chosen here is arbitrary and can be adjusted if more cases of this are found. - if (Precision.DefinitelyBigger(beatIndex, fractionalBeatIndex, 0.005)) + if (Precision.DefinitelyBigger(beatIndex, fractionalBeatIndex, 0.01)) beatIndex = (int)Math.Floor(fractionalBeatIndex); var colour = BindableBeatDivisor.GetColourFor(BindableBeatDivisor.GetDivisorForBeatIndex(beatIndex + placementIndex + 1, beatDivisor.Value), Colours); From de78518fea14b4c12c5a2db4bc74d65335f05521 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 21 Feb 2025 12:52:59 +0100 Subject: [PATCH 1099/1275] Fix "use current distance snap" button incorrectly factoring in last object with velocity Closes https://github.com/ppy/osu/issues/32003. --- osu.Game.Rulesets.Osu/Edit/OsuDistanceSnapProvider.cs | 6 +++++- osu.Game/Rulesets/Edit/ComposerDistanceSnapProvider.cs | 8 ++++---- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Edit/OsuDistanceSnapProvider.cs b/osu.Game.Rulesets.Osu/Edit/OsuDistanceSnapProvider.cs index 3c0889d027..45ce3206d2 100644 --- a/osu.Game.Rulesets.Osu/Edit/OsuDistanceSnapProvider.cs +++ b/osu.Game.Rulesets.Osu/Edit/OsuDistanceSnapProvider.cs @@ -1,10 +1,12 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System.Linq; using osu.Game.Graphics.UserInterface; using osu.Game.Input.Bindings; using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Objects; +using osu.Game.Rulesets.Objects.Types; using osu.Game.Rulesets.Osu.Objects; using osuTK; @@ -14,7 +16,9 @@ namespace osu.Game.Rulesets.Osu.Edit { public override double ReadCurrentDistanceSnap(HitObject before, HitObject after) { - float expectedDistance = DurationToDistance(after.StartTime - before.GetEndTime(), before.StartTime); + var lastObjectWithVelocity = EditorBeatmap.HitObjects.TakeWhile(ho => ho != after).OfType().LastOrDefault(); + + float expectedDistance = DurationToDistance(after.StartTime - before.GetEndTime(), before.StartTime, lastObjectWithVelocity); float actualDistance = Vector2.Distance(((OsuHitObject)before).EndPosition, ((OsuHitObject)after).Position); return actualDistance / expectedDistance; diff --git a/osu.Game/Rulesets/Edit/ComposerDistanceSnapProvider.cs b/osu.Game/Rulesets/Edit/ComposerDistanceSnapProvider.cs index d0b279f201..4129a6fb2c 100644 --- a/osu.Game/Rulesets/Edit/ComposerDistanceSnapProvider.cs +++ b/osu.Game/Rulesets/Edit/ComposerDistanceSnapProvider.cs @@ -52,7 +52,7 @@ namespace osu.Game.Rulesets.Edit private EditorClock editorClock { get; set; } = null!; [Resolved] - private EditorBeatmap editorBeatmap { get; set; } = null!; + protected EditorBeatmap EditorBeatmap { get; private set; } = null!; [Resolved] private IBeatSnapProvider beatSnapProvider { get; set; } = null!; @@ -100,7 +100,7 @@ namespace osu.Game.Rulesets.Edit } }); - DistanceSpacingMultiplier.Value = editorBeatmap.DistanceSpacing; + DistanceSpacingMultiplier.Value = EditorBeatmap.DistanceSpacing; DistanceSpacingMultiplier.BindValueChanged(multiplier => { distanceSpacingSlider.ContractedLabelText = $"D. S. ({multiplier.NewValue:0.##x})"; @@ -109,7 +109,7 @@ namespace osu.Game.Rulesets.Edit if (multiplier.NewValue != multiplier.OldValue) onScreenDisplay?.Display(new DistanceSpacingToast(multiplier.NewValue.ToLocalisableString(@"0.##x"), multiplier)); - editorBeatmap.DistanceSpacing = multiplier.NewValue; + EditorBeatmap.DistanceSpacing = multiplier.NewValue; }, true); DistanceSpacingMultiplier.BindDisabledChanged(disabled => distanceSpacingSlider.Alpha = disabled ? 0 : 1, true); @@ -267,7 +267,7 @@ namespace osu.Game.Rulesets.Edit public virtual float GetBeatSnapDistance(IHasSliderVelocity? withVelocity = null) { - return (float)(100 * (withVelocity?.SliderVelocityMultiplier ?? 1) * editorBeatmap.Difficulty.SliderMultiplier * 1 + return (float)(100 * (withVelocity?.SliderVelocityMultiplier ?? 1) * EditorBeatmap.Difficulty.SliderMultiplier * 1 / beatSnapProvider.BeatDivisor); } From c77fed637c89aab0e6374c307b4935fe1d6f9097 Mon Sep 17 00:00:00 2001 From: ziv_vy <134942175+ziv-vy@users.noreply.github.com> Date: Sun, 23 Feb 2025 01:01:39 +0200 Subject: [PATCH 1100/1275] Update MouseSettingsStrings.cs CAPITALISED ONE GODDAMN LETTER --- osu.Game/Localisation/MouseSettingsStrings.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Localisation/MouseSettingsStrings.cs b/osu.Game/Localisation/MouseSettingsStrings.cs index e61af07364..9609c2dd90 100644 --- a/osu.Game/Localisation/MouseSettingsStrings.cs +++ b/osu.Game/Localisation/MouseSettingsStrings.cs @@ -25,9 +25,9 @@ namespace osu.Game.Localisation public static LocalisableString HighPrecisionMouse => new TranslatableString(getKey(@"high_precision_mouse"), @"High precision mouse"); /// - /// "Attempts to bypass any operation system mouse acceleration. On windows, this is equivalent to what used to be known as "Raw Input"." + /// "Attempts to bypass any operation system mouse acceleration. On Windows, this is equivalent to what used to be known as "Raw Input"." /// - public static LocalisableString HighPrecisionMouseTooltip => new TranslatableString(getKey(@"high_precision_mouse_tooltip"), @"Attempts to bypass any operation system mouse acceleration. On windows, this is equivalent to what used to be known as ""Raw Input""."); + public static LocalisableString HighPrecisionMouseTooltip => new TranslatableString(getKey(@"high_precision_mouse_tooltip"), @"Attempts to bypass any operation system mouse acceleration. On Windows, this is equivalent to what used to be known as ""Raw Input""."); /// /// "Confine mouse cursor to window" From d95f31dc5af463423a72981f4328fbd0f0b6c654 Mon Sep 17 00:00:00 2001 From: Joseph Madamba Date: Sat, 22 Feb 2025 15:21:54 -0800 Subject: [PATCH 1101/1275] Also fix operating system terminology --- osu.Game/Localisation/MouseSettingsStrings.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Localisation/MouseSettingsStrings.cs b/osu.Game/Localisation/MouseSettingsStrings.cs index 9609c2dd90..c92c3b6ddc 100644 --- a/osu.Game/Localisation/MouseSettingsStrings.cs +++ b/osu.Game/Localisation/MouseSettingsStrings.cs @@ -25,9 +25,9 @@ namespace osu.Game.Localisation public static LocalisableString HighPrecisionMouse => new TranslatableString(getKey(@"high_precision_mouse"), @"High precision mouse"); /// - /// "Attempts to bypass any operation system mouse acceleration. On Windows, this is equivalent to what used to be known as "Raw Input"." + /// "Attempts to bypass any operating system mouse acceleration. On Windows, this is equivalent to what used to be known as "Raw Input"." /// - public static LocalisableString HighPrecisionMouseTooltip => new TranslatableString(getKey(@"high_precision_mouse_tooltip"), @"Attempts to bypass any operation system mouse acceleration. On Windows, this is equivalent to what used to be known as ""Raw Input""."); + public static LocalisableString HighPrecisionMouseTooltip => new TranslatableString(getKey(@"high_precision_mouse_tooltip"), @"Attempts to bypass any operating system mouse acceleration. On Windows, this is equivalent to what used to be known as ""Raw Input""."); /// /// "Confine mouse cursor to window" From 8b2582a69d07adf343855b729dd143777abbcbf6 Mon Sep 17 00:00:00 2001 From: finadoggie <75299710+Finadoggie@users.noreply.github.com> Date: Sat, 22 Feb 2025 16:54:27 -0800 Subject: [PATCH 1102/1275] Add tip pressure threshold slider ingame --- .../Settings/Sections/Input/TabletSettings.cs | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/osu.Game/Overlays/Settings/Sections/Input/TabletSettings.cs b/osu.Game/Overlays/Settings/Sections/Input/TabletSettings.cs index 00ffbc1120..2cce6f18ec 100644 --- a/osu.Game/Overlays/Settings/Sections/Input/TabletSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/Input/TabletSettings.cs @@ -45,6 +45,8 @@ namespace osu.Game.Overlays.Settings.Sections.Input private readonly BindableNumber rotation = new BindableNumber { MinValue = 0, MaxValue = 360 }; + private readonly BindableNumber pressureThreshold = new BindableNumber { MinValue = 0, MaxValue = 100 }; + [Resolved] private GameHost host { get; set; } @@ -213,6 +215,13 @@ namespace osu.Game.Overlays.Settings.Sections.Input Current = sizeY, CanBeShown = { BindTarget = enabled } }, + new SettingsSlider + { + TransferValueOnCommit = true, + LabelText = "Tip Threshold", + Current = pressureThreshold, + CanBeShown = { BindTarget = enabled } + }, } }, }; @@ -267,6 +276,8 @@ namespace osu.Game.Overlays.Settings.Sections.Input aspectRatioApplication = Schedule(() => forceAspectRatio(aspect.NewValue)); }); + pressureThreshold.BindTo(tabletHandler.PressureThreshold); + tablet.BindTo(tabletHandler.Tablet); tablet.BindValueChanged(val => Schedule(() => { From 543ad5b2a47591652d04ac66eb8730cafd7e06b9 Mon Sep 17 00:00:00 2001 From: Kunologist <2014709936@qq.com> Date: Mon, 24 Feb 2025 14:16:33 +0800 Subject: [PATCH 1103/1275] Add alt+wheel volume adjustment on result screen --- osu.Game/Screens/Ranking/ResultsScreen.cs | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/osu.Game/Screens/Ranking/ResultsScreen.cs b/osu.Game/Screens/Ranking/ResultsScreen.cs index fe0d805cee..8fb3c66054 100644 --- a/osu.Game/Screens/Ranking/ResultsScreen.cs +++ b/osu.Game/Screens/Ranking/ResultsScreen.cs @@ -26,6 +26,7 @@ using osu.Game.Localisation; using osu.Game.Online.API; using osu.Game.Online.Placeholders; using osu.Game.Overlays; +using osu.Game.Overlays.Volume; using osu.Game.Scoring; using osu.Game.Screens.Play; using osu.Game.Screens.Ranking.Expanded.Accuracy; @@ -122,6 +123,7 @@ namespace osu.Game.Screens.Ranking RelativeSizeAxes = Axes.Both, Children = new Drawable[] { + new GlobalScrollAdjustsVolume(), StatisticsPanel = createStatisticsPanel().With(panel => { panel.RelativeSizeAxes = Axes.Both; @@ -503,12 +505,24 @@ namespace osu.Game.Screens.Ranking { } + protected override bool OnScroll(ScrollEvent e) + { + // Match stable behaviour of only alt-scroll adjusting volume. + // This is the same behaviour as the song selection screen. + if (!e.CurrentState.Keyboard.AltPressed) + return true; + + return base.OnScroll(e); + } + protected partial class VerticalScrollContainer : OsuScrollContainer { protected override Container Content => content; private readonly Container content; + protected override bool OnScroll(ScrollEvent e) => !e.ControlPressed && !e.AltPressed && !e.ShiftPressed && !e.SuperPressed; + public VerticalScrollContainer() { Masking = false; From f4b427ee66bd169ab7270f9a4ef8f467d4ac4572 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 24 Feb 2025 09:15:20 +0100 Subject: [PATCH 1104/1275] Add failing test case --- .../TestSceneModDifficultyAdjustSettings.cs | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneModDifficultyAdjustSettings.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneModDifficultyAdjustSettings.cs index b40d0b10d2..30470c9c17 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneModDifficultyAdjustSettings.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneModDifficultyAdjustSettings.cs @@ -18,6 +18,7 @@ using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Osu.Mods; using osuTK; using osuTK.Graphics; +using osuTK.Input; namespace osu.Game.Tests.Visual.UserInterface { @@ -220,6 +221,29 @@ namespace osu.Game.Tests.Visual.UserInterface checkBindableAtValue("Circle Size", null); } + [Test] + public void TestResetToDefaultViaDoubleClickingNub() + { + setBeatmapWithDifficultyParameters(5); + + setSliderValue("Circle Size", 3); + setExtendedLimits(true); + + checkSliderAtValue("Circle Size", 3); + checkBindableAtValue("Circle Size", 3); + + AddStep("double click circle size nub", () => + { + var nub = this.ChildrenOfType.SliderNub>().First(); + InputManager.MoveMouseTo(nub); + InputManager.Click(MouseButton.Left); + InputManager.Click(MouseButton.Left); + }); + + checkSliderAtValue("Circle Size", 5); + checkBindableAtValue("Circle Size", null); + } + [Test] public void TestModSettingChangeTracker() { From d8cb3b68d3ea90268bb142a2ef6e1de782f2040a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 24 Feb 2025 09:34:52 +0100 Subject: [PATCH 1105/1275] Add "Team" channel type The lack of this bricks chat completely due to newtonsoft deserialisation errors: 2025-02-24 08:32:58 [verbose]: Processing response from https://dev.ppy.sh/api/v2/chat/updates failed with Newtonsoft.Json.JsonSerializationException: Error converting value "TEAM" to type 'osu.Game.Online.Chat.ChannelType'. Path 'presence[39].type', line 1, position 13765. 2025-02-24 08:32:58 [verbose]: ---> System.ArgumentException: Requested value 'TEAM' was not found. 2025-02-24 08:32:58 [verbose]: at Newtonsoft.Json.Utilities.EnumUtils.ParseEnum(Type enumType, NamingStrategy namingStrategy, String value, Boolean disallowNumber) 2025-02-24 08:32:58 [verbose]: at Newtonsoft.Json.Serialization.JsonSerializerInternalReader.EnsureType(JsonReader reader, Object value, CultureInfo culture, JsonContract contract, Type targetType) --- osu.Game/Online/Chat/ChannelType.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/osu.Game/Online/Chat/ChannelType.cs b/osu.Game/Online/Chat/ChannelType.cs index bd628e90c4..4fb890c2cc 100644 --- a/osu.Game/Online/Chat/ChannelType.cs +++ b/osu.Game/Online/Chat/ChannelType.cs @@ -14,5 +14,6 @@ namespace osu.Game.Online.Chat Group, System, Announce, + Team, } } From be8ec759488e3bb5e5479341c8de400a80f3e9ec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 24 Feb 2025 09:39:27 +0100 Subject: [PATCH 1106/1275] Display team chat channel in separate group --- osu.Game/Overlays/Chat/ChannelList/ChannelList.cs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/osu.Game/Overlays/Chat/ChannelList/ChannelList.cs b/osu.Game/Overlays/Chat/ChannelList/ChannelList.cs index f027888962..6e874e4ed8 100644 --- a/osu.Game/Overlays/Chat/ChannelList/ChannelList.cs +++ b/osu.Game/Overlays/Chat/ChannelList/ChannelList.cs @@ -39,6 +39,7 @@ namespace osu.Game.Overlays.Chat.ChannelList public ChannelGroup AnnounceChannelGroup { get; private set; } = null!; public ChannelGroup PublicChannelGroup { get; private set; } = null!; + public ChannelGroup TeamChannelGroup { get; private set; } = null!; public ChannelGroup PrivateChannelGroup { get; private set; } = null!; private OsuScrollContainer scroll = null!; @@ -82,6 +83,7 @@ namespace osu.Game.Overlays.Chat.ChannelList AnnounceChannelGroup = new ChannelGroup(ChatStrings.ChannelsListTitleANNOUNCE.ToUpper(), false), PublicChannelGroup = new ChannelGroup(ChatStrings.ChannelsListTitlePUBLIC.ToUpper(), false), selector = new ChannelListItem(ChannelListingChannel), + TeamChannelGroup = new ChannelGroup("TEAM", false), // TODO: replace with osu-web localisable string once available PrivateChannelGroup = new ChannelGroup(ChatStrings.ChannelsListTitlePM.ToUpper(), true), }, }, @@ -156,6 +158,9 @@ namespace osu.Game.Overlays.Chat.ChannelList case ChannelType.Announce: return AnnounceChannelGroup; + case ChannelType.Team: + return TeamChannelGroup; + default: return PublicChannelGroup; } From 194f05d2588fa7f23287b85c3968219102e33a51 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 24 Feb 2025 09:43:46 +0100 Subject: [PATCH 1107/1275] Add icons to chat channel group headers Matches web in appearance. Cross-reference: https://github.com/ppy/osu-web/blob/3c9e99eaf4bd9e73d2712f60d67f5bc95f9dfe2b/resources/js/chat/conversation-list.tsx#L13-L19 --- .../Overlays/Chat/ChannelList/ChannelList.cs | 35 ++++++++++++++----- 1 file changed, 26 insertions(+), 9 deletions(-) diff --git a/osu.Game/Overlays/Chat/ChannelList/ChannelList.cs b/osu.Game/Overlays/Chat/ChannelList/ChannelList.cs index 6e874e4ed8..ae68c9c82e 100644 --- a/osu.Game/Overlays/Chat/ChannelList/ChannelList.cs +++ b/osu.Game/Overlays/Chat/ChannelList/ChannelList.cs @@ -9,6 +9,7 @@ using osu.Framework.Extensions.LocalisationExtensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; +using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.UserInterface; using osu.Framework.Localisation; using osu.Framework.Testing; @@ -80,11 +81,12 @@ namespace osu.Game.Overlays.Chat.ChannelList RelativeSizeAxes = Axes.X, } }, - AnnounceChannelGroup = new ChannelGroup(ChatStrings.ChannelsListTitleANNOUNCE.ToUpper(), false), - PublicChannelGroup = new ChannelGroup(ChatStrings.ChannelsListTitlePUBLIC.ToUpper(), false), + // cross-reference for icons: https://github.com/ppy/osu-web/blob/3c9e99eaf4bd9e73d2712f60d67f5bc95f9dfe2b/resources/js/chat/conversation-list.tsx#L13-L19 + AnnounceChannelGroup = new ChannelGroup(ChatStrings.ChannelsListTitleANNOUNCE.ToUpper(), FontAwesome.Solid.Bullhorn, false), + PublicChannelGroup = new ChannelGroup(ChatStrings.ChannelsListTitlePUBLIC.ToUpper(), FontAwesome.Solid.Comments, false), selector = new ChannelListItem(ChannelListingChannel), - TeamChannelGroup = new ChannelGroup("TEAM", false), // TODO: replace with osu-web localisable string once available - PrivateChannelGroup = new ChannelGroup(ChatStrings.ChannelsListTitlePM.ToUpper(), true), + TeamChannelGroup = new ChannelGroup("TEAM", FontAwesome.Solid.Users, false), // TODO: replace with osu-web localisable string once available + PrivateChannelGroup = new ChannelGroup(ChatStrings.ChannelsListTitlePM.ToUpper(), FontAwesome.Solid.Envelope, true), }, }, }, @@ -179,7 +181,7 @@ namespace osu.Game.Overlays.Chat.ChannelList private readonly bool sortByRecent; public readonly ChannelListItemFlow ItemFlow; - public ChannelGroup(LocalisableString label, bool sortByRecent) + public ChannelGroup(LocalisableString label, IconUsage icon, bool sortByRecent) { this.sortByRecent = sortByRecent; Direction = FillDirection.Vertical; @@ -189,11 +191,26 @@ namespace osu.Game.Overlays.Chat.ChannelList Children = new Drawable[] { - new OsuSpriteText + new FillFlowContainer { - Text = label, - Margin = new MarginPadding { Left = 18, Bottom = 5 }, - Font = OsuFont.Torus.With(size: 12, weight: FontWeight.SemiBold), + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Direction = FillDirection.Horizontal, + Spacing = new Vector2(5), + Children = new Drawable[] + { + new OsuSpriteText + { + Text = label, + Margin = new MarginPadding { Left = 18, Bottom = 5 }, + Font = OsuFont.Torus.With(size: 12, weight: FontWeight.SemiBold), + }, + new SpriteIcon + { + Icon = icon, + Size = new Vector2(12), + }, + } }, ItemFlow = new ChannelListItemFlow(sortByRecent) { From 4ac4b308e10d041dec5960f808ce2d295171f3d2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 24 Feb 2025 09:48:03 +0100 Subject: [PATCH 1108/1275] Add visual test coverage of team channels --- .../Visual/Online/TestSceneChannelList.cs | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/osu.Game.Tests/Visual/Online/TestSceneChannelList.cs b/osu.Game.Tests/Visual/Online/TestSceneChannelList.cs index 5f77e084da..8f8cf036f1 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneChannelList.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneChannelList.cs @@ -115,6 +115,12 @@ namespace osu.Game.Tests.Visual.Online channelList.AddChannel(createRandomPrivateChannel()); }); + AddStep("Add Team Channels", () => + { + for (int i = 0; i < 10; i++) + channelList.AddChannel(createRandomTeamChannel()); + }); + AddStep("Add Announce Channels", () => { for (int i = 0; i < 2; i++) @@ -189,5 +195,16 @@ namespace osu.Game.Tests.Visual.Online Id = id, }; } + + private Channel createRandomTeamChannel() + { + int id = TestResources.GetNextTestID(); + return new Channel + { + Name = $"Team {id}", + Type = ChannelType.Team, + Id = id, + }; + } } } From 0312467c8840067054c6326d9da821329b7bf01e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 24 Feb 2025 12:30:37 +0100 Subject: [PATCH 1109/1275] Fix hash comparison being case sensitive when choosing files for partial beatmap submission Noticed when investigating https://github.com/ppy/osu/issues/32059, and also a likely cause for user reports like https://discord.com/channels/188630481301012481/1097318920991559880/1342962553101357066. Honestly I have no solid defence, Your Honour. I guess this just must not have been tested on the client side, only relied on server-side testing. --- osu.Game/Screens/Edit/Submission/BeatmapSubmissionScreen.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/Edit/Submission/BeatmapSubmissionScreen.cs b/osu.Game/Screens/Edit/Submission/BeatmapSubmissionScreen.cs index 66139bacec..13981bcb69 100644 --- a/osu.Game/Screens/Edit/Submission/BeatmapSubmissionScreen.cs +++ b/osu.Game/Screens/Edit/Submission/BeatmapSubmissionScreen.cs @@ -285,7 +285,7 @@ namespace osu.Game.Screens.Edit.Submission continue; } - if (localHash != onlineHash) + if (!localHash.Equals(onlineHash, StringComparison.OrdinalIgnoreCase)) filesToUpdate.Add(filename); } From 41db3c1501bbfc40f1eb9952fa9d332319ff347b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 24 Feb 2025 14:30:55 +0100 Subject: [PATCH 1110/1275] Fix taiko swell ending samples playing at results sometimes Closes https://github.com/ppy/osu/issues/32052. Sooooo... this is going to be a rant... To understand why this is going to require a rant, dear reader, please do the following: 1. Read the issue thread and follow the reproduction scenario (download map linked, fire up autoplay, seek near end, wait for results, hear the sample spam). 2. Now exit out to song select, *hide the toolbar*, and attempt reproducing the issue again. 3. Depending on ambient mood, laugh or cry. Now, *why on earth* would the *TOOLBAR* have any bearing on anything? Well, the chain of failure is something like this: - The toolbar hides for the duration of gameplay, naturally. - When progressing to results, the toolbar gets automatically unhidden. - This triggers invalidations on `ScrollingHitObjectContainer`. I'm not precisely sure which property it is that triggers the invalidations, but one clearly does. It may be position or size or whichever. - When the invalidation is triggered on `layoutCache`, the next `Update()` call is going to recompute lifetimes for ALL hitobject entries. - In case of swells, it happens that the calculated lifetime end of the swell is larger than what it actually ended up being determined as at the instant of judging the swell, and thus, the swell is *resurrected*, reassigned a DHO, and the DHO calls `UpdateState()` and plays the sample again despite the `samplePlayed` flag in `LegacySwell`, because all of that is ephemeral state that does not survive a hitobject getting resurrected. Now I *could* just fix this locally to the swell, maybe, by having some time lenience check, but the fact that hitobjects can be resurrected by the *toolbar* appearing, of all possible causes in the world, feels just completely wrong. So I'm adding a local check in SHOC to not overwrite lifetime ends of judged object entries. The reason why I'm making that check specific to end time is that I can see valid reasons why you would want to recompute lifetime *start* even on a judged object (such as playfield geometry changing in a significant way). I can't think of a valid reason to do that to lifetime *end*. --- .../Rulesets/UI/Scrolling/ScrollingHitObjectContainer.cs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/osu.Game/Rulesets/UI/Scrolling/ScrollingHitObjectContainer.cs b/osu.Game/Rulesets/UI/Scrolling/ScrollingHitObjectContainer.cs index 7841e65935..8b0076afa1 100644 --- a/osu.Game/Rulesets/UI/Scrolling/ScrollingHitObjectContainer.cs +++ b/osu.Game/Rulesets/UI/Scrolling/ScrollingHitObjectContainer.cs @@ -247,7 +247,12 @@ namespace osu.Game.Rulesets.UI.Scrolling // It is required that we set a lifetime end here to ensure that in scenarios like loading a Player instance to a seeked // location in a beatmap doesn't churn every hit object into a DrawableHitObject. Even in a pooled scenario, the overhead // of this can be quite crippling. - entry.LifetimeEnd = entry.HitObject.GetEndTime() + timeRange.Value; + // + // However, additionally do not attempt to alter lifetime of judged entries. + // This is to prevent freak accidents like objects suddenly becoming alive because of this estimate assigning a later lifetime + // than the object itself decided it should have when it underwent judgement. + if (!entry.Judged) + entry.LifetimeEnd = entry.HitObject.GetEndTime() + timeRange.Value; } private void updateLayoutRecursive(DrawableHitObject hitObject, double? parentHitObjectStartTime = null) From e8f7bcb6e625a6360b2bd4487186ac075e07ddc2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 24 Feb 2025 15:06:02 +0100 Subject: [PATCH 1111/1275] Only show team channel section when there is a team channel --- osu.Game.Tests/Visual/Online/TestSceneChannelList.cs | 6 +----- osu.Game/Overlays/Chat/ChannelList/ChannelList.cs | 7 +++---- 2 files changed, 4 insertions(+), 9 deletions(-) diff --git a/osu.Game.Tests/Visual/Online/TestSceneChannelList.cs b/osu.Game.Tests/Visual/Online/TestSceneChannelList.cs index 8f8cf036f1..364240502a 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneChannelList.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneChannelList.cs @@ -115,11 +115,7 @@ namespace osu.Game.Tests.Visual.Online channelList.AddChannel(createRandomPrivateChannel()); }); - AddStep("Add Team Channels", () => - { - for (int i = 0; i < 10; i++) - channelList.AddChannel(createRandomTeamChannel()); - }); + AddStep("Add Team Channel", () => channelList.AddChannel(createRandomTeamChannel())); AddStep("Add Announce Channels", () => { diff --git a/osu.Game/Overlays/Chat/ChannelList/ChannelList.cs b/osu.Game/Overlays/Chat/ChannelList/ChannelList.cs index ae68c9c82e..c0fc349c2c 100644 --- a/osu.Game/Overlays/Chat/ChannelList/ChannelList.cs +++ b/osu.Game/Overlays/Chat/ChannelList/ChannelList.cs @@ -106,6 +106,7 @@ namespace osu.Game.Overlays.Chat.ChannelList }; selector.OnRequestSelect += chan => OnRequestSelect?.Invoke(chan); + updateVisibility(); } public void AddChannel(Channel channel) @@ -170,10 +171,8 @@ namespace osu.Game.Overlays.Chat.ChannelList private void updateVisibility() { - if (AnnounceChannelGroup.ItemFlow.Children.Count == 0) - AnnounceChannelGroup.Hide(); - else - AnnounceChannelGroup.Show(); + AnnounceChannelGroup.Alpha = AnnounceChannelGroup.ItemFlow.Any() ? 1 : 0; + TeamChannelGroup.Alpha = TeamChannelGroup.ItemFlow.Any() ? 1 : 0; } public partial class ChannelGroup : FillFlowContainer From e13aa4a99b353f994e9bcf6c6df58d18f466bf66 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 24 Feb 2025 15:10:20 +0100 Subject: [PATCH 1112/1275] Do not allow leaving team channels --- osu.Game/Overlays/Chat/ChannelList/ChannelList.cs | 8 ++++++-- osu.Game/Overlays/Chat/ChannelList/ChannelListItem.cs | 4 +++- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/osu.Game/Overlays/Chat/ChannelList/ChannelList.cs b/osu.Game/Overlays/Chat/ChannelList/ChannelList.cs index c0fc349c2c..0a89775cc7 100644 --- a/osu.Game/Overlays/Chat/ChannelList/ChannelList.cs +++ b/osu.Game/Overlays/Chat/ChannelList/ChannelList.cs @@ -114,9 +114,13 @@ namespace osu.Game.Overlays.Chat.ChannelList if (channelMap.ContainsKey(channel)) return; - ChannelListItem item = new ChannelListItem(channel); + ChannelListItem item = new ChannelListItem(channel) + { + CanLeave = channel.Type != ChannelType.Team + }; item.OnRequestSelect += chan => OnRequestSelect?.Invoke(chan); - item.OnRequestLeave += chan => OnRequestLeave?.Invoke(chan); + if (item.CanLeave) + item.OnRequestLeave += chan => OnRequestLeave?.Invoke(chan); ChannelGroup group = getGroupFromChannel(channel); channelMap.Add(channel, item); diff --git a/osu.Game/Overlays/Chat/ChannelList/ChannelListItem.cs b/osu.Game/Overlays/Chat/ChannelList/ChannelListItem.cs index b197fe199d..6107f130ec 100644 --- a/osu.Game/Overlays/Chat/ChannelList/ChannelListItem.cs +++ b/osu.Game/Overlays/Chat/ChannelList/ChannelListItem.cs @@ -24,6 +24,8 @@ namespace osu.Game.Overlays.Chat.ChannelList public partial class ChannelListItem : OsuClickableContainer, IFilterable { public event Action? OnRequestSelect; + + public bool CanLeave { get; init; } = true; public event Action? OnRequestLeave; public readonly Channel Channel; @@ -160,7 +162,7 @@ namespace osu.Game.Overlays.Chat.ChannelList private ChannelListItemCloseButton? createCloseButton() { - if (isSelector) + if (isSelector || !CanLeave) return null; return new ChannelListItemCloseButton From c82cf4092879167f60bd76729730350d882c248f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 24 Feb 2025 15:24:18 +0100 Subject: [PATCH 1113/1275] Do not give swell ticks any visual representation Why is this a thing at all? How has it survived this long? I don't know. As far as I can tell this only manifests on selected beatmaps with "slow swells" that spend the entire beatmap moving in the background. On other beatmaps the tick is faded out, probably due to the initial transform application that normally "works" but fails hard on these slow swells. Can be seen on https://osu.ppy.sh/beatmapsets/1432454#taiko/2948222. --- .../Objects/Drawables/DrawableSwellTick.cs | 7 +------ .../Objects/Drawables/DrawableTaikoHitObject.cs | 6 +++++- 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableSwellTick.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableSwellTick.cs index 04dd01e066..88554ba257 100644 --- a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableSwellTick.cs +++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableSwellTick.cs @@ -4,9 +4,7 @@ #nullable disable using JetBrains.Annotations; -using osu.Framework.Graphics; using osu.Framework.Input.Events; -using osu.Game.Rulesets.Taiko.Skinning.Default; using osu.Game.Skinning; namespace osu.Game.Rulesets.Taiko.Objects.Drawables @@ -25,8 +23,6 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables { } - protected override void UpdateInitialTransforms() => this.FadeOut(); - public void TriggerResult(bool hit) { HitObject.StartTime = Time.Current; @@ -43,7 +39,6 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables public override bool OnPressed(KeyBindingPressEvent e) => false; - protected override SkinnableDrawable CreateMainPiece() => new SkinnableDrawable(new TaikoSkinComponentLookup(TaikoSkinComponents.DrumRollTick), - _ => new TickPiece()); + protected override SkinnableDrawable CreateMainPiece() => null; } } diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableTaikoHitObject.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableTaikoHitObject.cs index 0cf9651965..520ac2ba80 100644 --- a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableTaikoHitObject.cs +++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableTaikoHitObject.cs @@ -154,9 +154,13 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables if (MainPiece != null) Content.Remove(MainPiece, true); - Content.Add(MainPiece = CreateMainPiece()); + MainPiece = CreateMainPiece(); + + if (MainPiece != null) + Content.Add(MainPiece); } + [CanBeNull] protected abstract SkinnableDrawable CreateMainPiece(); } } From 1f562ab47d5f6959f66e0091ec82b778c3539f7b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 24 Feb 2025 09:18:19 +0100 Subject: [PATCH 1114/1275] Fix double-clicking difficulty adjust sliders not resetting the value to default correctly - Closes https://github.com/ppy/osu/issues/31888 - Supersedes / closes https://github.com/ppy/osu/pull/32060 --- .../Mods/OsuModDifficultyAdjust.cs | 9 +------- .../UserInterface/RoundedSliderBar.cs | 18 +++++++++++---- .../Mods/DifficultyAdjustSettingsControl.cs | 23 +++++++++++++------ 3 files changed, 30 insertions(+), 20 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModDifficultyAdjust.cs b/osu.Game.Rulesets.Osu/Mods/OsuModDifficultyAdjust.cs index f35b1abc42..10282ff988 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModDifficultyAdjust.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModDifficultyAdjust.cs @@ -3,7 +3,6 @@ using System.Linq; using osu.Framework.Bindables; -using osu.Framework.Graphics; using osu.Framework.Localisation; using osu.Game.Beatmaps; using osu.Game.Beatmaps.ControlPoints; @@ -63,13 +62,7 @@ namespace osu.Game.Rulesets.Osu.Mods private partial class ApproachRateSettingsControl : DifficultyAdjustSettingsControl { - protected override RoundedSliderBar CreateSlider(BindableNumber current) => - new ApproachRateSlider - { - RelativeSizeAxes = Axes.X, - Current = current, - KeyboardStep = 0.1f, - }; + protected override RoundedSliderBar CreateSlider(BindableNumber current) => new ApproachRateSlider(); /// /// A slider bar with more detailed approach rate info for its given value diff --git a/osu.Game/Graphics/UserInterface/RoundedSliderBar.cs b/osu.Game/Graphics/UserInterface/RoundedSliderBar.cs index aeab7c34b2..9a0183da64 100644 --- a/osu.Game/Graphics/UserInterface/RoundedSliderBar.cs +++ b/osu.Game/Graphics/UserInterface/RoundedSliderBar.cs @@ -10,6 +10,7 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Effects; using osu.Framework.Graphics.Shapes; +using osu.Framework.Graphics.UserInterface; using osu.Framework.Input.Events; using osu.Game.Overlays; using Vector2 = osuTK.Vector2; @@ -52,10 +53,21 @@ namespace osu.Game.Graphics.UserInterface } } + /// + /// The action to use to reset the value of to the default. + /// Triggered on double click. + /// + public Action ResetToDefault { get; internal set; } + public RoundedSliderBar() { Height = Nub.HEIGHT; RangePadding = Nub.DEFAULT_EXPANDED_SIZE / 2; + ResetToDefault = () => + { + if (!Current.Disabled) + Current.SetDefault(); + }; Children = new Drawable[] { new Container @@ -102,11 +114,7 @@ namespace osu.Game.Graphics.UserInterface Origin = Anchor.TopCentre, RelativePositionAxes = Axes.X, Current = { Value = true }, - OnDoubleClicked = () => - { - if (!Current.Disabled) - Current.SetDefault(); - }, + OnDoubleClicked = () => ResetToDefault.Invoke(), }, }, hoverClickSounds = new HoverClickSounds() diff --git a/osu.Game/Rulesets/Mods/DifficultyAdjustSettingsControl.cs b/osu.Game/Rulesets/Mods/DifficultyAdjustSettingsControl.cs index d04d7636ec..6697a8d848 100644 --- a/osu.Game/Rulesets/Mods/DifficultyAdjustSettingsControl.cs +++ b/osu.Game/Rulesets/Mods/DifficultyAdjustSettingsControl.cs @@ -31,12 +31,7 @@ namespace osu.Game.Rulesets.Mods protected sealed override Drawable CreateControl() => new SliderControl(sliderDisplayCurrent, CreateSlider); - protected virtual RoundedSliderBar CreateSlider(BindableNumber current) => new RoundedSliderBar - { - RelativeSizeAxes = Axes.X, - Current = current, - KeyboardStep = 0.1f, - }; + protected virtual RoundedSliderBar CreateSlider(BindableNumber current) => new RoundedSliderBar(); /// /// Guards against beatmap values displayed on slider bars being transferred to user override. @@ -111,7 +106,21 @@ namespace osu.Game.Rulesets.Mods { InternalChildren = new Drawable[] { - createSlider(currentNumber) + createSlider(currentNumber).With(slider => + { + slider.RelativeSizeAxes = Axes.X; + slider.Current = currentNumber; + slider.KeyboardStep = 0.1f; + // this looks redundant, but isn't because of the various games this component plays + // (`Current` is nullable and represents the underlying setting value, + // `currentNumber` is not nullable and represents what is getting displayed, + // therefore without this, double-clicking the slider would reset `currentNumber` to its bogus default of 0). + slider.ResetToDefault = () => + { + if (!Current.Disabled) + Current.SetDefault(); + }; + }) }; AutoSizeAxes = Axes.Y; From fc2d8bfe5f3b4ed3d1a0f7652dd84601e8115b75 Mon Sep 17 00:00:00 2001 From: finadoggie <75299710+Finadoggie@users.noreply.github.com> Date: Tue, 25 Feb 2025 00:25:51 -0800 Subject: [PATCH 1115/1275] Clamp slider from 0 to 1 --- osu.Game/Overlays/Settings/Sections/Input/TabletSettings.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Overlays/Settings/Sections/Input/TabletSettings.cs b/osu.Game/Overlays/Settings/Sections/Input/TabletSettings.cs index 2cce6f18ec..9d70e49659 100644 --- a/osu.Game/Overlays/Settings/Sections/Input/TabletSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/Input/TabletSettings.cs @@ -45,7 +45,7 @@ namespace osu.Game.Overlays.Settings.Sections.Input private readonly BindableNumber rotation = new BindableNumber { MinValue = 0, MaxValue = 360 }; - private readonly BindableNumber pressureThreshold = new BindableNumber { MinValue = 0, MaxValue = 100 }; + private readonly BindableNumber pressureThreshold = new BindableNumber { MinValue = 0.0f, MaxValue = 1.0f, Precision = 0.005f }; [Resolved] private GameHost host { get; set; } From e97c2fee0d2a57d2d13c2e20a76370daa325cd4c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 25 Feb 2025 12:57:38 +0100 Subject: [PATCH 1116/1275] 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 d49acd7b27..d4b49e492a 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 5ca49e80f6..d10a3d649a 100644 --- a/osu.iOS.props +++ b/osu.iOS.props @@ -17,6 +17,6 @@ -all - + From 13ca8c20f6fa71bd196e30a5987cb112cbc7214f Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Tue, 25 Feb 2025 21:54:13 +0900 Subject: [PATCH 1117/1275] Make results screens use tasks to fetch scores --- .../Visual/Ranking/TestSceneResultsScreen.cs | 17 +-- .../Spectate/MultiSpectatorResultsScreen.cs | 7 +- .../Playlists/PlaylistItemResultsScreen.cs | 112 ++++++++++-------- .../PlaylistItemScoreResultsScreen.cs | 5 +- .../PlaylistItemUserBestResultsScreen.cs | 5 +- osu.Game/Screens/Ranking/ResultsScreen.cs | 51 +++----- osu.Game/Screens/Ranking/SoloResultsScreen.cs | 31 +++-- 7 files changed, 117 insertions(+), 111 deletions(-) diff --git a/osu.Game.Tests/Visual/Ranking/TestSceneResultsScreen.cs b/osu.Game.Tests/Visual/Ranking/TestSceneResultsScreen.cs index 3a08756090..4acbdb4a76 100644 --- a/osu.Game.Tests/Visual/Ranking/TestSceneResultsScreen.cs +++ b/osu.Game.Tests/Visual/Ranking/TestSceneResultsScreen.cs @@ -17,7 +17,6 @@ using osu.Framework.Utils; using osu.Game.Beatmaps; using osu.Game.Database; using osu.Game.Graphics.UserInterface; -using osu.Game.Online.API; using osu.Game.Rulesets; using osu.Game.Rulesets.Difficulty; using osu.Game.Rulesets.Osu; @@ -416,7 +415,7 @@ namespace osu.Game.Tests.Visual.Ranking RetryOverlay = InternalChildren.OfType().SingleOrDefault(); } - protected override APIRequest FetchScores(Action> scoresCallback) + protected override Task> FetchScores() { var scores = new List(); @@ -428,9 +427,7 @@ namespace osu.Game.Tests.Visual.Ranking scores.Add(score); } - scoresCallback.Invoke(scores); - - return null; + return Task.FromResult>(scores); } } @@ -446,9 +443,9 @@ namespace osu.Game.Tests.Visual.Ranking this.fetchWaitTask = fetchWaitTask ?? Task.CompletedTask; } - protected override APIRequest FetchScores(Action> scoresCallback) + protected override Task> FetchScores() { - Task.Run(async () => + return Task.Run>(async () => { await fetchWaitTask; @@ -461,12 +458,10 @@ namespace osu.Game.Tests.Visual.Ranking scores.Add(score); } - scoresCallback?.Invoke(scores); - Schedule(() => FetchCompleted = true); - }); - return null; + return scores; + }); } } diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorResultsScreen.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorResultsScreen.cs index c240bbea0c..6e2f90e3b5 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorResultsScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorResultsScreen.cs @@ -1,9 +1,8 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using System; using System.Collections.Generic; -using osu.Game.Online.API; +using System.Threading.Tasks; using osu.Game.Scoring; using osu.Game.Screens.Play; @@ -23,8 +22,8 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate Scheduler.AddDelayed(() => StatisticsPanel.ToggleVisibility(), 1000); } - protected override APIRequest? FetchScores(Action> scoresCallback) => null; + protected override Task> FetchScores() => Task.FromResult>([]); - protected override APIRequest? FetchNextPage(int direction, Action> scoresCallback) => null; + protected override Task> FetchNextPage(int direction) => Task.FromResult>([]); } } diff --git a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistItemResultsScreen.cs b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistItemResultsScreen.cs index 13ef5d6f64..ed90b3b1ae 100644 --- a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistItemResultsScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistItemResultsScreen.cs @@ -5,6 +5,7 @@ using System; using System.Collections.Generic; using System.Diagnostics; using System.Linq; +using System.Threading.Tasks; using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; @@ -76,16 +77,21 @@ namespace osu.Game.Screens.OnlinePlay.Playlists protected abstract APIRequest CreateScoreRequest(); - protected sealed override APIRequest FetchScores(Action> scoresCallback) + protected override async Task> FetchScores() { // This performs two requests: // 1. A request to show the relevant score (and scores around). // 2. If that fails, a request to index the room starting from the highest score. + var requestTaskSource = new TaskCompletionSource(); var userScoreReq = CreateScoreRequest(); + userScoreReq.Success += requestTaskSource.SetResult; + userScoreReq.Failure += requestTaskSource.SetException; + API.Queue(userScoreReq); - userScoreReq.Success += userScore => + try { + var userScore = await requestTaskSource.Task; var allScores = new List { userScore }; // Other scores could have arrived between score submission and entering the results screen. Ensure the local player score position is up to date. @@ -113,88 +119,96 @@ namespace osu.Game.Screens.OnlinePlay.Playlists setPositions(lowerScores, userScore.Position.Value, 1); } - Schedule(() => - { - PerformSuccessCallback(scoresCallback, allScores); - hideLoadingSpinners(); - }); - }; - - // On failure, fallback to a normal index. - userScoreReq.Failure += _ => API.Queue(createIndexRequest(scoresCallback)); - - return userScoreReq; + return TransformScores(allScores); + } + catch (OperationCanceledException) + { + return []; + } + catch + { + return await fetchScoresAround(); + } + finally + { + Schedule(() => hideLoadingSpinners()); + } } - protected override APIRequest? FetchNextPage(int direction, Action> scoresCallback) + protected override async Task> FetchNextPage(int direction) { Debug.Assert(direction == 1 || direction == -1); MultiplayerScores? pivot = direction == -1 ? higherScores : lowerScores; - if (pivot?.Cursor == null) - return null; + return []; - if (pivot == higherScores) - LeftSpinner.Show(); - else - RightSpinner.Show(); + Schedule(() => + { + if (pivot == higherScores) + LeftSpinner.Show(); + else + RightSpinner.Show(); + }); - return createIndexRequest(scoresCallback, pivot); + return await fetchScoresAround(pivot); } /// /// Creates a with an optional score pivot. /// /// Does not queue the request. - /// The callback to perform with the resulting scores. /// An optional score pivot to retrieve scores around. Can be null to retrieve scores from the highest score. - /// The indexing . - private APIRequest createIndexRequest(Action> scoresCallback, MultiplayerScores? pivot = null) + private async Task> fetchScoresAround(MultiplayerScores? pivot = null) { + var requestTaskSource = new TaskCompletionSource(); var indexReq = pivot != null ? new IndexPlaylistScoresRequest(RoomId, PlaylistItem.ID, pivot.Cursor, pivot.Params) : new IndexPlaylistScoresRequest(RoomId, PlaylistItem.ID); + indexReq.Success += requestTaskSource.SetResult; + indexReq.Failure += requestTaskSource.SetException; + API.Queue(indexReq); - indexReq.Success += r => + try { + var index = await requestTaskSource.Task; + if (pivot == lowerScores) { - lowerScores = r; - setPositions(r, pivot, 1); + lowerScores = index; + setPositions(index, pivot, 1); } else { - higherScores = r; - setPositions(r, pivot, -1); + higherScores = index; + setPositions(index, pivot, -1); } - Schedule(() => - { - PerformSuccessCallback(scoresCallback, r.Scores, r); - hideLoadingSpinners(r); - }); - }; - - indexReq.Failure += _ => hideLoadingSpinners(pivot); - - return indexReq; + return TransformScores(index.Scores, index); + } + catch (OperationCanceledException) + { + return []; + } + finally + { + Schedule(() => hideLoadingSpinners(pivot)); + } } /// /// Transforms returned into s, ensure the is put into a sane state, and invokes a given success callback. /// - /// The callback to invoke with the final s. /// The s that were retrieved from s. /// An optional pivot around which the scores were retrieved. - protected virtual ScoreInfo[] PerformSuccessCallback(Action> callback, List scores, MultiplayerScores? pivot = null) + protected virtual ScoreInfo[] TransformScores(List scores, MultiplayerScores? pivot = null) { - var scoreInfos = scores.Select(s => s.CreateScoreInfo(ScoreManager, Rulesets, Beatmap.Value.BeatmapInfo)).OrderByTotalScore().ToArray(); - - // Invoke callback to add the scores. Exclude the score provided to this screen since it's added already. - callback.Invoke(scoreInfos.Where(s => s.OnlineID != Score?.OnlineID)); - - return scoreInfos; + // Exclude the score provided to this screen since it's added already. + return scores + .Where(s => s.ID != Score?.OnlineID) + .Select(s => s.CreateScoreInfo(ScoreManager, Rulesets, Beatmap.Value.BeatmapInfo)) + .OrderByTotalScore() + .ToArray(); } private void hideLoadingSpinners(MultiplayerScores? pivot = null) @@ -213,7 +227,7 @@ namespace osu.Game.Screens.OnlinePlay.Playlists /// The to set positions on. /// The pivot. /// The amount to increment the pivot position by for each in . - private void setPositions(MultiplayerScores scores, MultiplayerScores? pivot, int increment) + private static void setPositions(MultiplayerScores scores, MultiplayerScores? pivot, int increment) => setPositions(scores, pivot?.Scores[^1].Position ?? 0, increment); /// @@ -222,7 +236,7 @@ namespace osu.Game.Screens.OnlinePlay.Playlists /// The to set positions on. /// The pivot position. /// The amount to increment the pivot position by for each in . - private void setPositions(MultiplayerScores scores, int pivotPosition, int increment) + private static void setPositions(MultiplayerScores scores, int pivotPosition, int increment) { foreach (var s in scores.Scores) { diff --git a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistItemScoreResultsScreen.cs b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistItemScoreResultsScreen.cs index 05c03a4b28..c6c10e4d91 100644 --- a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistItemScoreResultsScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistItemScoreResultsScreen.cs @@ -1,7 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using System; using System.Collections.Generic; using System.Linq; using osu.Game.Online.API; @@ -31,9 +30,9 @@ namespace osu.Game.Screens.OnlinePlay.Playlists protected override APIRequest CreateScoreRequest() => new ShowPlaylistScoreRequest(RoomId, PlaylistItem.ID, scoreId); - protected override ScoreInfo[] PerformSuccessCallback(Action> callback, List scores, MultiplayerScores? pivot = null) + protected override ScoreInfo[] TransformScores(List scores, MultiplayerScores? pivot = null) { - var scoreInfos = base.PerformSuccessCallback(callback, scores, pivot); + var scoreInfos = base.TransformScores(scores, pivot); Schedule(() => SelectedScore.Value ??= scoreInfos.SingleOrDefault(s => s.OnlineID == scoreId)); return scoreInfos; } diff --git a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistItemUserBestResultsScreen.cs b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistItemUserBestResultsScreen.cs index 5b20496dba..1a0df0291c 100644 --- a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistItemUserBestResultsScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistItemUserBestResultsScreen.cs @@ -1,7 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using System; using System.Collections.Generic; using System.Linq; using osu.Game.Online.API; @@ -25,9 +24,9 @@ namespace osu.Game.Screens.OnlinePlay.Playlists protected override APIRequest CreateScoreRequest() => new ShowPlaylistUserScoreRequest(RoomId, PlaylistItem.ID, userId); - protected override ScoreInfo[] PerformSuccessCallback(Action> callback, List scores, MultiplayerScores? pivot = null) + protected override ScoreInfo[] TransformScores(List scores, MultiplayerScores? pivot = null) { - var scoreInfos = base.PerformSuccessCallback(callback, scores, pivot); + var scoreInfos = base.TransformScores(scores, pivot); Schedule(() => { diff --git a/osu.Game/Screens/Ranking/ResultsScreen.cs b/osu.Game/Screens/Ranking/ResultsScreen.cs index fe0d805cee..11e90a06b9 100644 --- a/osu.Game/Screens/Ranking/ResultsScreen.cs +++ b/osu.Game/Screens/Ranking/ResultsScreen.cs @@ -5,10 +5,12 @@ using System; using System.Collections.Generic; using System.Diagnostics; using System.Linq; +using System.Threading.Tasks; using osu.Framework.Allocation; using osu.Framework.Audio; using osu.Framework.Audio.Sample; using osu.Framework.Bindables; +using osu.Framework.Extensions; using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; @@ -23,7 +25,6 @@ using osu.Game.Graphics.Containers; using osu.Game.Graphics.UserInterface; using osu.Game.Input.Bindings; using osu.Game.Localisation; -using osu.Game.Online.API; using osu.Game.Online.Placeholders; using osu.Game.Overlays; using osu.Game.Scoring; @@ -60,9 +61,6 @@ namespace osu.Game.Screens.Ranking private bool skipExitTransition; - [Resolved] - private IAPIProvider api { get; set; } = null!; - protected StatisticsPanel StatisticsPanel { get; private set; } = null!; private Drawable bottomPanel = null!; @@ -237,10 +235,7 @@ namespace osu.Game.Screens.Ranking { base.LoadComplete(); - var req = FetchScores(fetchScoresCallback); - - if (req != null) - api.Queue(req); + FetchScores().ContinueWith(t => addScores(t.GetResultSafely())); StatisticsPanel.State.BindValueChanged(onStatisticsStateChanged, true); } @@ -251,18 +246,16 @@ namespace osu.Game.Screens.Ranking if (lastFetchCompleted) { - APIRequest? nextPageRequest = null; + Task> nextPageTask = Task.FromResult>([]); if (ScorePanelList.IsScrolledToStart) - nextPageRequest = FetchNextPage(-1, fetchScoresCallback); + nextPageTask = FetchNextPage(-1); else if (ScorePanelList.IsScrolledToEnd) - nextPageRequest = FetchNextPage(1, fetchScoresCallback); + nextPageTask = FetchNextPage(1); - if (nextPageRequest != null) - { - lastFetchCompleted = false; - api.Queue(nextPageRequest); - } + nextPageTask.ContinueWith(t => addScores(t.GetResultSafely()), TaskContinuationOptions.OnlyOnRanToCompletion); + + lastFetchCompleted = nextPageTask.IsCompletedSuccessfully; } } @@ -329,17 +322,13 @@ namespace osu.Game.Screens.Ranking /// /// Performs a fetch/refresh of scores to be displayed. /// - /// A callback which should be called when fetching is completed. Scheduling is not required. - /// An responsible for the fetch operation. This will be queued and performed automatically. - protected virtual APIRequest? FetchScores(Action> scoresCallback) => null; + protected virtual Task> FetchScores() => Task.FromResult>([]); /// - /// Performs a fetch of the next page of scores. This is invoked every frame until a non-null is returned. + /// Performs a fetch of the next page of scores. This is invoked every frame. /// /// The fetch direction. -1 to fetch scores greater than the current start of the list, and 1 to fetch scores lower than the current end of the list. - /// A callback which should be called when fetching is completed. Scheduling is not required. - /// An responsible for the fetch operation. This will be queued and performed automatically. - protected virtual APIRequest? FetchNextPage(int direction, Action> scoresCallback) => null; + protected virtual Task> FetchNextPage(int direction) => Task.FromResult>([]); /// /// Creates the to be used to display extended information about scores. @@ -351,10 +340,14 @@ namespace osu.Game.Screens.Ranking : new StatisticsPanel(); } - private void fetchScoresCallback(IEnumerable scores) => Schedule(() => + private void addScores(IEnumerable scores) => Schedule(() => { foreach (var s in scores) - addScore(s); + { + var panel = ScorePanelList.AddScore(s); + if (detachedPanel != null) + panel.Alpha = 0; + } // allow a frame for scroll container to adjust its dimensions with the added scores before fetching again. Schedule(() => lastFetchCompleted = true); @@ -409,14 +402,6 @@ namespace osu.Game.Screens.Ranking return false; } - private void addScore(ScoreInfo score) - { - var panel = ScorePanelList.AddScore(score); - - if (detachedPanel != null) - panel.Alpha = 0; - } - private ScorePanel? detachedPanel; private void onStatisticsStateChanged(ValueChangedEvent state) diff --git a/osu.Game/Screens/Ranking/SoloResultsScreen.cs b/osu.Game/Screens/Ranking/SoloResultsScreen.cs index 9f7604aa82..0593d5f91f 100644 --- a/osu.Game/Screens/Ranking/SoloResultsScreen.cs +++ b/osu.Game/Screens/Ranking/SoloResultsScreen.cs @@ -4,11 +4,13 @@ using System; using System.Collections.Generic; using System.Diagnostics; +using System.Threading.Tasks; using osu.Framework.Allocation; using osu.Game.Beatmaps; using osu.Game.Extensions; using osu.Game.Online.API; using osu.Game.Online.API.Requests; +using osu.Game.Online.API.Requests.Responses; using osu.Game.Rulesets; using osu.Game.Scoring; @@ -21,26 +23,36 @@ namespace osu.Game.Screens.Ranking [Resolved] private RulesetStore rulesets { get; set; } = null!; + [Resolved] + private IAPIProvider api { get; set; } = null!; + public SoloResultsScreen(ScoreInfo score) : base(score) { } - protected override APIRequest? FetchScores(Action> scoresCallback) + protected override async Task> FetchScores() { Debug.Assert(Score != null); if (Score.BeatmapInfo!.OnlineID <= 0 || Score.BeatmapInfo.Status <= BeatmapOnlineStatus.Pending) - return null; + return []; + + var requestTaskSource = new TaskCompletionSource(); getScoreRequest = new GetScoresRequest(Score.BeatmapInfo, Score.Ruleset); - getScoreRequest.Success += r => + getScoreRequest.Success += requestTaskSource.SetResult; + getScoreRequest.Failure += requestTaskSource.SetException; + api.Queue(getScoreRequest); + + try { + var scores = await requestTaskSource.Task; var toDisplay = new List(); - for (int i = 0; i < r.Scores.Count; ++i) + for (int i = 0; i < scores.Scores.Count; ++i) { - var score = r.Scores[i]; + var score = scores.Scores[i]; int position = i + 1; if (score.MatchesOnlineID(Score)) @@ -58,9 +70,12 @@ namespace osu.Game.Screens.Ranking } } - scoresCallback.Invoke(toDisplay); - }; - return getScoreRequest; + return toDisplay; + } + catch (OperationCanceledException) + { + return []; + } } protected override void Dispose(bool isDisposing) From dfae11101f8b968611a442691b794066a52538c7 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Tue, 25 Feb 2025 22:37:12 +0900 Subject: [PATCH 1118/1275] Populate playlists results screen with online beatmaps --- osu.Game/Online/Rooms/MultiplayerScore.cs | 3 +++ .../Playlists/PlaylistItemResultsScreen.cs | 26 ++++++++++++++++--- .../PlaylistItemScoreResultsScreen.cs | 5 ++-- .../PlaylistItemUserBestResultsScreen.cs | 5 ++-- 4 files changed, 31 insertions(+), 8 deletions(-) diff --git a/osu.Game/Online/Rooms/MultiplayerScore.cs b/osu.Game/Online/Rooms/MultiplayerScore.cs index 2adee26da3..74eaea8dbc 100644 --- a/osu.Game/Online/Rooms/MultiplayerScore.cs +++ b/osu.Game/Online/Rooms/MultiplayerScore.cs @@ -80,6 +80,9 @@ namespace osu.Game.Online.Rooms [JsonProperty("ruleset_id")] public int RulesetId { get; set; } + [JsonProperty("beatmap_id")] + public int BeatmapId { get; set; } + public ScoreInfo CreateScoreInfo(ScoreManager scoreManager, RulesetStore rulesets, [NotNull] BeatmapInfo beatmap) { var ruleset = rulesets.GetRuleset(RulesetId); diff --git a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistItemResultsScreen.cs b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistItemResultsScreen.cs index ed90b3b1ae..bba30ec312 100644 --- a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistItemResultsScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistItemResultsScreen.cs @@ -9,8 +9,11 @@ using System.Threading.Tasks; using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; +using osu.Game.Beatmaps; +using osu.Game.Database; using osu.Game.Graphics.UserInterface; using osu.Game.Online.API; +using osu.Game.Online.API.Requests.Responses; using osu.Game.Online.Rooms; using osu.Game.Rulesets; using osu.Game.Scoring; @@ -39,6 +42,9 @@ namespace osu.Game.Screens.OnlinePlay.Playlists [Resolved] protected RulesetStore Rulesets { get; private set; } = null!; + [Resolved] + private BeatmapLookupCache beatmapLookupCache { get; set; } = null!; + protected PlaylistItemResultsScreen(ScoreInfo? score, long roomId, PlaylistItem playlistItem) : base(score) { @@ -119,7 +125,7 @@ namespace osu.Game.Screens.OnlinePlay.Playlists setPositions(lowerScores, userScore.Position.Value, 1); } - return TransformScores(allScores); + return await TransformScores(allScores); } catch (OperationCanceledException) { @@ -184,7 +190,7 @@ namespace osu.Game.Screens.OnlinePlay.Playlists setPositions(index, pivot, -1); } - return TransformScores(index.Scores, index); + return await TransformScores(index.Scores, index); } catch (OperationCanceledException) { @@ -201,12 +207,24 @@ namespace osu.Game.Screens.OnlinePlay.Playlists /// /// The s that were retrieved from s. /// An optional pivot around which the scores were retrieved. - protected virtual ScoreInfo[] TransformScores(List scores, MultiplayerScores? pivot = null) + protected virtual async Task TransformScores(List scores, MultiplayerScores? pivot = null) { + APIBeatmap?[] beatmaps = await beatmapLookupCache.GetBeatmapsAsync(scores.Select(s => s.BeatmapId).Distinct().ToArray()); + + // Minimal data required to get various components in this screen to display correctly. + Dictionary beatmapsById = beatmaps.Where(b => b != null).ToDictionary(b => b!.OnlineID, b => new BeatmapInfo + { + Difficulty = new BeatmapDifficulty(b!.Difficulty), + DifficultyName = b.DifficultyName, + StarRating = b.StarRating, + Length = b.Length, + BPM = b.BPM + }); + // Exclude the score provided to this screen since it's added already. return scores .Where(s => s.ID != Score?.OnlineID) - .Select(s => s.CreateScoreInfo(ScoreManager, Rulesets, Beatmap.Value.BeatmapInfo)) + .Select(s => s.CreateScoreInfo(ScoreManager, Rulesets, beatmapsById.GetValueOrDefault(s.BeatmapId) ?? Beatmap.Value.BeatmapInfo)) .OrderByTotalScore() .ToArray(); } diff --git a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistItemScoreResultsScreen.cs b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistItemScoreResultsScreen.cs index c6c10e4d91..f74b30c3f7 100644 --- a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistItemScoreResultsScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistItemScoreResultsScreen.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Linq; +using System.Threading.Tasks; using osu.Game.Online.API; using osu.Game.Online.Rooms; using osu.Game.Scoring; @@ -30,9 +31,9 @@ namespace osu.Game.Screens.OnlinePlay.Playlists protected override APIRequest CreateScoreRequest() => new ShowPlaylistScoreRequest(RoomId, PlaylistItem.ID, scoreId); - protected override ScoreInfo[] TransformScores(List scores, MultiplayerScores? pivot = null) + protected override async Task TransformScores(List scores, MultiplayerScores? pivot = null) { - var scoreInfos = base.TransformScores(scores, pivot); + var scoreInfos = await base.TransformScores(scores, pivot); Schedule(() => SelectedScore.Value ??= scoreInfos.SingleOrDefault(s => s.OnlineID == scoreId)); return scoreInfos; } diff --git a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistItemUserBestResultsScreen.cs b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistItemUserBestResultsScreen.cs index 1a0df0291c..2e763666a7 100644 --- a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistItemUserBestResultsScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistItemUserBestResultsScreen.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Linq; +using System.Threading.Tasks; using osu.Game.Online.API; using osu.Game.Online.Rooms; using osu.Game.Scoring; @@ -24,9 +25,9 @@ namespace osu.Game.Screens.OnlinePlay.Playlists protected override APIRequest CreateScoreRequest() => new ShowPlaylistUserScoreRequest(RoomId, PlaylistItem.ID, userId); - protected override ScoreInfo[] TransformScores(List scores, MultiplayerScores? pivot = null) + protected override async Task TransformScores(List scores, MultiplayerScores? pivot = null) { - var scoreInfos = base.TransformScores(scores, pivot); + var scoreInfos = await base.TransformScores(scores, pivot); Schedule(() => { From 8a27b6689edf50cace897a3009640ff1ba8b2e7e Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Tue, 25 Feb 2025 22:51:36 +0900 Subject: [PATCH 1119/1275] Replace virtual async method with better abstraction --- .../Playlists/PlaylistItemResultsScreen.cs | 9 ++++----- .../Playlists/PlaylistItemScoreResultsScreen.cs | 8 +++----- .../Playlists/PlaylistItemUserBestResultsScreen.cs | 14 ++++---------- osu.Game/Screens/Ranking/ResultsScreen.cs | 10 ++++++++++ 4 files changed, 21 insertions(+), 20 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistItemResultsScreen.cs b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistItemResultsScreen.cs index bba30ec312..e9ba3bdb70 100644 --- a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistItemResultsScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistItemResultsScreen.cs @@ -125,7 +125,7 @@ namespace osu.Game.Screens.OnlinePlay.Playlists setPositions(lowerScores, userScore.Position.Value, 1); } - return await TransformScores(allScores); + return await transformScores(allScores); } catch (OperationCanceledException) { @@ -190,7 +190,7 @@ namespace osu.Game.Screens.OnlinePlay.Playlists setPositions(index, pivot, -1); } - return await TransformScores(index.Scores, index); + return await transformScores(index.Scores); } catch (OperationCanceledException) { @@ -203,11 +203,10 @@ namespace osu.Game.Screens.OnlinePlay.Playlists } /// - /// Transforms returned into s, ensure the is put into a sane state, and invokes a given success callback. + /// Transforms returned into s. /// /// The s that were retrieved from s. - /// An optional pivot around which the scores were retrieved. - protected virtual async Task TransformScores(List scores, MultiplayerScores? pivot = null) + private async Task transformScores(List scores) { APIBeatmap?[] beatmaps = await beatmapLookupCache.GetBeatmapsAsync(scores.Select(s => s.BeatmapId).Distinct().ToArray()); diff --git a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistItemScoreResultsScreen.cs b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistItemScoreResultsScreen.cs index f74b30c3f7..7f386cd293 100644 --- a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistItemScoreResultsScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistItemScoreResultsScreen.cs @@ -3,7 +3,6 @@ using System.Collections.Generic; using System.Linq; -using System.Threading.Tasks; using osu.Game.Online.API; using osu.Game.Online.Rooms; using osu.Game.Scoring; @@ -31,11 +30,10 @@ namespace osu.Game.Screens.OnlinePlay.Playlists protected override APIRequest CreateScoreRequest() => new ShowPlaylistScoreRequest(RoomId, PlaylistItem.ID, scoreId); - protected override async Task TransformScores(List scores, MultiplayerScores? pivot = null) + protected override void OnScoresAdded(IEnumerable scores) { - var scoreInfos = await base.TransformScores(scores, pivot); - Schedule(() => SelectedScore.Value ??= scoreInfos.SingleOrDefault(s => s.OnlineID == scoreId)); - return scoreInfos; + base.OnScoresAdded(scores); + SelectedScore.Value ??= scores.SingleOrDefault(s => s.OnlineID == scoreId); } } } diff --git a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistItemUserBestResultsScreen.cs b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistItemUserBestResultsScreen.cs index 2e763666a7..faeef93b71 100644 --- a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistItemUserBestResultsScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistItemUserBestResultsScreen.cs @@ -3,7 +3,6 @@ using System.Collections.Generic; using System.Linq; -using System.Threading.Tasks; using osu.Game.Online.API; using osu.Game.Online.Rooms; using osu.Game.Scoring; @@ -25,17 +24,12 @@ namespace osu.Game.Screens.OnlinePlay.Playlists protected override APIRequest CreateScoreRequest() => new ShowPlaylistUserScoreRequest(RoomId, PlaylistItem.ID, userId); - protected override async Task TransformScores(List scores, MultiplayerScores? pivot = null) + protected override void OnScoresAdded(IEnumerable scores) { - var scoreInfos = await base.TransformScores(scores, pivot); + base.OnScoresAdded(scores); - Schedule(() => - { - // Prefer selecting the local user's score, or otherwise default to the first visible score. - SelectedScore.Value ??= scoreInfos.FirstOrDefault(s => s.UserID == userId) ?? scoreInfos.FirstOrDefault(); - }); - - return scoreInfos; + // Prefer selecting the local user's score, or otherwise default to the first visible score. + SelectedScore.Value ??= scores.FirstOrDefault(s => s.UserID == userId) ?? scores.FirstOrDefault(); } } } diff --git a/osu.Game/Screens/Ranking/ResultsScreen.cs b/osu.Game/Screens/Ranking/ResultsScreen.cs index 11e90a06b9..ce86ac0815 100644 --- a/osu.Game/Screens/Ranking/ResultsScreen.cs +++ b/osu.Game/Screens/Ranking/ResultsScreen.cs @@ -357,8 +357,18 @@ namespace osu.Game.Screens.Ranking // This can happen if for example a beatmap that is part of a playlist hasn't been played yet. VerticalScrollContent.Add(new MessagePlaceholder(LeaderboardStrings.NoRecordsYet)); } + + OnScoresAdded(scores); }); + /// + /// Invoked after online scores are fetched and added to the list. + /// + /// The scores that were added. + protected virtual void OnScoresAdded(IEnumerable scores) + { + } + public override void OnEntering(ScreenTransitionEvent e) { base.OnEntering(e); From 3b5bf391da57e4ed3efcfd60f6e6fd3724f35b6d Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Tue, 25 Feb 2025 22:55:55 +0900 Subject: [PATCH 1120/1275] Arrays instead of enumerables --- .../Visual/Ranking/TestSceneResultsScreen.cs | 21 +++++++++---------- .../Spectate/MultiSpectatorResultsScreen.cs | 5 ++--- .../Playlists/PlaylistItemResultsScreen.cs | 6 +++--- .../PlaylistItemScoreResultsScreen.cs | 3 +-- .../PlaylistItemUserBestResultsScreen.cs | 3 +-- osu.Game/Screens/Ranking/ResultsScreen.cs | 10 ++++----- osu.Game/Screens/Ranking/SoloResultsScreen.cs | 4 ++-- 7 files changed, 24 insertions(+), 28 deletions(-) diff --git a/osu.Game.Tests/Visual/Ranking/TestSceneResultsScreen.cs b/osu.Game.Tests/Visual/Ranking/TestSceneResultsScreen.cs index 4acbdb4a76..b19288fd99 100644 --- a/osu.Game.Tests/Visual/Ranking/TestSceneResultsScreen.cs +++ b/osu.Game.Tests/Visual/Ranking/TestSceneResultsScreen.cs @@ -4,7 +4,6 @@ #nullable disable using System; -using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using NUnit.Framework; @@ -415,19 +414,19 @@ namespace osu.Game.Tests.Visual.Ranking RetryOverlay = InternalChildren.OfType().SingleOrDefault(); } - protected override Task> FetchScores() + protected override Task FetchScores() { - var scores = new List(); + var scores = new ScoreInfo[20]; - for (int i = 0; i < 20; i++) + for (int i = 0; i < scores.Length; i++) { var score = TestResources.CreateTestScoreInfo(); score.TotalScore += 10 - i; score.HasOnlineReplay = true; - scores.Add(score); + scores[i] = score; } - return Task.FromResult>(scores); + return Task.FromResult(scores); } } @@ -443,19 +442,19 @@ namespace osu.Game.Tests.Visual.Ranking this.fetchWaitTask = fetchWaitTask ?? Task.CompletedTask; } - protected override Task> FetchScores() + protected override Task FetchScores() { - return Task.Run>(async () => + return Task.Run(async () => { await fetchWaitTask; - var scores = new List(); + var scores = new ScoreInfo[20]; - for (int i = 0; i < 20; i++) + for (int i = 0; i < scores.Length; i++) { var score = TestResources.CreateTestScoreInfo(); score.TotalScore += 10 - i; - scores.Add(score); + scores[i] = score; } Schedule(() => FetchCompleted = true); diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorResultsScreen.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorResultsScreen.cs index 6e2f90e3b5..3cf1661c8d 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorResultsScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorResultsScreen.cs @@ -1,7 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using System.Collections.Generic; using System.Threading.Tasks; using osu.Game.Scoring; using osu.Game.Screens.Play; @@ -22,8 +21,8 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate Scheduler.AddDelayed(() => StatisticsPanel.ToggleVisibility(), 1000); } - protected override Task> FetchScores() => Task.FromResult>([]); + protected override Task FetchScores() => Task.FromResult([]); - protected override Task> FetchNextPage(int direction) => Task.FromResult>([]); + protected override Task FetchNextPage(int direction) => Task.FromResult([]); } } diff --git a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistItemResultsScreen.cs b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistItemResultsScreen.cs index e9ba3bdb70..0063bcd5f5 100644 --- a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistItemResultsScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistItemResultsScreen.cs @@ -83,7 +83,7 @@ namespace osu.Game.Screens.OnlinePlay.Playlists protected abstract APIRequest CreateScoreRequest(); - protected override async Task> FetchScores() + protected override async Task FetchScores() { // This performs two requests: // 1. A request to show the relevant score (and scores around). @@ -141,7 +141,7 @@ namespace osu.Game.Screens.OnlinePlay.Playlists } } - protected override async Task> FetchNextPage(int direction) + protected override async Task FetchNextPage(int direction) { Debug.Assert(direction == 1 || direction == -1); @@ -165,7 +165,7 @@ namespace osu.Game.Screens.OnlinePlay.Playlists /// /// Does not queue the request. /// An optional score pivot to retrieve scores around. Can be null to retrieve scores from the highest score. - private async Task> fetchScoresAround(MultiplayerScores? pivot = null) + private async Task fetchScoresAround(MultiplayerScores? pivot = null) { var requestTaskSource = new TaskCompletionSource(); var indexReq = pivot != null diff --git a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistItemScoreResultsScreen.cs b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistItemScoreResultsScreen.cs index 7f386cd293..74b12b6d3c 100644 --- a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistItemScoreResultsScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistItemScoreResultsScreen.cs @@ -1,7 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using System.Collections.Generic; using System.Linq; using osu.Game.Online.API; using osu.Game.Online.Rooms; @@ -30,7 +29,7 @@ namespace osu.Game.Screens.OnlinePlay.Playlists protected override APIRequest CreateScoreRequest() => new ShowPlaylistScoreRequest(RoomId, PlaylistItem.ID, scoreId); - protected override void OnScoresAdded(IEnumerable scores) + protected override void OnScoresAdded(ScoreInfo[] scores) { base.OnScoresAdded(scores); SelectedScore.Value ??= scores.SingleOrDefault(s => s.OnlineID == scoreId); diff --git a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistItemUserBestResultsScreen.cs b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistItemUserBestResultsScreen.cs index faeef93b71..866b094178 100644 --- a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistItemUserBestResultsScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistItemUserBestResultsScreen.cs @@ -1,7 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using System.Collections.Generic; using System.Linq; using osu.Game.Online.API; using osu.Game.Online.Rooms; @@ -24,7 +23,7 @@ namespace osu.Game.Screens.OnlinePlay.Playlists protected override APIRequest CreateScoreRequest() => new ShowPlaylistUserScoreRequest(RoomId, PlaylistItem.ID, userId); - protected override void OnScoresAdded(IEnumerable scores) + protected override void OnScoresAdded(ScoreInfo[] scores) { base.OnScoresAdded(scores); diff --git a/osu.Game/Screens/Ranking/ResultsScreen.cs b/osu.Game/Screens/Ranking/ResultsScreen.cs index ce86ac0815..cfee2aa77d 100644 --- a/osu.Game/Screens/Ranking/ResultsScreen.cs +++ b/osu.Game/Screens/Ranking/ResultsScreen.cs @@ -246,7 +246,7 @@ namespace osu.Game.Screens.Ranking if (lastFetchCompleted) { - Task> nextPageTask = Task.FromResult>([]); + Task nextPageTask = Task.FromResult([]); if (ScorePanelList.IsScrolledToStart) nextPageTask = FetchNextPage(-1); @@ -322,13 +322,13 @@ namespace osu.Game.Screens.Ranking /// /// Performs a fetch/refresh of scores to be displayed. /// - protected virtual Task> FetchScores() => Task.FromResult>([]); + protected virtual Task FetchScores() => Task.FromResult([]); /// /// Performs a fetch of the next page of scores. This is invoked every frame. /// /// The fetch direction. -1 to fetch scores greater than the current start of the list, and 1 to fetch scores lower than the current end of the list. - protected virtual Task> FetchNextPage(int direction) => Task.FromResult>([]); + protected virtual Task FetchNextPage(int direction) => Task.FromResult([]); /// /// Creates the to be used to display extended information about scores. @@ -340,7 +340,7 @@ namespace osu.Game.Screens.Ranking : new StatisticsPanel(); } - private void addScores(IEnumerable scores) => Schedule(() => + private void addScores(ScoreInfo[] scores) => Schedule(() => { foreach (var s in scores) { @@ -365,7 +365,7 @@ namespace osu.Game.Screens.Ranking /// Invoked after online scores are fetched and added to the list. /// /// The scores that were added. - protected virtual void OnScoresAdded(IEnumerable scores) + protected virtual void OnScoresAdded(ScoreInfo[] scores) { } diff --git a/osu.Game/Screens/Ranking/SoloResultsScreen.cs b/osu.Game/Screens/Ranking/SoloResultsScreen.cs index 0593d5f91f..9fdffce644 100644 --- a/osu.Game/Screens/Ranking/SoloResultsScreen.cs +++ b/osu.Game/Screens/Ranking/SoloResultsScreen.cs @@ -31,7 +31,7 @@ namespace osu.Game.Screens.Ranking { } - protected override async Task> FetchScores() + protected override async Task FetchScores() { Debug.Assert(Score != null); @@ -70,7 +70,7 @@ namespace osu.Game.Screens.Ranking } } - return toDisplay; + return toDisplay.ToArray(); } catch (OperationCanceledException) { From 116b5a335a658023e3b58d3ec5caedd78230a3d4 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Tue, 25 Feb 2025 22:56:38 +0900 Subject: [PATCH 1121/1275] `ConfigureAwait(false)` everywhere --- .../Playlists/PlaylistItemResultsScreen.cs | 14 +++++++------- osu.Game/Screens/Ranking/SoloResultsScreen.cs | 2 +- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistItemResultsScreen.cs b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistItemResultsScreen.cs index 0063bcd5f5..975cff0b68 100644 --- a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistItemResultsScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistItemResultsScreen.cs @@ -97,7 +97,7 @@ namespace osu.Game.Screens.OnlinePlay.Playlists try { - var userScore = await requestTaskSource.Task; + var userScore = await requestTaskSource.Task.ConfigureAwait(false); var allScores = new List { userScore }; // Other scores could have arrived between score submission and entering the results screen. Ensure the local player score position is up to date. @@ -125,7 +125,7 @@ namespace osu.Game.Screens.OnlinePlay.Playlists setPositions(lowerScores, userScore.Position.Value, 1); } - return await transformScores(allScores); + return await transformScores(allScores).ConfigureAwait(false); } catch (OperationCanceledException) { @@ -133,7 +133,7 @@ namespace osu.Game.Screens.OnlinePlay.Playlists } catch { - return await fetchScoresAround(); + return await fetchScoresAround().ConfigureAwait(false); } finally { @@ -157,7 +157,7 @@ namespace osu.Game.Screens.OnlinePlay.Playlists RightSpinner.Show(); }); - return await fetchScoresAround(pivot); + return await fetchScoresAround(pivot).ConfigureAwait(false); } /// @@ -177,7 +177,7 @@ namespace osu.Game.Screens.OnlinePlay.Playlists try { - var index = await requestTaskSource.Task; + var index = await requestTaskSource.Task.ConfigureAwait(false); if (pivot == lowerScores) { @@ -190,7 +190,7 @@ namespace osu.Game.Screens.OnlinePlay.Playlists setPositions(index, pivot, -1); } - return await transformScores(index.Scores); + return await transformScores(index.Scores).ConfigureAwait(false); } catch (OperationCanceledException) { @@ -208,7 +208,7 @@ namespace osu.Game.Screens.OnlinePlay.Playlists /// The s that were retrieved from s. private async Task transformScores(List scores) { - APIBeatmap?[] beatmaps = await beatmapLookupCache.GetBeatmapsAsync(scores.Select(s => s.BeatmapId).Distinct().ToArray()); + APIBeatmap?[] beatmaps = await beatmapLookupCache.GetBeatmapsAsync(scores.Select(s => s.BeatmapId).Distinct().ToArray()).ConfigureAwait(false); // Minimal data required to get various components in this screen to display correctly. Dictionary beatmapsById = beatmaps.Where(b => b != null).ToDictionary(b => b!.OnlineID, b => new BeatmapInfo diff --git a/osu.Game/Screens/Ranking/SoloResultsScreen.cs b/osu.Game/Screens/Ranking/SoloResultsScreen.cs index 9fdffce644..73bed3383b 100644 --- a/osu.Game/Screens/Ranking/SoloResultsScreen.cs +++ b/osu.Game/Screens/Ranking/SoloResultsScreen.cs @@ -47,7 +47,7 @@ namespace osu.Game.Screens.Ranking try { - var scores = await requestTaskSource.Task; + var scores = await requestTaskSource.Task.ConfigureAwait(false); var toDisplay = new List(); for (int i = 0; i < scores.Scores.Count; ++i) From bb457ca8e2fa2283d159fd214f6854046f38cebb Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Tue, 25 Feb 2025 23:17:02 +0900 Subject: [PATCH 1122/1275] Clean up completion handling --- osu.Game/Screens/Ranking/ResultsScreen.cs | 51 +++++++++++++---------- 1 file changed, 28 insertions(+), 23 deletions(-) diff --git a/osu.Game/Screens/Ranking/ResultsScreen.cs b/osu.Game/Screens/Ranking/ResultsScreen.cs index cfee2aa77d..397ad9c0b1 100644 --- a/osu.Game/Screens/Ranking/ResultsScreen.cs +++ b/osu.Game/Screens/Ranking/ResultsScreen.cs @@ -10,7 +10,6 @@ using osu.Framework.Allocation; using osu.Framework.Audio; using osu.Framework.Audio.Sample; using osu.Framework.Bindables; -using osu.Framework.Extensions; using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; @@ -66,7 +65,7 @@ namespace osu.Game.Screens.Ranking private Drawable bottomPanel = null!; private Container detachedPanelContainer = null!; - private bool lastFetchCompleted; + private Task lastFetchTask = Task.CompletedTask; /// /// Whether the user can retry the beatmap from the results screen. @@ -235,7 +234,7 @@ namespace osu.Game.Screens.Ranking { base.LoadComplete(); - FetchScores().ContinueWith(t => addScores(t.GetResultSafely())); + lastFetchTask = Task.Run(async () => await addScores(await FetchScores().ConfigureAwait(false)).ConfigureAwait(false)); StatisticsPanel.State.BindValueChanged(onStatisticsStateChanged, true); } @@ -244,18 +243,17 @@ namespace osu.Game.Screens.Ranking { base.Update(); - if (lastFetchCompleted) + if (lastFetchTask.IsCompleted) { - Task nextPageTask = Task.FromResult([]); + Task? nextPageTask = null; if (ScorePanelList.IsScrolledToStart) nextPageTask = FetchNextPage(-1); else if (ScorePanelList.IsScrolledToEnd) nextPageTask = FetchNextPage(1); - nextPageTask.ContinueWith(t => addScores(t.GetResultSafely()), TaskContinuationOptions.OnlyOnRanToCompletion); - - lastFetchCompleted = nextPageTask.IsCompletedSuccessfully; + if (nextPageTask != null) + lastFetchTask = Task.Run(async () => await addScores(await nextPageTask).ConfigureAwait(false)); } } @@ -340,26 +338,33 @@ namespace osu.Game.Screens.Ranking : new StatisticsPanel(); } - private void addScores(ScoreInfo[] scores) => Schedule(() => + private Task addScores(ScoreInfo[] scores) { - foreach (var s in scores) + var tcs = new TaskCompletionSource(); + + Schedule(() => { - var panel = ScorePanelList.AddScore(s); - if (detachedPanel != null) - panel.Alpha = 0; - } + foreach (var s in scores) + { + var panel = ScorePanelList.AddScore(s); + if (detachedPanel != null) + panel.Alpha = 0; + } - // allow a frame for scroll container to adjust its dimensions with the added scores before fetching again. - Schedule(() => lastFetchCompleted = true); + // allow a frame for scroll container to adjust its dimensions with the added scores before fetching again. + Schedule(() => tcs.SetResult()); - if (ScorePanelList.IsEmpty) - { - // This can happen if for example a beatmap that is part of a playlist hasn't been played yet. - VerticalScrollContent.Add(new MessagePlaceholder(LeaderboardStrings.NoRecordsYet)); - } + if (ScorePanelList.IsEmpty) + { + // This can happen if for example a beatmap that is part of a playlist hasn't been played yet. + VerticalScrollContent.Add(new MessagePlaceholder(LeaderboardStrings.NoRecordsYet)); + } - OnScoresAdded(scores); - }); + OnScoresAdded(scores); + }); + + return tcs.Task; + } /// /// Invoked after online scores are fetched and added to the list. From baf20d84843071e7c1c26418da36d1f4ff5c5a21 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Tue, 25 Feb 2025 23:17:23 +0900 Subject: [PATCH 1123/1275] Fix loading spinners not hiding correctly --- .../Playlists/PlaylistItemResultsScreen.cs | 19 +++++-------------- 1 file changed, 5 insertions(+), 14 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistItemResultsScreen.cs b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistItemResultsScreen.cs index 975cff0b68..f08b1818ab 100644 --- a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistItemResultsScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistItemResultsScreen.cs @@ -135,10 +135,6 @@ namespace osu.Game.Screens.OnlinePlay.Playlists { return await fetchScoresAround().ConfigureAwait(false); } - finally - { - Schedule(() => hideLoadingSpinners()); - } } protected override async Task FetchNextPage(int direction) @@ -196,10 +192,6 @@ namespace osu.Game.Screens.OnlinePlay.Playlists { return []; } - finally - { - Schedule(() => hideLoadingSpinners(pivot)); - } } /// @@ -228,14 +220,13 @@ namespace osu.Game.Screens.OnlinePlay.Playlists .ToArray(); } - private void hideLoadingSpinners(MultiplayerScores? pivot = null) + protected override void OnScoresAdded(ScoreInfo[] scores) { - CentreSpinner.Hide(); + base.OnScoresAdded(scores); - if (pivot == lowerScores) - RightSpinner.Hide(); - else if (pivot == higherScores) - LeftSpinner.Hide(); + CentreSpinner.Hide(); + RightSpinner.Hide(); + LeftSpinner.Hide(); } /// From 65a62d5440b57440b61578981691fd7bb6f2fb70 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 25 Feb 2025 15:38:48 +0100 Subject: [PATCH 1124/1275] Attempt to preserve sample control point bank when encoding beatmap This was reported internally in https://discord.com/channels/90072389919997952/1259818301517725707/1343470899357024286. The issue described was that sample specifications on control points in stable disappeared after the beatmap was updated from lazer. The reason why the sample specifications were getting dropped is that they got lost in the logic that attempts to translate per-hitobject samples that lazer has back into stable "green line" type control points. That process only attempted to preserve volume and custom sample bank, but did not keep the standard bank - likely because it's kind of superfluous information *for correct sample playback of the objects*, as the samples get encoded again for each object individually. However dropping this information makes for a subpar editing experience. The choice of which sample to pick the bank from is sort of arbitrary and I'm not sure if there's a correct one to pick. Intuitively picking the normal sample's bank (if there is one) seems most correct. --- osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs b/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs index 07e88ab956..d80d7e6b09 100644 --- a/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs +++ b/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs @@ -319,11 +319,13 @@ namespace osu.Game.Beatmaps.Formats SampleControlPoint createSampleControlPointFor(double time, IList samples) { int volume = samples.Max(o => o.Volume); + string bank = samples.Where(s => s.Name == HitSampleInfo.HIT_NORMAL).Select(s => s.Bank).FirstOrDefault() + ?? samples.Select(s => s.Bank).First(); int customIndex = samples.Any(o => o is ConvertHitObjectParser.LegacyHitSampleInfo) ? samples.OfType().Max(o => o.CustomSampleBank) : -1; - return new LegacyBeatmapDecoder.LegacySampleControlPoint { Time = time, SampleVolume = volume, CustomSampleBank = customIndex }; + return new LegacyBeatmapDecoder.LegacySampleControlPoint { Time = time, SampleVolume = volume, SampleBank = bank, CustomSampleBank = customIndex }; } } From 90290997a7b754a2506a4c10a8cc28cb3a0e33bd Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Wed, 26 Feb 2025 14:46:37 +0900 Subject: [PATCH 1125/1275] Fix score panel difficulty depending on local beatmap This is a very special case where online beatmap/ruleset models are being ferried via `ScoreInfo` in what appear to `BeatmapDifficultyCache` as local `BeatmapInfo`/`RulesetInfo` models. Here, BDC will incorrectly attempt to proceed with calculating true difficulty where it cannot, and return 0. This is fixed locally because `ScoreInfo` is a very weird model, and I'm not sure whether BDC should contain logic to work around this. --- .../Expanded/ExpandedPanelMiddleContent.cs | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/osu.Game/Screens/Ranking/Expanded/ExpandedPanelMiddleContent.cs b/osu.Game/Screens/Ranking/Expanded/ExpandedPanelMiddleContent.cs index 4bc559694a..9bef6a3f3a 100644 --- a/osu.Game/Screens/Ranking/Expanded/ExpandedPanelMiddleContent.cs +++ b/osu.Game/Screens/Ranking/Expanded/ExpandedPanelMiddleContent.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System; using System.Collections.Generic; using System.Linq; @@ -16,6 +14,7 @@ using osu.Framework.Localisation; using osu.Game.Beatmaps; using osu.Game.Beatmaps.Drawables; using osu.Game.Configuration; +using osu.Game.Database; using osu.Game.Graphics; using osu.Game.Graphics.Containers; using osu.Game.Graphics.Sprites; @@ -41,10 +40,10 @@ namespace osu.Game.Screens.Ranking.Expanded private readonly List statisticDisplays = new List(); - private RollingCounter scoreCounter; + private RollingCounter scoreCounter = null!; [Resolved] - private ScoreManager scoreManager { get; set; } + private ScoreManager scoreManager { get; set; } = null!; /// /// Creates a new . @@ -63,12 +62,19 @@ namespace osu.Game.Screens.Ranking.Expanded } [BackgroundDependencyLoader] - private void load(BeatmapDifficultyCache beatmapDifficultyCache) + private void load(RealmAccess realmAccess, BeatmapDifficultyCache beatmapDifficultyCache) { var beatmap = score.BeatmapInfo!; var metadata = beatmap.BeatmapSet?.Metadata ?? beatmap.Metadata; string creator = metadata.Author.Username; + StarDifficulty starDifficulty = new StarDifficulty(beatmap.StarRating, 0); + + // In some cases, the beatmap ferried through ScoreInfo actually represents an online beatmap. + // If it isn't, we may be able to compute a more accuracy difficulty from the ruleset and mods. + if (realmAccess.Run(r => r.Find(score.BeatmapInfo!.ID)) != null) + starDifficulty = beatmapDifficultyCache.GetDifficultyAsync(score.BeatmapInfo!, score.Ruleset, score.Mods).GetResultSafely() ?? starDifficulty; + var topStatistics = new List { new AccuracyStatistic(score.Accuracy), @@ -146,7 +152,7 @@ namespace osu.Game.Screens.Ranking.Expanded Spacing = new Vector2(5, 0), Children = new Drawable[] { - new StarRatingDisplay(beatmapDifficultyCache.GetDifficultyAsync(beatmap, score.Ruleset, score.Mods).GetResultSafely() ?? default) + new StarRatingDisplay(starDifficulty) { Anchor = Anchor.CentreLeft, Origin = Anchor.CentreLeft From 59cfcb3595aa79ea4384bca9af4472b48ace3917 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Wed, 26 Feb 2025 14:49:38 +0900 Subject: [PATCH 1126/1275] Prefer local models where available --- .../Playlists/PlaylistItemResultsScreen.cs | 52 +++++++++++++++---- 1 file changed, 42 insertions(+), 10 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistItemResultsScreen.cs b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistItemResultsScreen.cs index f08b1818ab..1be0a7cf81 100644 --- a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistItemResultsScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistItemResultsScreen.cs @@ -45,6 +45,9 @@ namespace osu.Game.Screens.OnlinePlay.Playlists [Resolved] private BeatmapLookupCache beatmapLookupCache { get; set; } = null!; + [Resolved] + private BeatmapManager beatmapManager { get; set; } = null!; + protected PlaylistItemResultsScreen(ScoreInfo? score, long roomId, PlaylistItem playlistItem) : base(score) { @@ -200,22 +203,43 @@ namespace osu.Game.Screens.OnlinePlay.Playlists /// The s that were retrieved from s. private async Task transformScores(List scores) { - APIBeatmap?[] beatmaps = await beatmapLookupCache.GetBeatmapsAsync(scores.Select(s => s.BeatmapId).Distinct().ToArray()).ConfigureAwait(false); + int[] allBeatmapIds = scores.Select(s => s.BeatmapId).Distinct().ToArray(); + BeatmapInfo[] localBeatmaps = allBeatmapIds.Select(id => beatmapManager.QueryBeatmap(b => b.OnlineID == id)) + .Where(b => b != null) + .ToArray()!; - // Minimal data required to get various components in this screen to display correctly. - Dictionary beatmapsById = beatmaps.Where(b => b != null).ToDictionary(b => b!.OnlineID, b => new BeatmapInfo + int[] missingBeatmapIds = allBeatmapIds.Except(localBeatmaps.Select(b => b.OnlineID)).ToArray(); + APIBeatmap[] onlineBeatmaps = (await beatmapLookupCache.GetBeatmapsAsync(missingBeatmapIds).ConfigureAwait(false)).Where(b => b != null).ToArray()!; + + Dictionary beatmapsById = new Dictionary(); + + foreach (var beatmap in localBeatmaps) + beatmapsById[beatmap.OnlineID] = beatmap; + + foreach (var beatmap in onlineBeatmaps) { - Difficulty = new BeatmapDifficulty(b!.Difficulty), - DifficultyName = b.DifficultyName, - StarRating = b.StarRating, - Length = b.Length, - BPM = b.BPM - }); + // Minimal data required to get various components in this screen to display correctly. + beatmapsById[beatmap.OnlineID] = new BeatmapInfo + { + Difficulty = new BeatmapDifficulty(beatmap.Difficulty), + DifficultyName = beatmap.DifficultyName, + StarRating = beatmap.StarRating, + Length = beatmap.Length, + BPM = beatmap.BPM + }; + } + + // Validate that we have all beatmaps we need. + foreach (int id in allBeatmapIds) + { + if (!beatmapsById.ContainsKey(id)) + throw new MissingBeatmapException(PlaylistItem, id); + } // Exclude the score provided to this screen since it's added already. return scores .Where(s => s.ID != Score?.OnlineID) - .Select(s => s.CreateScoreInfo(ScoreManager, Rulesets, beatmapsById.GetValueOrDefault(s.BeatmapId) ?? Beatmap.Value.BeatmapInfo)) + .Select(s => s.CreateScoreInfo(ScoreManager, Rulesets, beatmapsById[s.BeatmapId])) .OrderByTotalScore() .ToArray(); } @@ -280,5 +304,13 @@ namespace osu.Game.Screens.OnlinePlay.Playlists X = (float)(list.ScrollableExtent - list.Current - panelOffset); } } + + private class MissingBeatmapException : Exception + { + public MissingBeatmapException(PlaylistItem item, int beatmapId) + : base($"Missing beatmap {beatmapId} for playlist item {item.ID}") + { + } + } } } From b7d431fdde61b56f6f1831c366163da54c71d021 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Wed, 26 Feb 2025 15:04:43 +0900 Subject: [PATCH 1127/1275] Include author --- .../OnlinePlay/Playlists/PlaylistItemResultsScreen.cs | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistItemResultsScreen.cs b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistItemResultsScreen.cs index 1be0a7cf81..53cd81b2a1 100644 --- a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistItemResultsScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistItemResultsScreen.cs @@ -12,6 +12,7 @@ using osu.Framework.Graphics.Containers; using osu.Game.Beatmaps; using osu.Game.Database; using osu.Game.Graphics.UserInterface; +using osu.Game.Models; using osu.Game.Online.API; using osu.Game.Online.API.Requests.Responses; using osu.Game.Online.Rooms; @@ -222,6 +223,14 @@ namespace osu.Game.Screens.OnlinePlay.Playlists beatmapsById[beatmap.OnlineID] = new BeatmapInfo { Difficulty = new BeatmapDifficulty(beatmap.Difficulty), + Metadata = + { + Author = new RealmUser + { + Username = beatmap.Metadata.Author.Username, + OnlineID = beatmap.Metadata.Author.OnlineID, + } + }, DifficultyName = beatmap.DifficultyName, StarRating = beatmap.StarRating, Length = beatmap.Length, From abc12abdedfbb315996d5c16e5556cc9837d1e17 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 26 Feb 2025 16:48:18 +0900 Subject: [PATCH 1128/1275] Fix `PlayerTeamFlag` skinnable component not showing team details during replay For now, let's fetch on demand. Note that song select local leaderboard has the same issue. I feel we should be doing a lot more cached lookups (probaly with persisting across game restarts). Maybe even replacing the realm user storage. An issue for another day. --- osu.Game/Screens/Play/HUD/PlayerTeamFlag.cs | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/Play/HUD/PlayerTeamFlag.cs b/osu.Game/Screens/Play/HUD/PlayerTeamFlag.cs index f8ef03c58c..3f72099a45 100644 --- a/osu.Game/Screens/Play/HUD/PlayerTeamFlag.cs +++ b/osu.Game/Screens/Play/HUD/PlayerTeamFlag.cs @@ -3,8 +3,10 @@ using osu.Framework.Allocation; using osu.Framework.Bindables; +using osu.Framework.Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; +using osu.Game.Database; using osu.Game.Online.API; using osu.Game.Online.API.Requests.Responses; using osu.Game.Skinning; @@ -40,10 +42,19 @@ namespace osu.Game.Screens.Play.HUD } [BackgroundDependencyLoader] - private void load() + private void load(UserLookupCache userLookupCache) { if (gameplayState != null) - flag.Team = gameplayState.Score.ScoreInfo.User.Team; + { + if (gameplayState.Score.ScoreInfo.User.Team != null) + flag.Team = gameplayState.Score.ScoreInfo.User.Team; + else + { + // We only store very basic information about a user to realm, so there's a high chance we don't have the team information. + userLookupCache.GetUserAsync(gameplayState.Score.ScoreInfo.User.Id) + .ContinueWith(task => Schedule(() => flag.Team = task.GetResultSafely()?.Team)); + } + } else { apiUser = api.LocalUser.GetBoundCopy(); From e8b7ec0f9537db864b712ebc28ba63afabe3eeb3 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 26 Feb 2025 17:01:48 +0900 Subject: [PATCH 1129/1275] Adjust leaderboard score design slightly This design is about to get replaced, so I'm just making some minor adjustments since a lot of people complained about the font size in the last update. Of note, I'm only changing the font size which is one pt size lower than we'd usually use. Also overlapping the mod icons to create a bit more space (since there's already cases where they don't fit). Closes https://github.com/ppy/osu/issues/32055 as far as I'm concerned. I can read everything fine at 0.8x UI scale. --- 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 fc30f158f0..7306c2d21e 100644 --- a/osu.Game/Online/Leaderboards/LeaderboardScore.cs +++ b/osu.Game/Online/Leaderboards/LeaderboardScore.cs @@ -271,6 +271,7 @@ namespace osu.Game.Online.Leaderboards Anchor = Anchor.CentreRight, Origin = Anchor.CentreRight, AutoSizeAxes = Axes.Both, + Spacing = new Vector2(-10, 0), Direction = FillDirection.Horizontal, ChildrenEnumerable = Score.Mods.AsOrdered().Select(mod => new ModIcon(mod) { Scale = new Vector2(0.34f) }) }, @@ -425,7 +426,7 @@ namespace osu.Game.Online.Leaderboards public DateLabel(DateTimeOffset date) : base(date) { - Font = OsuFont.GetFont(size: 13, weight: FontWeight.Bold, italics: true); + Font = OsuFont.GetFont(size: 14, weight: FontWeight.Bold); } protected override string Format() => Date.ToShortRelativeTime(TimeSpan.FromSeconds(30)); From c7fd7cf9cd4071123ea83fb479cb8e543cdb1a0c Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Wed, 26 Feb 2025 17:39:56 +0900 Subject: [PATCH 1130/1275] Add missing ConfigureAwait --- osu.Game/Screens/Ranking/ResultsScreen.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/Ranking/ResultsScreen.cs b/osu.Game/Screens/Ranking/ResultsScreen.cs index 397ad9c0b1..26b13d026c 100644 --- a/osu.Game/Screens/Ranking/ResultsScreen.cs +++ b/osu.Game/Screens/Ranking/ResultsScreen.cs @@ -253,7 +253,7 @@ namespace osu.Game.Screens.Ranking nextPageTask = FetchNextPage(1); if (nextPageTask != null) - lastFetchTask = Task.Run(async () => await addScores(await nextPageTask).ConfigureAwait(false)); + lastFetchTask = Task.Run(async () => await addScores(await nextPageTask.ConfigureAwait(false)).ConfigureAwait(false)); } } From c45a403fe2b87db7b43d3500fe25e348b88e27ed Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 26 Feb 2025 18:00:18 +0900 Subject: [PATCH 1131/1275] Mostly revert sizes --- osu.Game/Online/Leaderboards/LeaderboardScore.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Online/Leaderboards/LeaderboardScore.cs b/osu.Game/Online/Leaderboards/LeaderboardScore.cs index 7306c2d21e..0db03efb68 100644 --- a/osu.Game/Online/Leaderboards/LeaderboardScore.cs +++ b/osu.Game/Online/Leaderboards/LeaderboardScore.cs @@ -395,7 +395,7 @@ namespace osu.Game.Online.Leaderboards Origin = Anchor.CentreLeft, Text = statistic.Value, Spacing = new Vector2(-1, 0), - Font = OsuFont.GetFont(size: 14, weight: FontWeight.Bold, fixedWidth: true) + Font = OsuFont.GetFont(size: 16, weight: FontWeight.Bold, fixedWidth: true) }, }, }; @@ -426,7 +426,7 @@ namespace osu.Game.Online.Leaderboards public DateLabel(DateTimeOffset date) : base(date) { - Font = OsuFont.GetFont(size: 14, weight: FontWeight.Bold); + Font = OsuFont.GetFont(size: 16, weight: FontWeight.Bold); } protected override string Format() => Date.ToShortRelativeTime(TimeSpan.FromSeconds(30)); From 3dde024650cc1564369dc0f23b462f876871400a Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Wed, 26 Feb 2025 18:00:16 +0900 Subject: [PATCH 1132/1275] Replace error handling with logs - Handling all errors matches master a little bit better. Logging exceptions in any case. - Not throwing when beatmaps are missing simplifies tests. --- .../Playlists/PlaylistItemResultsScreen.cs | 21 +++++++------------ osu.Game/Screens/Ranking/SoloResultsScreen.cs | 4 +++- 2 files changed, 10 insertions(+), 15 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistItemResultsScreen.cs b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistItemResultsScreen.cs index 53cd81b2a1..572bf535f7 100644 --- a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistItemResultsScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistItemResultsScreen.cs @@ -9,6 +9,7 @@ using System.Threading.Tasks; using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; +using osu.Framework.Logging; using osu.Game.Beatmaps; using osu.Game.Database; using osu.Game.Graphics.UserInterface; @@ -131,10 +132,6 @@ namespace osu.Game.Screens.OnlinePlay.Playlists return await transformScores(allScores).ConfigureAwait(false); } - catch (OperationCanceledException) - { - return []; - } catch { return await fetchScoresAround().ConfigureAwait(false); @@ -192,8 +189,9 @@ namespace osu.Game.Screens.OnlinePlay.Playlists return await transformScores(index.Scores).ConfigureAwait(false); } - catch (OperationCanceledException) + catch (Exception ex) { + Logger.Log($"Failed to fetch scores (room: {RoomId}, item: {PlaylistItem.ID}): {ex}"); return []; } } @@ -242,7 +240,10 @@ namespace osu.Game.Screens.OnlinePlay.Playlists foreach (int id in allBeatmapIds) { if (!beatmapsById.ContainsKey(id)) - throw new MissingBeatmapException(PlaylistItem, id); + { + Logger.Log($"Failed to fetch beatmap {id} to display scores for playlist item {PlaylistItem.ID}"); + beatmapsById[id] = Beatmap.Value.BeatmapInfo; + } } // Exclude the score provided to this screen since it's added already. @@ -313,13 +314,5 @@ namespace osu.Game.Screens.OnlinePlay.Playlists X = (float)(list.ScrollableExtent - list.Current - panelOffset); } } - - private class MissingBeatmapException : Exception - { - public MissingBeatmapException(PlaylistItem item, int beatmapId) - : base($"Missing beatmap {beatmapId} for playlist item {item.ID}") - { - } - } } } diff --git a/osu.Game/Screens/Ranking/SoloResultsScreen.cs b/osu.Game/Screens/Ranking/SoloResultsScreen.cs index 73bed3383b..3486d81e8a 100644 --- a/osu.Game/Screens/Ranking/SoloResultsScreen.cs +++ b/osu.Game/Screens/Ranking/SoloResultsScreen.cs @@ -6,6 +6,7 @@ using System.Collections.Generic; using System.Diagnostics; using System.Threading.Tasks; using osu.Framework.Allocation; +using osu.Framework.Logging; using osu.Game.Beatmaps; using osu.Game.Extensions; using osu.Game.Online.API; @@ -72,8 +73,9 @@ namespace osu.Game.Screens.Ranking return toDisplay.ToArray(); } - catch (OperationCanceledException) + catch (Exception ex) { + Logger.Log($"Failed to fetch scores (beatmap: {Score.BeatmapInfo}, ruleset: {Score.Ruleset}): {ex}"); return []; } } From c280c8fa1c463c280aee473b6c987d46a271dd25 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Wed, 26 Feb 2025 18:31:06 +0900 Subject: [PATCH 1133/1275] Add support to tests Somewhat informal because it isn't super easy to handle. --- .../TestScenePlaylistsResultsScreen.cs | 36 ++++++++++++++++++- 1 file changed, 35 insertions(+), 1 deletion(-) diff --git a/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsResultsScreen.cs b/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsResultsScreen.cs index 33bd573617..dc5fb20e16 100644 --- a/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsResultsScreen.cs +++ b/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsResultsScreen.cs @@ -7,9 +7,12 @@ using System.Linq; using System.Net; using Newtonsoft.Json.Linq; using NUnit.Framework; +using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Testing; +using osu.Framework.Utils; +using osu.Game.Database; using osu.Game.Graphics.Containers; using osu.Game.Graphics.UserInterface; using osu.Game.Online.API; @@ -32,6 +35,9 @@ namespace osu.Game.Tests.Visual.Playlists private const int scores_per_result = 10; private const int real_user_position = 200; + [Cached] + private readonly BeatmapLookupCache beatmapLookupCache = new BeatmapLookupCache(); + private ResultsScreen resultsScreen = null!; private int lowestScoreId; // Score ID of the lowest score in the list. @@ -41,6 +47,11 @@ namespace osu.Game.Tests.Visual.Playlists private int totalCount; private ScoreInfo userScore = null!; + public TestScenePlaylistsResultsScreen() + { + Add(beatmapLookupCache); + } + [SetUpSteps] public override void SetUpSteps() { @@ -279,6 +290,25 @@ namespace osu.Game.Tests.Visual.Playlists case IndexPlaylistScoresRequest: break; + case GetBeatmapsRequest getBeatmaps: + getBeatmaps.TriggerSuccess(new GetBeatmapsResponse + { + Beatmaps = getBeatmaps.BeatmapIds.Select(id => new APIBeatmap + { + OnlineID = id, + StarRating = id, + DifficultyName = $"Beatmap {id}", + BeatmapSet = new APIBeatmapSet + { + Title = $"Title {id}", + Artist = $"Artist {id}", + AuthorString = $"Author {id}" + } + }).ToList() + }); + + return true; + default: return false; } @@ -346,6 +376,7 @@ namespace osu.Game.Tests.Visual.Playlists Position = real_user_position, MaxCombo = userScore.MaxCombo, User = userScore.User, + BeatmapId = RNG.Next(0, 7), ScoresAround = new MultiplayerScoresAround { Higher = new MultiplayerScores(), @@ -364,6 +395,7 @@ namespace osu.Game.Tests.Visual.Playlists Passed = true, Rank = userScore.Rank, MaxCombo = userScore.MaxCombo, + BeatmapId = RNG.Next(0, 7), User = new APIUser { Id = 2 + i, @@ -379,6 +411,7 @@ namespace osu.Game.Tests.Visual.Playlists Passed = true, Rank = userScore.Rank, MaxCombo = userScore.MaxCombo, + BeatmapId = RNG.Next(0, 7), User = new APIUser { Id = 2 + i, @@ -396,7 +429,7 @@ namespace osu.Game.Tests.Visual.Playlists return multiplayerUserScore; } - private IndexedMultiplayerScores createIndexResponse(IndexPlaylistScoresRequest req, bool noScores = false) + private IndexedMultiplayerScores createIndexResponse(IndexPlaylistScoresRequest req, bool noScores) { var result = new IndexedMultiplayerScores(); @@ -413,6 +446,7 @@ namespace osu.Game.Tests.Visual.Playlists Passed = true, Rank = ScoreRank.X, MaxCombo = 1000, + BeatmapId = RNG.Next(0, 7), User = new APIUser { Id = 2 + i, From 76bf03b05dd92938290c23631e00f82fc945f631 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 26 Feb 2025 10:56:28 +0100 Subject: [PATCH 1134/1275] Add failing decoder test case for too many combo colours --- .../Formats/LegacyBeatmapDecoderTest.cs | 29 ++++++++ .../Resources/too-many-combo-colours.osu | 73 +++++++++++++++++++ 2 files changed, 102 insertions(+) create mode 100644 osu.Game.Tests/Resources/too-many-combo-colours.osu diff --git a/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapDecoderTest.cs b/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapDecoderTest.cs index adb1755c11..9747b654ae 100644 --- a/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapDecoderTest.cs +++ b/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapDecoderTest.cs @@ -404,6 +404,35 @@ namespace osu.Game.Tests.Beatmaps.Formats } } + [Test] + public void TestComboColourCountIsLimitedToEight() + { + var decoder = new LegacySkinDecoder(); + + using (var resStream = TestResources.OpenResource("too-many-combo-colours.osu")) + using (var stream = new LineBufferedReader(resStream)) + { + var comboColors = decoder.Decode(stream).ComboColours; + + Debug.Assert(comboColors != null); + + Color4[] expectedColors = + { + new Color4(142, 199, 255, 255), + new Color4(255, 128, 128, 255), + new Color4(128, 255, 255, 255), + new Color4(128, 255, 128, 255), + new Color4(255, 187, 255, 255), + new Color4(255, 177, 140, 255), + new Color4(100, 100, 100, 255), + new Color4(142, 199, 255, 255), + }; + Assert.AreEqual(expectedColors.Length, comboColors.Count); + for (int i = 0; i < expectedColors.Length; i++) + Assert.AreEqual(expectedColors[i], comboColors[i]); + } + } + [Test] public void TestGetLastObjectTime() { diff --git a/osu.Game.Tests/Resources/too-many-combo-colours.osu b/osu.Game.Tests/Resources/too-many-combo-colours.osu new file mode 100644 index 0000000000..477e362a6d --- /dev/null +++ b/osu.Game.Tests/Resources/too-many-combo-colours.osu @@ -0,0 +1,73 @@ +osu file format v14 + +[General] +AudioFilename: 03. Renatus - Soleily 192kbps.mp3 +AudioLeadIn: 0 +PreviewTime: 164471 +Countdown: 0 +SampleSet: Soft +StackLeniency: 0.7 +Mode: 0 +LetterboxInBreaks: 0 +WidescreenStoryboard: 0 + +[Editor] +Bookmarks: 11505,22054,32604,43153,53703,64252,74802,85351,95901,106450,116999,119637,130186,140735,151285,161834,164471,175020,185570,196119,206669,209306 +DistanceSpacing: 1.8 +BeatDivisor: 4 +GridSize: 4 +TimelineZoom: 2 + +[Metadata] +Title:Renatus +TitleUnicode:Renatus +Artist:Soleily +ArtistUnicode:Soleily +Creator:Gamu +Version:Insane +Source: +Tags:MBC7 Unisphere 地球ヤバイEP Chikyu Yabai +BeatmapID:557821 +BeatmapSetID:241526 + +[Difficulty] +HPDrainRate:6.5 +CircleSize:4 +OverallDifficulty:8 +ApproachRate:9 +SliderMultiplier:1.8 +SliderTickRate:2 + +[Events] +//Background and Video events +0,0,"machinetop_background.jpg",0,0 +//Break Periods +2,122474,140135 +//Storyboard Layer 0 (Background) +//Storyboard Layer 1 (Fail) +//Storyboard Layer 2 (Pass) +//Storyboard Layer 3 (Foreground) +//Storyboard Sound Samples + +[TimingPoints] +956,329.67032967033,4,2,0,60,1,0 + + +[Colours] +Combo1:142,199,255 +Combo2:255,128,128 +Combo3:128,255,255 +Combo4:128,255,128 +Combo5:255,187,255 +Combo6:255,177,140 +Combo7:100,100,100 +Combo8:142,199,255 +Combo9:255,128,128 +Combo10:128,255,255 +Combo11:128,255,128 +Combo12:255,187,255 +Combo13:255,177,140 +Combo14:100,100,100 + +[HitObjects] +192,168,956,6,0,P|184:128|200:80,1,90,4|0,1:2|0:0,0:0:0:0: From c2875423eeb264752954ab56f01a8ec2f702510d Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Wed, 26 Feb 2025 18:58:29 +0900 Subject: [PATCH 1135/1275] Cleanup score fetching a bit --- osu.Game/Screens/Ranking/ResultsScreen.cs | 51 ++++++++++++++++------- 1 file changed, 37 insertions(+), 14 deletions(-) diff --git a/osu.Game/Screens/Ranking/ResultsScreen.cs b/osu.Game/Screens/Ranking/ResultsScreen.cs index 26b13d026c..010f7e1a93 100644 --- a/osu.Game/Screens/Ranking/ResultsScreen.cs +++ b/osu.Game/Screens/Ranking/ResultsScreen.cs @@ -234,27 +234,19 @@ namespace osu.Game.Screens.Ranking { base.LoadComplete(); - lastFetchTask = Task.Run(async () => await addScores(await FetchScores().ConfigureAwait(false)).ConfigureAwait(false)); - StatisticsPanel.State.BindValueChanged(onStatisticsStateChanged, true); + + fetchScores(null); } protected override void Update() { base.Update(); - if (lastFetchTask.IsCompleted) - { - Task? nextPageTask = null; - - if (ScorePanelList.IsScrolledToStart) - nextPageTask = FetchNextPage(-1); - else if (ScorePanelList.IsScrolledToEnd) - nextPageTask = FetchNextPage(1); - - if (nextPageTask != null) - lastFetchTask = Task.Run(async () => await addScores(await nextPageTask.ConfigureAwait(false)).ConfigureAwait(false)); - } + if (ScorePanelList.IsScrolledToStart) + fetchScores(-1); + else if (ScorePanelList.IsScrolledToEnd) + fetchScores(1); } #region Applause @@ -317,6 +309,37 @@ namespace osu.Game.Screens.Ranking #endregion + /// + /// Fetches the next page of scores in the given direction. + /// + /// The direction, or null to fetch any scores. + private void fetchScores(int? direction) + { + Debug.Assert(direction == null || direction == -1 || direction == 1); + + if (!lastFetchTask.IsCompleted) + return; + + lastFetchTask = Task.Run(async () => + { + ScoreInfo[] scores; + + switch (direction) + { + default: + scores = await FetchScores().ConfigureAwait(false); + break; + + case -1: + case 1: + scores = await FetchNextPage(direction.Value).ConfigureAwait(false); + break; + } + + await addScores(scores).ConfigureAwait(false); + }); + } + /// /// Performs a fetch/refresh of scores to be displayed. /// From e48d36ad1edd2226b5e7afd9e3bc3e397d00d7e3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 26 Feb 2025 11:10:33 +0100 Subject: [PATCH 1136/1275] Add failing encoder test case for too many combo colours --- .../Formats/LegacyBeatmapEncoderTest.cs | 29 +++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapEncoderTest.cs b/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapEncoderTest.cs index c8a09786ec..caebf52026 100644 --- a/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapEncoderTest.cs +++ b/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapEncoderTest.cs @@ -28,6 +28,7 @@ using osu.Game.Skinning; using osu.Game.Storyboards; using osu.Game.Tests.Resources; using osuTK; +using osuTK.Graphics; namespace osu.Game.Tests.Beatmaps.Formats { @@ -184,6 +185,32 @@ namespace osu.Game.Tests.Beatmaps.Formats Assert.That(decodedSlider.Path.ControlPoints.Count, Is.EqualTo(5)); } + [Test] + public void TestOnlyEightComboColoursEncoded() + { + var beatmapSkin = new LegacyBeatmapSkin(new BeatmapInfo(), null) + { + Configuration = + { + CustomComboColours = + { + new Color4(1, 1, 1, 255), + new Color4(2, 2, 2, 255), + new Color4(3, 3, 3, 255), + new Color4(4, 4, 4, 255), + new Color4(5, 5, 5, 255), + new Color4(6, 6, 6, 255), + new Color4(7, 7, 7, 255), + new Color4(8, 8, 8, 255), + new Color4(9, 9, 9, 255), + } + } + }; + + var decodedAfterEncode = decodeFromLegacy(encodeToLegacy((new Beatmap(), beatmapSkin)), string.Empty); + Assert.That(decodedAfterEncode.skin.Configuration.CustomComboColours, Has.Count.EqualTo(8)); + } + private bool areComboColoursEqual(IHasComboColours a, IHasComboColours b) { // equal to null, no need to SequenceEqual @@ -212,6 +239,8 @@ namespace osu.Game.Tests.Beatmaps.Formats { var beatmap = new LegacyBeatmapDecoder { ApplyOffsets = false }.Decode(reader); var beatmapSkin = new TestLegacySkin(beatmaps_resource_store, name); + stream.Seek(0, SeekOrigin.Begin); + beatmapSkin.Configuration = new LegacySkinDecoder().Decode(reader); return (convert(beatmap), beatmapSkin); } } From 2167c7b8d56bbba00a2167f093a1ddf77d09baf4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 26 Feb 2025 11:13:57 +0100 Subject: [PATCH 1137/1275] Limit beatmap encoder & decoder to at most 8 combo colours --- osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs | 2 +- osu.Game/Beatmaps/Formats/LegacyDecoder.cs | 6 +++++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs b/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs index 07e88ab956..5529828de2 100644 --- a/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs +++ b/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs @@ -349,7 +349,7 @@ namespace osu.Game.Beatmaps.Formats writer.WriteLine("[Colours]"); - for (int i = 0; i < colours.Count; i++) + for (int i = 0; i < Math.Min(colours.Count, LegacyBeatmapDecoder.MAX_COMBO_COLOUR_COUNT); i++) { var comboColour = colours[i]; diff --git a/osu.Game/Beatmaps/Formats/LegacyDecoder.cs b/osu.Game/Beatmaps/Formats/LegacyDecoder.cs index ca4fadf458..6c290c4f1c 100644 --- a/osu.Game/Beatmaps/Formats/LegacyDecoder.cs +++ b/osu.Game/Beatmaps/Formats/LegacyDecoder.cs @@ -18,6 +18,8 @@ namespace osu.Game.Beatmaps.Formats { public const int LATEST_VERSION = 14; + public const int MAX_COMBO_COLOUR_COUNT = 8; + /// /// The .osu format (beatmap) version. /// @@ -126,7 +128,9 @@ namespace osu.Game.Beatmaps.Formats string[] split = pair.Value.Split(','); Color4 colour = convertSettingStringToColor4(split, allowAlpha, pair); - bool isCombo = pair.Key.StartsWith(@"Combo", StringComparison.Ordinal); + bool isCombo = pair.Key.StartsWith(@"Combo", StringComparison.Ordinal) + && int.TryParse(pair.Key[5..], out int comboIndex) + && comboIndex >= 1 && comboIndex <= MAX_COMBO_COLOUR_COUNT; if (isCombo) { From 6b76b8ccdda0ffe4a0b7d47e7fe3ddfd38e70d30 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 26 Feb 2025 11:16:37 +0100 Subject: [PATCH 1138/1275] Do not allow adding more than 8 combo colours in editor --- osu.Game/Graphics/UserInterfaceV2/FormColourPalette.cs | 10 ++++++---- osu.Game/Screens/Edit/Setup/ColoursSection.cs | 9 +++++++++ 2 files changed, 15 insertions(+), 4 deletions(-) diff --git a/osu.Game/Graphics/UserInterfaceV2/FormColourPalette.cs b/osu.Game/Graphics/UserInterfaceV2/FormColourPalette.cs index fad58841e3..258a97d79c 100644 --- a/osu.Game/Graphics/UserInterfaceV2/FormColourPalette.cs +++ b/osu.Game/Graphics/UserInterfaceV2/FormColourPalette.cs @@ -31,9 +31,12 @@ namespace osu.Game.Graphics.UserInterfaceV2 public LocalisableString Caption { get; init; } public LocalisableString HintText { get; init; } + public BindableBool CanAdd { get; } = new BindableBool(true); + private Box background = null!; private FormFieldCaption caption = null!; private FillFlowContainer flow = null!; + private RoundedButton addButton = null!; [Resolved] private OverlayColourProvider colourProvider { get; set; } = null!; @@ -47,8 +50,6 @@ namespace osu.Game.Graphics.UserInterfaceV2 Masking = true; CornerRadius = 5; - RoundedButton button; - InternalChildren = new Drawable[] { background = new Box @@ -76,7 +77,7 @@ namespace osu.Game.Graphics.UserInterfaceV2 AutoSizeAxes = Axes.Y, Direction = FillDirection.Full, Spacing = new Vector2(5), - Child = button = new RoundedButton + Child = addButton = new RoundedButton { Action = addNewColour, Size = new Vector2(70), @@ -87,7 +88,7 @@ namespace osu.Game.Graphics.UserInterfaceV2 }, }; - flow.SetLayoutPosition(button, float.MaxValue); + flow.SetLayoutPosition(addButton, float.MaxValue); } protected override void LoadComplete() @@ -99,6 +100,7 @@ namespace osu.Game.Graphics.UserInterfaceV2 if (args.Action != NotifyCollectionChangedAction.Replace) updateColours(); }, true); + CanAdd.BindValueChanged(_ => addButton.Alpha = CanAdd.Value ? 1 : 0, true); updateState(); } diff --git a/osu.Game/Screens/Edit/Setup/ColoursSection.cs b/osu.Game/Screens/Edit/Setup/ColoursSection.cs index 8de7f86523..865fe05c54 100644 --- a/osu.Game/Screens/Edit/Setup/ColoursSection.cs +++ b/osu.Game/Screens/Edit/Setup/ColoursSection.cs @@ -4,6 +4,7 @@ using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Localisation; +using osu.Game.Beatmaps.Formats; using osu.Game.Graphics.UserInterfaceV2; using osu.Game.Localisation; using osu.Game.Skinning; @@ -54,6 +55,8 @@ namespace osu.Game.Screens.Edit.Setup Beatmap.BeatmapSkin.ComboColours.Clear(); Beatmap.BeatmapSkin.ComboColours.AddRange(comboColours.Colours); + updateAddButtonVisibility(); + syncingColours = false; } }); @@ -68,8 +71,14 @@ namespace osu.Game.Screens.Edit.Setup comboColours.Colours.Clear(); comboColours.Colours.AddRange(Beatmap.BeatmapSkin?.ComboColours); + updateAddButtonVisibility(); + syncingColours = false; }); + + updateAddButtonVisibility(); + + void updateAddButtonVisibility() => comboColours.CanAdd.Value = comboColours.Colours.Count < LegacyBeatmapDecoder.MAX_COMBO_COLOUR_COUNT; } } } From f3632a466fbf88484d2c3be9e461a9e7610e40da Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 26 Feb 2025 12:01:30 +0100 Subject: [PATCH 1139/1275] Prevent closing team chat channels via Ctrl-W As pointed out in https://github.com/ppy/osu/pull/32079#issuecomment-2680297760. The comment suggested putting that logic in `ChannelManager` but honestly I kinda don't see it working out. It'd probably be multiple boolean arguments for `leaveChannel()` (because `sendLeaveRequest` or whatever already exists), and then there's this one usage in tournament client: https://github.com/ppy/osu/blob/31aded69714cf205c215893368d1f148c9a73319/osu.Game.Tournament/Components/TournamentMatchChatDisplay.cs#L57-L58 I'm not sure how that would interact with this particular change, but I think there is a nonzero possibility that it would interact badly. So in general I kinda just prefer steering clear of all that and adding a local one-liner. --- osu.Game/Overlays/ChatOverlay.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/osu.Game/Overlays/ChatOverlay.cs b/osu.Game/Overlays/ChatOverlay.cs index c49afa3a66..7f4ba3e2e2 100644 --- a/osu.Game/Overlays/ChatOverlay.cs +++ b/osu.Game/Overlays/ChatOverlay.cs @@ -228,7 +228,8 @@ namespace osu.Game.Overlays return true; case PlatformAction.DocumentClose: - channelManager.LeaveChannel(currentChannel.Value); + if (currentChannel.Value?.Type != ChannelType.Team) + channelManager.LeaveChannel(currentChannel.Value); return true; case PlatformAction.TabRestore: From d3c4afe65d8d86edb8c391d6db96849ef4f48709 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Thu, 27 Feb 2025 13:16:51 +0900 Subject: [PATCH 1140/1275] Fix typo --- osu.Game/Screens/Ranking/Expanded/ExpandedPanelMiddleContent.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/Ranking/Expanded/ExpandedPanelMiddleContent.cs b/osu.Game/Screens/Ranking/Expanded/ExpandedPanelMiddleContent.cs index 9bef6a3f3a..0190a6f959 100644 --- a/osu.Game/Screens/Ranking/Expanded/ExpandedPanelMiddleContent.cs +++ b/osu.Game/Screens/Ranking/Expanded/ExpandedPanelMiddleContent.cs @@ -71,7 +71,7 @@ namespace osu.Game.Screens.Ranking.Expanded StarDifficulty starDifficulty = new StarDifficulty(beatmap.StarRating, 0); // In some cases, the beatmap ferried through ScoreInfo actually represents an online beatmap. - // If it isn't, we may be able to compute a more accuracy difficulty from the ruleset and mods. + // If it isn't, we may be able to compute a more accurate difficulty from the ruleset and mods. if (realmAccess.Run(r => r.Find(score.BeatmapInfo!.ID)) != null) starDifficulty = beatmapDifficultyCache.GetDifficultyAsync(score.BeatmapInfo!, score.Ruleset, score.Mods).GetResultSafely() ?? starDifficulty; From d31588939c03fb365cf7acd09b6a441a49f100f7 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Thu, 27 Feb 2025 13:39:16 +0900 Subject: [PATCH 1141/1275] Disallow attempting to close multiplayer rooms --- osu.Game/Screens/OnlinePlay/Lounge/LoungeSubScreen.cs | 10 +--------- .../Multiplayer/MultiplayerLoungeSubScreen.cs | 3 +++ .../OnlinePlay/Playlists/PlaylistsLoungeSubScreen.cs | 11 +++++++++++ 3 files changed, 15 insertions(+), 9 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/Lounge/LoungeSubScreen.cs b/osu.Game/Screens/OnlinePlay/Lounge/LoungeSubScreen.cs index 0e08e398a4..30e7b0d31b 100644 --- a/osu.Game/Screens/OnlinePlay/Lounge/LoungeSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Lounge/LoungeSubScreen.cs @@ -21,7 +21,6 @@ using osu.Game.Graphics.Containers; using osu.Game.Graphics.UserInterface; using osu.Game.Input; using osu.Game.Online.API; -using osu.Game.Online.API.Requests; using osu.Game.Online.Rooms; using osu.Game.Overlays; using osu.Game.Rulesets; @@ -361,14 +360,7 @@ namespace osu.Game.Screens.OnlinePlay.Lounge api.Queue(req); } - public void Close(Room room) - { - Debug.Assert(room.RoomID != null); - - var request = new ClosePlaylistRequest(room.RoomID.Value); - request.Success += RefreshRooms; - api.Queue(request); - } + public abstract void Close(Room room); /// /// Push a room as a new subscreen. diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerLoungeSubScreen.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerLoungeSubScreen.cs index 873a9cde88..8f2490f77a 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerLoungeSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerLoungeSubScreen.cs @@ -99,6 +99,9 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer }); } + public override void Close(Room room) + => throw new NotSupportedException("Cannot close multiplayer rooms."); + protected override void OpenNewRoom(Room room) { if (!client.IsConnected.Value) diff --git a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsLoungeSubScreen.cs b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsLoungeSubScreen.cs index 6ed367328c..9de13eb270 100644 --- a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsLoungeSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsLoungeSubScreen.cs @@ -4,12 +4,14 @@ using System; using System.Collections.Generic; using System.ComponentModel; +using System.Diagnostics; using System.Linq; using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.UserInterface; using osu.Game.Graphics.UserInterface; using osu.Game.Online.API; +using osu.Game.Online.API.Requests; using osu.Game.Online.Rooms; using osu.Game.Screens.OnlinePlay.Components; using osu.Game.Screens.OnlinePlay.Lounge; @@ -74,6 +76,15 @@ namespace osu.Game.Screens.OnlinePlay.Playlists api.Queue(joinRoomRequest); } + public override void Close(Room room) + { + Debug.Assert(room.RoomID != null); + + var request = new ClosePlaylistRequest(room.RoomID.Value); + request.Success += RefreshRooms; + api.Queue(request); + } + protected override OsuButton CreateNewRoomButton() => new CreatePlaylistsRoomButton(); protected override Room CreateNewRoom() From 47ca5c90a5bada5733c89376916236b29c69467f Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Thu, 27 Feb 2025 14:50:35 +0900 Subject: [PATCH 1142/1275] Refactor post-join setup to not pass delegates around --- .../Online/Multiplayer/MultiplayerClient.cs | 77 +++++++++++-------- 1 file changed, 47 insertions(+), 30 deletions(-) diff --git a/osu.Game/Online/Multiplayer/MultiplayerClient.cs b/osu.Game/Online/Multiplayer/MultiplayerClient.cs index 636cba719b..1f85aa5d45 100644 --- a/osu.Game/Online/Multiplayer/MultiplayerClient.cs +++ b/osu.Game/Online/Multiplayer/MultiplayerClient.cs @@ -170,13 +170,23 @@ namespace osu.Game.Online.Multiplayer private readonly TaskChain joinOrLeaveTaskChain = new TaskChain(); private CancellationTokenSource? joinCancellationSource; + /// + /// Creates and joins a described by an API . + /// + /// The API describing the room to create. + /// If the current user is already in another room. public async Task CreateRoom(Room room) { if (Room != null) throw new InvalidOperationException("Cannot create a multiplayer room while already in one."); var cancellationSource = joinCancellationSource = new CancellationTokenSource(); - await initRoom(room, r => CreateRoomInternal(new MultiplayerRoom(room)), cancellationSource.Token).ConfigureAwait(false); + + await joinOrLeaveTaskChain.Add(async () => + { + var multiplayerRoom = await CreateRoomInternal(new MultiplayerRoom(room)).ConfigureAwait(false); + await setupJoinedRoom(room, multiplayerRoom, cancellationSource.Token).ConfigureAwait(false); + }, cancellationSource.Token).ConfigureAwait(false); } /// @@ -184,54 +194,61 @@ namespace osu.Game.Online.Multiplayer /// /// The API . /// An optional password to use for the join operation. + /// If the current user is already in another room, or does not represent an active room. public async Task JoinRoom(Room room, string? password = null) { if (Room != null) throw new InvalidOperationException("Cannot join a multiplayer room while already in one."); - Debug.Assert(room.RoomID != null); + if (room.RoomID == null) + throw new InvalidOperationException("Cannot join an inactive room."); var cancellationSource = joinCancellationSource = new CancellationTokenSource(); - await initRoom(room, r => JoinRoomInternal(room.RoomID.Value, password ?? room.Password), cancellationSource.Token).ConfigureAwait(false); - } - private async Task initRoom(Room room, Func> initFunc, CancellationToken cancellationToken) - { await joinOrLeaveTaskChain.Add(async () => { - // Initialise the server-side room. - MultiplayerRoom joinedRoom = await initFunc(room).ConfigureAwait(false); + var multiplayerRoom = await JoinRoomInternal(room.RoomID.Value, password ?? room.Password).ConfigureAwait(false); + await setupJoinedRoom(room, multiplayerRoom, cancellationSource.Token).ConfigureAwait(false); + }, cancellationSource.Token).ConfigureAwait(false); + } - // Populate users. - await PopulateUsers(joinedRoom.Users).ConfigureAwait(false); + /// + /// Performs post-join setup of a . + /// + /// The incoming API that was requested to be joined. + /// The resuling that was joined. + /// A token to cancel the process. + private async Task setupJoinedRoom(Room apiRoom, MultiplayerRoom joinedRoom, CancellationToken cancellationToken) + { + // Populate users. + await PopulateUsers(joinedRoom.Users).ConfigureAwait(false); - // Update the stored room (must be done on update thread for thread-safety). - await runOnUpdateThreadAsync(() => - { - Debug.Assert(Room == null); - Debug.Assert(APIRoom == null); + // Update the stored room (must be done on update thread for thread-safety). + await runOnUpdateThreadAsync(() => + { + Debug.Assert(Room == null); + Debug.Assert(APIRoom == null); - Room = joinedRoom; - APIRoom = room; + Room = joinedRoom; + APIRoom = apiRoom; - APIRoom.RoomID = joinedRoom.RoomID; - APIRoom.Playlist = joinedRoom.Playlist.Select(item => new PlaylistItem(item)).ToArray(); - APIRoom.CurrentPlaylistItem = APIRoom.Playlist.Single(item => item.ID == joinedRoom.Settings.PlaylistItemId); - // The server will null out the end date upon the host joining the room, but the null value is never communicated to the client. - APIRoom.EndDate = null; + APIRoom.RoomID = joinedRoom.RoomID; + APIRoom.Playlist = joinedRoom.Playlist.Select(item => new PlaylistItem(item)).ToArray(); + APIRoom.CurrentPlaylistItem = APIRoom.Playlist.Single(item => item.ID == joinedRoom.Settings.PlaylistItemId); + // The server will null out the end date upon the host joining the room, but the null value is never communicated to the client. + APIRoom.EndDate = null; - Debug.Assert(LocalUser != null); - addUserToAPIRoom(LocalUser); + Debug.Assert(LocalUser != null); + addUserToAPIRoom(LocalUser); - foreach (var user in joinedRoom.Users) - updateUserPlayingState(user.UserID, user.State); + foreach (var user in joinedRoom.Users) + updateUserPlayingState(user.UserID, user.State); - updateLocalRoomSettings(joinedRoom.Settings); + updateLocalRoomSettings(joinedRoom.Settings); - postServerShuttingDownNotification(); + postServerShuttingDownNotification(); - OnRoomJoined(); - }, cancellationToken).ConfigureAwait(false); + OnRoomJoined(); }, cancellationToken).ConfigureAwait(false); } From 0b453772da964dddd2ee73f677367293b26dbf2a Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 27 Feb 2025 15:14:53 +0900 Subject: [PATCH 1143/1275] Disable button instead of hiding (and add tooltip) --- .../Graphics/UserInterfaceV2/FormColourPalette.cs | 14 +++++++++++++- osu.Game/Graphics/UserInterfaceV2/RoundedButton.cs | 5 ++++- .../Overlays/BeatmapSet/Buttons/FavouriteButton.cs | 5 ++--- osu.Game/Overlays/Settings/SettingsButton.cs | 5 +---- .../Screens/OnlinePlay/Components/ReadyButton.cs | 5 ++--- .../Playlists/AddPlaylistToCollectionButton.cs | 5 ++--- 6 files changed, 24 insertions(+), 15 deletions(-) diff --git a/osu.Game/Graphics/UserInterfaceV2/FormColourPalette.cs b/osu.Game/Graphics/UserInterfaceV2/FormColourPalette.cs index 258a97d79c..a0348fa27a 100644 --- a/osu.Game/Graphics/UserInterfaceV2/FormColourPalette.cs +++ b/osu.Game/Graphics/UserInterfaceV2/FormColourPalette.cs @@ -100,7 +100,19 @@ namespace osu.Game.Graphics.UserInterfaceV2 if (args.Action != NotifyCollectionChangedAction.Replace) updateColours(); }, true); - CanAdd.BindValueChanged(_ => addButton.Alpha = CanAdd.Value ? 1 : 0, true); + CanAdd.BindValueChanged(canAdd => + { + if (canAdd.NewValue) + { + addButton.Enabled.Value = true; + addButton.TooltipText = string.Empty; + } + else + { + addButton.Enabled.Value = false; + addButton.TooltipText = "Maximum combo colours reached"; + } + }, true); updateState(); } diff --git a/osu.Game/Graphics/UserInterfaceV2/RoundedButton.cs b/osu.Game/Graphics/UserInterfaceV2/RoundedButton.cs index 6aded3fe32..9b57ebb200 100644 --- a/osu.Game/Graphics/UserInterfaceV2/RoundedButton.cs +++ b/osu.Game/Graphics/UserInterfaceV2/RoundedButton.cs @@ -8,6 +8,7 @@ using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Colour; using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Cursor; using osu.Framework.Input.Events; using osu.Framework.Localisation; using osu.Game.Graphics.Backgrounds; @@ -17,7 +18,7 @@ using osuTK.Graphics; namespace osu.Game.Graphics.UserInterfaceV2 { - public partial class RoundedButton : OsuButton, IFilterable + public partial class RoundedButton : OsuButton, IFilterable, IHasTooltip { protected TrianglesV2? Triangles { get; private set; } @@ -107,5 +108,7 @@ namespace osu.Game.Graphics.UserInterfaceV2 } public bool FilteringActive { get; set; } + + public virtual LocalisableString TooltipText { get; set; } } } diff --git a/osu.Game/Overlays/BeatmapSet/Buttons/FavouriteButton.cs b/osu.Game/Overlays/BeatmapSet/Buttons/FavouriteButton.cs index cbdb2ea190..eab394c8f6 100644 --- a/osu.Game/Overlays/BeatmapSet/Buttons/FavouriteButton.cs +++ b/osu.Game/Overlays/BeatmapSet/Buttons/FavouriteButton.cs @@ -7,7 +7,6 @@ using System.Diagnostics; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; -using osu.Framework.Graphics.Cursor; using osu.Framework.Graphics.Sprites; using osu.Framework.Localisation; using osu.Game.Graphics.UserInterface; @@ -21,7 +20,7 @@ using APIUser = osu.Game.Online.API.Requests.Responses.APIUser; namespace osu.Game.Overlays.BeatmapSet.Buttons { - public partial class FavouriteButton : HeaderButton, IHasTooltip + public partial class FavouriteButton : HeaderButton { public readonly Bindable BeatmapSet = new Bindable(); @@ -32,7 +31,7 @@ namespace osu.Game.Overlays.BeatmapSet.Buttons private readonly IBindable localUser = new Bindable(); - public LocalisableString TooltipText + public override LocalisableString TooltipText { get { diff --git a/osu.Game/Overlays/Settings/SettingsButton.cs b/osu.Game/Overlays/Settings/SettingsButton.cs index 3f5d612eb8..196ddca953 100644 --- a/osu.Game/Overlays/Settings/SettingsButton.cs +++ b/osu.Game/Overlays/Settings/SettingsButton.cs @@ -6,13 +6,12 @@ using System.Collections.Generic; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; -using osu.Framework.Graphics.Cursor; using osu.Framework.Localisation; using osu.Game.Graphics.UserInterfaceV2; namespace osu.Game.Overlays.Settings { - public partial class SettingsButton : RoundedButton, IHasTooltip, IConditionalFilterable + public partial class SettingsButton : RoundedButton, IConditionalFilterable { public SettingsButton() { @@ -25,8 +24,6 @@ namespace osu.Game.Overlays.Settings public BindableBool CanBeShown { get; } = new BindableBool(true); IBindable IConditionalFilterable.CanBeShown => CanBeShown; - public LocalisableString TooltipText { get; set; } - public override IEnumerable FilterTerms { get diff --git a/osu.Game/Screens/OnlinePlay/Components/ReadyButton.cs b/osu.Game/Screens/OnlinePlay/Components/ReadyButton.cs index 2e669fd1b2..56e2719e9c 100644 --- a/osu.Game/Screens/OnlinePlay/Components/ReadyButton.cs +++ b/osu.Game/Screens/OnlinePlay/Components/ReadyButton.cs @@ -3,7 +3,6 @@ using osu.Framework.Allocation; using osu.Framework.Bindables; -using osu.Framework.Graphics.Cursor; using osu.Framework.Localisation; using osu.Game.Graphics.UserInterfaceV2; using osu.Game.Online; @@ -11,7 +10,7 @@ using osu.Game.Online.Rooms; namespace osu.Game.Screens.OnlinePlay.Components { - public abstract partial class ReadyButton : RoundedButton, IHasTooltip + public abstract partial class ReadyButton : RoundedButton { public new readonly BindableBool Enabled = new BindableBool(); @@ -29,7 +28,7 @@ namespace osu.Game.Screens.OnlinePlay.Components private void updateState() => base.Enabled.Value = availability.Value.State == DownloadState.LocallyAvailable && Enabled.Value; - public virtual LocalisableString TooltipText + public override LocalisableString TooltipText { get { diff --git a/osu.Game/Screens/OnlinePlay/Playlists/AddPlaylistToCollectionButton.cs b/osu.Game/Screens/OnlinePlay/Playlists/AddPlaylistToCollectionButton.cs index 741173f9a3..47629981f1 100644 --- a/osu.Game/Screens/OnlinePlay/Playlists/AddPlaylistToCollectionButton.cs +++ b/osu.Game/Screens/OnlinePlay/Playlists/AddPlaylistToCollectionButton.cs @@ -5,7 +5,6 @@ using System; using System.Collections.Generic; using System.Linq; using osu.Framework.Allocation; -using osu.Framework.Graphics.Cursor; using osu.Framework.Localisation; using osu.Game.Beatmaps; using osu.Game.Collections; @@ -18,7 +17,7 @@ using Realms; namespace osu.Game.Screens.OnlinePlay.Playlists { - public partial class AddPlaylistToCollectionButton : RoundedButton, IHasTooltip + public partial class AddPlaylistToCollectionButton : RoundedButton { private readonly Room room; @@ -161,7 +160,7 @@ namespace osu.Game.Screens.OnlinePlay.Playlists collectionSubscription?.Dispose(); } - public LocalisableString TooltipText + public override LocalisableString TooltipText { get { From 5b318edbfbd9aa3ece3a491a9a641d7eee3a4c20 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 27 Feb 2025 14:57:42 +0100 Subject: [PATCH 1144/1275] Fix sliders not being selectable if the body is hidden but the head is still visible Closes https://github.com/ppy/osu/issues/31998. Previously: https://github.com/ppy/osu/commit/1648f2efa306f587714178f113e69d8ad8c4ac02, https://github.com/ppy/osu/pull/31923. Oh input handling, how I love ya. --- .../Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs index 39c0681dba..60f335c419 100644 --- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs +++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs @@ -626,7 +626,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) { - if (BodyPiece.ReceivePositionalInputAt(screenSpacePos) && (IsSelected || DrawableObject.Body.Alpha > 0)) + if (BodyPiece.ReceivePositionalInputAt(screenSpacePos) && (IsSelected || DrawableObject.Body.Alpha > 0 || DrawableObject.HeadCircle.Alpha > 0)) return true; if (ControlPointVisualiser == null) From 09131740992b15ca322054e5c8aee784c6eade79 Mon Sep 17 00:00:00 2001 From: Zihad Date: Fri, 28 Feb 2025 00:20:58 +0600 Subject: [PATCH 1145/1275] Fix settings control not visible because of previous search This also makes `SettingsPanel`'s `SearchTextBox` protected from private so that `SettingsOverlay` can access it. --- osu.Game/Overlays/SettingsOverlay.cs | 3 +++ osu.Game/Overlays/SettingsPanel.cs | 16 ++++++++-------- 2 files changed, 11 insertions(+), 8 deletions(-) diff --git a/osu.Game/Overlays/SettingsOverlay.cs b/osu.Game/Overlays/SettingsOverlay.cs index 1157860e03..8a39d75565 100644 --- a/osu.Game/Overlays/SettingsOverlay.cs +++ b/osu.Game/Overlays/SettingsOverlay.cs @@ -68,6 +68,9 @@ namespace osu.Game.Overlays public void ShowAtControl() where T : Drawable { + // if search isn't cleared then the target control won't be visible if it doesn't match the query + SearchTextBox.Current.Value = ""; + Show(); // wait for load of sections diff --git a/osu.Game/Overlays/SettingsPanel.cs b/osu.Game/Overlays/SettingsPanel.cs index df50e0f339..d8b054eaf8 100644 --- a/osu.Game/Overlays/SettingsPanel.cs +++ b/osu.Game/Overlays/SettingsPanel.cs @@ -54,7 +54,7 @@ namespace osu.Game.Overlays public SettingsSectionsContainer SectionsContainer { get; private set; } - private SeekLimitedSearchTextBox searchTextBox; + protected SeekLimitedSearchTextBox SearchTextBox; protected override string PopInSampleName => "UI/settings-pop-in"; protected override double PopInOutSampleBalance => -OsuGameBase.SFX_STEREO_STRENGTH; @@ -135,7 +135,7 @@ namespace osu.Game.Overlays }, Anchor = Anchor.TopCentre, Origin = Anchor.TopCentre, - Child = searchTextBox = new SettingsSearchTextBox + Child = SearchTextBox = new SettingsSearchTextBox { RelativeSizeAxes = Axes.X, Origin = Anchor.TopCentre, @@ -183,8 +183,8 @@ namespace osu.Game.Overlays Sidebar?.MoveToX(0, TRANSITION_LENGTH, Easing.OutQuint); this.FadeTo(1, TRANSITION_LENGTH / 2, Easing.OutQuint); - searchTextBox.TakeFocus(); - searchTextBox.HoldFocus = true; + SearchTextBox.TakeFocus(); + SearchTextBox.HoldFocus = true; } protected virtual float ExpandedPosition => 0; @@ -199,8 +199,8 @@ namespace osu.Game.Overlays Sidebar?.MoveToX(-sidebar_width, TRANSITION_LENGTH, Easing.OutQuint); this.FadeTo(0, TRANSITION_LENGTH / 2, Easing.OutQuint); - searchTextBox.HoldFocus = false; - if (searchTextBox.HasFocus) + SearchTextBox.HoldFocus = false; + if (SearchTextBox.HasFocus) GetContainingFocusManager()!.ChangeFocus(null); } @@ -208,7 +208,7 @@ namespace osu.Game.Overlays protected override void OnFocus(FocusEvent e) { - searchTextBox.TakeFocus(); + SearchTextBox.TakeFocus(); base.OnFocus(e); } @@ -234,7 +234,7 @@ namespace osu.Game.Overlays loading.Hide(); - searchTextBox.Current.BindValueChanged(term => SectionsContainer.SearchTerm = term.NewValue, true); + SearchTextBox.Current.BindValueChanged(term => SectionsContainer.SearchTerm = term.NewValue, true); loadSidebarButtons(); }); From a659936c57a1f51b917102bc737bfbc22187973e Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 28 Feb 2025 13:19:19 +0900 Subject: [PATCH 1146/1275] Inline some methods --- .../Multiplayer/Match/MultiplayerMatchSettingsOverlay.cs | 4 +--- .../OnlinePlay/Playlists/PlaylistsRoomSettingsOverlay.cs | 8 ++------ 2 files changed, 3 insertions(+), 9 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerMatchSettingsOverlay.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerMatchSettingsOverlay.cs index eda3bace40..f74de26f1f 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerMatchSettingsOverlay.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerMatchSettingsOverlay.cs @@ -444,7 +444,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match if (!ApplyButton.Enabled.Value) return; - hideError(); + ErrorText.FadeOut(50); Debug.Assert(applyingSettingsOperation == null); applyingSettingsOperation = ongoingOperationTracker.BeginOperation(); @@ -480,8 +480,6 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match } } - private void hideError() => ErrorText.FadeOut(50); - private void onSuccess() => Schedule(() => { Debug.Assert(applyingSettingsOperation != null); diff --git a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSettingsOverlay.cs b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSettingsOverlay.cs index b3d1d577ed..9c0363f40e 100644 --- a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSettingsOverlay.cs +++ b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSettingsOverlay.cs @@ -437,7 +437,7 @@ namespace osu.Game.Screens.OnlinePlay.Playlists if (!ApplyButton.Enabled.Value) return; - hideError(); + ErrorText.FadeOut(50); room.Name = NameField.Text; room.Availability = AvailabilityPicker.Current.Value; @@ -448,15 +448,11 @@ namespace osu.Game.Screens.OnlinePlay.Playlists loadingLayer.Show(); var req = new CreateRoomRequest(room); - req.Success += onSuccess; + req.Success += _ => loadingLayer.Hide(); req.Failure += e => onError(req.Response?.Error ?? e.Message); api.Queue(req); } - private void hideError() => ErrorText.FadeOut(50); - - private void onSuccess(Room room) => loadingLayer.Hide(); - private void onError(string text) { // see https://github.com/ppy/osu-web/blob/2c97aaeb64fb4ed97c747d8383a35b30f57428c7/app/Models/Multiplayer/PlaylistItem.php#L48. From e1723ec1bbfe40e70754b1971b9e1602eed4a7a5 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 28 Feb 2025 14:05:49 +0900 Subject: [PATCH 1147/1275] Adjust preview time display to not conflict with bookmarks --- .../Timelines/Summary/Parts/PreviewTimePart.cs | 5 +++++ .../Components/Timelines/Summary/SummaryTimeline.cs | 13 ++++++------- 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/osu.Game/Screens/Edit/Components/Timelines/Summary/Parts/PreviewTimePart.cs b/osu.Game/Screens/Edit/Components/Timelines/Summary/Parts/PreviewTimePart.cs index 67bb1ef500..72b58bcb5f 100644 --- a/osu.Game/Screens/Edit/Components/Timelines/Summary/Parts/PreviewTimePart.cs +++ b/osu.Game/Screens/Edit/Components/Timelines/Summary/Parts/PreviewTimePart.cs @@ -3,6 +3,7 @@ using osu.Framework.Allocation; using osu.Framework.Bindables; +using osu.Framework.Graphics; using osu.Framework.Graphics.Cursor; using osu.Framework.Localisation; using osu.Game.Extensions; @@ -36,6 +37,10 @@ namespace osu.Game.Screens.Edit.Components.Timelines.Summary.Parts : base(time) { Alpha = 0.8f; + + // Display as a small circle on the middle line as to not clash with other displays. + RelativeSizeAxes = Axes.None; + Height = Width = 5; } [BackgroundDependencyLoader] diff --git a/osu.Game/Screens/Edit/Components/Timelines/Summary/SummaryTimeline.cs b/osu.Game/Screens/Edit/Components/Timelines/Summary/SummaryTimeline.cs index c01481e840..568137cce1 100644 --- a/osu.Game/Screens/Edit/Components/Timelines/Summary/SummaryTimeline.cs +++ b/osu.Game/Screens/Edit/Components/Timelines/Summary/SummaryTimeline.cs @@ -52,13 +52,6 @@ namespace osu.Game.Screens.Edit.Components.Timelines.Summary }, } }, - new PreviewTimePart - { - Anchor = Anchor.Centre, - Origin = Anchor.TopCentre, - RelativeSizeAxes = Axes.Both, - Height = 0.4f, - }, new BreakPart { Anchor = Anchor.Centre, @@ -85,6 +78,12 @@ namespace osu.Game.Screens.Edit.Components.Timelines.Summary RelativeSizeAxes = Axes.Both, Height = 0.4f }, + new PreviewTimePart + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + RelativeSizeAxes = Axes.Both, + }, new MarkerPart { RelativeSizeAxes = Axes.Both }, }; } From 3e8dafa3c51d6c6434d56ac0c51ffe4800c23fd4 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 28 Feb 2025 14:43:00 +0900 Subject: [PATCH 1148/1275] Add basic setup for mania legacy barline implementation --- .../Objects/Drawables/DrawableBarLine.cs | 3 +- .../Skinning/Default/DefaultBarLine.cs | 4 ++- .../Skinning/Legacy/LegacyBarLine.cs | 33 +++++++++++++++++++ .../Legacy/ManiaLegacySkinTransformer.cs | 2 +- 4 files changed, 38 insertions(+), 4 deletions(-) create mode 100644 osu.Game.Rulesets.Mania/Skinning/Legacy/LegacyBarLine.cs diff --git a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableBarLine.cs b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableBarLine.cs index 25fed1a84c..be0f84d7fd 100644 --- a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableBarLine.cs +++ b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableBarLine.cs @@ -26,6 +26,7 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables : base(barLine) { RelativeSizeAxes = Axes.X; + AutoSizeAxes = Axes.Y; } [BackgroundDependencyLoader] @@ -36,8 +37,6 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables Anchor = Anchor.Centre, Origin = Anchor.Centre, }); - - Major.BindValueChanged(major => Height = major.NewValue ? 1.7f : 1.2f, true); } protected override void OnApply() diff --git a/osu.Game.Rulesets.Mania/Skinning/Default/DefaultBarLine.cs b/osu.Game.Rulesets.Mania/Skinning/Default/DefaultBarLine.cs index ef75e9df11..05fba1241f 100644 --- a/osu.Game.Rulesets.Mania/Skinning/Default/DefaultBarLine.cs +++ b/osu.Game.Rulesets.Mania/Skinning/Default/DefaultBarLine.cs @@ -24,7 +24,7 @@ namespace osu.Game.Rulesets.Mania.Skinning.Default [BackgroundDependencyLoader] private void load(DrawableHitObject drawableHitObject) { - RelativeSizeAxes = Axes.Both; + RelativeSizeAxes = Axes.X; // Avoid flickering due to no anti-aliasing of boxes by default. var edgeSmoothness = new Vector2(0.3f); @@ -75,6 +75,8 @@ namespace osu.Game.Rulesets.Mania.Skinning.Default private void updateMajor(ValueChangedEvent major) { + Height = major.NewValue ? 1.7f : 1.2f; + mainLine.Alpha = major.NewValue ? 0.5f : 0.2f; leftAnchor.Alpha = rightAnchor.Alpha = major.NewValue ? mainLine.Alpha * 0.3f : 0; } diff --git a/osu.Game.Rulesets.Mania/Skinning/Legacy/LegacyBarLine.cs b/osu.Game.Rulesets.Mania/Skinning/Legacy/LegacyBarLine.cs new file mode 100644 index 0000000000..64ea1df2ae --- /dev/null +++ b/osu.Game.Rulesets.Mania/Skinning/Legacy/LegacyBarLine.cs @@ -0,0 +1,33 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Allocation; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; +using osuTK; + +namespace osu.Game.Rulesets.Mania.Skinning.Legacy +{ + public partial class LegacyBarLine : CompositeDrawable + { + [BackgroundDependencyLoader] + private void load() + { + RelativeSizeAxes = Axes.X; + Height = 1.2f; + + // Avoid flickering due to no anti-aliasing of boxes by default. + var edgeSmoothness = new Vector2(0.3f); + + AddInternal(new Box + { + Name = "Bar line", + EdgeSmoothness = edgeSmoothness, + Anchor = Anchor.BottomCentre, + Origin = Anchor.BottomCentre, + RelativeSizeAxes = Axes.Both, + }); + } + } +} diff --git a/osu.Game.Rulesets.Mania/Skinning/Legacy/ManiaLegacySkinTransformer.cs b/osu.Game.Rulesets.Mania/Skinning/Legacy/ManiaLegacySkinTransformer.cs index 76af569b95..c321fcda87 100644 --- a/osu.Game.Rulesets.Mania/Skinning/Legacy/ManiaLegacySkinTransformer.cs +++ b/osu.Game.Rulesets.Mania/Skinning/Legacy/ManiaLegacySkinTransformer.cs @@ -163,7 +163,7 @@ namespace osu.Game.Rulesets.Mania.Skinning.Legacy return new LegacyStageForeground(); case ManiaSkinComponents.BarLine: - return null; // Not yet implemented. + return new LegacyBarLine(); default: throw new UnsupportedSkinComponentException(lookup); From cb29459a1e5c2d97a68a548c592ea3140513632d Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 28 Feb 2025 15:13:13 +0900 Subject: [PATCH 1149/1275] Add support for legacy osu!mania barline height and colour spec --- .../Objects/Drawables/DrawableBarLine.cs | 4 ++-- osu.Game.Rulesets.Mania/Skinning/Legacy/LegacyBarLine.cs | 9 +++++++-- osu.Game/Skinning/LegacyManiaSkinConfiguration.cs | 1 + osu.Game/Skinning/LegacyManiaSkinConfigurationLookup.cs | 3 +++ osu.Game/Skinning/LegacyManiaSkinDecoder.cs | 4 ++++ osu.Game/Skinning/LegacySkin.cs | 6 ++++++ 6 files changed, 23 insertions(+), 4 deletions(-) diff --git a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableBarLine.cs b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableBarLine.cs index be0f84d7fd..c9fc0763a8 100644 --- a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableBarLine.cs +++ b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableBarLine.cs @@ -26,10 +26,10 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables : base(barLine) { RelativeSizeAxes = Axes.X; - AutoSizeAxes = Axes.Y; + Height = 1; } - [BackgroundDependencyLoader] + [BackgroundDependencyLoader(true)] private void load() { AddInternal(new SkinnableDrawable(new ManiaSkinComponentLookup(ManiaSkinComponents.BarLine), _ => new DefaultBarLine()) diff --git a/osu.Game.Rulesets.Mania/Skinning/Legacy/LegacyBarLine.cs b/osu.Game.Rulesets.Mania/Skinning/Legacy/LegacyBarLine.cs index 64ea1df2ae..ce48c49b2e 100644 --- a/osu.Game.Rulesets.Mania/Skinning/Legacy/LegacyBarLine.cs +++ b/osu.Game.Rulesets.Mania/Skinning/Legacy/LegacyBarLine.cs @@ -5,17 +5,22 @@ using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; +using osu.Game.Skinning; using osuTK; +using osuTK.Graphics; namespace osu.Game.Rulesets.Mania.Skinning.Legacy { public partial class LegacyBarLine : CompositeDrawable { [BackgroundDependencyLoader] - private void load() + private void load(ISkinSource skin) { + float skinHeight = skin.GetManiaSkinConfig(LegacyManiaSkinConfigurationLookups.BarLineHeight)?.Value ?? 1; + RelativeSizeAxes = Axes.X; - Height = 1.2f; + Height = 1.2f * skinHeight; + Colour = skin.GetManiaSkinConfig(LegacyManiaSkinConfigurationLookups.BarLineColour)?.Value ?? Color4.White; // Avoid flickering due to no anti-aliasing of boxes by default. var edgeSmoothness = new Vector2(0.3f); diff --git a/osu.Game/Skinning/LegacyManiaSkinConfiguration.cs b/osu.Game/Skinning/LegacyManiaSkinConfiguration.cs index db1f216b6e..1e6fa44e68 100644 --- a/osu.Game/Skinning/LegacyManiaSkinConfiguration.cs +++ b/osu.Game/Skinning/LegacyManiaSkinConfiguration.cs @@ -41,6 +41,7 @@ namespace osu.Game.Skinning public float LightPosition = (480 - 413) * POSITION_SCALE_FACTOR; public float ComboPosition = 111 * POSITION_SCALE_FACTOR; public float ScorePosition = 300 * POSITION_SCALE_FACTOR; + public float BarLineHeight = 1; public bool ShowJudgementLine = true; public bool KeysUnderNotes; public int LightFramePerSecond = 60; diff --git a/osu.Game/Skinning/LegacyManiaSkinConfigurationLookup.cs b/osu.Game/Skinning/LegacyManiaSkinConfigurationLookup.cs index ee354de68b..e94fb23681 100644 --- a/osu.Game/Skinning/LegacyManiaSkinConfigurationLookup.cs +++ b/osu.Game/Skinning/LegacyManiaSkinConfigurationLookup.cs @@ -70,6 +70,9 @@ namespace osu.Game.Skinning RightStageImage, BottomStageImage, + BarLineHeight, + BarLineColour, + // ReSharper disable once InconsistentNaming Hit300g, diff --git a/osu.Game/Skinning/LegacyManiaSkinDecoder.cs b/osu.Game/Skinning/LegacyManiaSkinDecoder.cs index 09866ef237..2739743387 100644 --- a/osu.Game/Skinning/LegacyManiaSkinDecoder.cs +++ b/osu.Game/Skinning/LegacyManiaSkinDecoder.cs @@ -86,6 +86,10 @@ namespace osu.Game.Skinning parseArrayValue(pair.Value, currentConfig.ColumnWidth); break; + case "BarlineHeight": + currentConfig.BarLineHeight = float.Parse(pair.Value, CultureInfo.InvariantCulture); + break; + case "HitPosition": currentConfig.HitPosition = (480 - Math.Clamp(float.Parse(pair.Value, CultureInfo.InvariantCulture), 240, 480)) * LegacyManiaSkinConfiguration.POSITION_SCALE_FACTOR; break; diff --git a/osu.Game/Skinning/LegacySkin.cs b/osu.Game/Skinning/LegacySkin.cs index 08fa068830..51c1473303 100644 --- a/osu.Game/Skinning/LegacySkin.cs +++ b/osu.Game/Skinning/LegacySkin.cs @@ -198,9 +198,15 @@ namespace osu.Game.Skinning case LegacyManiaSkinConfigurationLookups.ComboBreakColour: return SkinUtils.As(getCustomColour(existing, "ColourBreak")); + case LegacyManiaSkinConfigurationLookups.BarLineColour: + return SkinUtils.As(getCustomColour(existing, "ColourBarline")); + case LegacyManiaSkinConfigurationLookups.MinimumColumnWidth: return SkinUtils.As(new Bindable(existing.MinimumColumnWidth)); + case LegacyManiaSkinConfigurationLookups.BarLineHeight: + return SkinUtils.As(new Bindable(existing.BarLineHeight)); + case LegacyManiaSkinConfigurationLookups.NoteBodyStyle: if (existing.NoteBodyStyle != null) From 306b30cb12238b48e2259d4611185821701d34a9 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Fri, 28 Feb 2025 15:51:54 +0900 Subject: [PATCH 1150/1275] Add failing test --- .../TestSceneMultiplayerMatchSubScreen.cs | 23 +++++++++++++++++++ .../OnlinePlay/Match/DrawableMatchRoom.cs | 9 ++++---- 2 files changed, 28 insertions(+), 4 deletions(-) diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSubScreen.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSubScreen.cs index e95209f993..7058532196 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSubScreen.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSubScreen.cs @@ -317,6 +317,29 @@ namespace osu.Game.Tests.Visual.Multiplayer AddAssert("score multiplier = 1.20", () => this.ChildrenOfType().Single().ModMultiplier.Value, () => Is.EqualTo(1.2).Within(0.01)); } + [Test] + public void TestChangeSettingsButtonVisibleForHost() + { + AddStep("add playlist item", () => + { + SelectedRoom.Value!.Playlist = + [ + new PlaylistItem(new TestBeatmap(new OsuRuleset().RulesetInfo).BeatmapInfo) + { + RulesetID = new OsuRuleset().RulesetInfo.OnlineID + } + ]; + }); + ClickButtonWhenEnabled(); + + AddUntilStep("wait for join", () => RoomJoined); + + AddUntilStep("button visible", () => this.ChildrenOfType().Single().ChangeSettingsButton?.Alpha, () => Is.GreaterThan(0)); + AddStep("join other user", void () => MultiplayerClient.AddUser(new APIUser { Id = PLAYER_1_ID })); + AddStep("make other user host", () => MultiplayerClient.TransferHost(PLAYER_1_ID)); + AddAssert("button hidden", () => this.ChildrenOfType().Single().ChangeSettingsButton?.Alpha, () => Is.EqualTo(0)); + } + private partial class TestMultiplayerMatchSubScreen : MultiplayerMatchSubScreen { [Resolved(canBeNull: true)] diff --git a/osu.Game/Screens/OnlinePlay/Match/DrawableMatchRoom.cs b/osu.Game/Screens/OnlinePlay/Match/DrawableMatchRoom.cs index 08bcf32edf..b10e83a05c 100644 --- a/osu.Game/Screens/OnlinePlay/Match/DrawableMatchRoom.cs +++ b/osu.Game/Screens/OnlinePlay/Match/DrawableMatchRoom.cs @@ -25,12 +25,13 @@ namespace osu.Game.Screens.OnlinePlay.Match set => selectedItem.Current = value; } + public Drawable? ChangeSettingsButton { get; private set; } + [Resolved] private IAPIProvider api { get; set; } = null!; private readonly BindableWithCurrent selectedItem = new BindableWithCurrent(); private readonly bool allowEdit; - private Drawable? editButton; public DrawableMatchRoom(Room room, bool allowEdit = true) : base(room) @@ -45,7 +46,7 @@ namespace osu.Game.Screens.OnlinePlay.Match { if (allowEdit) { - ButtonsContainer.Add(editButton = new PurpleRoundedButton + ButtonsContainer.Add(ChangeSettingsButton = new PurpleRoundedButton { RelativeSizeAxes = Axes.Y, Anchor = Anchor.Centre, @@ -73,8 +74,8 @@ namespace osu.Game.Screens.OnlinePlay.Match private void updateRoomHost() { - if (editButton != null) - editButton.Alpha = Room.Host?.Equals(api.LocalUser.Value) == true ? 1 : 0; + if (ChangeSettingsButton != null) + ChangeSettingsButton.Alpha = Room.Host?.Equals(api.LocalUser.Value) == true ? 1 : 0; } protected override UpdateableBeatmapBackgroundSprite CreateBackground() => base.CreateBackground().With(d => From a09ef5d96d0bcd9c56ccd1eb6747fa5ba6d0e449 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Fri, 28 Feb 2025 15:52:02 +0900 Subject: [PATCH 1151/1275] Fix API room host not being populated --- osu.Game/Online/Multiplayer/MultiplayerClient.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/osu.Game/Online/Multiplayer/MultiplayerClient.cs b/osu.Game/Online/Multiplayer/MultiplayerClient.cs index 1f85aa5d45..3c627c7a47 100644 --- a/osu.Game/Online/Multiplayer/MultiplayerClient.cs +++ b/osu.Game/Online/Multiplayer/MultiplayerClient.cs @@ -222,6 +222,8 @@ namespace osu.Game.Online.Multiplayer { // Populate users. await PopulateUsers(joinedRoom.Users).ConfigureAwait(false); + if (joinedRoom.Host != null) + await PopulateUsers([joinedRoom.Host]).ConfigureAwait(false); // Update the stored room (must be done on update thread for thread-safety). await runOnUpdateThreadAsync(() => @@ -233,6 +235,7 @@ namespace osu.Game.Online.Multiplayer APIRoom = apiRoom; APIRoom.RoomID = joinedRoom.RoomID; + APIRoom.Host = joinedRoom.Host?.User; APIRoom.Playlist = joinedRoom.Playlist.Select(item => new PlaylistItem(item)).ToArray(); APIRoom.CurrentPlaylistItem = APIRoom.Playlist.Single(item => item.ID == joinedRoom.Settings.PlaylistItemId); // The server will null out the end date upon the host joining the room, but the null value is never communicated to the client. From 02b950223c055aad3e192cdff99d56f2c5b2c83f Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 28 Feb 2025 16:06:12 +0900 Subject: [PATCH 1152/1275] Adjust x offsets to work again for keyboard selection --- osu.Game/Screens/SelectV2/PanelBase.cs | 13 ++++++------- osu.Game/Screens/SelectV2/PanelBeatmap.cs | 2 -- 2 files changed, 6 insertions(+), 9 deletions(-) diff --git a/osu.Game/Screens/SelectV2/PanelBase.cs b/osu.Game/Screens/SelectV2/PanelBase.cs index 805cbac8eb..1e47401013 100644 --- a/osu.Game/Screens/SelectV2/PanelBase.cs +++ b/osu.Game/Screens/SelectV2/PanelBase.cs @@ -23,8 +23,6 @@ namespace osu.Game.Screens.SelectV2 { private const float corner_radius = 10; - private const float left_edge_x_offset = 20f; - private const float keyboard_active_x_offset = 25f; private const float active_x_offset = 50f; private const float duration = 500; @@ -162,6 +160,7 @@ namespace osu.Game.Screens.SelectV2 base.LoadComplete(); Expanded.BindValueChanged(_ => updateDisplay()); + Selected.BindValueChanged(_ => updateDisplay()); KeyboardSelected.BindValueChanged(_ => updateDisplay(), true); } @@ -199,13 +198,13 @@ namespace osu.Game.Screens.SelectV2 private void updateXOffset() { - float x = PanelXOffset + active_x_offset + keyboard_active_x_offset + left_edge_x_offset; + float x = PanelXOffset; - if (Expanded.Value) - x -= active_x_offset; + if (!Expanded.Value && !Selected.Value) + x += active_x_offset; - if (KeyboardSelected.Value) - x -= keyboard_active_x_offset; + if (!KeyboardSelected.Value) + x += active_x_offset * 0.5f; this.TransformTo(nameof(Padding), new MarginPadding { Left = x }, duration, Easing.OutQuint); } diff --git a/osu.Game/Screens/SelectV2/PanelBeatmap.cs b/osu.Game/Screens/SelectV2/PanelBeatmap.cs index b27e5cae14..0ce6b1a9a2 100644 --- a/osu.Game/Screens/SelectV2/PanelBeatmap.cs +++ b/osu.Game/Screens/SelectV2/PanelBeatmap.cs @@ -163,8 +163,6 @@ namespace osu.Game.Screens.SelectV2 computeStarRating(); updateKeyCount(); }, true); - - Selected.BindValueChanged(s => Expanded.Value = s.NewValue, true); } protected override void PrepareForUse() From a8fbac0f0dbf628ee284e9b3c27554d00697f1e8 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 28 Feb 2025 16:27:18 +0900 Subject: [PATCH 1153/1275] Add better selection visibility via another tint layer --- osu.Game/Screens/SelectV2/PanelBase.cs | 53 +++++++++++++++++++++----- 1 file changed, 44 insertions(+), 9 deletions(-) diff --git a/osu.Game/Screens/SelectV2/PanelBase.cs b/osu.Game/Screens/SelectV2/PanelBase.cs index 1e47401013..d3132a106e 100644 --- a/osu.Game/Screens/SelectV2/PanelBase.cs +++ b/osu.Game/Screens/SelectV2/PanelBase.cs @@ -38,6 +38,8 @@ namespace osu.Game.Screens.SelectV2 private Container iconContainer = null!; private Box activationFlash = null!; private Box hoverLayer = null!; + private Box keyboardSelectionLayer = null!; + private Box selectionLayer = null!; public Container TopLevelContent { get; private set; } = null!; @@ -137,6 +139,24 @@ namespace osu.Game.Screens.SelectV2 hoverLayer = new Box { Alpha = 0, + Colour = colours.Blue.Opacity(0.1f), + Blending = BlendingParameters.Additive, + RelativeSizeAxes = Axes.Both, + }, + selectionLayer = new Box + { + Alpha = 0, + Colour = ColourInfo.GradientHorizontal(colours.Yellow.Opacity(0), colours.Yellow.Opacity(0.5f)), + Blending = BlendingParameters.Additive, + RelativeSizeAxes = Axes.Both, + Width = 0.7f, + Anchor = Anchor.TopRight, + Origin = Anchor.TopRight, + }, + keyboardSelectionLayer = new Box + { + Alpha = 0, + Colour = colours.Yellow.Opacity(0.1f), Blending = BlendingParameters.Additive, RelativeSizeAxes = Axes.Both, }, @@ -151,7 +171,6 @@ namespace osu.Game.Screens.SelectV2 } }; - hoverLayer.Colour = colours.Blue.Opacity(0.1f); backgroundGradient.Colour = ColourInfo.GradientHorizontal(colourProvider.Background3, colourProvider.Background4); } @@ -159,9 +178,27 @@ namespace osu.Game.Screens.SelectV2 { base.LoadComplete(); - Expanded.BindValueChanged(_ => updateDisplay()); - Selected.BindValueChanged(_ => updateDisplay()); - KeyboardSelected.BindValueChanged(_ => updateDisplay(), true); + Expanded.BindValueChanged(_ => updateDisplay(), true); + + Selected.BindValueChanged(selected => + { + if (selected.NewValue) + selectionLayer.FadeIn(100, Easing.OutQuint); + else + selectionLayer.FadeOut(200, Easing.OutQuint); + + updateXOffset(); + }, true); + + KeyboardSelected.BindValueChanged(selected => + { + if (selected.NewValue) + keyboardSelectionLayer.FadeIn(100, Easing.OutQuint); + else + keyboardSelectionLayer.FadeOut(1000, Easing.OutQuint); + + updateXOffset(); + }, true); } protected override void PrepareForUse() @@ -211,9 +248,7 @@ namespace osu.Game.Screens.SelectV2 private void updateHover() { - bool hovered = IsHovered || KeyboardSelected.Value; - - if (hovered) + if (IsHovered) hoverLayer.FadeIn(100, Easing.OutQuint); else hoverLayer.FadeOut(1000, Easing.OutQuint); @@ -221,13 +256,13 @@ namespace osu.Game.Screens.SelectV2 protected override bool OnHover(HoverEvent e) { - updateDisplay(); + updateHover(); return true; } protected override void OnHoverLost(HoverLostEvent e) { - updateDisplay(); + updateHover(); base.OnHoverLost(e); } From 1e46dc6b0a23cf2fa9677104b9101d8f3f94a18d Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 28 Feb 2025 16:27:42 +0900 Subject: [PATCH 1154/1275] Adjust animation duration to roughly match scroll operations Previous value felt wrong when using keyboard selection for iteration. --- osu.Game/Screens/SelectV2/PanelBase.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/SelectV2/PanelBase.cs b/osu.Game/Screens/SelectV2/PanelBase.cs index d3132a106e..2a32b1a95f 100644 --- a/osu.Game/Screens/SelectV2/PanelBase.cs +++ b/osu.Game/Screens/SelectV2/PanelBase.cs @@ -25,7 +25,7 @@ namespace osu.Game.Screens.SelectV2 private const float active_x_offset = 50f; - private const float duration = 500; + private const float duration = 400; protected float PanelXOffset { get; init; } From 51cb0bea1ce61ffd3ca8b3bdb641f8f4840601d1 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 28 Feb 2025 16:45:49 +0900 Subject: [PATCH 1155/1275] Fix carousel taking up too much space on new song select implementation --- osu.Game/Screens/SelectV2/SongSelectV2.cs | 29 +++++++++++++++++------ 1 file changed, 22 insertions(+), 7 deletions(-) diff --git a/osu.Game/Screens/SelectV2/SongSelectV2.cs b/osu.Game/Screens/SelectV2/SongSelectV2.cs index 3943d059f9..23139c8742 100644 --- a/osu.Game/Screens/SelectV2/SongSelectV2.cs +++ b/osu.Game/Screens/SelectV2/SongSelectV2.cs @@ -39,17 +39,32 @@ namespace osu.Game.Screens.SelectV2 { AddRangeInternal(new Drawable[] { - new Container + new GridContainer // used for max width implementation { + Anchor = Anchor.TopRight, + Origin = Anchor.TopRight, RelativeSizeAxes = Axes.Both, - Padding = new MarginPadding { Bottom = ScreenFooter.HEIGHT }, - Child = new BeatmapCarousel + ColumnDimensions = new[] { - Anchor = Anchor.TopRight, - Origin = Anchor.TopRight, - RelativeSizeAxes = Axes.Both, - Width = 0.6f, + new Dimension(), + new Dimension(GridSizeMode.Relative, 0.5f, maxSize: 750), }, + Content = new[] + { + new[] + { + Empty(), + new Container + { + RelativeSizeAxes = Axes.Both, + Padding = new MarginPadding { Bottom = ScreenFooter.HEIGHT }, + Child = new BeatmapCarousel + { + RelativeSizeAxes = Axes.Both + }, + }, + } + } }, modSelectOverlay, }); From 0e257038e8b49400f5082570d5867c4c7ef23c3b Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 28 Feb 2025 16:47:57 +0900 Subject: [PATCH 1156/1275] Fix status pills displaying wrong --- osu.Game/Beatmaps/Drawables/BeatmapSetOnlineStatusPill.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Beatmaps/Drawables/BeatmapSetOnlineStatusPill.cs b/osu.Game/Beatmaps/Drawables/BeatmapSetOnlineStatusPill.cs index 599d1b380a..7b99ad40de 100644 --- a/osu.Game/Beatmaps/Drawables/BeatmapSetOnlineStatusPill.cs +++ b/osu.Game/Beatmaps/Drawables/BeatmapSetOnlineStatusPill.cs @@ -101,7 +101,7 @@ namespace osu.Game.Beatmaps.Drawables { if (Status == BeatmapOnlineStatus.None) { - this.FadeOut(animation_duration, Easing.OutQuint); + Hide(); return; } From 8fc744e9dc7d0045232a6c1eda3c17160c366947 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 28 Feb 2025 17:55:11 +0900 Subject: [PATCH 1157/1275] Make `TestSceneSongSelect` work with local database It was pointless before. --- .../SongSelectV2/TestSceneSongSelect.cs | 36 ++----------------- 1 file changed, 3 insertions(+), 33 deletions(-) diff --git a/osu.Game.Tests/Visual/SongSelectV2/TestSceneSongSelect.cs b/osu.Game.Tests/Visual/SongSelectV2/TestSceneSongSelect.cs index 33474d7449..6d180c76d9 100644 --- a/osu.Game.Tests/Visual/SongSelectV2/TestSceneSongSelect.cs +++ b/osu.Game.Tests/Visual/SongSelectV2/TestSceneSongSelect.cs @@ -9,16 +9,10 @@ using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Cursor; -using osu.Framework.Platform; using osu.Framework.Screens; using osu.Framework.Testing; -using osu.Game.Beatmaps; -using osu.Game.Configuration; using osu.Game.Database; -using osu.Game.Online.API; -using osu.Game.Overlays; using osu.Game.Overlays.Mods; -using osu.Game.Rulesets; using osu.Game.Rulesets.Catch; using osu.Game.Rulesets.Mania; using osu.Game.Rulesets.Mods; @@ -29,7 +23,6 @@ using osu.Game.Screens; using osu.Game.Screens.Footer; using osu.Game.Screens.Menu; using osu.Game.Screens.SelectV2.Footer; -using osu.Game.Tests.Resources; using osuTK.Input; namespace osu.Game.Tests.Visual.SongSelectV2 @@ -42,8 +35,6 @@ namespace osu.Game.Tests.Visual.SongSelectV2 [Cached] private readonly OsuLogo logo; - private BeatmapManager beatmapManager = null!; - protected override bool UseOnlineAPI => true; public TestSceneSongSelect() @@ -66,32 +57,12 @@ namespace osu.Game.Tests.Visual.SongSelectV2 } [BackgroundDependencyLoader] - private void load(GameHost host, IAPIProvider onlineAPI) + private void load() { - BeatmapStore beatmapStore; - BeatmapUpdater beatmapUpdater; - BeatmapDifficultyCache difficultyCache; + RealmDetachedBeatmapStore beatmapStore; - // These DI caches are required to ensure for interactive runs this test scene doesn't nuke all user beatmaps in the local install. - // At a point we have isolated interactive test runs enough, this can likely be removed. - Dependencies.Cache(new RealmRulesetStore(Realm)); - Dependencies.Cache(Realm); - Dependencies.Cache(difficultyCache = new BeatmapDifficultyCache()); - Dependencies.Cache(beatmapManager = new BeatmapManager(LocalStorage, Realm, onlineAPI, Audio, Resources, host, Beatmap.Default, difficultyCache)); - Dependencies.CacheAs(beatmapUpdater = new BeatmapUpdater(beatmapManager, difficultyCache, onlineAPI, LocalStorage)); - Dependencies.CacheAs(beatmapStore = new RealmDetachedBeatmapStore()); - - beatmapManager.ProcessBeatmap = (set, scope) => beatmapUpdater.Process(set, scope); - - MusicController music; - Dependencies.Cache(music = new MusicController()); - - // required to get bindables attached - Add(difficultyCache); - Add(music); + Dependencies.CacheAs(beatmapStore = new RealmDetachedBeatmapStore()); Add(beatmapStore); - - Dependencies.Cache(new OsuConfigManager(LocalStorage)); } protected override void LoadComplete() @@ -109,7 +80,6 @@ namespace osu.Game.Tests.Visual.SongSelectV2 AddStep("load screen", () => Stack.Push(new Screens.SelectV2.SongSelectV2())); AddUntilStep("wait for load", () => Stack.CurrentScreen is Screens.SelectV2.SongSelectV2 songSelect && songSelect.IsLoaded); - AddStep("import test beatmap", () => beatmapManager.Import(TestResources.GetTestBeatmapForImport())); } [Test] From 993473c0810e55ce0b1143f0f147e88d10c65396 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Fri, 28 Feb 2025 18:40:54 +0900 Subject: [PATCH 1158/1275] Pass through artist/title in beatmap transform --- .../Screens/OnlinePlay/Playlists/PlaylistItemResultsScreen.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistItemResultsScreen.cs b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistItemResultsScreen.cs index 572bf535f7..184de2f50c 100644 --- a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistItemResultsScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistItemResultsScreen.cs @@ -223,6 +223,8 @@ namespace osu.Game.Screens.OnlinePlay.Playlists Difficulty = new BeatmapDifficulty(beatmap.Difficulty), Metadata = { + Artist = beatmap.Metadata.Artist, + Title = beatmap.Metadata.Title, Author = new RealmUser { Username = beatmap.Metadata.Author.Username, From ffef6ae1853d84120abf52f3c93382b4863bd556 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 28 Feb 2025 13:34:00 +0100 Subject: [PATCH 1159/1275] Fix possible crash when scaling objects in editor The specific fail case here is when `s.{X,Y}` is 0, and `s{Lower,Upper}Bound` is `Infinity`. Because IEEE math is IEEE math, `0 * Infinity` is `NaN`. `MathHelper.Clamp()` is written the following way: https://github.com/ppy/osuTK/blob/af742f1afd01828efc7bc9fe77536b54aab8b419/src/osuTK/Math/MathHelper.cs#L284-L306 `Math.{Min,Max}` are both documented as reporting `NaN` when any of their operands are `NaN`: https://learn.microsoft.com/en-us/dotnet/api/system.math.min?view=net-8.0#system-math-min(system-single-system-single) https://learn.microsoft.com/en-us/dotnet/api/system.math.max?view=net-8.0#system-math-max(system-single-system-single) which means that if a `NaN` happens to sneak into the bounds, it will start spreading outwards in an uncontrolled manner, and likely crash things. In contrast, the standard library provided `Math.Clamp()` is written like so: https://github.com/dotnet/runtime/blob/577c36cee56480dec4d4610b35605b5d5836888b/src/libraries/System.Private.CoreLib/src/System/Math.cs#L711-L729 With this implementation, if either bound is `NaN`, it will essentially not be checked (because any and all comparisons involving `NaN` return false). This prevents the spread of `NaN`s, all the way to positions of hitobjects, and thus fixes the crash. --- .../Edit/OsuSelectionScaleHandler.cs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Edit/OsuSelectionScaleHandler.cs b/osu.Game.Rulesets.Osu/Edit/OsuSelectionScaleHandler.cs index e3ab95c402..4c3db207f2 100644 --- a/osu.Game.Rulesets.Osu/Edit/OsuSelectionScaleHandler.cs +++ b/osu.Game.Rulesets.Osu/Edit/OsuSelectionScaleHandler.cs @@ -263,12 +263,12 @@ namespace osu.Game.Rulesets.Osu.Edit { case Axes.X: (sLowerBound, sUpperBound) = computeBounds(lowerBounds - b, upperBounds - b, a); - s.X = MathHelper.Clamp(s.X, sLowerBound, sUpperBound); + s.X = Math.Clamp(s.X, sLowerBound, sUpperBound); break; case Axes.Y: (sLowerBound, sUpperBound) = computeBounds(lowerBounds - a, upperBounds - a, b); - s.Y = MathHelper.Clamp(s.Y, sLowerBound, sUpperBound); + s.Y = Math.Clamp(s.Y, sLowerBound, sUpperBound); break; case Axes.Both: @@ -276,11 +276,11 @@ namespace osu.Game.Rulesets.Osu.Edit // Therefore the ratio s.X / s.Y will be maintained (sLowerBound, sUpperBound) = computeBounds(lowerBounds, upperBounds, a * s.X + b * s.Y); s.X = s.X < 0 - ? MathHelper.Clamp(s.X, s.X * sUpperBound, s.X * sLowerBound) - : MathHelper.Clamp(s.X, s.X * sLowerBound, s.X * sUpperBound); + ? Math.Clamp(s.X, s.X * sUpperBound, s.X * sLowerBound) + : Math.Clamp(s.X, s.X * sLowerBound, s.X * sUpperBound); s.Y = s.Y < 0 - ? MathHelper.Clamp(s.Y, s.Y * sUpperBound, s.Y * sLowerBound) - : MathHelper.Clamp(s.Y, s.Y * sLowerBound, s.Y * sUpperBound); + ? Math.Clamp(s.Y, s.Y * sUpperBound, s.Y * sLowerBound) + : Math.Clamp(s.Y, s.Y * sLowerBound, s.Y * sUpperBound); break; } From 35b0ff80bb6094a32d9c5c2b93203faf491b68fd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 28 Feb 2025 13:41:56 +0100 Subject: [PATCH 1160/1275] Mark `MathHelper.Clamp()` as banned API See previous commit for partial rationale. There's an argument to be made about the `NaN`-spreading semantics being desirable because at least something will loudly fail in that case, but I'm not so sure about that these days. It feels like either way if `NaN`s are produced, then things are outside of any control, and chances are the game can probably continue without crashing. And, this move reduces our dependence on osuTK, which has already been living on borrowed time for years now and is only awaiting someone brave to go excise it. --- CodeAnalysis/BannedSymbols.txt | 3 +++ .../Beatmaps/PippidonBeatmapConverter.cs | 4 ++-- .../Skinning/Argon/ArgonBananaPiece.cs | 3 ++- .../HitCircles/Components/HitCircleOverlapMarker.cs | 3 ++- .../Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs | 4 ++-- osu.Game/Overlays/NotificationOverlayToastTray.cs | 2 +- osu.Game/Screens/Play/HUDOverlay.cs | 6 +++--- osu.Game/Screens/Utility/CircleGameplay.cs | 4 ++-- .../Utility/SampleComponents/LatencyMovableBox.cs | 9 +++++---- osu.Game/Screens/Utility/ScrollingGameplay.cs | 2 +- 10 files changed, 23 insertions(+), 17 deletions(-) diff --git a/CodeAnalysis/BannedSymbols.txt b/CodeAnalysis/BannedSymbols.txt index 550f7c8e11..08b79fc2c0 100644 --- a/CodeAnalysis/BannedSymbols.txt +++ b/CodeAnalysis/BannedSymbols.txt @@ -18,3 +18,6 @@ M:Humanizer.InflectorExtensions.Pascalize(System.String);Humanizer's .Pascalize( M:Humanizer.InflectorExtensions.Camelize(System.String);Humanizer's .Camelize() extension method changes behaviour depending on CultureInfo.CurrentCulture. Use StringDehumanizeExtensions.ToCamelCase() instead. M:Humanizer.InflectorExtensions.Underscore(System.String);Humanizer's .Underscore() extension method changes behaviour depending on CultureInfo.CurrentCulture. Use StringDehumanizeExtensions.ToSnakeCase() instead. M:Humanizer.InflectorExtensions.Kebaberize(System.String);Humanizer's .Kebaberize() extension method changes behaviour depending on CultureInfo.CurrentCulture. Use StringDehumanizeExtensions.ToKebabCase() instead. +M:osuTK.MathHelper.Clamp(System.Int32,System.Int32,System.Int32)~System.Int32;Use Math.Clamp() instead. +M:osuTK.MathHelper.Clamp(System.Single,System.Single,System.Single)~System.Single;This osuTK helper has unsafe semantics when one of the bounds provided is NaN. Use Math.Clamp() instead. +M:osuTK.MathHelper.Clamp(System.Double,System.Double,System.Double)~System.Double;This osuTK helper has unsafe semantics when one of the bounds provided is NaN. Use Math.Clamp() instead. diff --git a/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon/Beatmaps/PippidonBeatmapConverter.cs b/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon/Beatmaps/PippidonBeatmapConverter.cs index 0a4fa84ce1..dd8337abee 100644 --- a/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon/Beatmaps/PippidonBeatmapConverter.cs +++ b/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon/Beatmaps/PippidonBeatmapConverter.cs @@ -1,6 +1,7 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System; using System.Collections.Generic; using System.Linq; using System.Threading; @@ -9,7 +10,6 @@ using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects.Types; using osu.Game.Rulesets.Pippidon.Objects; using osu.Game.Rulesets.Pippidon.UI; -using osuTK; namespace osu.Game.Rulesets.Pippidon.Beatmaps { @@ -40,7 +40,7 @@ namespace osu.Game.Rulesets.Pippidon.Beatmaps }; } - private int getLane(HitObject hitObject) => (int)MathHelper.Clamp( + private int getLane(HitObject hitObject) => (int)Math.Clamp( (getUsablePosition(hitObject) - minPosition) / (maxPosition - minPosition) * PippidonPlayfield.LANE_COUNT, 0, PippidonPlayfield.LANE_COUNT - 1); private float getUsablePosition(HitObject h) => (h as IHasYPosition)?.Y ?? ((IHasXPosition)h).X; diff --git a/osu.Game.Rulesets.Catch/Skinning/Argon/ArgonBananaPiece.cs b/osu.Game.Rulesets.Catch/Skinning/Argon/ArgonBananaPiece.cs index 8cdb490922..810dc7eed5 100644 --- a/osu.Game.Rulesets.Catch/Skinning/Argon/ArgonBananaPiece.cs +++ b/osu.Game.Rulesets.Catch/Skinning/Argon/ArgonBananaPiece.cs @@ -1,6 +1,7 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System; using osu.Framework.Allocation; using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; @@ -110,7 +111,7 @@ namespace osu.Game.Rulesets.Catch.Skinning.Argon double duration = ObjectState.HitObject.StartTime - ObjectState.DisplayStartTime; - fadeContent.Alpha = MathHelper.Clamp( + fadeContent.Alpha = Math.Clamp( Interpolation.ValueAt( Time.Current, 1f, 0f, ObjectState.DisplayStartTime + duration * lens_flare_start, diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/HitCircles/Components/HitCircleOverlapMarker.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/HitCircles/Components/HitCircleOverlapMarker.cs index 8ed9d0476a..7a5b01ce79 100644 --- a/osu.Game.Rulesets.Osu/Edit/Blueprints/HitCircles/Components/HitCircleOverlapMarker.cs +++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/HitCircles/Components/HitCircleOverlapMarker.cs @@ -3,6 +3,7 @@ #nullable disable +using System; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; @@ -76,7 +77,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.HitCircles.Components if (hasReachedObject && showHitMarkers.Value) { float alpha = Interpolation.ValueAt(editorTime, 0, 1f, hitObjectTime, hitObjectTime + FADE_OUT_EXTENSION, Easing.In); - float ringScale = MathHelper.Clamp(Interpolation.ValueAt(editorTime, 0, 1f, hitObjectTime, hitObjectTime + FADE_OUT_EXTENSION / 2, Easing.OutQuint), 0, 1); + float ringScale = Math.Clamp(Interpolation.ValueAt(editorTime, 0, 1f, hitObjectTime, hitObjectTime + FADE_OUT_EXTENSION / 2, Easing.OutQuint), 0, 1); ring.Scale = new Vector2(1 + 0.1f * ringScale); content.Alpha = 0.9f * (1 - alpha); diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs index 39c0681dba..52575bdd67 100644 --- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs +++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs @@ -270,14 +270,14 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders if (adjustVelocity) { proposedVelocity = proposedDistance / oldDuration; - proposedDistance = MathHelper.Clamp(proposedDistance, 0.1 * oldDuration, 10 * oldDuration); + proposedDistance = Math.Clamp(proposedDistance, 0.1 * oldDuration, 10 * oldDuration); } else { double minDistance = distanceSnapProvider?.GetBeatSnapDistance() * oldVelocityMultiplier ?? 1; // Add a small amount to the proposed distance to make it easier to snap to the full length of the slider. proposedDistance = distanceSnapProvider?.FindSnappedDistance((float)proposedDistance + 1, HitObject.StartTime, HitObject) ?? proposedDistance; - proposedDistance = MathHelper.Clamp(proposedDistance, minDistance, HitObject.Path.CalculatedDistance); + proposedDistance = Math.Clamp(proposedDistance, minDistance, HitObject.Path.CalculatedDistance); } if (Precision.AlmostEquals(proposedDistance, HitObject.Path.Distance) && Precision.AlmostEquals(proposedVelocity, HitObject.SliderVelocityMultiplier)) diff --git a/osu.Game/Overlays/NotificationOverlayToastTray.cs b/osu.Game/Overlays/NotificationOverlayToastTray.cs index ddb2e02fb8..dd60e303f6 100644 --- a/osu.Game/Overlays/NotificationOverlayToastTray.cs +++ b/osu.Game/Overlays/NotificationOverlayToastTray.cs @@ -174,7 +174,7 @@ namespace osu.Game.Overlays } height = toastFlow.DrawHeight + 120; - alpha = MathHelper.Clamp(toastFlow.DrawHeight / 41, 0, 1) * maxNotificationAlpha; + alpha = Math.Clamp(toastFlow.DrawHeight / 41, 0, 1) * maxNotificationAlpha; } toastContentBackground.Height = (float)Interpolation.DampContinuously(toastContentBackground.Height, height, 10, Clock.ElapsedFrameTime); diff --git a/osu.Game/Screens/Play/HUDOverlay.cs b/osu.Game/Screens/Play/HUDOverlay.cs index 8bfa8dd6ff..19190ac362 100644 --- a/osu.Game/Screens/Play/HUDOverlay.cs +++ b/osu.Game/Screens/Play/HUDOverlay.cs @@ -278,17 +278,17 @@ namespace osu.Game.Screens.Play processDrawables(rulesetComponents); if (lowestTopScreenSpaceRight.HasValue) - TopRightElements.Y = MathHelper.Clamp(ToLocalSpace(new Vector2(0, lowestTopScreenSpaceRight.Value)).Y, 0, DrawHeight - TopRightElements.DrawHeight); + TopRightElements.Y = Math.Clamp(ToLocalSpace(new Vector2(0, lowestTopScreenSpaceRight.Value)).Y, 0, DrawHeight - TopRightElements.DrawHeight); else TopRightElements.Y = 0; if (lowestTopScreenSpaceLeft.HasValue) - LeaderboardFlow.Y = MathHelper.Clamp(ToLocalSpace(new Vector2(0, lowestTopScreenSpaceLeft.Value)).Y, 0, DrawHeight - LeaderboardFlow.DrawHeight); + LeaderboardFlow.Y = Math.Clamp(ToLocalSpace(new Vector2(0, lowestTopScreenSpaceLeft.Value)).Y, 0, DrawHeight - LeaderboardFlow.DrawHeight); else LeaderboardFlow.Y = 0; if (highestBottomScreenSpace.HasValue) - bottomRightElements.Y = BottomScoringElementsHeight = -MathHelper.Clamp(DrawHeight - ToLocalSpace(highestBottomScreenSpace.Value).Y, 0, DrawHeight - bottomRightElements.DrawHeight); + bottomRightElements.Y = BottomScoringElementsHeight = -Math.Clamp(DrawHeight - ToLocalSpace(highestBottomScreenSpace.Value).Y, 0, DrawHeight - bottomRightElements.DrawHeight); else bottomRightElements.Y = 0; diff --git a/osu.Game/Screens/Utility/CircleGameplay.cs b/osu.Game/Screens/Utility/CircleGameplay.cs index 1f970c5121..0f328d04fb 100644 --- a/osu.Game/Screens/Utility/CircleGameplay.cs +++ b/osu.Game/Screens/Utility/CircleGameplay.cs @@ -201,8 +201,8 @@ namespace osu.Game.Screens.Utility { double preempt = (float)IBeatmapDifficultyInfo.DifficultyRange(SampleApproachRate.Value, 1800, 1200, 450); - approach.Scale = new Vector2(1 + 4 * (float)MathHelper.Clamp((HitTime - Clock.CurrentTime) / preempt, 0, 100)); - Alpha = (float)MathHelper.Clamp((Clock.CurrentTime - HitTime + 600) / 400, 0, 1); + approach.Scale = new Vector2(1 + 4 * (float)Math.Clamp((HitTime - Clock.CurrentTime) / preempt, 0, 100)); + Alpha = (float)Math.Clamp((Clock.CurrentTime - HitTime + 600) / 400, 0, 1); if (Clock.CurrentTime > HitTime + duration) Expire(); diff --git a/osu.Game/Screens/Utility/SampleComponents/LatencyMovableBox.cs b/osu.Game/Screens/Utility/SampleComponents/LatencyMovableBox.cs index dcfcf602bf..ef1b848945 100644 --- a/osu.Game/Screens/Utility/SampleComponents/LatencyMovableBox.cs +++ b/osu.Game/Screens/Utility/SampleComponents/LatencyMovableBox.cs @@ -1,6 +1,7 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System; using osu.Framework.Graphics; using osu.Framework.Graphics.Shapes; using osu.Framework.Input.Events; @@ -55,22 +56,22 @@ namespace osu.Game.Screens.Utility.SampleComponents { case Key.F: case Key.Up: - box.Y = MathHelper.Clamp(box.Y - movementAmount, 0.1f, 0.9f); + box.Y = Math.Clamp(box.Y - movementAmount, 0.1f, 0.9f); break; case Key.J: case Key.Down: - box.Y = MathHelper.Clamp(box.Y + movementAmount, 0.1f, 0.9f); + box.Y = Math.Clamp(box.Y + movementAmount, 0.1f, 0.9f); break; case Key.Z: case Key.Left: - box.X = MathHelper.Clamp(box.X - movementAmount, 0.1f, 0.9f); + box.X = Math.Clamp(box.X - movementAmount, 0.1f, 0.9f); break; case Key.X: case Key.Right: - box.X = MathHelper.Clamp(box.X + movementAmount, 0.1f, 0.9f); + box.X = Math.Clamp(box.X + movementAmount, 0.1f, 0.9f); break; } } diff --git a/osu.Game/Screens/Utility/ScrollingGameplay.cs b/osu.Game/Screens/Utility/ScrollingGameplay.cs index 5038c53b4a..c0264f5734 100644 --- a/osu.Game/Screens/Utility/ScrollingGameplay.cs +++ b/osu.Game/Screens/Utility/ScrollingGameplay.cs @@ -165,7 +165,7 @@ namespace osu.Game.Screens.Utility { double preempt = (float)IBeatmapDifficultyInfo.DifficultyRange(SampleApproachRate.Value, 1800, 1200, 450); - Alpha = (float)MathHelper.Clamp((Clock.CurrentTime - HitTime + 600) / 400, 0, 1); + Alpha = (float)Math.Clamp((Clock.CurrentTime - HitTime + 600) / 400, 0, 1); Y = judgement_position - (float)((HitTime - Clock.CurrentTime) / preempt); if (Clock.CurrentTime > HitTime + duration) From 88089fb0144a54d99b2e586f2d1b8e4512494604 Mon Sep 17 00:00:00 2001 From: Zihad Date: Fri, 28 Feb 2025 19:03:39 +0600 Subject: [PATCH 1161/1275] make `SettingsPanel.SearchTextBox`'s setter private --- osu.Game/Overlays/SettingsPanel.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Overlays/SettingsPanel.cs b/osu.Game/Overlays/SettingsPanel.cs index d8b054eaf8..9b268c573f 100644 --- a/osu.Game/Overlays/SettingsPanel.cs +++ b/osu.Game/Overlays/SettingsPanel.cs @@ -54,7 +54,7 @@ namespace osu.Game.Overlays public SettingsSectionsContainer SectionsContainer { get; private set; } - protected SeekLimitedSearchTextBox SearchTextBox; + protected SeekLimitedSearchTextBox SearchTextBox { get; private set; } protected override string PopInSampleName => "UI/settings-pop-in"; protected override double PopInOutSampleBalance => -OsuGameBase.SFX_STEREO_STRENGTH; From 0d7c00ae09d65d7c4a53abd1860d3029e1c004bd Mon Sep 17 00:00:00 2001 From: Zihad Date: Fri, 28 Feb 2025 19:04:47 +0600 Subject: [PATCH 1162/1275] use `Bindable.SetDefault` for clearing search text --- osu.Game/Overlays/SettingsOverlay.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Overlays/SettingsOverlay.cs b/osu.Game/Overlays/SettingsOverlay.cs index 8a39d75565..630675a717 100644 --- a/osu.Game/Overlays/SettingsOverlay.cs +++ b/osu.Game/Overlays/SettingsOverlay.cs @@ -69,7 +69,7 @@ namespace osu.Game.Overlays where T : Drawable { // if search isn't cleared then the target control won't be visible if it doesn't match the query - SearchTextBox.Current.Value = ""; + SearchTextBox.Current.SetDefault(); Show(); From 8032b6893274a152a12226572e89a000262c5583 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 28 Feb 2025 16:59:39 +0900 Subject: [PATCH 1163/1275] Stop using padding for panel x offsets --- osu.Game/Screens/SelectV2/PanelBase.cs | 11 +++++++---- osu.Game/Screens/SelectV2/PanelBeatmap.cs | 4 ++-- 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/osu.Game/Screens/SelectV2/PanelBase.cs b/osu.Game/Screens/SelectV2/PanelBase.cs index 2a32b1a95f..1dc645ba53 100644 --- a/osu.Game/Screens/SelectV2/PanelBase.cs +++ b/osu.Game/Screens/SelectV2/PanelBase.cs @@ -61,6 +61,11 @@ namespace osu.Game.Screens.SelectV2 } } + // content is offset by PanelXOffset, make sure we only handle input at the actual visible + // offset region. + public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => + TopLevelContent.ReceivePositionalInputAt(screenSpacePos); + [BackgroundDependencyLoader] private void load(OverlayColourProvider colourProvider, OsuColour colours) { @@ -219,8 +224,6 @@ namespace osu.Game.Screens.SelectV2 private void updateDisplay() { - backgroundLayer.TransformTo(nameof(Padding), backgroundLayer.Padding with { Vertical = Expanded.Value ? 2f : 0f }, duration, Easing.OutQuint); - var backgroundColour = accentColour ?? Color4.White; var edgeEffectColour = accentColour ?? Color4Extensions.FromHex(@"4EBFFF"); @@ -235,7 +238,7 @@ namespace osu.Game.Screens.SelectV2 private void updateXOffset() { - float x = PanelXOffset; + float x = PanelXOffset + corner_radius; if (!Expanded.Value && !Selected.Value) x += active_x_offset; @@ -243,7 +246,7 @@ namespace osu.Game.Screens.SelectV2 if (!KeyboardSelected.Value) x += active_x_offset * 0.5f; - this.TransformTo(nameof(Padding), new MarginPadding { Left = x }, duration, Easing.OutQuint); + TopLevelContent.MoveToX(x, duration, Easing.OutQuint); } private void updateHover() diff --git a/osu.Game/Screens/SelectV2/PanelBeatmap.cs b/osu.Game/Screens/SelectV2/PanelBeatmap.cs index 0ce6b1a9a2..d4bf3519fa 100644 --- a/osu.Game/Screens/SelectV2/PanelBeatmap.cs +++ b/osu.Game/Screens/SelectV2/PanelBeatmap.cs @@ -54,7 +54,7 @@ namespace osu.Game.Screens.SelectV2 public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) { - var inputRectangle = DrawRectangle; + var inputRectangle = TopLevelContent.DrawRectangle; // Cover the gaps introduced by the spacing between BeatmapPanels so that clicks will not fall through the carousel. // @@ -62,7 +62,7 @@ namespace osu.Game.Screens.SelectV2 // larger hit target. inputRectangle = inputRectangle.Inflate(new MarginPadding { Vertical = BeatmapCarousel.SPACING }); - return inputRectangle.Contains(ToLocalSpace(screenSpacePos)); + return inputRectangle.Contains(TopLevelContent.ToLocalSpace(screenSpacePos)); } [BackgroundDependencyLoader] From 29c35529d27b730847d03896c04c03a9e95efd3b Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 28 Feb 2025 17:02:09 +0900 Subject: [PATCH 1164/1275] Fix activation flash being applied twice (and adjust duration) --- osu.Game/Screens/SelectV2/PanelBase.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/osu.Game/Screens/SelectV2/PanelBase.cs b/osu.Game/Screens/SelectV2/PanelBase.cs index 1dc645ba53..b9d9bbd20a 100644 --- a/osu.Game/Screens/SelectV2/PanelBase.cs +++ b/osu.Game/Screens/SelectV2/PanelBase.cs @@ -217,7 +217,6 @@ namespace osu.Game.Screens.SelectV2 protected override bool OnClick(ClickEvent e) { - activationFlash.FadeOutFromOne(500, Easing.OutQuint); carousel?.Activate(Item!); return true; } @@ -287,7 +286,7 @@ namespace osu.Game.Screens.SelectV2 public virtual void Activated() { - activationFlash.FadeOutFromOne(500, Easing.OutQuint); + activationFlash.FadeOutFromOne(1000, Easing.OutQuint); } #endregion From 4beac64bdb6c2dee8492ea8b113498b78ef5f36a Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 28 Feb 2025 17:19:30 +0900 Subject: [PATCH 1165/1275] Remove unused container level --- osu.Game/Screens/SelectV2/PanelBase.cs | 43 ++++++++++++-------------- 1 file changed, 19 insertions(+), 24 deletions(-) diff --git a/osu.Game/Screens/SelectV2/PanelBase.cs b/osu.Game/Screens/SelectV2/PanelBase.cs index b9d9bbd20a..36f4f13a3b 100644 --- a/osu.Game/Screens/SelectV2/PanelBase.cs +++ b/osu.Game/Screens/SelectV2/PanelBase.cs @@ -32,7 +32,6 @@ namespace osu.Game.Screens.SelectV2 private Box backgroundBorder = null!; private Box backgroundGradient = null!; private Box backgroundAccentGradient = null!; - private Container backgroundLayer = null!; private Container backgroundLayerHorizontalPadding = null!; private Container backgroundContainer = null!; private Container iconContainer = null!; @@ -66,6 +65,9 @@ namespace osu.Game.Screens.SelectV2 public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => TopLevelContent.ReceivePositionalInputAt(screenSpacePos); + [Resolved] + private BeatmapCarousel? carousel { get; set; } + [BackgroundDependencyLoader] private void load(OverlayColourProvider colourProvider, OsuColour colours) { @@ -102,30 +104,26 @@ namespace osu.Game.Screens.SelectV2 backgroundLayerHorizontalPadding = new Container { RelativeSizeAxes = Axes.Both, - Child = backgroundLayer = new Container + Child = new Container { RelativeSizeAxes = Axes.Both, - Child = new Container + Masking = true, + CornerRadius = corner_radius, + Children = new Drawable[] { - Masking = true, - CornerRadius = corner_radius, - RelativeSizeAxes = Axes.Both, - Children = new Drawable[] + backgroundGradient = new Box { - backgroundGradient = new Box - { - RelativeSizeAxes = Axes.Both, - }, - backgroundAccentGradient = new Box - { - RelativeSizeAxes = Axes.Both, - }, - backgroundContainer = new Container - { - RelativeSizeAxes = Axes.Both, - }, - } - }, + RelativeSizeAxes = Axes.Both, + }, + backgroundAccentGradient = new Box + { + RelativeSizeAxes = Axes.Both, + }, + backgroundContainer = new Container + { + RelativeSizeAxes = Axes.Both, + }, + } }, } }, @@ -212,9 +210,6 @@ namespace osu.Game.Screens.SelectV2 this.FadeInFromZero(duration, Easing.OutQuint); } - [Resolved] - private BeatmapCarousel? carousel { get; set; } - protected override bool OnClick(ClickEvent e) { carousel?.Activate(Item!); From 38de3566b14b4d08a17c806f2891fa85c82dfafd Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 28 Feb 2025 17:37:18 +0900 Subject: [PATCH 1166/1275] Adjust set panel display and animations slightly --- .../SelectV2/BeatmapSetPanelBackground.cs | 2 +- osu.Game/Screens/SelectV2/PanelBase.cs | 12 ++++++------ osu.Game/Screens/SelectV2/PanelBeatmapSet.cs | 16 +++++++++++----- 3 files changed, 18 insertions(+), 12 deletions(-) diff --git a/osu.Game/Screens/SelectV2/BeatmapSetPanelBackground.cs b/osu.Game/Screens/SelectV2/BeatmapSetPanelBackground.cs index 435a0ad262..798acf62ee 100644 --- a/osu.Game/Screens/SelectV2/BeatmapSetPanelBackground.cs +++ b/osu.Game/Screens/SelectV2/BeatmapSetPanelBackground.cs @@ -16,7 +16,7 @@ namespace osu.Game.Screens.SelectV2 { public partial class BeatmapSetPanelBackground : ModelBackedDrawable { - protected override bool TransformImmediately => true; + protected override double TransformDuration => 400; public WorkingBeatmap? Beatmap { diff --git a/osu.Game/Screens/SelectV2/PanelBase.cs b/osu.Game/Screens/SelectV2/PanelBase.cs index 36f4f13a3b..05a1a55c03 100644 --- a/osu.Game/Screens/SelectV2/PanelBase.cs +++ b/osu.Game/Screens/SelectV2/PanelBase.cs @@ -25,7 +25,7 @@ namespace osu.Game.Screens.SelectV2 private const float active_x_offset = 50f; - private const float duration = 400; + protected const float DURATION = 400; protected float PanelXOffset { get; init; } @@ -207,7 +207,7 @@ namespace osu.Game.Screens.SelectV2 protected override void PrepareForUse() { base.PrepareForUse(); - this.FadeInFromZero(duration, Easing.OutQuint); + this.FadeInFromZero(DURATION, Easing.OutQuint); } protected override bool OnClick(ClickEvent e) @@ -221,10 +221,10 @@ namespace osu.Game.Screens.SelectV2 var backgroundColour = accentColour ?? Color4.White; var edgeEffectColour = accentColour ?? Color4Extensions.FromHex(@"4EBFFF"); - backgroundAccentGradient.FadeColour(ColourInfo.GradientHorizontal(backgroundColour.Opacity(0.25f), backgroundColour.Opacity(0f)), duration, Easing.OutQuint); - backgroundBorder.FadeColour(backgroundColour, duration, Easing.OutQuint); + backgroundAccentGradient.FadeColour(ColourInfo.GradientHorizontal(backgroundColour.Opacity(0.25f), backgroundColour.Opacity(0f)), DURATION, Easing.OutQuint); + backgroundBorder.FadeColour(backgroundColour, DURATION, Easing.OutQuint); - TopLevelContent.FadeEdgeEffectTo(Expanded.Value ? edgeEffectColour.Opacity(0.5f) : Color4.Black.Opacity(0.4f), duration, Easing.OutQuint); + TopLevelContent.FadeEdgeEffectTo(Expanded.Value ? edgeEffectColour.Opacity(0.5f) : Color4.Black.Opacity(0.4f), DURATION, Easing.OutQuint); updateXOffset(); updateHover(); @@ -240,7 +240,7 @@ namespace osu.Game.Screens.SelectV2 if (!KeyboardSelected.Value) x += active_x_offset * 0.5f; - TopLevelContent.MoveToX(x, duration, Easing.OutQuint); + TopLevelContent.MoveToX(x, DURATION, Easing.OutQuint); } private void updateHover() diff --git a/osu.Game/Screens/SelectV2/PanelBeatmapSet.cs b/osu.Game/Screens/SelectV2/PanelBeatmapSet.cs index 5c38fe8e04..512fbacec1 100644 --- a/osu.Game/Screens/SelectV2/PanelBeatmapSet.cs +++ b/osu.Game/Screens/SelectV2/PanelBeatmapSet.cs @@ -48,7 +48,7 @@ namespace osu.Game.Screens.SelectV2 Icon = chevronIcon = new Container { - Size = new Vector2(22), + Size = new Vector2(0, 22), Child = new SpriteIcon { Anchor = Anchor.Centre, @@ -128,10 +128,16 @@ namespace osu.Game.Screens.SelectV2 private void onExpanded() { - const float duration = 500; - - chevronIcon.ResizeWidthTo(Expanded.Value ? 22 : 0f, duration, Easing.OutQuint); - chevronIcon.FadeTo(Expanded.Value ? 1f : 0f, duration, Easing.OutQuint); + if (Expanded.Value) + { + chevronIcon.ResizeWidthTo(18, 600, Easing.OutElasticQuarter); + chevronIcon.FadeTo(1f, DURATION, Easing.OutQuint); + } + else + { + chevronIcon.ResizeWidthTo(0f, DURATION, Easing.OutQuint); + chevronIcon.FadeTo(0f, DURATION, Easing.OutQuint); + } } protected override void PrepareForUse() From 881534eb7f3d71e817d511c64ca368e0e6eca069 Mon Sep 17 00:00:00 2001 From: Jamie Taylor Date: Sat, 1 Mar 2025 01:51:37 +0900 Subject: [PATCH 1167/1275] Add SFX for kiai/star fountain activation --- osu.Game/Screens/Menu/KiaiMenuFountains.cs | 14 +++++++++++++- osu.Game/Screens/Play/KiaiGameplayFountains.cs | 14 +++++++++++++- 2 files changed, 26 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/Menu/KiaiMenuFountains.cs b/osu.Game/Screens/Menu/KiaiMenuFountains.cs index 7978e9fa91..dbbff4a9f5 100644 --- a/osu.Game/Screens/Menu/KiaiMenuFountains.cs +++ b/osu.Game/Screens/Menu/KiaiMenuFountains.cs @@ -3,6 +3,8 @@ using System; using osu.Framework.Allocation; +using osu.Framework.Audio; +using osu.Framework.Audio.Sample; using osu.Framework.Graphics; using osu.Framework.Utils; using osu.Game.Graphics.Containers; @@ -14,8 +16,11 @@ namespace osu.Game.Screens.Menu private StarFountain leftFountain = null!; private StarFountain rightFountain = null!; + private Sample? sample; + private SampleChannel? sampleChannel; + [BackgroundDependencyLoader] - private void load() + private void load(AudioManager audio) { RelativeSizeAxes = Axes.Both; @@ -34,6 +39,8 @@ namespace osu.Game.Screens.Menu X = -250, }, }; + + sample = audio.Samples.Get(@"Gameplay/fountain-shoot"); } private bool isTriggered; @@ -73,6 +80,11 @@ namespace osu.Game.Screens.Menu rightFountain.Shoot(1); break; } + + // Track sample channel to avoid overlapping playback + sampleChannel?.Stop(); + sampleChannel = sample?.GetChannel(); + sampleChannel?.Play(); } } } diff --git a/osu.Game/Screens/Play/KiaiGameplayFountains.cs b/osu.Game/Screens/Play/KiaiGameplayFountains.cs index d4e61dc5a0..7e09f50133 100644 --- a/osu.Game/Screens/Play/KiaiGameplayFountains.cs +++ b/osu.Game/Screens/Play/KiaiGameplayFountains.cs @@ -3,6 +3,8 @@ using System; using osu.Framework.Allocation; +using osu.Framework.Audio; +using osu.Framework.Audio.Sample; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Utils; @@ -19,8 +21,11 @@ namespace osu.Game.Screens.Play private Bindable kiaiStarFountains = null!; + private Sample? sample; + private SampleChannel? sampleChannel; + [BackgroundDependencyLoader] - private void load(OsuConfigManager config) + private void load(OsuConfigManager config, AudioManager audio) { kiaiStarFountains = config.GetBindable(OsuSetting.StarFountains); @@ -41,6 +46,8 @@ namespace osu.Game.Screens.Play X = -75, }, }; + + sample = audio.Samples.Get(@"Gameplay/fountain-shoot"); } private bool isTriggered; @@ -66,6 +73,11 @@ namespace osu.Game.Screens.Play { leftFountain.Shoot(1); rightFountain.Shoot(-1); + + // Track sample channel to avoid overlapping playback + sampleChannel?.Stop(); + sampleChannel = sample?.GetChannel(); + sampleChannel?.Play(); } public partial class GameplayStarFountain : StarFountain From ec6ff240f38ef69d37c50437c8f97b5fa3804c90 Mon Sep 17 00:00:00 2001 From: "Giovanni D." Date: Sun, 2 Mar 2025 00:49:04 -0800 Subject: [PATCH 1168/1275] Add taskbar flashing when a multiplayer game is starting --- osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerPlayer.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerPlayer.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerPlayer.cs index 111b453adb..e5bc683d19 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerPlayer.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerPlayer.cs @@ -30,6 +30,9 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer [Resolved] private MultiplayerClient client { get; set; } = null!; + [Resolved] + private OsuGame? game { get; set; } + private IBindable isConnected = null!; private readonly TaskCompletionSource resultsReady = new TaskCompletionSource(); @@ -142,6 +145,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer if (client.LocalUser?.State == MultiplayerUserState.Loaded) { + game?.Window?.Flash(); loadingDisplay.Show(); client.ChangeState(MultiplayerUserState.ReadyForGameplay); } From 35a21b44a698f0cbe84db036f03c1f26202a8d75 Mon Sep 17 00:00:00 2001 From: "Giovanni D." Date: Sun, 2 Mar 2025 20:43:32 -0800 Subject: [PATCH 1169/1275] Change timing of the flash --- osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerPlayer.cs | 4 ---- .../OnlinePlay/Multiplayer/MultiplayerPlayerLoader.cs | 5 +++++ 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerPlayer.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerPlayer.cs index e5bc683d19..111b453adb 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerPlayer.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerPlayer.cs @@ -30,9 +30,6 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer [Resolved] private MultiplayerClient client { get; set; } = null!; - [Resolved] - private OsuGame? game { get; set; } - private IBindable isConnected = null!; private readonly TaskCompletionSource resultsReady = new TaskCompletionSource(); @@ -145,7 +142,6 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer if (client.LocalUser?.State == MultiplayerUserState.Loaded) { - game?.Window?.Flash(); loadingDisplay.Show(); client.ChangeState(MultiplayerUserState.ReadyForGameplay); } diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerPlayerLoader.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerPlayerLoader.cs index 7eb7f6610e..dd9cb56862 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerPlayerLoader.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerPlayerLoader.cs @@ -18,6 +18,9 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer [Resolved] private MultiplayerClient multiplayerClient { get; set; } = null!; + [Resolved] + private OsuGame? game { get; set; } + private Player? player; public MultiplayerPlayerLoader(Func createPlayer) @@ -39,6 +42,8 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer { base.OnPlayerLoaded(); + game?.Window?.Flash(); + multiplayerClient.ChangeState(MultiplayerUserState.Loaded) .ContinueWith(task => failAndBail(task.Exception?.Message ?? "Server error"), TaskContinuationOptions.NotOnRanToCompletion); } From ad9a963bd0fa831c30f7a79abf62a797aa087c3f Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Mon, 3 Mar 2025 14:19:19 +0900 Subject: [PATCH 1170/1275] Exit loop when cancellation requested The following manages to create all hitobjects but proceeds to get stuck in this method: `dotnet run -- difficulty 1607040 -r:2` --- osu.Game/Rulesets/Objects/HitObject.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/osu.Game/Rulesets/Objects/HitObject.cs b/osu.Game/Rulesets/Objects/HitObject.cs index 9f980769e2..d9e62ccecb 100644 --- a/osu.Game/Rulesets/Objects/HitObject.cs +++ b/osu.Game/Rulesets/Objects/HitObject.cs @@ -114,6 +114,8 @@ namespace osu.Game.Rulesets.Objects { foreach (HitObject hitObject in nestedHitObjects) { + cancellationToken.ThrowIfCancellationRequested(); + if (hitObject is IHasComboInformation n) { n.ComboIndexBindable.BindTo(hasCombo.ComboIndexBindable); From 52dad09b2011c014b2ec5acb4947aacbc3ba4d90 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Mon, 3 Mar 2025 14:19:38 +0900 Subject: [PATCH 1171/1275] Cancel slider generation when requested Didn't notice a particular case with this one, just came up as I was looking through code. --- osu.Game/Rulesets/Objects/SliderEventGenerator.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/osu.Game/Rulesets/Objects/SliderEventGenerator.cs b/osu.Game/Rulesets/Objects/SliderEventGenerator.cs index f5146d1675..e5e15042ff 100644 --- a/osu.Game/Rulesets/Objects/SliderEventGenerator.cs +++ b/osu.Game/Rulesets/Objects/SliderEventGenerator.cs @@ -46,6 +46,8 @@ namespace osu.Game.Rulesets.Objects for (int span = 0; span < spanCount; span++) { + cancellationToken.ThrowIfCancellationRequested(); + double spanStartTime = startTime + span * spanDuration; bool reversed = span % 2 == 1; From 033952029eecd814a62567c58eeafb5fe3fe5c99 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Mon, 3 Mar 2025 14:46:13 +0900 Subject: [PATCH 1172/1275] Cancel `ApplyDefaults()` when requested Also didn't notice a particular case here, but if all code passes up until we get to the `foreach (var h in nestedHitObjects)` below, then we could end up stuck here for quite a while. --- osu.Game/Rulesets/Objects/HitObject.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/osu.Game/Rulesets/Objects/HitObject.cs b/osu.Game/Rulesets/Objects/HitObject.cs index d9e62ccecb..07e07b25d3 100644 --- a/osu.Game/Rulesets/Objects/HitObject.cs +++ b/osu.Game/Rulesets/Objects/HitObject.cs @@ -104,6 +104,8 @@ namespace osu.Game.Rulesets.Objects /// The cancellation token. public void ApplyDefaults(ControlPointInfo controlPointInfo, IBeatmapDifficultyInfo difficulty, CancellationToken cancellationToken = default) { + cancellationToken.ThrowIfCancellationRequested(); + ApplyDefaultsToSelf(controlPointInfo, difficulty); nestedHitObjects.Clear(); From 47747aed3e9feb09c3b6d9f82703cedda8db3035 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 3 Mar 2025 08:40:51 +0100 Subject: [PATCH 1173/1275] Add guards to prevent clamp calls with invalid bounds --- osu.Game/Screens/Play/HUDOverlay.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Game/Screens/Play/HUDOverlay.cs b/osu.Game/Screens/Play/HUDOverlay.cs index 19190ac362..78c602d8f1 100644 --- a/osu.Game/Screens/Play/HUDOverlay.cs +++ b/osu.Game/Screens/Play/HUDOverlay.cs @@ -277,17 +277,17 @@ namespace osu.Game.Screens.Play if (rulesetComponents != null) processDrawables(rulesetComponents); - if (lowestTopScreenSpaceRight.HasValue) + if (lowestTopScreenSpaceRight.HasValue && DrawHeight - TopRightElements.DrawHeight > 0) TopRightElements.Y = Math.Clamp(ToLocalSpace(new Vector2(0, lowestTopScreenSpaceRight.Value)).Y, 0, DrawHeight - TopRightElements.DrawHeight); else TopRightElements.Y = 0; - if (lowestTopScreenSpaceLeft.HasValue) + if (lowestTopScreenSpaceLeft.HasValue && DrawHeight - LeaderboardFlow.DrawHeight > 0) LeaderboardFlow.Y = Math.Clamp(ToLocalSpace(new Vector2(0, lowestTopScreenSpaceLeft.Value)).Y, 0, DrawHeight - LeaderboardFlow.DrawHeight); else LeaderboardFlow.Y = 0; - if (highestBottomScreenSpace.HasValue) + if (highestBottomScreenSpace.HasValue && DrawHeight - bottomRightElements.DrawHeight > 0) bottomRightElements.Y = BottomScoringElementsHeight = -Math.Clamp(DrawHeight - ToLocalSpace(highestBottomScreenSpace.Value).Y, 0, DrawHeight - bottomRightElements.DrawHeight); else bottomRightElements.Y = 0; From 0a50fb1dfac7b0898c134f98c47a459fbbeb769c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 3 Mar 2025 09:32:27 +0100 Subject: [PATCH 1174/1275] Add failing test case --- .../Beatmaps/BeatmapExtensionsTest.cs | 58 +++++++++++++++++++ 1 file changed, 58 insertions(+) create mode 100644 osu.Game.Tests/Beatmaps/BeatmapExtensionsTest.cs diff --git a/osu.Game.Tests/Beatmaps/BeatmapExtensionsTest.cs b/osu.Game.Tests/Beatmaps/BeatmapExtensionsTest.cs new file mode 100644 index 0000000000..1dda2e314d --- /dev/null +++ b/osu.Game.Tests/Beatmaps/BeatmapExtensionsTest.cs @@ -0,0 +1,58 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using NUnit.Framework; +using osu.Game.Beatmaps; +using osu.Game.Beatmaps.Timing; +using osu.Game.Rulesets.Osu.Objects; + +namespace osu.Game.Tests.Beatmaps +{ + public class BeatmapExtensionsTest + { + [Test] + public void TestLengthCalculations() + { + var beatmap = new Beatmap + { + HitObjects = + { + new HitCircle { StartTime = 5_000 }, + new HitCircle { StartTime = 300_000 }, + new Spinner { StartTime = 280_000, Duration = 40_000 } + }, + Breaks = + { + new BreakPeriod(50_000, 75_000), + new BreakPeriod(100_000, 150_000), + } + }; + + Assert.That(beatmap.CalculatePlayableBounds(), Is.EqualTo((5_000, 320_000))); + Assert.That(beatmap.CalculatePlayableLength(), Is.EqualTo(315_000)); // 320_000 - 5_000 + Assert.That(beatmap.CalculateDrainLength(), Is.EqualTo(240_000)); // 315_000 - (25_000 + 50_000) = 315_000 - 75_000 + } + + [Test] + public void TestDrainLengthCannotGoNegative() + { + var beatmap = new Beatmap + { + HitObjects = + { + new HitCircle { StartTime = 5_000 }, + new HitCircle { StartTime = 300_000 }, + new Spinner { StartTime = 280_000, Duration = 40_000 } + }, + Breaks = + { + new BreakPeriod(0, 350_000), + } + }; + + Assert.That(beatmap.CalculatePlayableBounds(), Is.EqualTo((5_000, 320_000))); + Assert.That(beatmap.CalculatePlayableLength(), Is.EqualTo(315_000)); // 320_000 - 5_000 + Assert.That(beatmap.CalculateDrainLength(), Is.EqualTo(0)); // break period encompasses entire beatmap + } + } +} From 87fb8da3517ae0f2d0669dc3afa9b233454c49bc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 3 Mar 2025 09:35:46 +0100 Subject: [PATCH 1175/1275] Fix drain length calculation helper method being able to return negative durations This is the principal failure behind https://github.com/ppy/osu-server-beatmap-submission/issues/40. --- osu.Game/Beatmaps/IBeatmap.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Beatmaps/IBeatmap.cs b/osu.Game/Beatmaps/IBeatmap.cs index 826d4e19a7..f95fcefd7e 100644 --- a/osu.Game/Beatmaps/IBeatmap.cs +++ b/osu.Game/Beatmaps/IBeatmap.cs @@ -161,7 +161,7 @@ namespace osu.Game.Beatmaps /// /// Find the total milliseconds between the first and last hittable objects, excluding any break time. /// - public static double CalculateDrainLength(this IBeatmap beatmap) => CalculatePlayableLength(beatmap.HitObjects) - beatmap.TotalBreakTime; + public static double CalculateDrainLength(this IBeatmap beatmap) => Math.Max(CalculatePlayableLength(beatmap.HitObjects) - beatmap.TotalBreakTime, 0); /// /// Find the timestamps in milliseconds of the start and end of the playable region. From 52860def6c7fb40dcd1d6291f867751c7d08aecb Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 3 Mar 2025 18:53:41 +0900 Subject: [PATCH 1176/1275] Always zoom timeline to centre rather than focus point Closes https://github.com/ppy/osu/issues/32183. --- .../Edit/Compose/Components/Timeline/ZoomableScrollContainer.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/ZoomableScrollContainer.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/ZoomableScrollContainer.cs index 9db14ce4c4..b483f23d1d 100644 --- a/osu.Game/Screens/Edit/Compose/Components/Timeline/ZoomableScrollContainer.cs +++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/ZoomableScrollContainer.cs @@ -141,7 +141,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline if (e.AltPressed) { // zoom when holding alt. - AdjustZoomRelatively(e.ScrollDelta.Y, zoomedContent.ToLocalSpace(e.ScreenSpaceMousePosition).X); + AdjustZoomRelatively(e.ScrollDelta.Y); return true; } From f32a8e8741f4dcd8d915be78a93686ab101d1d74 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 3 Mar 2025 18:54:46 +0900 Subject: [PATCH 1177/1275] Update resources --- osu.Game/osu.Game.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj index 614f1409bf..e35eaf5645 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -36,7 +36,7 @@ - + From 28f9e734f0d3dbf374d90b72e8380e1021aab98e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 3 Mar 2025 12:23:52 +0100 Subject: [PATCH 1178/1275] Add failing test case --- .../TestScenePlaylistsResultsScreen.cs | 103 +++++++++++++----- 1 file changed, 76 insertions(+), 27 deletions(-) diff --git a/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsResultsScreen.cs b/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsResultsScreen.cs index dc5fb20e16..469f7c8b74 100644 --- a/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsResultsScreen.cs +++ b/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsResultsScreen.cs @@ -69,9 +69,11 @@ namespace osu.Game.Tests.Visual.Playlists totalCount = 0; userScore = TestResources.CreateTestScoreInfo(); + userScore.OnlineID = 1; userScore.TotalScore = 0; userScore.Statistics = new Dictionary(); userScore.MaximumStatistics = new Dictionary(); + userScore.Position = real_user_position; // Beatmap is required to be an actual beatmap so the scores can get their scores correctly // calculated for standardised scoring, else the tests that rely on ordering will fall over. @@ -243,6 +245,35 @@ namespace osu.Game.Tests.Visual.Playlists AddAssert("placeholder shown", () => this.ChildrenOfType().Count(), () => Is.EqualTo(1)); } + [Test] + public void TestFetchingAllTheWayToFirstNeverDisplaysNegativePosition() + { + AddStep("set user position", () => userScore.Position = 20); + AddStep("bind user score info handler", () => bindHandler(userScore: userScore)); + + createResultsWithScore(() => userScore); + waitForDisplay(); + + AddStep("bind delayed handler", () => bindHandler(true)); + + for (int i = 0; i < 2; i++) + { + AddStep("simulate user falling down ranking", () => userScore.Position += 2); + AddStep("scroll to left", () => resultsScreen.ChildrenOfType().Single().ChildrenOfType().Single().ScrollToStart(false)); + + AddAssert("left loading spinner shown", () => + resultsScreen.ChildrenOfType().Single(l => l.Anchor == Anchor.CentreLeft).State.Value == Visibility.Visible); + + waitForDisplay(); + + AddAssert("left loading spinner hidden", () => + resultsScreen.ChildrenOfType().Single(l => l.Anchor == Anchor.CentreLeft).State.Value == Visibility.Hidden); + } + + AddAssert("total count is 34", () => this.ChildrenOfType().Count(), () => Is.EqualTo(34)); + AddUntilStep("all panels have non-negative position", () => this.ChildrenOfType().All(p => p.ScorePosition.Value > 0)); + } + private void createResultsWithScore(Func getScore) { AddStep("load results", () => @@ -331,7 +362,7 @@ namespace osu.Game.Tests.Visual.Playlists if (userScore == null) triggerFail(s); else - triggerSuccess(s, createUserResponse(userScore)); + triggerSuccess(s, () => createUserResponse(userScore)); break; @@ -339,12 +370,12 @@ namespace osu.Game.Tests.Visual.Playlists if (userScore == null) triggerFail(u); else - triggerSuccess(u, createUserResponse(userScore)); + triggerSuccess(u, () => createUserResponse(userScore)); break; case IndexPlaylistScoresRequest i: - triggerSuccess(i, createIndexResponse(i, noScores)); + triggerSuccess(i, () => createIndexResponse(i, noScores)); break; } }, delay); @@ -352,11 +383,11 @@ namespace osu.Game.Tests.Visual.Playlists return true; }; - private void triggerSuccess(APIRequest req, T result) + private void triggerSuccess(APIRequest req, Func result) where T : class { requestComplete = true; - req.TriggerSuccess(result); + req.TriggerSuccess(result.Invoke()); } private void triggerFail(APIRequest req) @@ -367,28 +398,13 @@ namespace osu.Game.Tests.Visual.Playlists private MultiplayerScore createUserResponse(ScoreInfo userScore) { - var multiplayerUserScore = new MultiplayerScore - { - ID = highestScoreId, - Accuracy = userScore.Accuracy, - Passed = userScore.Passed, - Rank = userScore.Rank, - Position = real_user_position, - MaxCombo = userScore.MaxCombo, - User = userScore.User, - BeatmapId = RNG.Next(0, 7), - ScoresAround = new MultiplayerScoresAround - { - Higher = new MultiplayerScores(), - Lower = new MultiplayerScores() - } - }; + var multiplayerUserScore = createMultiplayerUserScore(userScore); totalCount++; for (int i = 1; i <= scores_per_result; i++) { - multiplayerUserScore.ScoresAround.Lower.Scores.Add(new MultiplayerScore + multiplayerUserScore.ScoresAround!.Lower!.Scores.Add(new MultiplayerScore { ID = getNextLowestScoreId(), Accuracy = userScore.Accuracy, @@ -404,7 +420,7 @@ namespace osu.Game.Tests.Visual.Playlists }, }); - multiplayerUserScore.ScoresAround.Higher.Scores.Add(new MultiplayerScore + multiplayerUserScore.ScoresAround!.Higher!.Scores.Add(new MultiplayerScore { ID = getNextHighestScoreId(), Accuracy = userScore.Accuracy, @@ -423,12 +439,32 @@ namespace osu.Game.Tests.Visual.Playlists totalCount += 2; } - addCursor(multiplayerUserScore.ScoresAround.Lower); - addCursor(multiplayerUserScore.ScoresAround.Higher); + addCursor(multiplayerUserScore.ScoresAround!.Lower!); + addCursor(multiplayerUserScore.ScoresAround!.Higher!); return multiplayerUserScore; } + private MultiplayerScore createMultiplayerUserScore(ScoreInfo userScore) + { + return new MultiplayerScore + { + ID = highestScoreId, + Accuracy = userScore.Accuracy, + Passed = userScore.Passed, + Rank = userScore.Rank, + Position = userScore.Position, + MaxCombo = userScore.MaxCombo, + User = userScore.User, + BeatmapId = RNG.Next(0, 7), + ScoresAround = new MultiplayerScoresAround + { + Higher = new MultiplayerScores(), + Lower = new MultiplayerScores() + } + }; + } + private IndexedMultiplayerScores createIndexResponse(IndexPlaylistScoresRequest req, bool noScores) { var result = new IndexedMultiplayerScores(); @@ -437,11 +473,21 @@ namespace osu.Game.Tests.Visual.Playlists string sort = req.IndexParams?.Properties["sort"].ToObject() ?? "score_desc"; + bool reachedEnd = false; + for (int i = 1; i <= scores_per_result; i++) { + int nextId = sort == "score_asc" ? getNextHighestScoreId() : getNextLowestScoreId(); + + if (userScore.OnlineID - nextId >= userScore.Position) + { + reachedEnd = true; + break; + } + result.Scores.Add(new MultiplayerScore { - ID = sort == "score_asc" ? getNextHighestScoreId() : getNextLowestScoreId(), + ID = nextId, Accuracy = 1, Passed = true, Rank = ScoreRank.X, @@ -458,7 +504,10 @@ namespace osu.Game.Tests.Visual.Playlists totalCount++; } - addCursor(result); + if (!reachedEnd) + addCursor(result); + + result.UserScore = createMultiplayerUserScore(userScore); return result; } From bf4fa58f72c61ff217c2d20a48f86d9aa65a4862 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 3 Mar 2025 12:56:28 +0100 Subject: [PATCH 1179/1275] Fix playlists results screens potentially displaying negative score positions Closes https://github.com/ppy/osu/issues/31434. --- .../Playlists/PlaylistItemResultsScreen.cs | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistItemResultsScreen.cs b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistItemResultsScreen.cs index 184de2f50c..0e539936d8 100644 --- a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistItemResultsScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistItemResultsScreen.cs @@ -185,6 +185,24 @@ namespace osu.Game.Screens.OnlinePlay.Playlists { higherScores = index; setPositions(index, pivot, -1); + + // when paginating the results, it's possible for the user's score to naturally fall down the rankings. + // unmitigated, this can cause scores at the very top of the rankings to have zero or negative positions + // because the positions are counted backwards from the user's score, which has increased in this case during pagination. + // if this happens, just give the top score the first position. + // note that this isn't 100% correct, but it *is* however the most reliable way to mask the problem. + int smallestPosition = index.Scores.Min(s => s.Position ?? 1); + + if (smallestPosition < 1) + { + int offset = 1 - smallestPosition; + + foreach (var scorePanel in ScorePanelList.GetScorePanels()) + scorePanel.ScorePosition.Value += offset; + + foreach (var score in index.Scores) + score.Position += offset; + } } return await transformScores(index.Scores).ConfigureAwait(false); From d7d5eec58ca4e5438ba22686c7f5f1c1f0a70ca3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 3 Mar 2025 13:52:10 +0100 Subject: [PATCH 1180/1275] Update failing assertions Change in behaviour is expected in this case. --- .../Visual/Editing/TestSceneZoomableScrollContainer.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game.Tests/Visual/Editing/TestSceneZoomableScrollContainer.cs b/osu.Game.Tests/Visual/Editing/TestSceneZoomableScrollContainer.cs index 1c8a18e131..2c84e76b2e 100644 --- a/osu.Game.Tests/Visual/Editing/TestSceneZoomableScrollContainer.cs +++ b/osu.Game.Tests/Visual/Editing/TestSceneZoomableScrollContainer.cs @@ -128,12 +128,12 @@ namespace osu.Game.Tests.Visual.Editing AddStep("Press alt down", () => InputManager.PressKey(Key.AltLeft)); AddStep("Scroll by 3", () => InputManager.ScrollBy(new Vector2(0, 3))); AddAssert("Box not at 0", () => !Precision.AlmostEquals(boxQuad.TopLeft, scrollQuad.TopLeft)); - AddAssert("Box 1/4 at 1/4", () => Precision.AlmostEquals(boxQuad.TopLeft.X + 0.25f * boxQuad.Size.X, scrollQuad.TopLeft.X + 0.25f * scrollQuad.Size.X)); + AddAssert("Box 1/2 at 1/2", () => Precision.AlmostEquals(boxQuad.TopLeft.X + 0.5f * boxQuad.Size.X, scrollQuad.TopLeft.X + 0.5f * scrollQuad.Size.X)); // Scroll out at 0.25 AddStep("Scroll by -3", () => InputManager.ScrollBy(new Vector2(0, -3))); AddAssert("Box at 0", () => Precision.AlmostEquals(boxQuad.TopLeft, scrollQuad.TopLeft)); - AddAssert("Box 1/4 at 1/4", () => Precision.AlmostEquals(boxQuad.TopLeft.X + 0.25f * boxQuad.Size.X, scrollQuad.TopLeft.X + 0.25f * scrollQuad.Size.X)); + AddAssert("Box 1/2 at 1/2", () => Precision.AlmostEquals(boxQuad.TopLeft.X + 0.5f * boxQuad.Size.X, scrollQuad.TopLeft.X + 0.5f * scrollQuad.Size.X)); AddStep("Release alt", () => InputManager.ReleaseKey(Key.AltLeft)); } From 23a5d6dc401a9944a544eae923da134fa75a090f Mon Sep 17 00:00:00 2001 From: andy840119 Date: Mon, 3 Mar 2025 22:09:48 +0800 Subject: [PATCH 1181/1275] This method is not being used anymore. see: https://github.com/ppy/osu/pull/26643 --- .../Screens/Edit/Compose/Components/SelectionHandler.cs | 8 -------- 1 file changed, 8 deletions(-) diff --git a/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs b/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs index 39fff169b7..bfe7fe523f 100644 --- a/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs +++ b/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs @@ -151,14 +151,6 @@ namespace osu.Game.Screens.Edit.Compose.Components /// public virtual SelectionRotationHandler CreateRotationHandler() => new SelectionRotationHandler(); - /// - /// Handles the selected items being scaled. - /// - /// The delta scale to apply, in local coordinates. - /// The point of reference where the scale is originating from. - /// Whether any items could be scaled. - public virtual bool HandleScale(Vector2 scale, Anchor anchor) => false; - /// /// Creates the handler to use for scale operations. /// From cab849b5d91cb1aab055798d1b1e353feba0c598 Mon Sep 17 00:00:00 2001 From: Joseph Madamba Date: Mon, 3 Mar 2025 14:23:39 -0800 Subject: [PATCH 1182/1275] Use web localisable string for team channel label --- osu.Game/Overlays/Chat/ChannelList/ChannelList.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Overlays/Chat/ChannelList/ChannelList.cs b/osu.Game/Overlays/Chat/ChannelList/ChannelList.cs index 0a89775cc7..03f6923455 100644 --- a/osu.Game/Overlays/Chat/ChannelList/ChannelList.cs +++ b/osu.Game/Overlays/Chat/ChannelList/ChannelList.cs @@ -85,7 +85,7 @@ namespace osu.Game.Overlays.Chat.ChannelList AnnounceChannelGroup = new ChannelGroup(ChatStrings.ChannelsListTitleANNOUNCE.ToUpper(), FontAwesome.Solid.Bullhorn, false), PublicChannelGroup = new ChannelGroup(ChatStrings.ChannelsListTitlePUBLIC.ToUpper(), FontAwesome.Solid.Comments, false), selector = new ChannelListItem(ChannelListingChannel), - TeamChannelGroup = new ChannelGroup("TEAM", FontAwesome.Solid.Users, false), // TODO: replace with osu-web localisable string once available + TeamChannelGroup = new ChannelGroup(ChatStrings.ChannelsListTitleTEAM.ToUpper(), FontAwesome.Solid.Users, false), PrivateChannelGroup = new ChannelGroup(ChatStrings.ChannelsListTitlePM.ToUpper(), FontAwesome.Solid.Envelope, true), }, }, From 550ff85550056bb947e67ead816c87004885da91 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Tue, 4 Mar 2025 11:22:47 +0900 Subject: [PATCH 1183/1275] Cancel difficulty calculation after 10 seconds by default --- .../Difficulty/DifficultyCalculator.cs | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/osu.Game/Rulesets/Difficulty/DifficultyCalculator.cs b/osu.Game/Rulesets/Difficulty/DifficultyCalculator.cs index 14acc9b908..add24f7866 100644 --- a/osu.Game/Rulesets/Difficulty/DifficultyCalculator.cs +++ b/osu.Game/Rulesets/Difficulty/DifficultyCalculator.cs @@ -62,6 +62,11 @@ namespace osu.Game.Rulesets.Difficulty /// A structure describing the difficulty of the beatmap. public DifficultyAttributes Calculate([NotNull] IEnumerable mods, CancellationToken cancellationToken = default) { + using var timedCancellationSource = new CancellationTokenSource(TimeSpan.FromSeconds(10)); + + if (!cancellationToken.CanBeCanceled) + cancellationToken = timedCancellationSource.Token; + cancellationToken.ThrowIfCancellationRequested(); preProcess(mods, cancellationToken); @@ -98,6 +103,11 @@ namespace osu.Game.Rulesets.Difficulty /// The set of . public List CalculateTimed([NotNull] IEnumerable mods, CancellationToken cancellationToken = default) { + using var timedCancellationSource = new CancellationTokenSource(TimeSpan.FromSeconds(10)); + + if (!cancellationToken.CanBeCanceled) + cancellationToken = timedCancellationSource.Token; + cancellationToken.ThrowIfCancellationRequested(); preProcess(mods, cancellationToken); @@ -166,15 +176,10 @@ namespace osu.Game.Rulesets.Difficulty /// /// The original list of s. /// The cancellation token. - private void preProcess([NotNull] IEnumerable mods, CancellationToken cancellationToken = default) + private void preProcess([NotNull] IEnumerable mods, CancellationToken cancellationToken) { playableMods = mods.Select(m => m.DeepClone()).ToArray(); - - // Only pass through the cancellation token if it's non-default. - // This allows for the default timeout to be applied for playable beatmap construction. - Beatmap = cancellationToken == default - ? beatmap.GetPlayableBeatmap(ruleset, playableMods) - : beatmap.GetPlayableBeatmap(ruleset, playableMods, cancellationToken); + Beatmap = beatmap.GetPlayableBeatmap(ruleset, playableMods, cancellationToken); var track = new TrackVirtual(10000); playableMods.OfType().ForEach(m => m.ApplyToTrack(track)); From df25734834b005d4b072e0338aaa892e4f776d1c Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Tue, 4 Mar 2025 11:36:36 +0900 Subject: [PATCH 1184/1275] Fix intermittent score panel test --- .../Visual/Ranking/TestSceneExpandedPanelMiddleContent.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Game.Tests/Visual/Ranking/TestSceneExpandedPanelMiddleContent.cs b/osu.Game.Tests/Visual/Ranking/TestSceneExpandedPanelMiddleContent.cs index 02a321d22f..eade5aaf5d 100644 --- a/osu.Game.Tests/Visual/Ranking/TestSceneExpandedPanelMiddleContent.cs +++ b/osu.Game.Tests/Visual/Ranking/TestSceneExpandedPanelMiddleContent.cs @@ -80,7 +80,7 @@ namespace osu.Game.Tests.Visual.Ranking showPanel(TestResources.CreateTestScoreInfo(beatmap)); }); - AddAssert("pp display faded out", () => + AddUntilStep("pp display faded out", () => { var ppDisplay = this.ChildrenOfType().Single(); return ppDisplay.Alpha == 0.5 && ppDisplay.TooltipText == ResultsScreenStrings.NoPPForUnrankedBeatmaps; @@ -97,7 +97,7 @@ namespace osu.Game.Tests.Visual.Ranking showPanel(score); }); - AddAssert("pp display faded out", () => + AddUntilStep("pp display faded out", () => { var ppDisplay = this.ChildrenOfType().Single(); return ppDisplay.Alpha == 0.5 && ppDisplay.TooltipText == ResultsScreenStrings.NoPPForUnrankedMods; @@ -116,7 +116,7 @@ namespace osu.Game.Tests.Visual.Ranking showPanel(score); }); - AddAssert("pp display faded out", () => this.ChildrenOfType().Single().Alpha == 1); + AddUntilStep("pp display faded out", () => this.ChildrenOfType().Single().Alpha == 1); } [Test] From 963df165df34db1c0020c44bb5c0c343fb24cab1 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Tue, 4 Mar 2025 12:33:33 +0900 Subject: [PATCH 1185/1275] Add failing test --- .../StatefulMultiplayerClientTest.cs | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/osu.Game.Tests/NonVisual/Multiplayer/StatefulMultiplayerClientTest.cs b/osu.Game.Tests/NonVisual/Multiplayer/StatefulMultiplayerClientTest.cs index 559db16751..a6d715df62 100644 --- a/osu.Game.Tests/NonVisual/Multiplayer/StatefulMultiplayerClientTest.cs +++ b/osu.Game.Tests/NonVisual/Multiplayer/StatefulMultiplayerClientTest.cs @@ -6,6 +6,7 @@ using Humanizer; using NUnit.Framework; using osu.Framework.Extensions.ObjectExtensions; using osu.Framework.Testing; +using osu.Game.Extensions; using osu.Game.Online.API.Requests.Responses; using osu.Game.Online.Multiplayer; using osu.Game.Online.Rooms; @@ -93,6 +94,29 @@ namespace osu.Game.Tests.NonVisual.Multiplayer checkPlayingUserCount(1); } + [Test] + public void TestJoinRoomWithManyUsers() + { + AddStep("leave room", () => MultiplayerClient.LeaveRoom()); + AddUntilStep("wait for room part", () => !RoomJoined); + + AddStep("create room with many users", () => + { + var newRoom = new Room(); + newRoom.CopyFrom(SelectedRoom.Value!); + + newRoom.RoomID = null; + MultiplayerClient.RoomSetupAction = room => + { + room.Users.AddRange(Enumerable.Range(PLAYER_1_ID, 100).Select(id => new MultiplayerRoomUser(id))); + }; + + RoomManager.CreateRoom(newRoom); + }); + + AddUntilStep("wait for room join", () => RoomJoined); + } + private void checkPlayingUserCount(int expectedCount) => AddAssert($"{"user".ToQuantity(expectedCount)} playing", () => MultiplayerClient.CurrentMatchPlayingUserIds.Count == expectedCount); From 3024a98658a62a4042d9946a8a72c85c98b8be97 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Tue, 4 Mar 2025 12:34:02 +0900 Subject: [PATCH 1186/1275] Fix unable to join multiplayer rooms with many users --- .../Online/API/Requests/GetUsersRequest.cs | 8 +++--- .../Online/Multiplayer/MultiplayerClient.cs | 27 ++++++++++--------- 2 files changed, 19 insertions(+), 16 deletions(-) diff --git a/osu.Game/Online/API/Requests/GetUsersRequest.cs b/osu.Game/Online/API/Requests/GetUsersRequest.cs index cd75ff4e31..fe7ba8c33d 100644 --- a/osu.Game/Online/API/Requests/GetUsersRequest.cs +++ b/osu.Game/Online/API/Requests/GetUsersRequest.cs @@ -13,14 +13,14 @@ namespace osu.Game.Online.API.Requests /// public class GetUsersRequest : APIRequest { - public readonly int[] UserIds; + public const int MAX_IDS_PER_REQUEST = 50; - private const int max_ids_per_request = 50; + public readonly int[] UserIds; public GetUsersRequest(int[] userIds) { - if (userIds.Length > max_ids_per_request) - throw new ArgumentException($"{nameof(GetUsersRequest)} calls only support up to {max_ids_per_request} IDs at once"); + if (userIds.Length > MAX_IDS_PER_REQUEST) + throw new ArgumentException($"{nameof(GetUsersRequest)} calls only support up to {MAX_IDS_PER_REQUEST} IDs at once"); UserIds = userIds; } diff --git a/osu.Game/Online/Multiplayer/MultiplayerClient.cs b/osu.Game/Online/Multiplayer/MultiplayerClient.cs index 2d445ea25a..9abc013b66 100644 --- a/osu.Game/Online/Multiplayer/MultiplayerClient.cs +++ b/osu.Game/Online/Multiplayer/MultiplayerClient.cs @@ -815,19 +815,22 @@ namespace osu.Game.Online.Multiplayer /// The s to populate. protected async Task PopulateUsers(IEnumerable multiplayerUsers) { - var request = new GetUsersRequest(multiplayerUsers.Select(u => u.UserID).Distinct().ToArray()); - - await API.PerformAsync(request).ConfigureAwait(false); - - if (request.Response == null) - return; - - Dictionary users = request.Response.Users.ToDictionary(user => user.Id); - - foreach (var multiplayerUser in multiplayerUsers) + foreach (int[] userChunk in multiplayerUsers.Select(u => u.UserID).Distinct().Chunk(GetUsersRequest.MAX_IDS_PER_REQUEST)) { - if (users.TryGetValue(multiplayerUser.UserID, out var user)) - multiplayerUser.User = user; + var request = new GetUsersRequest(userChunk); + + await API.PerformAsync(request).ConfigureAwait(false); + + if (request.Response == null) + return; + + Dictionary users = request.Response.Users.ToDictionary(user => user.Id); + + foreach (var multiplayerUser in multiplayerUsers) + { + if (users.TryGetValue(multiplayerUser.UserID, out var user)) + multiplayerUser.User = user; + } } } From 4a00662092a13cd1e6352400ec76403dff80f657 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Tue, 4 Mar 2025 14:02:45 +0900 Subject: [PATCH 1187/1275] Fix thread safety when kicking multiplayer users --- .../Online/Multiplayer/MultiplayerClient.cs | 61 ++++++++++--------- .../OnlinePlay/Multiplayer/Multiplayer.cs | 8 +-- 2 files changed, 35 insertions(+), 34 deletions(-) diff --git a/osu.Game/Online/Multiplayer/MultiplayerClient.cs b/osu.Game/Online/Multiplayer/MultiplayerClient.cs index 59a4547e9e..91b4ed448c 100644 --- a/osu.Game/Online/Multiplayer/MultiplayerClient.cs +++ b/osu.Game/Online/Multiplayer/MultiplayerClient.cs @@ -486,18 +486,44 @@ namespace osu.Game.Online.Multiplayer }, false); } - Task IMultiplayerClient.UserLeft(MultiplayerRoomUser user) => - handleUserLeft(user, UserLeft); + Task IMultiplayerClient.UserLeft(MultiplayerRoomUser user) + { + Scheduler.Add(() => handleUserLeft(user, UserLeft), false); + return Task.CompletedTask; + } Task IMultiplayerClient.UserKicked(MultiplayerRoomUser user) { - if (LocalUser == null) - return Task.CompletedTask; + Scheduler.Add(() => + { + if (LocalUser == null) + return; - if (user.Equals(LocalUser)) - LeaveRoom(); + if (user.Equals(LocalUser)) + LeaveRoom(); - return handleUserLeft(user, UserKicked); + handleUserLeft(user, UserKicked); + }, false); + + return Task.CompletedTask; + } + + private void handleUserLeft(MultiplayerRoomUser user, Action? callback) + { + Debug.Assert(ThreadSafety.IsUpdateThread); + + if (Room == null) + return; + + Room.Users.Remove(user); + PlayingUserIds.Remove(user.UserID); + + Debug.Assert(APIRoom != null); + APIRoom.RecentParticipants = APIRoom.RecentParticipants.Where(u => u.Id != user.UserID).ToArray(); + APIRoom.ParticipantCount--; + + callback?.Invoke(user); + RoomUpdated?.Invoke(); } async Task IMultiplayerClient.Invited(int invitedBy, long roomID, string password) @@ -544,27 +570,6 @@ namespace osu.Game.Online.Multiplayer APIRoom.ParticipantCount++; } - private Task handleUserLeft(MultiplayerRoomUser user, Action? callback) - { - Scheduler.Add(() => - { - if (Room == null) - return; - - Room.Users.Remove(user); - PlayingUserIds.Remove(user.UserID); - - Debug.Assert(APIRoom != null); - APIRoom.RecentParticipants = APIRoom.RecentParticipants.Where(u => u.Id != user.UserID).ToArray(); - APIRoom.ParticipantCount--; - - callback?.Invoke(user); - RoomUpdated?.Invoke(); - }, false); - - return Task.CompletedTask; - } - Task IMultiplayerClient.HostChanged(int userId) { Scheduler.Add(() => diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Multiplayer.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Multiplayer.cs index dfed32aebc..0b06a16d98 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Multiplayer.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Multiplayer.cs @@ -28,11 +28,9 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer private void onRoomUpdated() { - if (client.Room == null) + if (client.Room == null || client.LocalUser == null) return; - Debug.Assert(client.LocalUser != null); - // If the user exits gameplay before score submission completes, we'll transition to idle when results has been prepared. if (client.LocalUser.State == MultiplayerUserState.Results && this.IsCurrentScreen()) transitionFromResults(); @@ -62,11 +60,9 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer { base.OnResuming(e); - if (client.Room == null) + if (client.Room == null || client.LocalUser == null) return; - Debug.Assert(client.LocalUser != null); - if (!(e.Last is MultiplayerPlayerLoader playerLoader)) return; From b73a872b94f1053f57612ad32c1d07c88b8d908c Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Tue, 4 Mar 2025 14:11:32 +0900 Subject: [PATCH 1188/1275] Fix broken test --- .../NonVisual/Multiplayer/StatefulMultiplayerClientTest.cs | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/osu.Game.Tests/NonVisual/Multiplayer/StatefulMultiplayerClientTest.cs b/osu.Game.Tests/NonVisual/Multiplayer/StatefulMultiplayerClientTest.cs index 959f09361f..4019ff6730 100644 --- a/osu.Game.Tests/NonVisual/Multiplayer/StatefulMultiplayerClientTest.cs +++ b/osu.Game.Tests/NonVisual/Multiplayer/StatefulMultiplayerClientTest.cs @@ -97,16 +97,12 @@ namespace osu.Game.Tests.NonVisual.Multiplayer AddStep("create room with many users", () => { - var newRoom = new Room(); - newRoom.CopyFrom(SelectedRoom.Value!); - - newRoom.RoomID = null; MultiplayerClient.RoomSetupAction = room => { room.Users.AddRange(Enumerable.Range(PLAYER_1_ID, 100).Select(id => new MultiplayerRoomUser(id))); }; - RoomManager.CreateRoom(newRoom); + MultiplayerClient.JoinRoom(MultiplayerClient.ServerSideRooms.Single()).ConfigureAwait(false); }); AddUntilStep("wait for room join", () => RoomJoined); From 0696cfa4f241516b83face7a526b8626de01930c Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 4 Mar 2025 14:40:33 +0900 Subject: [PATCH 1189/1275] `LoungePollingComponent` -> `LoungeListingPoller` --- .../Visual/Multiplayer/TestSceneMultiplayer.cs | 2 +- ...ollingComponent.cs => LoungeListingPoller.cs} | 4 ++-- .../Screens/OnlinePlay/Lounge/LoungeSubScreen.cs | 16 ++++++++-------- 3 files changed, 11 insertions(+), 11 deletions(-) rename osu.Game/Screens/OnlinePlay/Lounge/{LoungePollingComponent.cs => LoungeListingPoller.cs} (91%) diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs index a87216287d..ec0117a990 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs @@ -805,7 +805,7 @@ namespace osu.Game.Tests.Visual.Multiplayer AddUntilStep("wait for room", () => this.ChildrenOfType().Any()); AddStep("select room", () => InputManager.Key(Key.Down)); - AddStep("disable polling", () => this.ChildrenOfType().Single().TimeBetweenPolls.Value = 0); + AddStep("disable polling", () => this.ChildrenOfType().Single().TimeBetweenPolls.Value = 0); AddStep("change server-side settings", () => { multiplayerClient.ServerSideRooms[0].Name = "New name"; diff --git a/osu.Game/Screens/OnlinePlay/Lounge/LoungePollingComponent.cs b/osu.Game/Screens/OnlinePlay/Lounge/LoungeListingPoller.cs similarity index 91% rename from osu.Game/Screens/OnlinePlay/Lounge/LoungePollingComponent.cs rename to osu.Game/Screens/OnlinePlay/Lounge/LoungeListingPoller.cs index 420a96cf8a..d92ae7eb6e 100644 --- a/osu.Game/Screens/OnlinePlay/Lounge/LoungePollingComponent.cs +++ b/osu.Game/Screens/OnlinePlay/Lounge/LoungeListingPoller.cs @@ -14,9 +14,9 @@ using osu.Game.Screens.OnlinePlay.Lounge.Components; namespace osu.Game.Screens.OnlinePlay.Lounge { /// - /// A that polls for the lounge listing. + /// Polls for rooms for the main lounge listing. /// - public partial class LoungePollingComponent : PollingComponent + public partial class LoungeListingPoller : PollingComponent { [Resolved] private IAPIProvider api { get; set; } = null!; diff --git a/osu.Game/Screens/OnlinePlay/Lounge/LoungeSubScreen.cs b/osu.Game/Screens/OnlinePlay/Lounge/LoungeSubScreen.cs index e83334eb69..12c0bb12e2 100644 --- a/osu.Game/Screens/OnlinePlay/Lounge/LoungeSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Lounge/LoungeSubScreen.cs @@ -75,7 +75,7 @@ namespace osu.Game.Screens.OnlinePlay.Lounge private readonly IBindable operationInProgress = new Bindable(); private readonly IBindable isIdle = new BindableBool(); private RoomsContainer roomsContainer = null!; - private LoungePollingComponent pollingComponent = null!; + private LoungeListingPoller listingPoller = null!; private PopoverContainer popoverContainer = null!; private LoadingLayer loadingLayer = null!; private SearchTextBox searchTextBox = null!; @@ -92,7 +92,7 @@ namespace osu.Game.Screens.OnlinePlay.Lounge InternalChildren = new Drawable[] { - pollingComponent = new LoungePollingComponent + listingPoller = new LoungeListingPoller { RoomsReceived = onListingReceived, Filter = { BindTarget = filter } @@ -187,7 +187,7 @@ namespace osu.Game.Screens.OnlinePlay.Lounge { roomsContainer.Rooms.Clear(); hasListingResults.Value = false; - pollingComponent.PollImmediately(); + listingPoller.PollImmediately(); }); updateFilter(); @@ -268,7 +268,7 @@ namespace osu.Game.Screens.OnlinePlay.Lounge onReturning(); // Poll for any newly-created rooms (including potentially the user's own). - pollingComponent.PollImmediately(); + listingPoller.PollImmediately(); } public override bool OnExiting(ScreenExitEvent e) @@ -379,7 +379,7 @@ namespace osu.Game.Screens.OnlinePlay.Lounge protected virtual void OpenNewRoom(Room room) => this.Push(CreateRoomSubScreen(room)); - public void RefreshRooms() => pollingComponent.PollImmediately(); + public void RefreshRooms() => listingPoller.PollImmediately(); private void updateLoadingLayer() { @@ -392,11 +392,11 @@ namespace osu.Game.Screens.OnlinePlay.Lounge private void updatePollingRate(bool isCurrentScreen) { if (!isCurrentScreen) - pollingComponent.TimeBetweenPolls.Value = 0; + listingPoller.TimeBetweenPolls.Value = 0; else - pollingComponent.TimeBetweenPolls.Value = isIdle.Value ? 120000 : 15000; + listingPoller.TimeBetweenPolls.Value = isIdle.Value ? 120000 : 15000; - Logger.Log($"Polling adjusted (listing: {pollingComponent.TimeBetweenPolls.Value})"); + Logger.Log($"Polling adjusted (listing: {listingPoller.TimeBetweenPolls.Value})"); } protected abstract OsuButton CreateNewRoomButton(); From 77d5b1d5dd605f94b81205d00fc055697e77a7ef Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Tue, 4 Mar 2025 14:36:54 +0900 Subject: [PATCH 1190/1275] Fix multiplayer not joining correct chat channel --- osu.Game/Online/Multiplayer/MultiplayerClient.cs | 1 + osu.Game/Online/Multiplayer/MultiplayerRoom.cs | 7 +++++++ osu.Game/Online/Rooms/Room.cs | 2 +- 3 files changed, 9 insertions(+), 1 deletion(-) diff --git a/osu.Game/Online/Multiplayer/MultiplayerClient.cs b/osu.Game/Online/Multiplayer/MultiplayerClient.cs index 59a4547e9e..82836a00f0 100644 --- a/osu.Game/Online/Multiplayer/MultiplayerClient.cs +++ b/osu.Game/Online/Multiplayer/MultiplayerClient.cs @@ -235,6 +235,7 @@ namespace osu.Game.Online.Multiplayer APIRoom = apiRoom; APIRoom.RoomID = joinedRoom.RoomID; + APIRoom.ChannelId = joinedRoom.ChannelID; APIRoom.Host = joinedRoom.Host?.User; APIRoom.Playlist = joinedRoom.Playlist.Select(item => new PlaylistItem(item)).ToArray(); APIRoom.CurrentPlaylistItem = APIRoom.Playlist.Single(item => item.ID == joinedRoom.Settings.PlaylistItemId); diff --git a/osu.Game/Online/Multiplayer/MultiplayerRoom.cs b/osu.Game/Online/Multiplayer/MultiplayerRoom.cs index f7bd4490ff..b8b90d907f 100644 --- a/osu.Game/Online/Multiplayer/MultiplayerRoom.cs +++ b/osu.Game/Online/Multiplayer/MultiplayerRoom.cs @@ -59,6 +59,12 @@ namespace osu.Game.Online.Multiplayer [Key(7)] public IList ActiveCountdowns { get; set; } = new List(); + /// + /// The ID of the chat channel for the room. + /// + [Key(8)] + public int ChannelID { get; set; } + [JsonConstructor] [SerializationConstructor] public MultiplayerRoom(long roomId) @@ -69,6 +75,7 @@ namespace osu.Game.Online.Multiplayer public MultiplayerRoom(Room room) { RoomID = room.RoomID ?? 0; + ChannelID = room.ChannelId; Settings = new MultiplayerRoomSettings(room); Host = room.Host != null ? new MultiplayerRoomUser(room.Host.OnlineID) : null; Playlist = room.Playlist.Select(p => new MultiplayerPlaylistItem(p)).ToArray(); diff --git a/osu.Game/Online/Rooms/Room.cs b/osu.Game/Online/Rooms/Room.cs index c5e292a19d..e965f9c187 100644 --- a/osu.Game/Online/Rooms/Room.cs +++ b/osu.Game/Online/Rooms/Room.cs @@ -242,7 +242,7 @@ namespace osu.Game.Online.Rooms public int ChannelId { get => channelId; - private set => SetField(ref channelId, value); + set => SetField(ref channelId, value); } /// From 9e8a6117280fa4ccf1dbe7fb545ca072f397d085 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 4 Mar 2025 15:05:12 +0900 Subject: [PATCH 1191/1275] Rename `RoomsContainer` and scope down bindables --- .../TestSceneLoungeRoomsContainer.cs | 4 +-- .../TestScenePlaylistsLoungeSubScreen.cs | 16 +++++----- .../OnlinePlay/DrawableRoomPlaylist.cs | 2 +- .../{RoomsContainer.cs => RoomListing.cs} | 29 ++++++++++++++----- .../OnlinePlay/Lounge/LoungeSubScreen.cs | 14 ++++----- 5 files changed, 39 insertions(+), 26 deletions(-) rename osu.Game/Screens/OnlinePlay/Lounge/Components/{RoomsContainer.cs => RoomListing.cs} (91%) diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneLoungeRoomsContainer.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneLoungeRoomsContainer.cs index 772eb91174..b43433fe8d 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneLoungeRoomsContainer.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneLoungeRoomsContainer.cs @@ -21,7 +21,7 @@ namespace osu.Game.Tests.Visual.Multiplayer public partial class TestSceneLoungeRoomsContainer : OnlinePlayTestScene { private BindableList rooms = null!; - private RoomsContainer container = null!; + private RoomListing container = null!; public override void SetUpSteps() { @@ -36,7 +36,7 @@ namespace osu.Game.Tests.Visual.Multiplayer Anchor = Anchor.Centre, Origin = Anchor.Centre, Width = 0.5f, - Child = container = new RoomsContainer + Child = container = new RoomListing { RelativeSizeAxes = Axes.Both, Rooms = { BindTarget = rooms }, diff --git a/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsLoungeSubScreen.cs b/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsLoungeSubScreen.cs index 35bf6dc28a..ceb3a32402 100644 --- a/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsLoungeSubScreen.cs +++ b/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsLoungeSubScreen.cs @@ -26,7 +26,7 @@ namespace osu.Game.Tests.Visual.Playlists AddUntilStep("wait for present", () => loungeScreen.IsCurrentScreen()); } - private RoomsContainer roomsContainer => loungeScreen.ChildrenOfType().First(); + private RoomListing roomListing => loungeScreen.ChildrenOfType().First(); [Test] public void TestManyRooms() @@ -41,13 +41,13 @@ namespace osu.Game.Tests.Visual.Playlists createRooms(GenerateRooms(30)); - AddStep("move mouse to third room", () => InputManager.MoveMouseTo(roomsContainer.DrawableRooms[2])); + AddStep("move mouse to third room", () => InputManager.MoveMouseTo(roomListing.DrawableRooms[2])); AddStep("hold down", () => InputManager.PressButton(MouseButton.Left)); - AddStep("drag to top", () => InputManager.MoveMouseTo(roomsContainer.DrawableRooms[0])); + AddStep("drag to top", () => InputManager.MoveMouseTo(roomListing.DrawableRooms[0])); AddAssert("first and second room masked", () - => !checkRoomVisible(roomsContainer.DrawableRooms[0]) && - !checkRoomVisible(roomsContainer.DrawableRooms[1])); + => !checkRoomVisible(roomListing.DrawableRooms[0]) && + !checkRoomVisible(roomListing.DrawableRooms[1])); } [Test] @@ -55,10 +55,10 @@ namespace osu.Game.Tests.Visual.Playlists { createRooms(GenerateRooms(30)); - AddStep("select last room", () => roomsContainer.DrawableRooms[^1].TriggerClick()); + AddStep("select last room", () => roomListing.DrawableRooms[^1].TriggerClick()); - AddUntilStep("first room is masked", () => !checkRoomVisible(roomsContainer.DrawableRooms[0])); - AddUntilStep("last room is not masked", () => checkRoomVisible(roomsContainer.DrawableRooms[^1])); + AddUntilStep("first room is masked", () => !checkRoomVisible(roomListing.DrawableRooms[0])); + AddUntilStep("last room is not masked", () => checkRoomVisible(roomListing.DrawableRooms[^1])); } private bool checkRoomVisible(DrawableRoom room) => diff --git a/osu.Game/Screens/OnlinePlay/DrawableRoomPlaylist.cs b/osu.Game/Screens/OnlinePlay/DrawableRoomPlaylist.cs index 207e0bdf55..c9d8365852 100644 --- a/osu.Game/Screens/OnlinePlay/DrawableRoomPlaylist.cs +++ b/osu.Game/Screens/OnlinePlay/DrawableRoomPlaylist.cs @@ -204,7 +204,7 @@ namespace osu.Game.Screens.OnlinePlay ScrollContainer.ScrollIntoView(drawableItem); } - #region Key selection logic (shared with BeatmapCarousel and RoomsContainer) + #region Key selection logic (shared with BeatmapCarousel and RoomListing) public bool OnPressed(KeyBindingPressEvent e) { diff --git a/osu.Game/Screens/OnlinePlay/Lounge/Components/RoomsContainer.cs b/osu.Game/Screens/OnlinePlay/Lounge/Components/RoomListing.cs similarity index 91% rename from osu.Game/Screens/OnlinePlay/Lounge/Components/RoomsContainer.cs rename to osu.Game/Screens/OnlinePlay/Lounge/Components/RoomListing.cs index 65f969bc7b..1c3db87aaf 100644 --- a/osu.Game/Screens/OnlinePlay/Lounge/Components/RoomsContainer.cs +++ b/osu.Game/Screens/OnlinePlay/Lounge/Components/RoomListing.cs @@ -21,12 +21,25 @@ using osuTK; namespace osu.Game.Screens.OnlinePlay.Lounge.Components { - public partial class RoomsContainer : CompositeDrawable, IKeyBindingHandler + public partial class RoomListing : CompositeDrawable, IKeyBindingHandler { + /// + /// Rooms which should be displayed. Should be managed externally. + /// public readonly BindableList Rooms = new BindableList(); - public readonly Bindable SelectedRoom = new Bindable(); + + /// + /// The current filter criteria. Should be managed externally. + /// public readonly Bindable Filter = new Bindable(); + /// + /// The currently user-selected room. + /// + public IBindable SelectedRoom => selectedRoom; + + private readonly Bindable selectedRoom = new Bindable(); + public IReadOnlyList DrawableRooms => roomFlow.FlowingChildren.Cast().ToArray(); private readonly ScrollContainer scroll; @@ -35,7 +48,7 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components // handle deselection public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => true; - public RoomsContainer() + public RoomListing() { InternalChild = scroll = new OsuScrollContainer { @@ -158,7 +171,7 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components { foreach (var room in rooms) { - var drawableRoom = new DrawableLoungeRoom(room) { SelectedRoom = SelectedRoom }; + var drawableRoom = new DrawableLoungeRoom(room) { SelectedRoom = selectedRoom }; roomFlow.Add(drawableRoom); @@ -177,7 +190,7 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components // selection may have a lease due to being in a sub screen. if (SelectedRoom.Value == r && !SelectedRoom.Disabled) - SelectedRoom.Value = null; + selectedRoom.Value = null; } } @@ -187,13 +200,13 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components // selection may have a lease due to being in a sub screen. if (!SelectedRoom.Disabled) - SelectedRoom.Value = null; + selectedRoom.Value = null; } protected override bool OnClick(ClickEvent e) { if (!SelectedRoom.Disabled) - SelectedRoom.Value = null; + selectedRoom.Value = null; return base.OnClick(e); } @@ -240,7 +253,7 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components // we already have a valid selection only change selection if we still have a room to switch to. if (room != null) - SelectedRoom.Value = room; + selectedRoom.Value = room; } #endregion diff --git a/osu.Game/Screens/OnlinePlay/Lounge/LoungeSubScreen.cs b/osu.Game/Screens/OnlinePlay/Lounge/LoungeSubScreen.cs index 12c0bb12e2..c1c65a744a 100644 --- a/osu.Game/Screens/OnlinePlay/Lounge/LoungeSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Lounge/LoungeSubScreen.cs @@ -38,7 +38,7 @@ namespace osu.Game.Screens.OnlinePlay.Lounge protected override BackgroundScreen CreateBackground() => new LoungeBackgroundScreen { - SelectedRoom = { BindTarget = roomsContainer.SelectedRoom } + SelectedRoom = { BindTarget = roomListing.SelectedRoom } }; protected override UserActivity InitialActivity => new UserActivity.SearchingForLobby(); @@ -74,7 +74,7 @@ namespace osu.Game.Screens.OnlinePlay.Lounge private readonly Bindable hasListingResults = new Bindable(); private readonly IBindable operationInProgress = new Bindable(); private readonly IBindable isIdle = new BindableBool(); - private RoomsContainer roomsContainer = null!; + private RoomListing roomListing = null!; private LoungeListingPoller listingPoller = null!; private PopoverContainer popoverContainer = null!; private LoadingLayer loadingLayer = null!; @@ -106,7 +106,7 @@ namespace osu.Game.Screens.OnlinePlay.Lounge Horizontal = WaveOverlayContainer.WIDTH_PADDING, Top = Header.HEIGHT + controls_area_height + 20, }, - Child = roomsContainer = new RoomsContainer + Child = roomListing = new RoomListing { RelativeSizeAxes = Axes.Both, Filter = { BindTarget = filter }, @@ -185,7 +185,7 @@ namespace osu.Game.Screens.OnlinePlay.Lounge filter.BindValueChanged(_ => { - roomsContainer.Rooms.Clear(); + roomListing.Rooms.Clear(); hasListingResults.Value = false; listingPoller.PollImmediately(); }); @@ -195,11 +195,11 @@ namespace osu.Game.Screens.OnlinePlay.Lounge private void onListingReceived(Room[] result) { - Dictionary localRoomsById = roomsContainer.Rooms.ToDictionary(r => r.RoomID!.Value); + Dictionary localRoomsById = roomListing.Rooms.ToDictionary(r => r.RoomID!.Value); Dictionary resultRoomsById = result.ToDictionary(r => r.RoomID!.Value); // Remove all local rooms no longer in the result set. - roomsContainer.Rooms.RemoveAll(r => !resultRoomsById.ContainsKey(r.RoomID!.Value)); + roomListing.Rooms.RemoveAll(r => !resultRoomsById.ContainsKey(r.RoomID!.Value)); // Add or update local rooms with the result set. foreach (var r in result) @@ -207,7 +207,7 @@ namespace osu.Game.Screens.OnlinePlay.Lounge if (localRoomsById.TryGetValue(r.RoomID!.Value, out Room? existingRoom)) existingRoom.CopyFrom(r); else - roomsContainer.Rooms.Add(r); + roomListing.Rooms.Add(r); } hasListingResults.Value = true; From a0888a7f2c5f7839243c7502a755643dddb664d3 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 4 Mar 2025 15:51:08 +0900 Subject: [PATCH 1192/1275] Attempt to fix common editor test failures See https://github.com/ppy/osu/actions/runs/13623586844/job/38143232417?pr=32180 for one example. Arguably the bindable usage in [`ControlPointPart`](https://github.com/ppy/osu/blob/2365b065a4994f38fe67bab7d193e5a09bee538c/osu.Game/Screens/Edit/Components/Timelines/Summary/Parts/ControlPointPart.cs#L24-L26) is dangerous, but it's only dangerous in tests (because control points aren't mutated outside the editor) so I'm willing to turn a blind eye for now to favour async loading support. --- .../Editing/TestSceneEditorBeatmapCreation.cs | 39 +++++++++++++++++-- 1 file changed, 36 insertions(+), 3 deletions(-) diff --git a/osu.Game.Tests/Visual/Editing/TestSceneEditorBeatmapCreation.cs b/osu.Game.Tests/Visual/Editing/TestSceneEditorBeatmapCreation.cs index b7990b64c1..1413c4f436 100644 --- a/osu.Game.Tests/Visual/Editing/TestSceneEditorBeatmapCreation.cs +++ b/osu.Game.Tests/Visual/Editing/TestSceneEditorBeatmapCreation.cs @@ -171,6 +171,8 @@ namespace osu.Game.Tests.Visual.Editing return difficultyName != null && difficultyName != firstDifficultyName; }); + ensureEditorLoaded(); + AddAssert("created difficulty has timing point", () => { var timingPoint = EditorBeatmap.ControlPointInfo.TimingPoints.Single(); @@ -215,6 +217,8 @@ namespace osu.Game.Tests.Visual.Editing return difficultyName != null && difficultyName != previousDifficultyName; }); + ensureEditorLoaded(); + AddStep("set unique difficulty name", () => EditorBeatmap.BeatmapInfo.DifficultyName = previousDifficultyName = Guid.NewGuid().ToString()); AddStep("add timing point", () => EditorBeatmap.ControlPointInfo.Add(0, new TimingControlPoint { BeatLength = 1000 })); AddStep("add effect points", () => @@ -239,6 +243,8 @@ namespace osu.Game.Tests.Visual.Editing return difficultyName != null && difficultyName != previousDifficultyName; }); + ensureEditorLoaded(); + AddAssert("created difficulty has timing point", () => { var timingPoint = EditorBeatmap.ControlPointInfo.TimingPoints.Single(); @@ -287,6 +293,8 @@ namespace osu.Game.Tests.Visual.Editing return difficultyName != null && difficultyName != firstDifficultyName; }); + ensureEditorLoaded(); + AddAssert("created difficulty has timing point", () => { var timingPoint = EditorBeatmap.ControlPointInfo.TimingPoints.Single(); @@ -367,6 +375,8 @@ namespace osu.Game.Tests.Visual.Editing return difficultyName != null && difficultyName != originalDifficultyName; }); + ensureEditorLoaded(); + AddAssert("created difficulty has copy suffix in name", () => EditorBeatmap.BeatmapInfo.DifficultyName == copyDifficultyName); AddAssert("created difficulty has timing point", () => { @@ -377,7 +387,9 @@ namespace osu.Game.Tests.Visual.Editing AddAssert("approach rate correctly copied", () => EditorBeatmap.Difficulty.ApproachRate == 4); AddAssert("combo colours correctly copied", () => EditorBeatmap.BeatmapSkin.AsNonNull().ComboColours.Count == 2); + ensureEditorLoaded(); AddAssert("status is modified", () => EditorBeatmap.BeatmapInfo.Status == BeatmapOnlineStatus.LocallyModified); + AddAssert("online ID not copied", () => EditorBeatmap.BeatmapInfo.OnlineID == -1); AddStep("save beatmap", () => Editor.Save()); @@ -440,6 +452,8 @@ namespace osu.Game.Tests.Visual.Editing return difficultyName != null && difficultyName != originalDifficultyName; }); + ensureEditorLoaded(); + AddStep("save without changes", () => Editor.Save()); AddAssert("collection still points to old beatmap", () => !collection.BeatmapMD5Hashes.Contains(EditorBeatmap.BeatmapInfo.MD5Hash) @@ -477,6 +491,9 @@ namespace osu.Game.Tests.Visual.Editing string? difficultyName = Editor.ChildrenOfType().SingleOrDefault()?.BeatmapInfo.DifficultyName; return difficultyName != null && difficultyName != "New Difficulty"; }); + + ensureEditorLoaded(); + AddAssert("new difficulty has correct name", () => EditorBeatmap.BeatmapInfo.DifficultyName == "New Difficulty (1)"); AddAssert("new difficulty persisted", () => { @@ -514,6 +531,10 @@ namespace osu.Game.Tests.Visual.Editing return difficultyName != null && difficultyName != duplicate_difficulty_name; }); + ensureEditorLoaded(); + + ensureEditorLoaded(); + AddStep("set difficulty name", () => EditorBeatmap.BeatmapInfo.DifficultyName = duplicate_difficulty_name); AddStep("try to save beatmap", () => Editor.Save()); AddAssert("beatmap set not corrupted", () => @@ -540,6 +561,8 @@ namespace osu.Game.Tests.Visual.Editing return set != null && set.PerformRead(s => s.Beatmaps.Count == 1 && s.Files.Count == 1); }); + ensureEditorLoaded(); + AddStep("create new difficulty", () => Editor.CreateNewDifficulty(new CatchRuleset().RulesetInfo)); AddUntilStep("wait for created", () => @@ -547,7 +570,8 @@ namespace osu.Game.Tests.Visual.Editing string? difficultyName = Editor.ChildrenOfType().SingleOrDefault()?.BeatmapInfo.DifficultyName; return difficultyName != null && difficultyName != duplicate_difficulty_name; }); - AddUntilStep("wait for editor load", () => Editor.IsLoaded && DialogOverlay.IsLoaded); + + ensureEditorLoaded(); AddStep("add hitobjects", () => EditorBeatmap.AddRange(new[] { @@ -584,6 +608,9 @@ namespace osu.Game.Tests.Visual.Editing string? difficultyName = Editor.ChildrenOfType().SingleOrDefault()?.BeatmapInfo.DifficultyName; return difficultyName != null && difficultyName == "New Difficulty"; }); + + ensureEditorLoaded(); + AddAssert("new difficulty persisted", () => { var set = beatmapManager.QueryBeatmapSet(s => s.ID == setId); @@ -610,6 +637,9 @@ namespace osu.Game.Tests.Visual.Editing string? difficultyName = Editor.ChildrenOfType().SingleOrDefault()?.BeatmapInfo.DifficultyName; return difficultyName != null && difficultyName == "New Difficulty (1)"; }); + + ensureEditorLoaded(); + AddAssert("new difficulty persisted", () => { var set = beatmapManager.QueryBeatmapSet(s => s.ID == setId); @@ -735,6 +765,8 @@ namespace osu.Game.Tests.Visual.Editing AddAssert("other audio not removed", () => Beatmap.Value.BeatmapSetInfo.Files.Any(f => f.Filename == "audio (1).mp3")); } + private void ensureEditorLoaded() => AddUntilStep("wait for editor load", () => Editor.IsLoaded && DialogOverlay.IsLoaded); + private void createNewDifficulty() { string? currentDifficulty = null; @@ -748,13 +780,14 @@ namespace osu.Game.Tests.Visual.Editing AddUntilStep("wait for dialog", () => DialogOverlay.CurrentDialog is CreateNewDifficultyDialog); AddStep("confirm creation with no objects", () => DialogOverlay.CurrentDialog!.PerformOkAction()); + AddUntilStep("wait for created", () => { string? difficultyName = Editor.ChildrenOfType().SingleOrDefault()?.BeatmapInfo.DifficultyName; return difficultyName != null && difficultyName != currentDifficulty; }); + ensureEditorLoaded(); - AddUntilStep("wait for editor load", () => Editor.IsLoaded); AddStep("enter setup mode", () => Editor.Mode.Value = EditorScreenMode.SongSetup); AddUntilStep("wait for load", () => Editor.ChildrenOfType().Any()); } @@ -765,7 +798,7 @@ namespace osu.Game.Tests.Visual.Editing AddStep($"switch to difficulty #{index + 1}", () => Editor.SwitchToDifficulty(Beatmap.Value.BeatmapSetInfo.Beatmaps.ElementAt(index))); - AddUntilStep("wait for editor load", () => Editor.IsLoaded); + ensureEditorLoaded(); AddStep("enter setup mode", () => Editor.Mode.Value = EditorScreenMode.SongSetup); AddUntilStep("wait for load", () => Editor.ChildrenOfType().Any()); } From 4085ee805a717e2f0869a445b294b08d9730e2e6 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 4 Mar 2025 15:29:13 +0900 Subject: [PATCH 1193/1275] Adjust scale and display of rooms in multiplayer lounge Just a quick pass because the rooms were definitely larger than they should be. --- .../Lounge/Components/RoomListing.cs | 25 ++++- .../OnlinePlay/Lounge/LoungeSubScreen.cs | 104 ++++++++++++------ 2 files changed, 90 insertions(+), 39 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/Lounge/Components/RoomListing.cs b/osu.Game/Screens/OnlinePlay/Lounge/Components/RoomListing.cs index 1c3db87aaf..0276601656 100644 --- a/osu.Game/Screens/OnlinePlay/Lounge/Components/RoomListing.cs +++ b/osu.Game/Screens/OnlinePlay/Lounge/Components/RoomListing.cs @@ -45,14 +45,20 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components private readonly ScrollContainer scroll; private readonly FillFlowContainer roomFlow; + private const float display_scale = 0.8f; + // handle deselection public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => true; public RoomListing() { - InternalChild = scroll = new OsuScrollContainer + InternalChild = scroll = new Scroll { + Masking = false, RelativeSizeAxes = Axes.Both, + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre, + Width = display_scale, ScrollbarOverlapsContent = false, Padding = new MarginPadding { Right = 5 }, Child = new OsuContextMenuContainer @@ -64,12 +70,18 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y, Direction = FillDirection.Vertical, - Spacing = new Vector2(10), + Spacing = new Vector2(5), + Margin = new MarginPadding { Vertical = 10 }, } } }; } + private partial class Scroll : OsuScrollContainer + { + public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => true; + } + protected override void LoadComplete() { SelectedRoom.BindValueChanged(onSelectedRoomChanged, true); @@ -171,7 +183,14 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components { foreach (var room in rooms) { - var drawableRoom = new DrawableLoungeRoom(room) { SelectedRoom = selectedRoom }; + var drawableRoom = new DrawableLoungeRoom(room) + { + SelectedRoom = selectedRoom, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Scale = new Vector2(display_scale), + Width = 1 / display_scale, + }; roomFlow.Add(drawableRoom); diff --git a/osu.Game/Screens/OnlinePlay/Lounge/LoungeSubScreen.cs b/osu.Game/Screens/OnlinePlay/Lounge/LoungeSubScreen.cs index c1c65a744a..c84f49fef6 100644 --- a/osu.Game/Screens/OnlinePlay/Lounge/LoungeSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Lounge/LoungeSubScreen.cs @@ -8,9 +8,12 @@ using System.Linq; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Extensions; +using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; +using osu.Framework.Graphics.Colour; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Cursor; +using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.UserInterface; using osu.Framework.Input.Events; using osu.Framework.Logging; @@ -27,6 +30,7 @@ using osu.Game.Screens.OnlinePlay.Lounge.Components; using osu.Game.Screens.OnlinePlay.Match; using osu.Game.Users; using osuTK; +using osuTK.Graphics; namespace osu.Game.Screens.OnlinePlay.Lounge { @@ -85,11 +89,15 @@ namespace osu.Game.Screens.OnlinePlay.Lounge [BackgroundDependencyLoader(true)] private void load() { + Masking = true; + const float controls_area_height = 25f; if (idleTracker != null) isIdle.BindTo(idleTracker.IsIdle); + Color4 bg = Color4Extensions.FromHex("#070405"); + InternalChildren = new Drawable[] { listingPoller = new LoungeListingPoller @@ -113,56 +121,80 @@ namespace osu.Game.Screens.OnlinePlay.Lounge } }, loadingLayer = new LoadingLayer(true), - new FillFlowContainer + new Container { - Name = @"Header area flow", + Name = "Header area", RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y, - Padding = new MarginPadding { Horizontal = WaveOverlayContainer.WIDTH_PADDING }, - Direction = FillDirection.Vertical, Children = new Drawable[] { - new Container + new Box { - RelativeSizeAxes = Axes.X, - Height = Header.HEIGHT, - Child = searchTextBox = new BasicSearchTextBox - { - Anchor = Anchor.CentreRight, - Origin = Anchor.CentreRight, - RelativeSizeAxes = Axes.X, - Width = 0.6f, - }, + Colour = ColourInfo.GradientVertical(bg, bg.Opacity(0.75f)), + RelativeSizeAxes = Axes.Both, + Height = 0.8f, }, - new Container + new Box { + Colour = ColourInfo.GradientVertical(bg.Opacity(0.75f), bg.Opacity(0)), + RelativeSizeAxes = Axes.Both, + RelativePositionAxes = Axes.Both, + Y = 0.8f, + // Intentionally taller than the header for a more gradual fade + Height = 0.5f, + }, + new FillFlowContainer + { + Name = @"Header area flow", RelativeSizeAxes = Axes.X, - Height = controls_area_height, + AutoSizeAxes = Axes.Y, + Padding = new MarginPadding { Horizontal = WaveOverlayContainer.WIDTH_PADDING }, + Direction = FillDirection.Vertical, Children = new Drawable[] { - Buttons.WithChild(CreateNewRoomButton().With(d => + new Container { - d.Anchor = Anchor.BottomLeft; - d.Origin = Anchor.BottomLeft; - d.Size = new Vector2(150, 37.5f); - d.Action = () => Open(); - })), - new FillFlowContainer - { - Anchor = Anchor.TopRight, - Origin = Anchor.TopRight, - AutoSizeAxes = Axes.Both, - Direction = FillDirection.Horizontal, - Spacing = new Vector2(10), - ChildrenEnumerable = CreateFilterControls().Select(f => f.With(d => + RelativeSizeAxes = Axes.X, + Height = Header.HEIGHT, + Child = searchTextBox = new BasicSearchTextBox { - d.Anchor = Anchor.TopRight; - d.Origin = Anchor.TopRight; - })) + Anchor = Anchor.CentreRight, + Origin = Anchor.CentreRight, + RelativeSizeAxes = Axes.X, + Width = 0.6f, + }, + }, + new Container + { + RelativeSizeAxes = Axes.X, + Height = controls_area_height, + Children = new Drawable[] + { + Buttons.WithChild(CreateNewRoomButton().With(d => + { + d.Anchor = Anchor.BottomLeft; + d.Origin = Anchor.BottomLeft; + d.Size = new Vector2(150, 37.5f); + d.Action = () => Open(); + })), + new FillFlowContainer + { + Anchor = Anchor.TopRight, + Origin = Anchor.TopRight, + AutoSizeAxes = Axes.Both, + Direction = FillDirection.Horizontal, + Spacing = new Vector2(10), + ChildrenEnumerable = CreateFilterControls().Select(f => f.With(d => + { + d.Anchor = Anchor.TopRight; + d.Origin = Anchor.TopRight; + })) + } + } } - } - } - }, + }, + }, + } }, }; } From 4a16b4bd984f8564eec8c940be245ffb3f5014ee Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 4 Mar 2025 16:15:40 +0900 Subject: [PATCH 1194/1275] Fix typo in xmldoc --- osu.Game/Online/Chat/ChannelManager.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Online/Chat/ChannelManager.cs b/osu.Game/Online/Chat/ChannelManager.cs index 74e85c595c..e9ca0a8ed2 100644 --- a/osu.Game/Online/Chat/ChannelManager.cs +++ b/osu.Game/Online/Chat/ChannelManager.cs @@ -411,7 +411,7 @@ namespace osu.Game.Online.Chat } /// - /// Find an existing channel instance for the provided channel. Lookup is performed basd on ID. + /// Find an existing channel instance for the provided channel. Lookup is performed based on ID. /// The provided channel may be used if an existing instance is not found. /// /// A candidate channel to be used for lookup or permanently on lookup failure. From b19c2c7f9faae5025ece2352e5617b29d6f744f9 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Tue, 4 Mar 2025 17:01:41 +0900 Subject: [PATCH 1195/1275] Update recently-added test --- .../Visual/Multiplayer/TestSceneMultiplayerMatchSubScreen.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSubScreen.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSubScreen.cs index c01cb70955..e5e4921a17 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSubScreen.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSubScreen.cs @@ -318,7 +318,7 @@ namespace osu.Game.Tests.Visual.Multiplayer { AddStep("add playlist item", () => { - SelectedRoom.Value!.Playlist = + room.Playlist = [ new PlaylistItem(new TestBeatmap(new OsuRuleset().RulesetInfo).BeatmapInfo) { From 5b0e54a77d712e4b7b924eeb6d2092dc5aa8848a Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Tue, 4 Mar 2025 17:22:19 +0900 Subject: [PATCH 1196/1275] Remove duplicated assert --- osu.Game.Tests/Visual/Editing/TestSceneEditorBeatmapCreation.cs | 2 -- 1 file changed, 2 deletions(-) diff --git a/osu.Game.Tests/Visual/Editing/TestSceneEditorBeatmapCreation.cs b/osu.Game.Tests/Visual/Editing/TestSceneEditorBeatmapCreation.cs index 1413c4f436..996e87ff8a 100644 --- a/osu.Game.Tests/Visual/Editing/TestSceneEditorBeatmapCreation.cs +++ b/osu.Game.Tests/Visual/Editing/TestSceneEditorBeatmapCreation.cs @@ -533,8 +533,6 @@ namespace osu.Game.Tests.Visual.Editing ensureEditorLoaded(); - ensureEditorLoaded(); - AddStep("set difficulty name", () => EditorBeatmap.BeatmapInfo.DifficultyName = duplicate_difficulty_name); AddStep("try to save beatmap", () => Editor.Save()); AddAssert("beatmap set not corrupted", () => From f0d6641adf8a4076c886c9dfa321d2281e925361 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 4 Mar 2025 17:44:48 +0900 Subject: [PATCH 1197/1275] Add basic subclassing and implement beatmap-start flow --- .../SongSelectV2/TestSceneSongSelect.cs | 4 +- .../TestSceneSongSelectNavigation.cs | 2 +- osu.Game/Screens/SelectV2/BeatmapCarousel.cs | 8 ++++ osu.Game/Screens/SelectV2/SoloSongSelect.cs | 28 +++++++++++++ .../{SongSelectV2.cs => SongSelect.cs} | 41 ++++++++++++------- 5 files changed, 65 insertions(+), 18 deletions(-) create mode 100644 osu.Game/Screens/SelectV2/SoloSongSelect.cs rename osu.Game/Screens/SelectV2/{SongSelectV2.cs => SongSelect.cs} (84%) diff --git a/osu.Game.Tests/Visual/SongSelectV2/TestSceneSongSelect.cs b/osu.Game.Tests/Visual/SongSelectV2/TestSceneSongSelect.cs index 6d180c76d9..630f3c95ee 100644 --- a/osu.Game.Tests/Visual/SongSelectV2/TestSceneSongSelect.cs +++ b/osu.Game.Tests/Visual/SongSelectV2/TestSceneSongSelect.cs @@ -78,8 +78,8 @@ namespace osu.Game.Tests.Visual.SongSelectV2 { base.SetUpSteps(); - AddStep("load screen", () => Stack.Push(new Screens.SelectV2.SongSelectV2())); - AddUntilStep("wait for load", () => Stack.CurrentScreen is Screens.SelectV2.SongSelectV2 songSelect && songSelect.IsLoaded); + AddStep("load screen", () => Stack.Push(new Screens.SelectV2.SoloSongSelect())); + AddUntilStep("wait for load", () => Stack.CurrentScreen is Screens.SelectV2.SongSelect songSelect && songSelect.IsLoaded); } [Test] diff --git a/osu.Game.Tests/Visual/SongSelectV2/TestSceneSongSelectNavigation.cs b/osu.Game.Tests/Visual/SongSelectV2/TestSceneSongSelectNavigation.cs index 5173cb5673..a7ca3cd18c 100644 --- a/osu.Game.Tests/Visual/SongSelectV2/TestSceneSongSelectNavigation.cs +++ b/osu.Game.Tests/Visual/SongSelectV2/TestSceneSongSelectNavigation.cs @@ -16,7 +16,7 @@ namespace osu.Game.Tests.Visual.SongSelectV2 base.SetUpSteps(); AddStep("press enter", () => InputManager.Key(Key.Enter)); AddWaitStep("wait", 5); - PushAndConfirm(() => new Screens.SelectV2.SongSelectV2()); + PushAndConfirm(() => new Screens.SelectV2.SoloSongSelect()); } [Test] diff --git a/osu.Game/Screens/SelectV2/BeatmapCarousel.cs b/osu.Game/Screens/SelectV2/BeatmapCarousel.cs index c6bce228dc..7372847402 100644 --- a/osu.Game/Screens/SelectV2/BeatmapCarousel.cs +++ b/osu.Game/Screens/SelectV2/BeatmapCarousel.cs @@ -20,6 +20,8 @@ namespace osu.Game.Screens.SelectV2 [Cached] public partial class BeatmapCarousel : Carousel { + public Action? RequestPresentBeatmap { private get; init; } + public const float SPACING = 5f; private IBindableList detachedBeatmaps = null!; @@ -128,6 +130,12 @@ namespace osu.Game.Screens.SelectV2 return; case BeatmapInfo beatmapInfo: + if (ReferenceEquals(CurrentSelection, beatmapInfo)) + { + RequestPresentBeatmap?.Invoke(beatmapInfo); + return; + } + CurrentSelection = beatmapInfo; return; } diff --git a/osu.Game/Screens/SelectV2/SoloSongSelect.cs b/osu.Game/Screens/SelectV2/SoloSongSelect.cs new file mode 100644 index 0000000000..e6ecdc6705 --- /dev/null +++ b/osu.Game/Screens/SelectV2/SoloSongSelect.cs @@ -0,0 +1,28 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using osu.Framework.Screens; +using osu.Game.Screens.Play; + +namespace osu.Game.Screens.SelectV2 +{ + public partial class SoloSongSelect : SongSelect + { + protected override bool OnStart() + { + this.Push(new PlayerLoaderV2(() => new SoloPlayer())); + return false; + } + + private partial class PlayerLoaderV2 : PlayerLoader + { + public override bool ShowFooter => true; + + public PlayerLoaderV2(Func createPlayer) + : base(createPlayer) + { + } + } + } +} diff --git a/osu.Game/Screens/SelectV2/SongSelectV2.cs b/osu.Game/Screens/SelectV2/SongSelect.cs similarity index 84% rename from osu.Game/Screens/SelectV2/SongSelectV2.cs rename to osu.Game/Screens/SelectV2/SongSelect.cs index 23139c8742..5458a02583 100644 --- a/osu.Game/Screens/SelectV2/SongSelectV2.cs +++ b/osu.Game/Screens/SelectV2/SongSelect.cs @@ -1,7 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using System; using System.Collections.Generic; using osu.Framework.Allocation; using osu.Framework.Graphics; @@ -11,7 +10,7 @@ using osu.Game.Overlays; using osu.Game.Overlays.Mods; using osu.Game.Screens.Footer; using osu.Game.Screens.Menu; -using osu.Game.Screens.Play; +using osu.Game.Screens.Select; using osu.Game.Screens.SelectV2.Footer; namespace osu.Game.Screens.SelectV2 @@ -20,7 +19,7 @@ namespace osu.Game.Screens.SelectV2 /// This screen is intended to house all components introduced in the new song select design to add transitions and examine the overall look. /// This will be gradually built upon and ultimately replace once everything is in place. /// - public partial class SongSelectV2 : OsuScreen + public abstract partial class SongSelect : OsuScreen { private const float logo_scale = 0.4f; @@ -29,6 +28,8 @@ namespace osu.Game.Screens.SelectV2 [Cached] private readonly OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Aquamarine); + private BeatmapCarousel carousel = null!; + public override bool ShowFooter => true; [Resolved] @@ -58,8 +59,9 @@ namespace osu.Game.Screens.SelectV2 { RelativeSizeAxes = Axes.Both, Padding = new MarginPadding { Bottom = ScreenFooter.HEIGHT }, - Child = new BeatmapCarousel + Child = carousel = new BeatmapCarousel { + RequestPresentBeatmap = _ => OnStart(), RelativeSizeAxes = Axes.Both }, }, @@ -141,11 +143,17 @@ namespace osu.Game.Screens.SelectV2 logo.Action = () => { - this.Push(new PlayerLoaderV2(() => new SoloPlayer())); + OnStart(); return false; }; } + /// + /// Called when a selection is made. + /// + /// If a resultant action occurred that takes the user away from SongSelect. + protected abstract bool OnStart(); + protected override void LogoSuspending(OsuLogo logo) { base.LogoSuspending(logo); @@ -160,19 +168,22 @@ namespace osu.Game.Screens.SelectV2 logo.FadeOut(120, Easing.Out); } + /// + /// Set the query to the search text box. + /// + /// The string to search. + public void Search(string query) + { + carousel.Filter(new FilterCriteria + { + // TODO: this should only set the text of the current criteria, not use a completely new criteria. + SearchText = query, + }); + } + private partial class SoloModSelectOverlay : UserModSelectOverlay { protected override bool ShowPresets => true; } - - private partial class PlayerLoaderV2 : PlayerLoader - { - public override bool ShowFooter => true; - - public PlayerLoaderV2(Func createPlayer) - : base(createPlayer) - { - } - } } } From 1be3b990e7589b2c1f1ae8e9fec64989f79902c2 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 4 Mar 2025 18:09:58 +0900 Subject: [PATCH 1198/1275] Add transition for selecting a beatmap --- osu.Game/Screens/SelectV2/BeatmapCarousel.cs | 27 +++++++++++++++++ osu.Game/Screens/SelectV2/Carousel.cs | 32 ++++++++++---------- osu.Game/Screens/SelectV2/SongSelect.cs | 12 ++++++-- 3 files changed, 52 insertions(+), 19 deletions(-) diff --git a/osu.Game/Screens/SelectV2/BeatmapCarousel.cs b/osu.Game/Screens/SelectV2/BeatmapCarousel.cs index 7372847402..1c730169eb 100644 --- a/osu.Game/Screens/SelectV2/BeatmapCarousel.cs +++ b/osu.Game/Screens/SelectV2/BeatmapCarousel.cs @@ -10,6 +10,7 @@ using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Pooling; +using osu.Framework.Utils; using osu.Game.Beatmaps; using osu.Game.Database; using osu.Game.Graphics.UserInterface; @@ -260,6 +261,32 @@ namespace osu.Game.Screens.SelectV2 #endregion + #region Animation + + /// + /// Moves non-selected beatmaps to the right, hiding off-screen. + /// + public bool VisuallyFocusSelected { get; set; } + + private float selectionFocusOffset; + + protected override void Update() + { + base.Update(); + + selectionFocusOffset = (float)Interpolation.DampContinuously(selectionFocusOffset, VisuallyFocusSelected ? 300 : 0, 100, Time.Elapsed); + + foreach (var panel in Scroll.Panels) + { + var c = (ICarouselPanel)panel; + + if (!c.Selected.Value) + panel.X += selectionFocusOffset; + } + } + + #endregion + #region Filtering public FilterCriteria Criteria { get; private set; } = new FilterCriteria(); diff --git a/osu.Game/Screens/SelectV2/Carousel.cs b/osu.Game/Screens/SelectV2/Carousel.cs index e50281e713..1a120e69e7 100644 --- a/osu.Game/Screens/SelectV2/Carousel.cs +++ b/osu.Game/Screens/SelectV2/Carousel.cs @@ -75,7 +75,7 @@ namespace osu.Game.Screens.SelectV2 /// /// The number of items currently actualised into drawables. /// - public int VisibleItems => scroll.Panels.Count; + public int VisibleItems => Scroll.Panels.Count; /// /// The currently selected model. Generally of type T. @@ -185,7 +185,7 @@ namespace osu.Game.Screens.SelectV2 /// The item to find a related drawable representation. /// The drawable representation if it exists. protected Drawable? GetMaterialisedDrawableForItem(CarouselItem item) => - scroll.Panels.SingleOrDefault(p => ((ICarouselPanel)p).Item == item); + Scroll.Panels.SingleOrDefault(p => ((ICarouselPanel)p).Item == item); /// /// When a user is traversing the carousel via group selection keys, assert whether the item provided is a valid target. @@ -222,11 +222,11 @@ namespace osu.Game.Screens.SelectV2 #region Initialisation - private readonly CarouselScrollContainer scroll; + protected readonly CarouselScrollContainer Scroll; protected Carousel() { - InternalChild = scroll = new CarouselScrollContainer + InternalChild = Scroll = new CarouselScrollContainer { RelativeSizeAxes = Axes.Both, }; @@ -499,13 +499,13 @@ namespace osu.Game.Screens.SelectV2 // If a keyboard selection is currently made, we want to keep the view stable around the selection. // That means that we should offset the immediate scroll position by any change in Y position for the selection. if (prevKeyboard.YPosition != null && currentKeyboardSelection.YPosition != prevKeyboard.YPosition) - scroll.OffsetScrollPosition((float)(currentKeyboardSelection.YPosition!.Value - prevKeyboard.YPosition.Value)); + Scroll.OffsetScrollPosition((float)(currentKeyboardSelection.YPosition!.Value - prevKeyboard.YPosition.Value)); } private void scrollToSelection() { if (currentKeyboardSelection.CarouselItem != null) - scroll.ScrollTo(currentKeyboardSelection.CarouselItem.CarouselYPosition - visibleHalfHeight); + Scroll.ScrollTo(currentKeyboardSelection.CarouselItem.CarouselYPosition - visibleHalfHeight); } #endregion @@ -519,12 +519,12 @@ namespace osu.Game.Screens.SelectV2 /// /// The position of the lower visible bound with respect to the current scroll position. /// - private float visibleBottomBound => (float)(scroll.Current + DrawHeight + BleedBottom); + private float visibleBottomBound => (float)(Scroll.Current + DrawHeight + BleedBottom); /// /// The position of the upper visible bound with respect to the current scroll position. /// - private float visibleUpperBound => (float)(scroll.Current - BleedTop); + private float visibleUpperBound => (float)(Scroll.Current - BleedTop); /// /// Half the height of the visible content. @@ -557,7 +557,7 @@ namespace osu.Game.Screens.SelectV2 double selectedYPos = currentSelection.CarouselItem?.CarouselYPosition ?? 0; - foreach (var panel in scroll.Panels) + foreach (var panel in Scroll.Panels) { var c = (ICarouselPanel)panel; @@ -566,12 +566,12 @@ namespace osu.Game.Screens.SelectV2 continue; float normalisedDepth = (float)(Math.Abs(selectedYPos - c.DrawYPosition) / DrawHeight); - scroll.Panels.ChangeChildDepth(panel, c.Item.DepthLayer + normalisedDepth); + Scroll.Panels.ChangeChildDepth(panel, c.Item.DepthLayer + normalisedDepth); if (c.DrawYPosition != c.Item.CarouselYPosition) c.DrawYPosition = Interpolation.DampContinuously(c.DrawYPosition, c.Item.CarouselYPosition, 50, Time.Elapsed); - Vector2 posInScroll = scroll.ToLocalSpace(panel.ScreenSpaceDrawQuad.Centre); + Vector2 posInScroll = Scroll.ToLocalSpace(panel.ScreenSpaceDrawQuad.Centre); float dist = Math.Abs(1f - posInScroll.Y / visibleHalfHeight); panel.X = offsetX(dist, visibleHalfHeight); @@ -628,7 +628,7 @@ namespace osu.Game.Screens.SelectV2 toDisplay.RemoveAll(i => !i.IsVisible); // Iterate over all panels which are already displayed and figure which need to be displayed / removed. - foreach (var panel in scroll.Panels) + foreach (var panel in Scroll.Panels) { var carouselPanel = (ICarouselPanel)panel; @@ -658,7 +658,7 @@ namespace osu.Game.Screens.SelectV2 carouselPanel.DrawYPosition = item.CarouselYPosition; carouselPanel.Item = item; - scroll.Add(drawable); + Scroll.Add(drawable); } // Update the total height of all items (to make the scroll container scrollable through the full height even though @@ -666,10 +666,10 @@ namespace osu.Game.Screens.SelectV2 if (carouselItems.Count > 0) { var lastItem = carouselItems[^1]; - scroll.SetLayoutHeight((float)(lastItem.CarouselYPosition + lastItem.DrawHeight + visibleHalfHeight)); + Scroll.SetLayoutHeight((float)(lastItem.CarouselYPosition + lastItem.DrawHeight + visibleHalfHeight)); } else - scroll.SetLayoutHeight(0); + Scroll.SetLayoutHeight(0); } private static void expirePanelImmediately(Drawable panel) @@ -713,7 +713,7 @@ namespace osu.Game.Screens.SelectV2 /// Implementation of scroll container which handles very large vertical lists by internally using double precision /// for pre-display Y values. /// - private partial class CarouselScrollContainer : UserTrackingScrollContainer, IKeyBindingHandler + protected partial class CarouselScrollContainer : UserTrackingScrollContainer, IKeyBindingHandler { public readonly Container Panels; diff --git a/osu.Game/Screens/SelectV2/SongSelect.cs b/osu.Game/Screens/SelectV2/SongSelect.cs index 5458a02583..70452de99a 100644 --- a/osu.Game/Screens/SelectV2/SongSelect.cs +++ b/osu.Game/Screens/SelectV2/SongSelect.cs @@ -99,9 +99,13 @@ namespace osu.Game.Screens.SelectV2 base.OnEntering(e); } + private const double fade_duration = 300; + public override void OnResuming(ScreenTransitionEvent e) { - this.FadeIn(); + this.FadeIn(fade_duration, Easing.OutQuint); + + carousel.VisuallyFocusSelected = false; // required due to https://github.com/ppy/osu-framework/issues/3218 modSelectOverlay.SelectedMods.Disabled = false; @@ -112,16 +116,18 @@ namespace osu.Game.Screens.SelectV2 public override void OnSuspending(ScreenTransitionEvent e) { - this.Delay(400).FadeOut(); + this.Delay(100).FadeOut(fade_duration, Easing.OutQuint); modSelectOverlay.SelectedMods.UnbindFrom(Mods); + carousel.VisuallyFocusSelected = true; + base.OnSuspending(e); } public override bool OnExiting(ScreenExitEvent e) { - this.Delay(400).FadeOut(); + this.FadeOut(fade_duration, Easing.OutQuint); return base.OnExiting(e); } From 918315aa65a5d5447d8e7b98c16a81611aa5f7e9 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 4 Mar 2025 19:19:53 +0900 Subject: [PATCH 1199/1275] Split out methods so retrieving the room is not a callback function --- .../StatefulMultiplayerClientTest.cs | 3 +- .../TestSceneMultiSpectatorLeaderboard.cs | 3 +- .../TestSceneMultiSpectatorScreen.cs | 4 +- .../TestSceneMultiplayerMatchSongSelect.cs | 5 ++- .../TestSceneMultiplayerParticipantsList.cs | 3 +- .../Multiplayer/TestSceneMultiplayerPlayer.cs | 3 +- .../TestSceneMultiplayerPlaylist.cs | 4 +- .../TestSceneMultiplayerQueueList.cs | 4 +- .../TestSceneMultiplayerSpectateButton.cs | 4 +- .../Multiplayer/MultiplayerTestScene.cs | 44 ++++++++----------- 10 files changed, 43 insertions(+), 34 deletions(-) diff --git a/osu.Game.Tests/NonVisual/Multiplayer/StatefulMultiplayerClientTest.cs b/osu.Game.Tests/NonVisual/Multiplayer/StatefulMultiplayerClientTest.cs index 230a996942..8364e58bdc 100644 --- a/osu.Game.Tests/NonVisual/Multiplayer/StatefulMultiplayerClientTest.cs +++ b/osu.Game.Tests/NonVisual/Multiplayer/StatefulMultiplayerClientTest.cs @@ -19,7 +19,8 @@ namespace osu.Game.Tests.NonVisual.Multiplayer public override void SetUpSteps() { base.SetUpSteps(); - JoinDefaultRoom(); + AddStep("join room", () => JoinRoom(CreateDefaultRoom())); + WaitForJoined(); } [Test] diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiSpectatorLeaderboard.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiSpectatorLeaderboard.cs index 1821c2f3bc..60358dfbc4 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiSpectatorLeaderboard.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiSpectatorLeaderboard.cs @@ -24,7 +24,8 @@ namespace osu.Game.Tests.Visual.Multiplayer { base.SetUpSteps(); - JoinDefaultRoom(); + AddStep("join room", () => JoinRoom(CreateDefaultRoom())); + WaitForJoined(); AddStep("reset", () => { diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiSpectatorScreen.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiSpectatorScreen.cs index 3fdbe02906..aa98dc59db 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiSpectatorScreen.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiSpectatorScreen.cs @@ -66,7 +66,9 @@ namespace osu.Game.Tests.Visual.Multiplayer AddStep("clear playing users", () => playingUsers.Clear()); - JoinDefaultRoom(r => room = r); + AddStep("create room", () => room = CreateDefaultRoom()); + AddStep("join room", () => JoinRoom(room)); + WaitForJoined(); } [TestCase(1)] diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSongSelect.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSongSelect.cs index 287d7f5816..9c85bdd57a 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSongSelect.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSongSelect.cs @@ -62,7 +62,10 @@ namespace osu.Game.Tests.Visual.Multiplayer public override void SetUpSteps() { base.SetUpSteps(); - JoinDefaultRoom(r => room = r); + + AddStep("create room", () => room = CreateDefaultRoom()); + AddStep("join room", () => JoinRoom(room)); + WaitForJoined(); } private void setUp() diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerParticipantsList.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerParticipantsList.cs index b5655afb8c..ed3fd4a6f8 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerParticipantsList.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerParticipantsList.cs @@ -32,7 +32,8 @@ namespace osu.Game.Tests.Visual.Multiplayer { base.SetUpSteps(); - JoinDefaultRoom(); + AddStep("join room", () => JoinRoom(CreateDefaultRoom())); + WaitForJoined(); createNewParticipantsList(); } diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerPlayer.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerPlayer.cs index 1a5be48cad..99bec1e714 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerPlayer.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerPlayer.cs @@ -25,7 +25,8 @@ namespace osu.Game.Tests.Visual.Multiplayer public override void SetUpSteps() { base.SetUpSteps(); - JoinDefaultRoom(); + AddStep("join room", () => JoinRoom(CreateDefaultRoom())); + WaitForJoined(); } [Test] diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerPlaylist.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerPlaylist.cs index 54932db7c6..7c8691d5d1 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerPlaylist.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerPlaylist.cs @@ -47,7 +47,9 @@ namespace osu.Game.Tests.Visual.Multiplayer { base.SetUpSteps(); - JoinDefaultRoom(r => room = r); + AddStep("create room", () => room = CreateDefaultRoom()); + AddStep("join room", () => JoinRoom(room)); + WaitForJoined(); AddStep("create list", () => { diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerQueueList.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerQueueList.cs index 5eba67bab5..1a7b677798 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerQueueList.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerQueueList.cs @@ -43,7 +43,9 @@ namespace osu.Game.Tests.Visual.Multiplayer { base.SetUpSteps(); - JoinDefaultRoom(r => room = r); + AddStep("create room", () => room = CreateDefaultRoom()); + AddStep("join room", () => JoinRoom(room)); + WaitForJoined(); AddStep("create playlist", () => { diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerSpectateButton.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerSpectateButton.cs index f92721b04b..9e6734ce99 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerSpectateButton.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerSpectateButton.cs @@ -47,7 +47,9 @@ namespace osu.Game.Tests.Visual.Multiplayer { base.SetUpSteps(); - JoinDefaultRoom(r => room = r); + AddStep("create room", () => room = CreateDefaultRoom()); + AddStep("join room", () => JoinRoom(room)); + WaitForJoined(); AddStep("create button", () => { diff --git a/osu.Game/Tests/Visual/Multiplayer/MultiplayerTestScene.cs b/osu.Game/Tests/Visual/Multiplayer/MultiplayerTestScene.cs index 8150807f4f..ac587d3bb2 100644 --- a/osu.Game/Tests/Visual/Multiplayer/MultiplayerTestScene.cs +++ b/osu.Game/Tests/Visual/Multiplayer/MultiplayerTestScene.cs @@ -1,7 +1,7 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using System; +using osu.Game.Online.Multiplayer; using osu.Game.Online.Rooms; using osu.Game.Tests.Beatmaps; using osu.Game.Tests.Visual.OnlinePlay; @@ -24,34 +24,28 @@ namespace osu.Game.Tests.Visual.Multiplayer public bool RoomJoined => MultiplayerClient.RoomJoined; + protected Room CreateDefaultRoom() + { + return new Room + { + Name = "test name", + Type = MatchType.HeadToHead, + Playlist = + [ + new PlaylistItem(new TestBeatmap(Ruleset.Value).BeatmapInfo) + { + RulesetID = Ruleset.Value.OnlineID + } + ] + }; + } + /// /// Creates and joins a basic multiplayer room. /// - /// A callback that may be used to further set up the room. - protected void JoinDefaultRoom(Action? setupFunc = null) - { - AddStep("join room", () => - { - Room room = new Room - { - Name = "test name", - Type = MatchType.HeadToHead, - Playlist = - [ - new PlaylistItem(new TestBeatmap(Ruleset.Value).BeatmapInfo) - { - RulesetID = Ruleset.Value.OnlineID - } - ] - }; + protected void JoinRoom(Room room) => MultiplayerClient.CreateRoom(room).FireAndForget(); - setupFunc?.Invoke(room); - - MultiplayerClient.CreateRoom(room).ConfigureAwait(false); - }); - - AddUntilStep("wait for room join", () => RoomJoined); - } + protected void WaitForJoined() => AddUntilStep("wait for room join", () => RoomJoined); protected override OnlinePlayTestSceneDependencies CreateOnlinePlayDependencies() => new MultiplayerTestSceneDependencies(); } From 21d35f9dae085c6c9bee4369af6e261e98dfc21e Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 4 Mar 2025 19:40:31 +0900 Subject: [PATCH 1200/1275] Use alternative method of offsetting X that conveys flow better --- osu.Game/Screens/SelectV2/BeatmapCarousel.cs | 11 ++++------- osu.Game/Screens/SelectV2/Carousel.cs | 13 +++++++++---- 2 files changed, 13 insertions(+), 11 deletions(-) diff --git a/osu.Game/Screens/SelectV2/BeatmapCarousel.cs b/osu.Game/Screens/SelectV2/BeatmapCarousel.cs index 1c730169eb..1c1f6fa7fb 100644 --- a/osu.Game/Screens/SelectV2/BeatmapCarousel.cs +++ b/osu.Game/Screens/SelectV2/BeatmapCarousel.cs @@ -275,14 +275,11 @@ namespace osu.Game.Screens.SelectV2 base.Update(); selectionFocusOffset = (float)Interpolation.DampContinuously(selectionFocusOffset, VisuallyFocusSelected ? 300 : 0, 100, Time.Elapsed); + } - foreach (var panel in Scroll.Panels) - { - var c = (ICarouselPanel)panel; - - if (!c.Selected.Value) - panel.X += selectionFocusOffset; - } + protected override float GetPanelXOffset(Drawable panel) + { + return base.GetPanelXOffset(panel) + (((ICarouselPanel)panel).Selected.Value ? 0 : selectionFocusOffset); } #endregion diff --git a/osu.Game/Screens/SelectV2/Carousel.cs b/osu.Game/Screens/SelectV2/Carousel.cs index 1a120e69e7..5339b5358b 100644 --- a/osu.Game/Screens/SelectV2/Carousel.cs +++ b/osu.Game/Screens/SelectV2/Carousel.cs @@ -571,10 +571,7 @@ namespace osu.Game.Screens.SelectV2 if (c.DrawYPosition != c.Item.CarouselYPosition) c.DrawYPosition = Interpolation.DampContinuously(c.DrawYPosition, c.Item.CarouselYPosition, 50, Time.Elapsed); - Vector2 posInScroll = Scroll.ToLocalSpace(panel.ScreenSpaceDrawQuad.Centre); - float dist = Math.Abs(1f - posInScroll.Y / visibleHalfHeight); - - panel.X = offsetX(dist, visibleHalfHeight); + panel.X = GetPanelXOffset(panel); c.Selected.Value = c.Item == currentSelection?.CarouselItem; c.KeyboardSelected.Value = c.Item == currentKeyboardSelection?.CarouselItem; @@ -582,6 +579,14 @@ namespace osu.Game.Screens.SelectV2 } } + protected virtual float GetPanelXOffset(Drawable panel) + { + Vector2 posInScroll = Scroll.ToLocalSpace(panel.ScreenSpaceDrawQuad.Centre); + float dist = Math.Abs(1f - posInScroll.Y / visibleHalfHeight); + + return offsetX(dist, visibleHalfHeight); + } + /// /// Computes the x-offset of currently visible items. Makes the carousel appear round. /// From b5696f97a072439946e0af495e9f4191d864fad7 Mon Sep 17 00:00:00 2001 From: Zihad Date: Tue, 4 Mar 2025 03:05:03 +0600 Subject: [PATCH 1201/1275] Show current beatmap info in window title --- osu.Game/OsuGame.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs index d23d27c89e..3b55c320b3 100644 --- a/osu.Game/OsuGame.cs +++ b/osu.Game/OsuGame.cs @@ -828,6 +828,8 @@ namespace osu.Game { beatmap.OldValue?.CancelAsyncLoad(); beatmap.NewValue?.BeginAsyncLoad(); + + Host.Window.Title = $"{Name} - {beatmap.NewValue.BeatmapInfo.GetDisplayTitleRomanisable(true, false)}"; } private void modsChanged(ValueChangedEvent> mods) From c051ff84d293e5c2408e7ff59f55e176f0d1f1a8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 4 Mar 2025 13:04:23 +0100 Subject: [PATCH 1202/1275] Add UI for assigning custom tags to beatmaps Visual part for https://github.com/ppy/osu/issues/31913. Opening separately for appropriate visual UI adjustments. Also mostly ready to be hooked up to the results screen, pending merge of https://github.com/ppy/osu-web/pull/11951. --- .../Visual/Ranking/TestSceneUserTagControl.cs | 85 +++ osu.Game/Beatmaps/APIBeatmapTag.cs | 16 + osu.Game/Configuration/SessionStatics.cs | 13 +- .../API/Requests/AddBeatmapTagRequest.cs | 31 + .../Online/API/Requests/ListTagsRequest.cs | 12 + .../API/Requests/RemoveBeatmapTagRequest.cs | 29 + .../API/Requests/Responses/APIBeatmap.cs | 6 + .../Online/API/Requests/Responses/APITag.cs | 19 + .../Requests/Responses/APITagCollection.cs | 14 + osu.Game/Screens/Ranking/UserTagControl.cs | 537 ++++++++++++++++++ 10 files changed, 756 insertions(+), 6 deletions(-) create mode 100644 osu.Game.Tests/Visual/Ranking/TestSceneUserTagControl.cs create mode 100644 osu.Game/Beatmaps/APIBeatmapTag.cs create mode 100644 osu.Game/Online/API/Requests/AddBeatmapTagRequest.cs create mode 100644 osu.Game/Online/API/Requests/ListTagsRequest.cs create mode 100644 osu.Game/Online/API/Requests/RemoveBeatmapTagRequest.cs create mode 100644 osu.Game/Online/API/Requests/Responses/APITag.cs create mode 100644 osu.Game/Online/API/Requests/Responses/APITagCollection.cs create mode 100644 osu.Game/Screens/Ranking/UserTagControl.cs diff --git a/osu.Game.Tests/Visual/Ranking/TestSceneUserTagControl.cs b/osu.Game.Tests/Visual/Ranking/TestSceneUserTagControl.cs new file mode 100644 index 0000000000..ebfd553815 --- /dev/null +++ b/osu.Game.Tests/Visual/Ranking/TestSceneUserTagControl.cs @@ -0,0 +1,85 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Linq; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Cursor; +using osu.Framework.Testing; +using osu.Game.Beatmaps; +using osu.Game.Online.API; +using osu.Game.Online.API.Requests; +using osu.Game.Online.API.Requests.Responses; +using osu.Game.Screens.Ranking; + +namespace osu.Game.Tests.Visual.Ranking +{ + public partial class TestSceneUserTagControl : OsuTestScene + { + private DummyAPIAccess dummyAPI => (DummyAPIAccess)API; + + [SetUpSteps] + public void SetUpSteps() + { + AddStep("set up working beatmap", () => + { + Beatmap.Value.BeatmapInfo.OnlineID = 42; + }); + AddStep("set up network requests", () => + { + dummyAPI.HandleRequest = request => + { + switch (request) + { + case ListTagsRequest listTagsRequest: + { + Scheduler.AddDelayed(() => listTagsRequest.TriggerSuccess(new APITagCollection + { + Tags = + [ + new APITag { Id = 1, Name = "tech", Description = "Tests uncommon skills.", }, + new APITag { Id = 2, Name = "alt", Description = "Colloquial term for maps which use rhythms that encourage the player to alternate notes. Typically distinct from burst or stream maps.", }, + new APITag { Id = 3, Name = "aim", Description = "Category for difficulty relating to cursor movement.", }, + new APITag { Id = 4, Name = "tap", Description = "Category for difficulty relating to tapping input.", }, + ] + }), 500); + return true; + } + + case GetBeatmapSetRequest getBeatmapSetRequest: + { + var beatmapSet = CreateAPIBeatmapSet(Beatmap.Value.BeatmapInfo); + beatmapSet.Beatmaps.Single().TopTags = + [ + new APIBeatmapTag { TagId = 3, VoteCount = 9 }, + ]; + Scheduler.AddDelayed(() => getBeatmapSetRequest.TriggerSuccess(beatmapSet), 500); + return true; + } + + case AddBeatmapTagRequest: + case RemoveBeatmapTagRequest: + { + Scheduler.AddDelayed(request.TriggerSuccess, 500); + return true; + } + } + + return false; + }; + }); + AddStep("create control", () => + { + Child = new PopoverContainer + { + RelativeSizeAxes = Axes.Both, + Child = new UserTagControl + { + Width = 500, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + } + }; + }); + } + } +} diff --git a/osu.Game/Beatmaps/APIBeatmapTag.cs b/osu.Game/Beatmaps/APIBeatmapTag.cs new file mode 100644 index 0000000000..5f4f9b851d --- /dev/null +++ b/osu.Game/Beatmaps/APIBeatmapTag.cs @@ -0,0 +1,16 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using Newtonsoft.Json; + +namespace osu.Game.Beatmaps +{ + public class APIBeatmapTag + { + [JsonProperty("tag_id")] + public long TagId { get; set; } + + [JsonProperty("count")] + public int VoteCount { get; set; } + } +} diff --git a/osu.Game/Configuration/SessionStatics.cs b/osu.Game/Configuration/SessionStatics.cs index d2069e4027..b816d1a88b 100644 --- a/osu.Game/Configuration/SessionStatics.cs +++ b/osu.Game/Configuration/SessionStatics.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Framework; using osu.Game.Graphics.UserInterface; using osu.Game.Input; @@ -27,11 +25,12 @@ namespace osu.Game.Configuration SetDefault(Static.FeaturedArtistDisclaimerShownOnce, false); SetDefault(Static.LastHoverSoundPlaybackTime, (double?)null); SetDefault(Static.LastModSelectPanelSamplePlaybackTime, (double?)null); - SetDefault(Static.SeasonalBackgrounds, null); + SetDefault(Static.SeasonalBackgrounds, null); SetDefault(Static.TouchInputActive, RuntimeInfo.IsMobile); - SetDefault(Static.LastLocalUserScore, null); - SetDefault(Static.LastAppliedOffsetScore, null); - SetDefault(Static.UserOnlineActivity, null); + SetDefault(Static.LastLocalUserScore, null); + SetDefault(Static.LastAppliedOffsetScore, null); + SetDefault(Static.UserOnlineActivity, null); + SetDefault(Static.AllBeatmapTags, null); } /// @@ -99,5 +98,7 @@ namespace osu.Game.Configuration /// The activity for the current user to broadcast to other players. /// UserOnlineActivity, + + AllBeatmapTags, } } diff --git a/osu.Game/Online/API/Requests/AddBeatmapTagRequest.cs b/osu.Game/Online/API/Requests/AddBeatmapTagRequest.cs new file mode 100644 index 0000000000..4fa02dc569 --- /dev/null +++ b/osu.Game/Online/API/Requests/AddBeatmapTagRequest.cs @@ -0,0 +1,31 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Globalization; +using System.Net.Http; +using osu.Framework.IO.Network; + +namespace osu.Game.Online.API.Requests +{ + public class AddBeatmapTagRequest : APIRequest + { + public int BeatmapID { get; } + public long TagID { get; } + + public AddBeatmapTagRequest(int beatmapID, long tagID) + { + BeatmapID = beatmapID; + TagID = tagID; + } + + protected override WebRequest CreateWebRequest() + { + var req = base.CreateWebRequest(); + req.Method = HttpMethod.Post; + req.AddParameter(@"tag_id", TagID.ToString(CultureInfo.InvariantCulture), RequestParameterType.Query); + return req; + } + + protected override string Target => $@"beatmaps/{BeatmapID}/tags"; + } +} diff --git a/osu.Game/Online/API/Requests/ListTagsRequest.cs b/osu.Game/Online/API/Requests/ListTagsRequest.cs new file mode 100644 index 0000000000..ac4b1a3e2a --- /dev/null +++ b/osu.Game/Online/API/Requests/ListTagsRequest.cs @@ -0,0 +1,12 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Game.Online.API.Requests.Responses; + +namespace osu.Game.Online.API.Requests +{ + public class ListTagsRequest : APIRequest + { + protected override string Target => "tags"; + } +} diff --git a/osu.Game/Online/API/Requests/RemoveBeatmapTagRequest.cs b/osu.Game/Online/API/Requests/RemoveBeatmapTagRequest.cs new file mode 100644 index 0000000000..8090dd2cb0 --- /dev/null +++ b/osu.Game/Online/API/Requests/RemoveBeatmapTagRequest.cs @@ -0,0 +1,29 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Net.Http; +using osu.Framework.IO.Network; + +namespace osu.Game.Online.API.Requests.Responses +{ + public class RemoveBeatmapTagRequest : APIRequest + { + public int BeatmapID { get; } + public long TagID { get; } + + public RemoveBeatmapTagRequest(int beatmapID, long tagID) + { + BeatmapID = beatmapID; + TagID = tagID; + } + + protected override WebRequest CreateWebRequest() + { + var req = base.CreateWebRequest(); + req.Method = HttpMethod.Delete; + return req; + } + + protected override string Target => $@"beatmaps/{BeatmapID}/tags/{TagID}"; + } +} diff --git a/osu.Game/Online/API/Requests/Responses/APIBeatmap.cs b/osu.Game/Online/API/Requests/Responses/APIBeatmap.cs index e5ecfe2c99..f06d0ef274 100644 --- a/osu.Game/Online/API/Requests/Responses/APIBeatmap.cs +++ b/osu.Game/Online/API/Requests/Responses/APIBeatmap.cs @@ -95,6 +95,12 @@ namespace osu.Game.Online.API.Requests.Responses [JsonProperty(@"failtimes")] public APIFailTimes? FailTimes { get; set; } + [JsonProperty(@"top_tag_ids")] + public APIBeatmapTag[]? TopTags { get; set; } + + [JsonProperty(@"own_tag_ids")] + public long[]? OwnTagIds { get; set; } + [JsonProperty(@"max_combo")] public int? MaxCombo { get; set; } diff --git a/osu.Game/Online/API/Requests/Responses/APITag.cs b/osu.Game/Online/API/Requests/Responses/APITag.cs new file mode 100644 index 0000000000..4dd18663af --- /dev/null +++ b/osu.Game/Online/API/Requests/Responses/APITag.cs @@ -0,0 +1,19 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using Newtonsoft.Json; + +namespace osu.Game.Online.API.Requests.Responses +{ + public class APITag + { + [JsonProperty("id")] + public long Id { get; set; } + + [JsonProperty("name")] + public string Name { get; set; } = string.Empty; + + [JsonProperty("description")] + public string Description { get; set; } = string.Empty; + } +} diff --git a/osu.Game/Online/API/Requests/Responses/APITagCollection.cs b/osu.Game/Online/API/Requests/Responses/APITagCollection.cs new file mode 100644 index 0000000000..a177699348 --- /dev/null +++ b/osu.Game/Online/API/Requests/Responses/APITagCollection.cs @@ -0,0 +1,14 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using Newtonsoft.Json; + +namespace osu.Game.Online.API.Requests.Responses +{ + public class APITagCollection + { + [JsonProperty("tags")] + public APITag[] Tags { get; set; } = Array.Empty(); + } +} diff --git a/osu.Game/Screens/Ranking/UserTagControl.cs b/osu.Game/Screens/Ranking/UserTagControl.cs new file mode 100644 index 0000000000..6b7d22a7c2 --- /dev/null +++ b/osu.Game/Screens/Ranking/UserTagControl.cs @@ -0,0 +1,537 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using System.Collections.Generic; +using System.Collections.Specialized; +using System.Linq; +using osu.Framework.Allocation; +using osu.Framework.Bindables; +using osu.Framework.Caching; +using osu.Framework.Extensions; +using osu.Framework.Extensions.LocalisationExtensions; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Cursor; +using osu.Framework.Graphics.Effects; +using osu.Framework.Graphics.Shapes; +using osu.Framework.Graphics.Sprites; +using osu.Framework.Graphics.UserInterface; +using osu.Game.Beatmaps; +using osu.Game.Configuration; +using osu.Game.Extensions; +using osu.Game.Graphics; +using osu.Game.Graphics.Containers; +using osu.Game.Graphics.Sprites; +using osu.Game.Graphics.UserInterface; +using osu.Game.Graphics.UserInterfaceV2; +using osu.Game.Online.API; +using osu.Game.Online.API.Requests; +using osu.Game.Online.API.Requests.Responses; +using osuTK; + +namespace osu.Game.Screens.Ranking +{ + public partial class UserTagControl : CompositeDrawable + { + public override bool HandlePositionalInput => true; + + private readonly Cached layout = new Cached(); + + private FillFlowContainer tagFlow = null!; + private LoadingLayer loadingLayer = null!; + + private BindableList displayedTags { get; } = new BindableList(); + private BindableList extraTags { get; } = new BindableList(); + + private Bindable allTags = null!; + private readonly Bindable apiBeatmap = new Bindable(); + + [Resolved] + private IAPIProvider api { get; set; } = null!; + + [Resolved] + private Bindable beatmap { get; set; } = null!; + + [BackgroundDependencyLoader] + private void load(SessionStatics sessionStatics) + { + AutoSizeAxes = Axes.Y; + InternalChildren = new Drawable[] + { + new FillFlowContainer + { + Direction = FillDirection.Vertical, + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Spacing = new Vector2(8), + Children = new Drawable[] + { + tagFlow = new FillFlowContainer + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Direction = FillDirection.Full, + LayoutDuration = 300, + LayoutEasing = Easing.OutQuint, + Spacing = new Vector2(4), + }, + new ExtraTagsButton + { + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre, + OnTagSelected = onExtraTagSelected, + ExtraTags = { BindTarget = extraTags }, + }, + }, + }, + loadingLayer = new LoadingLayer + { + RelativeSizeAxes = Axes.Both, + State = { Value = Visibility.Visible } + }, + }; + + allTags = sessionStatics.GetBindable(Static.AllBeatmapTags); + + if (allTags.Value == null) + { + var listTagsRequest = new ListTagsRequest(); + listTagsRequest.Success += tags => allTags.Value = tags.Tags.ToArray(); + api.Queue(listTagsRequest); + } + + var getBeatmapSetRequest = new GetBeatmapSetRequest(beatmap.Value.BeatmapInfo.BeatmapSet!.OnlineID); + getBeatmapSetRequest.Success += set => apiBeatmap.Value = set.Beatmaps.SingleOrDefault(b => b.MatchesOnlineID(beatmap.Value.BeatmapInfo)); + api.Queue(getBeatmapSetRequest); + } + + private void onExtraTagSelected(UserTag tag) + { + loadingLayer.Show(); + extraTags.Remove(tag); + + var req = new AddBeatmapTagRequest(beatmap.Value.BeatmapInfo.OnlineID, tag.Id); + req.Success += () => + { + tag.Voted.Value = true; + tag.VoteCount.Value += 1; + displayedTags.Add(tag); + loadingLayer.Hide(); + }; + req.Failure += _ => extraTags.Add(tag); + api.Queue(req); + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + allTags.BindValueChanged(_ => updateTags()); + apiBeatmap.BindValueChanged(_ => updateTags()); + updateTags(); + + displayedTags.BindCollectionChanged(displayTags, true); + } + + private void updateTags() + { + if (allTags.Value == null || apiBeatmap.Value?.TopTags == null) + return; + + var allTagsById = allTags.Value.ToDictionary(t => t.Id); + var ownTagIds = apiBeatmap.Value.OwnTagIds?.ToHashSet() ?? new HashSet(); + + foreach (var topTag in apiBeatmap.Value.TopTags) + { + if (allTagsById.Remove(topTag.TagId, out var tag)) + { + displayedTags.Add(new UserTag(tag) + { + VoteCount = { Value = topTag.VoteCount }, + Voted = { Value = ownTagIds.Contains(tag.Id) } + }); + } + } + + extraTags.AddRange(allTagsById.Select(t => new UserTag(t.Value))); + + loadingLayer.Hide(); + } + + private void displayTags(object? sender, NotifyCollectionChangedEventArgs e) + { + var oldItems = tagFlow.ToArray(); + + switch (e.Action) + { + case NotifyCollectionChangedAction.Add: + { + for (int i = 0; i < e.NewItems!.Count; i++) + { + var tag = (UserTag)e.NewItems[i]!; + var drawableTag = new DrawableUserTag(tag); + tagFlow.Insert(tagFlow.Count, drawableTag); + tag.VoteCount.BindValueChanged(sortTags, true); + layout.Invalidate(); + } + + break; + } + + case NotifyCollectionChangedAction.Remove: + { + for (int i = 0; i < e.OldItems!.Count; i++) + { + var tag = (UserTag)e.OldItems[i]!; + tag.VoteCount.ValueChanged -= sortTags; + tagFlow.Remove(oldItems[e.OldStartingIndex + i], true); + } + + break; + } + + case NotifyCollectionChangedAction.Reset: + { + tagFlow.Clear(); + break; + } + } + } + + private void sortTags(ValueChangedEvent _) => layout.Invalidate(); + + protected override void Update() + { + base.Update(); + + if (!layout.IsValid && !IsHovered) + { + var sortedTags = new Dictionary( + displayedTags.OrderByDescending(t => t.VoteCount.Value) + .ThenByDescending(t => t.Voted.Value) + .Select((tag, index) => new KeyValuePair(tag, index))); + + foreach (var drawableTag in tagFlow) + tagFlow.SetLayoutPosition(drawableTag, sortedTags[drawableTag.UserTag]); + + layout.Validate(); + } + } + + private partial class DrawableUserTag : OsuAnimatedButton + { + public readonly UserTag UserTag; + + private readonly Bindable voteCount = new Bindable(); + private readonly BindableBool voted = new BindableBool(); + private readonly Bindable confirmed = new BindableBool(); + + private Box mainBackground = null!; + private Box voteBackground = null!; + private OsuSpriteText tagNameText = null!; + private OsuSpriteText voteCountText = null!; + private LoadingSpinner spinner = null!; + + [Resolved] + private OsuColour colours { get; set; } = null!; + + [Resolved] + private Bindable beatmap { get; set; } = null!; + + [Resolved] + private IAPIProvider api { get; set; } = null!; + + private APIRequest? requestInFlight; + + public DrawableUserTag(UserTag userTag) + { + UserTag = userTag; + voteCount.BindTo(userTag.VoteCount); + voted.BindTo(userTag.Voted); + + AutoSizeAxes = Axes.Both; + } + + [BackgroundDependencyLoader] + private void load() + { + Anchor = Anchor.Centre; + Origin = Anchor.Centre; + CornerRadius = 8; + Masking = true; + EdgeEffect = new EdgeEffectParameters + { + Colour = colours.Lime1, + Radius = 5, + Type = EdgeEffectType.Glow, + }; + Content.AddRange(new Drawable[] + { + mainBackground = new Box + { + RelativeSizeAxes = Axes.Both, + Depth = float.MaxValue, + }, + new FillFlowContainer + { + AutoSizeAxes = Axes.Both, + Direction = FillDirection.Horizontal, + Padding = new MarginPadding { Left = 6, Right = 3, Vertical = 3, }, + Spacing = new Vector2(5), + Children = new Drawable[] + { + tagNameText = new OsuSpriteText + { + Text = UserTag.Name, + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + }, + new Container + { + AutoSizeAxes = Axes.Both, + CornerRadius = 5, + Masking = true, + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + Children = new Drawable[] + { + voteBackground = new Box + { + RelativeSizeAxes = Axes.Both, + }, + voteCountText = new OsuSpriteText + { + Margin = new MarginPadding { Horizontal = 6, Vertical = 3, }, + }, + spinner = new LoadingSpinner(withBox: true) + { + Alpha = 0, + Size = new Vector2(18), + } + } + } + } + } + }); + + TooltipText = UserTag.Description; + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + const double transition_duration = 300; + + voteCount.BindValueChanged(_ => + { + voteCountText.Text = voteCount.Value.ToLocalisableString(); + confirmed.Value = voteCount.Value >= 10; + }, true); + voted.BindValueChanged(v => + { + if (v.NewValue) + { + voteBackground.FadeColour(colours.Lime3, transition_duration, Easing.OutQuint); + voteCountText.FadeColour(Colour4.Black, transition_duration, Easing.OutQuint); + } + else + { + voteBackground.FadeColour(colours.Gray2, transition_duration, Easing.OutQuint); + voteCountText.FadeColour(Colour4.White, transition_duration, Easing.OutQuint); + } + }, true); + confirmed.BindValueChanged(c => + { + if (c.NewValue) + { + mainBackground.FadeColour(colours.Lime1, transition_duration, Easing.OutQuint); + tagNameText.FadeColour(Colour4.Black, transition_duration, Easing.OutQuint); + FadeEdgeEffectTo(0.5f, transition_duration, Easing.OutQuint); + } + else + { + mainBackground.FadeColour(colours.Gray4, transition_duration, Easing.OutQuint); + tagNameText.FadeColour(Colour4.White, transition_duration, Easing.OutQuint); + FadeEdgeEffectTo(0f, transition_duration, Easing.OutQuint); + } + }, true); + FinishTransforms(true); + + Action = () => + { + if (requestInFlight != null) + return; + + spinner.Show(); + + APIRequest request; + + switch (voted.Value) + { + case true: + var removeReq = new RemoveBeatmapTagRequest(beatmap.Value.BeatmapInfo.OnlineID, UserTag.Id); + removeReq.Success += () => + { + voteCount.Value -= 1; + voted.Value = false; + }; + request = removeReq; + break; + + case false: + var addReq = new AddBeatmapTagRequest(beatmap.Value.BeatmapInfo.OnlineID, UserTag.Id); + addReq.Success += () => + { + voteCount.Value += 1; + voted.Value = true; + }; + request = addReq; + break; + } + + request.Success += () => + { + spinner.Hide(); + requestInFlight = null; + }; + request.Failure += _ => + { + spinner.Hide(); + requestInFlight = null; + }; + api.Queue(requestInFlight = request); + }; + } + } + + private partial class ExtraTagsButton : GrayButton, IHasPopover + { + public BindableList ExtraTags { get; } = new BindableList(); + + public Action? OnTagSelected { get; set; } + + public ExtraTagsButton() + : base(FontAwesome.Solid.Plus) + { + Size = new Vector2(30); + + Action = this.ShowPopover; + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + ExtraTags.BindCollectionChanged((_, _) => Enabled.Value = ExtraTags.Count > 0, true); + } + + public Popover GetPopover() => new ExtraTagsPopover + { + ExtraTags = { BindTarget = ExtraTags }, + OnSelected = OnTagSelected, + }; + } + + private partial class ExtraTagsPopover : OsuPopover + { + public BindableList ExtraTags { get; } = new BindableList(); + + public Action? OnSelected { get; set; } + + [BackgroundDependencyLoader] + private void load() + { + Child = new OsuScrollContainer + { + Width = 250, + Height = 200, + ScrollbarOverlapsContent = false, + Child = new FillFlowContainer + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Padding = new MarginPadding { Right = 5 }, + Spacing = new Vector2(10), + ChildrenEnumerable = ExtraTags.Select(tag => new DrawableExtraTag(tag) + { + Action = () => + { + OnSelected?.Invoke(tag); + this.HidePopover(); + } + }) + } + }; + } + } + + private partial class DrawableExtraTag : OsuAnimatedButton + { + private readonly UserTag tag; + + public DrawableExtraTag(UserTag tag) + { + this.tag = tag; + + RelativeSizeAxes = Axes.X; + AutoSizeAxes = Axes.Y; + Anchor = Origin = Anchor.Centre; + } + + [BackgroundDependencyLoader] + private void load(OsuColour colours) + { + Content.AddRange(new Drawable[] + { + new Box + { + RelativeSizeAxes = Axes.Both, + Colour = colours.GreySeaFoamDark, + Depth = float.MaxValue, + }, + new FillFlowContainer + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Direction = FillDirection.Vertical, + Spacing = new Vector2(2), + Padding = new MarginPadding(5), + Children = new Drawable[] + { + new OsuTextFlowContainer(t => t.Font = OsuFont.Default.With(weight: FontWeight.Bold)) + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Text = tag.Name, + }, + new OsuTextFlowContainer(t => t.Font = OsuFont.Default.With(size: 14)) + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Text = tag.Description, + } + } + } + }); + } + } + } + + public record UserTag + { + public long Id { get; } + public string Name { get; } + public string Description { get; set; } + public BindableInt VoteCount { get; } = new BindableInt(); + public BindableBool Voted { get; } = new BindableBool(); + + public UserTag(APITag tag) + { + Id = tag.Id; + Name = tag.Name; + Description = tag.Description; + } + } +} From 2abe75629eefb21f53ab4144210c7e3cf30ce8fc Mon Sep 17 00:00:00 2001 From: Zihad Date: Tue, 4 Mar 2025 18:28:03 +0600 Subject: [PATCH 1203/1275] Skip window title update for dummy beatmap --- osu.Game/OsuGame.cs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs index 3b55c320b3..fb9be8860c 100644 --- a/osu.Game/OsuGame.cs +++ b/osu.Game/OsuGame.cs @@ -829,7 +829,11 @@ namespace osu.Game beatmap.OldValue?.CancelAsyncLoad(); beatmap.NewValue?.BeginAsyncLoad(); - Host.Window.Title = $"{Name} - {beatmap.NewValue.BeatmapInfo.GetDisplayTitleRomanisable(true, false)}"; + // prevent weird window title saying please load a beatmap + if (beatmap.NewValue is null or DummyWorkingBeatmap) + Host.Window.Title = Name; + else + Host.Window.Title = $"{Name} - {beatmap.NewValue.BeatmapInfo.GetDisplayTitleRomanisable(true, false)}"; } private void modsChanged(ValueChangedEvent> mods) From dff354247eeb9490105f82991a2f98ad8b6efc02 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 4 Mar 2025 22:21:36 +0900 Subject: [PATCH 1204/1275] Change `ModSelectOverlay.ShowPresets` to `init` --- .../Visual/UserInterface/TestSceneModSelectOverlay.cs | 5 ++++- .../Visual/UserInterface/TestSceneScreenFooter.cs | 9 ++------- .../UserInterface/TestSceneScreenFooterButtonMods.cs | 3 +-- osu.Game/Overlays/Mods/ModSelectOverlay.cs | 6 +++--- osu.Game/Screens/Select/SongSelect.cs | 10 ++++------ osu.Game/Screens/SelectV2/SongSelect.cs | 10 ++++------ osu.Game/Tests/Visual/Gameplay/ScoringTestScene.cs | 3 +-- 7 files changed, 19 insertions(+), 27 deletions(-) diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs index 280497e861..6eb9263c7e 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs @@ -1030,7 +1030,10 @@ namespace osu.Game.Tests.Visual.UserInterface private partial class TestModSelectOverlay : UserModSelectOverlay { - protected override bool ShowPresets => true; + public TestModSelectOverlay() + { + ShowPresets = true; + } } private class TestUnimplementedMod : Mod diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneScreenFooter.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneScreenFooter.cs index a4cf8a276f..fc8777068d 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneScreenFooter.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneScreenFooter.cs @@ -23,7 +23,7 @@ namespace osu.Game.Tests.Visual.UserInterface { private DependencyProvidingContainer contentContainer = null!; private ScreenFooter screenFooter = null!; - private TestModSelectOverlay modOverlay = null!; + private UserModSelectOverlay modOverlay = null!; [SetUp] public void SetUp() => Schedule(() => @@ -39,7 +39,7 @@ namespace osu.Game.Tests.Visual.UserInterface }, Children = new Drawable[] { - modOverlay = new TestModSelectOverlay(), + modOverlay = new UserModSelectOverlay { ShowPresets = true }, new PopoverContainer { RelativeSizeAxes = Axes.Both, @@ -196,11 +196,6 @@ namespace osu.Game.Tests.Visual.UserInterface AddAssert("external overlay content still not shown", () => this.ChildrenOfType().SingleOrDefault()?.IsPresent, () => Is.Not.True); } - private partial class TestModSelectOverlay : UserModSelectOverlay - { - protected override bool ShowPresets => true; - } - private partial class TestShearedOverlayContainer : ShearedOverlayContainer { public TestShearedOverlayContainer() diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneScreenFooterButtonMods.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneScreenFooterButtonMods.cs index ba53eb83c4..e86f83ee15 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneScreenFooterButtonMods.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneScreenFooterButtonMods.cs @@ -115,11 +115,10 @@ namespace osu.Game.Tests.Visual.UserInterface private partial class TestModSelectOverlay : UserModSelectOverlay { - protected override bool ShowPresets => true; - public TestModSelectOverlay() : base(OverlayColourScheme.Aquamarine) { + ShowPresets = true; } } diff --git a/osu.Game/Overlays/Mods/ModSelectOverlay.cs b/osu.Game/Overlays/Mods/ModSelectOverlay.cs index daac925dfb..ac589fbebf 100644 --- a/osu.Game/Overlays/Mods/ModSelectOverlay.cs +++ b/osu.Game/Overlays/Mods/ModSelectOverlay.cs @@ -35,7 +35,7 @@ using osuTK.Input; namespace osu.Game.Overlays.Mods { - public abstract partial class ModSelectOverlay : ShearedOverlayContainer, ISamplePlaybackDisabler, IKeyBindingHandler + public partial class ModSelectOverlay : ShearedOverlayContainer, ISamplePlaybackDisabler, IKeyBindingHandler { public const int BUTTON_WIDTH = 200; @@ -96,7 +96,7 @@ namespace osu.Game.Overlays.Mods /// /// Whether the column with available mod presets should be shown. /// - protected virtual bool ShowPresets => false; + public bool ShowPresets { get; init; } protected virtual ModColumn CreateModColumn(ModType modType) => new ModColumn(modType, false); @@ -125,7 +125,7 @@ namespace osu.Game.Overlays.Mods [Resolved] private ScreenFooter? footer { get; set; } - protected ModSelectOverlay(OverlayColourScheme colourScheme = OverlayColourScheme.Green) + public ModSelectOverlay(OverlayColourScheme colourScheme = OverlayColourScheme.Green) : base(colourScheme) { } diff --git a/osu.Game/Screens/Select/SongSelect.cs b/osu.Game/Screens/Select/SongSelect.cs index c20dcb8593..1496eb96f9 100644 --- a/osu.Game/Screens/Select/SongSelect.cs +++ b/osu.Game/Screens/Select/SongSelect.cs @@ -426,7 +426,10 @@ namespace osu.Game.Screens.Select (beatmapOptionsButton = new FooterButtonOptions(), BeatmapOptions) }; - protected virtual ModSelectOverlay CreateModSelectOverlay() => new SoloModSelectOverlay(); + protected virtual ModSelectOverlay CreateModSelectOverlay() => new UserModSelectOverlay + { + ShowPresets = true, + }; private DependencyContainer dependencies = null!; @@ -1152,10 +1155,5 @@ namespace osu.Game.Screens.Select return base.OnHover(e); } } - - internal partial class SoloModSelectOverlay : UserModSelectOverlay - { - protected override bool ShowPresets => true; - } } } diff --git a/osu.Game/Screens/SelectV2/SongSelect.cs b/osu.Game/Screens/SelectV2/SongSelect.cs index 70452de99a..ad29f846c4 100644 --- a/osu.Game/Screens/SelectV2/SongSelect.cs +++ b/osu.Game/Screens/SelectV2/SongSelect.cs @@ -23,7 +23,10 @@ namespace osu.Game.Screens.SelectV2 { private const float logo_scale = 0.4f; - private readonly ModSelectOverlay modSelectOverlay = new SoloModSelectOverlay(); + private readonly ModSelectOverlay modSelectOverlay = new ModSelectOverlay + { + ShowPresets = true, + }; [Cached] private readonly OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Aquamarine); @@ -186,10 +189,5 @@ namespace osu.Game.Screens.SelectV2 SearchText = query, }); } - - private partial class SoloModSelectOverlay : UserModSelectOverlay - { - protected override bool ShowPresets => true; - } } } diff --git a/osu.Game/Tests/Visual/Gameplay/ScoringTestScene.cs b/osu.Game/Tests/Visual/Gameplay/ScoringTestScene.cs index 6908f7f1b4..21d0b8e7a8 100644 --- a/osu.Game/Tests/Visual/Gameplay/ScoringTestScene.cs +++ b/osu.Game/Tests/Visual/Gameplay/ScoringTestScene.cs @@ -658,11 +658,10 @@ namespace osu.Game.Tests.Visual.Gameplay private partial class TestModSelectOverlay : UserModSelectOverlay { - protected override bool ShowPresets => false; - public TestModSelectOverlay() : base(OverlayColourScheme.Aquamarine) { + ShowPresets = false; } } } From 14b5c0bf10389b924fa8ca515c2e27457fdcc119 Mon Sep 17 00:00:00 2001 From: Zihad Date: Tue, 4 Mar 2025 19:56:48 +0600 Subject: [PATCH 1205/1275] Update window title in input thread --- osu.Game/OsuGame.cs | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs index fb9be8860c..a80d646e15 100644 --- a/osu.Game/OsuGame.cs +++ b/osu.Game/OsuGame.cs @@ -830,10 +830,11 @@ namespace osu.Game beatmap.NewValue?.BeginAsyncLoad(); // prevent weird window title saying please load a beatmap - if (beatmap.NewValue is null or DummyWorkingBeatmap) - Host.Window.Title = Name; - else - Host.Window.Title = $"{Name} - {beatmap.NewValue.BeatmapInfo.GetDisplayTitleRomanisable(true, false)}"; + string newTitle = Name; + if (beatmap.NewValue != null && beatmap.NewValue is not DummyWorkingBeatmap) + newTitle = $"{Name} - {beatmap.NewValue.BeatmapInfo.GetDisplayTitleRomanisable(true, false)}"; + + Host.InputThread.Scheduler.AddOnce(s => Host.Window.Title = s, newTitle); } private void modsChanged(ValueChangedEvent> mods) From 8ce6003a3e156cac95f448f40f473d9023c278df Mon Sep 17 00:00:00 2001 From: Zihad Date: Tue, 4 Mar 2025 20:36:53 +0600 Subject: [PATCH 1206/1275] Skip updating window title in headless mode --- osu.Game/OsuGame.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs index a80d646e15..2b9e2cb9cd 100644 --- a/osu.Game/OsuGame.cs +++ b/osu.Game/OsuGame.cs @@ -829,6 +829,9 @@ namespace osu.Game beatmap.OldValue?.CancelAsyncLoad(); beatmap.NewValue?.BeginAsyncLoad(); + if (Host.Window == null) + return; + // prevent weird window title saying please load a beatmap string newTitle = Name; if (beatmap.NewValue != null && beatmap.NewValue is not DummyWorkingBeatmap) From 9ca12744957f9d660d13a50e13060b2aa772b0e5 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 5 Mar 2025 13:51:56 +0900 Subject: [PATCH 1207/1275] Rename test scene to match new `RoomListing` class name --- ...TestSceneLoungeRoomsContainer.cs => TestSceneRoomListing.cs} | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename osu.Game.Tests/Visual/Multiplayer/{TestSceneLoungeRoomsContainer.cs => TestSceneRoomListing.cs} (99%) diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneLoungeRoomsContainer.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneRoomListing.cs similarity index 99% rename from osu.Game.Tests/Visual/Multiplayer/TestSceneLoungeRoomsContainer.cs rename to osu.Game.Tests/Visual/Multiplayer/TestSceneRoomListing.cs index 23e15b0501..27c5758afa 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneLoungeRoomsContainer.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneRoomListing.cs @@ -18,7 +18,7 @@ using osuTK.Input; namespace osu.Game.Tests.Visual.Multiplayer { - public partial class TestSceneLoungeRoomsContainer : OnlinePlayTestScene + public partial class TestSceneRoomListing : OnlinePlayTestScene { private BindableList rooms = null!; private IBindable selectedRoom = null!; From 3661107e4ffc4478ac7fe50e5189877f71b44cc6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 5 Mar 2025 08:05:15 +0100 Subject: [PATCH 1208/1275] Update property name in line with web changes --- osu.Game/Online/API/Requests/Responses/APIBeatmap.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Online/API/Requests/Responses/APIBeatmap.cs b/osu.Game/Online/API/Requests/Responses/APIBeatmap.cs index f06d0ef274..66e17739a8 100644 --- a/osu.Game/Online/API/Requests/Responses/APIBeatmap.cs +++ b/osu.Game/Online/API/Requests/Responses/APIBeatmap.cs @@ -98,7 +98,7 @@ namespace osu.Game.Online.API.Requests.Responses [JsonProperty(@"top_tag_ids")] public APIBeatmapTag[]? TopTags { get; set; } - [JsonProperty(@"own_tag_ids")] + [JsonProperty(@"current_user_tag_ids")] public long[]? OwnTagIds { get; set; } [JsonProperty(@"max_combo")] From abc4955e8131de912aaec22941d352cb558a7297 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 5 Mar 2025 09:21:47 +0100 Subject: [PATCH 1209/1275] Add failing test coverage --- .../SongSelect/TestSceneBeatmapLeaderboard.cs | 71 ++++++++++++++++--- .../SongSelectComponentsTestScene.cs | 5 +- .../SongSelectV2/TestSceneLeaderboardScore.cs | 66 +++++++++++++++++ 3 files changed, 129 insertions(+), 13 deletions(-) diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapLeaderboard.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapLeaderboard.cs index c234cc8a9c..23d6725491 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapLeaderboard.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapLeaderboard.cs @@ -12,6 +12,8 @@ using osu.Framework.Graphics; using osu.Framework.Platform; using osu.Framework.Testing; using osu.Game.Beatmaps; +using osu.Game.Graphics.Cursor; +using osu.Game.Graphics.UserInterface; using osu.Game.Online.API.Requests.Responses; using osu.Game.Online.Leaderboards; using osu.Game.Overlays; @@ -20,14 +22,16 @@ using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Osu; using osu.Game.Rulesets.Osu.Mods; using osu.Game.Scoring; +using osu.Game.Screens.Select; using osu.Game.Screens.Select.Leaderboards; using osu.Game.Tests.Resources; using osu.Game.Users; using osuTK; +using osuTK.Input; namespace osu.Game.Tests.Visual.SongSelect { - public partial class TestSceneBeatmapLeaderboard : OsuTestScene + public partial class TestSceneBeatmapLeaderboard : OsuManualInputManagerTestScene { private readonly FailableLeaderboard leaderboard; @@ -37,6 +41,7 @@ namespace osu.Game.Tests.Visual.SongSelect private ScoreManager scoreManager = null!; private RulesetStore rulesetStore = null!; private BeatmapManager beatmapManager = null!; + private PlaySongSelect songSelect = null!; protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent) { @@ -45,25 +50,36 @@ namespace osu.Game.Tests.Visual.SongSelect dependencies.Cache(rulesetStore = new RealmRulesetStore(Realm)); dependencies.Cache(beatmapManager = new BeatmapManager(LocalStorage, Realm, null, dependencies.Get(), Resources, dependencies.Get(), Beatmap.Default)); dependencies.Cache(scoreManager = new ScoreManager(rulesetStore, () => beatmapManager, LocalStorage, Realm, API)); + dependencies.CacheAs(songSelect = new PlaySongSelect()); Dependencies.Cache(Realm); return dependencies; } + [BackgroundDependencyLoader] + private void load() + { + LoadComponent(songSelect); + } + public TestSceneBeatmapLeaderboard() { - AddRange(new Drawable[] + Add(new OsuContextMenuContainer { - dialogOverlay = new DialogOverlay + RelativeSizeAxes = Axes.Both, + Children = new Drawable[] { - Depth = -1 - }, - leaderboard = new FailableLeaderboard - { - Origin = Anchor.Centre, - Anchor = Anchor.Centre, - Size = new Vector2(550f, 450f), - Scope = BeatmapLeaderboardScope.Global, + dialogOverlay = new DialogOverlay + { + Depth = -1 + }, + leaderboard = new FailableLeaderboard + { + Origin = Anchor.Centre, + Anchor = Anchor.Centre, + Size = new Vector2(550f, 450f), + Scope = BeatmapLeaderboardScope.Global, + } } }); } @@ -187,6 +203,39 @@ namespace osu.Game.Tests.Visual.SongSelect AddStep(@"None selected", () => leaderboard.SetErrorState(LeaderboardState.NoneSelected)); } + [Test] + public void TestUseTheseModsDoesNotCopySystemMods() + { + AddStep(@"set scores", () => leaderboard.SetScores(leaderboard.Scores, new ScoreInfo + { + Position = 999, + Rank = ScoreRank.XH, + Accuracy = 1, + MaxCombo = 244, + TotalScore = 1707827, + Ruleset = new OsuRuleset().RulesetInfo, + Mods = new Mod[] { new OsuModHidden(), new ModScoreV2(), }, + User = new APIUser + { + Id = 6602580, + Username = @"waaiiru", + CountryCode = CountryCode.ES, + } + })); + AddStep("right click panel", () => + { + InputManager.MoveMouseTo(this.ChildrenOfType().Single()); + InputManager.Click(MouseButton.Right); + }); + AddStep("click use these mods", () => + { + InputManager.MoveMouseTo(this.ChildrenOfType().Single()); + InputManager.Click(MouseButton.Left); + }); + AddAssert("song select received HD", () => songSelect.Mods.Value.Any(m => m is OsuModHidden)); + AddAssert("song select did not receive SV2", () => !songSelect.Mods.Value.Any(m => m is ModScoreV2)); + } + private void showPersonalBestWithNullPosition() { leaderboard.SetScores(leaderboard.Scores, new ScoreInfo diff --git a/osu.Game.Tests/Visual/SongSelectV2/SongSelectComponentsTestScene.cs b/osu.Game.Tests/Visual/SongSelectV2/SongSelectComponentsTestScene.cs index b7b0101a7c..8694722acc 100644 --- a/osu.Game.Tests/Visual/SongSelectV2/SongSelectComponentsTestScene.cs +++ b/osu.Game.Tests/Visual/SongSelectV2/SongSelectComponentsTestScene.cs @@ -6,16 +6,17 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; using osu.Framework.Testing; +using osu.Game.Graphics.Cursor; using osu.Game.Overlays; namespace osu.Game.Tests.Visual.SongSelectV2 { - public abstract partial class SongSelectComponentsTestScene : OsuTestScene + public abstract partial class SongSelectComponentsTestScene : OsuManualInputManagerTestScene { [Cached] protected readonly OverlayColourProvider ColourProvider = new OverlayColourProvider(OverlayColourScheme.Aquamarine); - protected override Container Content { get; } = new Container + protected override Container Content { get; } = new OsuContextMenuContainer { RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y, diff --git a/osu.Game.Tests/Visual/SongSelectV2/TestSceneLeaderboardScore.cs b/osu.Game.Tests/Visual/SongSelectV2/TestSceneLeaderboardScore.cs index a7d0d70c03..26d39c9203 100644 --- a/osu.Game.Tests/Visual/SongSelectV2/TestSceneLeaderboardScore.cs +++ b/osu.Game.Tests/Visual/SongSelectV2/TestSceneLeaderboardScore.cs @@ -7,9 +7,11 @@ using NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; +using osu.Framework.Testing; using osu.Framework.Utils; using osu.Game.Configuration; using osu.Game.Graphics.Sprites; +using osu.Game.Graphics.UserInterface; using osu.Game.Online.API.Requests.Responses; using osu.Game.Overlays; using osu.Game.Rulesets.Mania; @@ -22,6 +24,7 @@ using osu.Game.Screens.SelectV2.Leaderboards; using osu.Game.Tests.Resources; using osu.Game.Users; using osuTK; +using osuTK.Input; namespace osu.Game.Tests.Visual.SongSelectV2 { @@ -102,6 +105,69 @@ namespace osu.Game.Tests.Visual.SongSelectV2 }); } + [Test] + public void TestUseTheseModsDoesNotCopySystemMods() + { + LeaderboardScoreV2 score = null!; + + AddStep("create content", () => + { + Children = new Drawable[] + { + fillFlow = new FillFlowContainer + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Spacing = new Vector2(0f, 2f), + Shear = new Vector2(OsuGame.SHEAR, 0) + }, + drawWidthText = new OsuSpriteText(), + }; + + var scoreInfo = new ScoreInfo + { + Position = 999, + Rank = ScoreRank.X, + Accuracy = 1, + MaxCombo = 244, + TotalScore = RNG.Next(1_800_000, 2_000_000), + MaximumStatistics = { { HitResult.Great, 3000 } }, + Mods = new Mod[] { new OsuModHidden(), new ModScoreV2(), }, + Ruleset = new OsuRuleset().RulesetInfo, + User = new APIUser + { + Id = 6602580, + Username = @"waaiiru", + CountryCode = CountryCode.ES, + CoverUrl = @"https://osu.ppy.sh/images/headers/profile-covers/c1.jpg", + }, + Date = DateTimeOffset.Now.AddYears(-2), + }; + + fillFlow.Add(score = new LeaderboardScoreV2(scoreInfo) + { + Rank = scoreInfo.Position, + Shear = Vector2.Zero, + }); + + score.Show(); + }); + AddStep("right click panel", () => + { + InputManager.MoveMouseTo(score); + InputManager.Click(MouseButton.Right); + }); + AddStep("click use these mods", () => + { + InputManager.MoveMouseTo(this.ChildrenOfType().Single()); + InputManager.Click(MouseButton.Left); + }); + AddAssert("mods received HD", () => score.SelectedMods.Value.Any(m => m is OsuModHidden)); + AddAssert("mods did not receive SV2", () => !score.SelectedMods.Value.Any(m => m is ModScoreV2)); + } + public override void SetUpSteps() { AddToggleStep("toggle scoring mode", v => config.SetValue(OsuSetting.ScoreDisplayMode, v ? ScoringMode.Classic : ScoringMode.Standardised)); From d9a1dcf9b972af6864b3522e3048a433dbd4ef77 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 5 Mar 2025 09:25:56 +0100 Subject: [PATCH 1210/1275] Fix "use these mods" option applying to system mods Closes https://github.com/ppy/osu/issues/32229. --- osu.Game/Online/Leaderboards/LeaderboardScore.cs | 5 ++++- .../Screens/SelectV2/Leaderboards/LeaderboardScoreV2.cs | 6 +++++- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/osu.Game/Online/Leaderboards/LeaderboardScore.cs b/osu.Game/Online/Leaderboards/LeaderboardScore.cs index 0db03efb68..ea42c515a6 100644 --- a/osu.Game/Online/Leaderboards/LeaderboardScore.cs +++ b/osu.Game/Online/Leaderboards/LeaderboardScore.cs @@ -453,7 +453,10 @@ namespace osu.Game.Online.Leaderboards List items = new List(); if (Score.Mods.Length > 0 && songSelect != null) - items.Add(new OsuMenuItem("Use these mods", MenuItemType.Highlighted, () => songSelect.Mods.Value = Score.Mods)); + { + // system mods should never be copied across regardless of anything. + items.Add(new OsuMenuItem("Use these mods", MenuItemType.Highlighted, () => songSelect.Mods.Value = Score.Mods.Where(m => m.Type != ModType.System).ToArray())); + } if (Score.OnlineID > 0) items.Add(new OsuMenuItem(CommonStrings.CopyLink, MenuItemType.Standard, () => clipboard?.SetText($@"{api.Endpoints.WebsiteUrl}/scores/{Score.OnlineID}"))); diff --git a/osu.Game/Screens/SelectV2/Leaderboards/LeaderboardScoreV2.cs b/osu.Game/Screens/SelectV2/Leaderboards/LeaderboardScoreV2.cs index 978d6eca32..71cc80af49 100644 --- a/osu.Game/Screens/SelectV2/Leaderboards/LeaderboardScoreV2.cs +++ b/osu.Game/Screens/SelectV2/Leaderboards/LeaderboardScoreV2.cs @@ -781,7 +781,11 @@ namespace osu.Game.Screens.SelectV2.Leaderboards List items = new List(); if (score.Mods.Length > 0) - items.Add(new OsuMenuItem("Use these mods", MenuItemType.Highlighted, () => SelectedMods.Value = score.Mods.Where(m => IsValidMod.Invoke(m)).ToArray())); + { + // system mods should never be copied across regardless of anything. + items.Add(new OsuMenuItem("Use these mods", MenuItemType.Highlighted, + () => SelectedMods.Value = score.Mods.Where(m => IsValidMod.Invoke(m) && m.Type != ModType.System).ToArray())); + } if (score.OnlineID > 0) items.Add(new OsuMenuItem(CommonStrings.CopyLink, MenuItemType.Standard, () => clipboard?.SetText($@"{api.Endpoints.WebsiteUrl}/scores/{score.OnlineID}"))); From 097dd701396a476ca5a7a5c03dbbee6b2f623ed5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 5 Mar 2025 09:33:33 +0100 Subject: [PATCH 1211/1275] Add another failing test --- .../DailyChallenge/TestSceneDailyChallenge.cs | 37 +++++++++++++++++++ .../OnlinePlay/TestRoomRequestsHandler.cs | 1 + 2 files changed, 38 insertions(+) diff --git a/osu.Game.Tests/Visual/DailyChallenge/TestSceneDailyChallenge.cs b/osu.Game.Tests/Visual/DailyChallenge/TestSceneDailyChallenge.cs index 0742ed5eb9..c974a852f3 100644 --- a/osu.Game.Tests/Visual/DailyChallenge/TestSceneDailyChallenge.cs +++ b/osu.Game.Tests/Visual/DailyChallenge/TestSceneDailyChallenge.cs @@ -6,6 +6,8 @@ using System.Linq; using NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Screens; +using osu.Framework.Testing; +using osu.Game.Graphics.UserInterface; using osu.Game.Localisation; using osu.Game.Online.API; using osu.Game.Online.Metadata; @@ -13,9 +15,11 @@ using osu.Game.Online.Rooms; using osu.Game.Overlays; using osu.Game.Overlays.Notifications; using osu.Game.Rulesets.Osu.Mods; +using osu.Game.Screens.SelectV2.Leaderboards; using osu.Game.Tests.Resources; using osu.Game.Tests.Visual.Metadata; using osu.Game.Tests.Visual.OnlinePlay; +using osuTK.Input; namespace osu.Game.Tests.Visual.DailyChallenge { @@ -57,6 +61,39 @@ namespace osu.Game.Tests.Visual.DailyChallenge AddStep("push screen", () => LoadScreen(new Screens.OnlinePlay.DailyChallenge.DailyChallenge(room))); } + [Test] + public void TestUseTheseModsUnavailableIfNoFreeMods() + { + var room = new Room + { + RoomID = 1234, + Name = "Daily Challenge: June 4, 2024", + Playlist = + [ + new PlaylistItem(TestResources.CreateTestBeatmapSetInfo().Beatmaps.First()) + { + RequiredMods = [new APIMod(new OsuModTraceable())], + AllowedMods = [] + } + ], + EndDate = DateTimeOffset.Now.AddHours(12), + Category = RoomCategory.DailyChallenge + }; + + AddStep("add room", () => API.Perform(new CreateRoomRequest(room))); + Screens.OnlinePlay.DailyChallenge.DailyChallenge screen = null!; + AddStep("push screen", () => LoadScreen(screen = new Screens.OnlinePlay.DailyChallenge.DailyChallenge(room))); + AddUntilStep("wait for pushed", () => screen.IsCurrentScreen()); + AddStep("force transforms to finish", () => FinishTransforms(true)); + AddStep("right click second score", () => + { + InputManager.MoveMouseTo(this.ChildrenOfType().ElementAt(1)); + InputManager.Click(MouseButton.Right); + }); + AddAssert("use these mods not present", + () => this.ChildrenOfType().All(m => m.Items.All(item => item.Text.Value != "Use these mods"))); + } + [Test] public void TestNotifications() { diff --git a/osu.Game/Tests/Visual/OnlinePlay/TestRoomRequestsHandler.cs b/osu.Game/Tests/Visual/OnlinePlay/TestRoomRequestsHandler.cs index 0ae3a73e5d..46c1251d42 100644 --- a/osu.Game/Tests/Visual/OnlinePlay/TestRoomRequestsHandler.cs +++ b/osu.Game/Tests/Visual/OnlinePlay/TestRoomRequestsHandler.cs @@ -126,6 +126,7 @@ namespace osu.Game.Tests.Visual.OnlinePlay MaxCombo = 100, TotalScore = 200000, User = new APIUser { Username = "worst user" }, + Mods = [new APIMod { Acronym = @"TD" }], Statistics = new Dictionary() }, }, From 0ac3a80406fa295e648e5e83e09d1e8a6c2a7773 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 5 Mar 2025 09:40:11 +0100 Subject: [PATCH 1212/1275] Fix "use these mods" option showing if it can't do anything Closes https://github.com/ppy/osu/issues/32230. --- osu.Game/Online/Leaderboards/LeaderboardScore.cs | 10 +++++----- .../SelectV2/Leaderboards/LeaderboardScoreV2.cs | 11 +++++------ 2 files changed, 10 insertions(+), 11 deletions(-) diff --git a/osu.Game/Online/Leaderboards/LeaderboardScore.cs b/osu.Game/Online/Leaderboards/LeaderboardScore.cs index ea42c515a6..28b20c0c05 100644 --- a/osu.Game/Online/Leaderboards/LeaderboardScore.cs +++ b/osu.Game/Online/Leaderboards/LeaderboardScore.cs @@ -452,11 +452,11 @@ namespace osu.Game.Online.Leaderboards { List items = new List(); - if (Score.Mods.Length > 0 && songSelect != null) - { - // system mods should never be copied across regardless of anything. - items.Add(new OsuMenuItem("Use these mods", MenuItemType.Highlighted, () => songSelect.Mods.Value = Score.Mods.Where(m => m.Type != ModType.System).ToArray())); - } + // system mods should never be copied across regardless of anything. + var copyableMods = Score.Mods.Where(m => m.Type != ModType.System).ToArray(); + + if (copyableMods.Length > 0 && songSelect != null) + items.Add(new OsuMenuItem("Use these mods", MenuItemType.Highlighted, () => songSelect.Mods.Value = copyableMods)); if (Score.OnlineID > 0) items.Add(new OsuMenuItem(CommonStrings.CopyLink, MenuItemType.Standard, () => clipboard?.SetText($@"{api.Endpoints.WebsiteUrl}/scores/{Score.OnlineID}"))); diff --git a/osu.Game/Screens/SelectV2/Leaderboards/LeaderboardScoreV2.cs b/osu.Game/Screens/SelectV2/Leaderboards/LeaderboardScoreV2.cs index 71cc80af49..b54f007f38 100644 --- a/osu.Game/Screens/SelectV2/Leaderboards/LeaderboardScoreV2.cs +++ b/osu.Game/Screens/SelectV2/Leaderboards/LeaderboardScoreV2.cs @@ -780,12 +780,11 @@ namespace osu.Game.Screens.SelectV2.Leaderboards { List items = new List(); - if (score.Mods.Length > 0) - { - // system mods should never be copied across regardless of anything. - items.Add(new OsuMenuItem("Use these mods", MenuItemType.Highlighted, - () => SelectedMods.Value = score.Mods.Where(m => IsValidMod.Invoke(m) && m.Type != ModType.System).ToArray())); - } + // system mods should never be copied across regardless of anything. + var copyableMods = score.Mods.Where(m => IsValidMod.Invoke(m) && m.Type != ModType.System).ToArray(); + + if (copyableMods.Length > 0) + items.Add(new OsuMenuItem("Use these mods", MenuItemType.Highlighted, () => SelectedMods.Value = copyableMods)); if (score.OnlineID > 0) items.Add(new OsuMenuItem(CommonStrings.CopyLink, MenuItemType.Standard, () => clipboard?.SetText($@"{api.Endpoints.WebsiteUrl}/scores/{score.OnlineID}"))); From 7975c301a846e9c28b02e3ab388912344ff95cce Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 5 Mar 2025 12:32:58 +0100 Subject: [PATCH 1213/1275] Try to fix test --- osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapLeaderboard.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapLeaderboard.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapLeaderboard.cs index 23d6725491..bfb835cad1 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapLeaderboard.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapLeaderboard.cs @@ -222,6 +222,7 @@ namespace osu.Game.Tests.Visual.SongSelect CountryCode = CountryCode.ES, } })); + AddUntilStep("wait for scores", () => this.ChildrenOfType().Count(), () => Is.GreaterThan(0)); AddStep("right click panel", () => { InputManager.MoveMouseTo(this.ChildrenOfType().Single()); From 3f461c07348581a313a727e92411769ea8c30c6a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 5 Mar 2025 14:11:44 +0100 Subject: [PATCH 1214/1275] Add "discard unsaved changes" operation to beatmap editor Apparently useful in modding workflows when you want to test out a few different variants of a thing. Re-uses `Ctrl-L` binding from stable. Some folks may argue that the dialog makes the hotkey pointless, but I really do want to protect users from accidental data loss, and also if you want to power through it quickly, you can hit the 1 key when the dialog shows, which will bypass the hold-to-activate period (which wasn't intentional, but so many people want a bypass at this point that we're probably keeping that behaviour for power users). --- .../Editor/TestSceneManiaEditorSaving.cs | 4 +-- .../Edit/Setup/ManiaDifficultySection.cs | 2 +- .../Input/Bindings/GlobalActionContainer.cs | 4 +++ osu.Game/Localisation/EditorDialogsStrings.cs | 5 +++ .../GlobalActionKeyBindingStrings.cs | 5 +++ .../Edit/DiscardUnsavedChangesDialog.cs | 33 +++++++++++++++++++ osu.Game/Screens/Edit/Editor.cs | 29 ++++++++++++++-- ...Dialog.cs => SaveAndReloadEditorDialog.cs} | 4 +-- 8 files changed, 79 insertions(+), 7 deletions(-) create mode 100644 osu.Game/Screens/Edit/DiscardUnsavedChangesDialog.cs rename osu.Game/Screens/Edit/{ReloadEditorDialog.cs => SaveAndReloadEditorDialog.cs} (86%) diff --git a/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneManiaEditorSaving.cs b/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneManiaEditorSaving.cs index d9ba721646..ebaa8bcea2 100644 --- a/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneManiaEditorSaving.cs +++ b/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneManiaEditorSaving.cs @@ -29,7 +29,7 @@ namespace osu.Game.Rulesets.Mania.Tests.Editor { keyCount.Current.Value = 8; }); - AddUntilStep("dialog visible", () => Game.ChildrenOfType().SingleOrDefault()?.CurrentDialog, Is.InstanceOf); + AddUntilStep("dialog visible", () => Game.ChildrenOfType().SingleOrDefault()?.CurrentDialog, Is.InstanceOf); AddStep("refuse", () => InputManager.Key(Key.Number2)); AddAssert("key count is 5", () => keyCount.Current.Value, () => Is.EqualTo(5)); @@ -37,7 +37,7 @@ namespace osu.Game.Rulesets.Mania.Tests.Editor { keyCount.Current.Value = 8; }); - AddUntilStep("dialog visible", () => Game.ChildrenOfType().Single().CurrentDialog, Is.InstanceOf); + AddUntilStep("dialog visible", () => Game.ChildrenOfType().Single().CurrentDialog, Is.InstanceOf); AddStep("acquiesce", () => InputManager.Key(Key.Number1)); AddUntilStep("beatmap became 8K", () => Game.Beatmap.Value.BeatmapInfo.Difficulty.CircleSize, () => Is.EqualTo(8)); } diff --git a/osu.Game.Rulesets.Mania/Edit/Setup/ManiaDifficultySection.cs b/osu.Game.Rulesets.Mania/Edit/Setup/ManiaDifficultySection.cs index 48e59877df..a5c3c2264c 100644 --- a/osu.Game.Rulesets.Mania/Edit/Setup/ManiaDifficultySection.cs +++ b/osu.Game.Rulesets.Mania/Edit/Setup/ManiaDifficultySection.cs @@ -134,7 +134,7 @@ namespace osu.Game.Rulesets.Mania.Edit.Setup updatingKeyCount = true; - editor.Reload().ContinueWith(t => + editor.SaveAndReload().ContinueWith(t => { if (!t.GetResultSafely()) { diff --git a/osu.Game/Input/Bindings/GlobalActionContainer.cs b/osu.Game/Input/Bindings/GlobalActionContainer.cs index e4dc2d503b..6de2dabe2b 100644 --- a/osu.Game/Input/Bindings/GlobalActionContainer.cs +++ b/osu.Game/Input/Bindings/GlobalActionContainer.cs @@ -155,6 +155,7 @@ namespace osu.Game.Input.Bindings new KeyBinding(new[] { InputKey.Control, InputKey.Shift, InputKey.B }, GlobalAction.EditorRemoveClosestBookmark), new KeyBinding(new[] { InputKey.Alt, InputKey.Left }, GlobalAction.EditorSeekToPreviousBookmark), new KeyBinding(new[] { InputKey.Alt, InputKey.Right }, GlobalAction.EditorSeekToNextBookmark), + new KeyBinding(new[] { InputKey.Control, InputKey.L }, GlobalAction.EditorDiscardUnsavedChanges), }; private static IEnumerable editorTestPlayKeyBindings => new[] @@ -502,6 +503,9 @@ namespace osu.Game.Input.Bindings [LocalisableDescription(typeof(GlobalActionKeyBindingStrings), nameof(GlobalActionKeyBindingStrings.EditorToggleMoveControl))] EditorToggleMoveControl, + + [LocalisableDescription(typeof(GlobalActionKeyBindingStrings), nameof(GlobalActionKeyBindingStrings.EditorDiscardUnsavedChanges))] + EditorDiscardUnsavedChanges, } public enum GlobalActionCategory diff --git a/osu.Game/Localisation/EditorDialogsStrings.cs b/osu.Game/Localisation/EditorDialogsStrings.cs index 94f28c617c..3617dca81f 100644 --- a/osu.Game/Localisation/EditorDialogsStrings.cs +++ b/osu.Game/Localisation/EditorDialogsStrings.cs @@ -54,6 +54,11 @@ namespace osu.Game.Localisation /// public static LocalisableString EditorReloadDialogHeader => new TranslatableString(getKey(@"editor_reload_dialog_header"), @"The editor must be reloaded to apply this change. The beatmap will be saved."); + /// + /// "Discard all unsaved changes? This cannot be undone." + /// + public static LocalisableString DiscardUnsavedChangesDialogHeader => new TranslatableString(getKey(@"discard_unsaved_changes_dialog_header"), @"Discard all unsaved changes? This cannot be undone."); + private static string getKey(string key) => $@"{prefix}:{key}"; } } diff --git a/osu.Game/Localisation/GlobalActionKeyBindingStrings.cs b/osu.Game/Localisation/GlobalActionKeyBindingStrings.cs index 5713df57c9..34b9e1fecc 100644 --- a/osu.Game/Localisation/GlobalActionKeyBindingStrings.cs +++ b/osu.Game/Localisation/GlobalActionKeyBindingStrings.cs @@ -459,6 +459,11 @@ namespace osu.Game.Localisation /// public static LocalisableString EditorToggleMoveControl => new TranslatableString(getKey(@"editor_toggle_move_control"), @"Toggle movement control"); + /// + /// "Discard unsaved changes" + /// + public static LocalisableString EditorDiscardUnsavedChanges => new TranslatableString(getKey(@"editor_discard_unsaved_changes"), @"Discard unsaved changes"); + private static string getKey(string key) => $@"{prefix}:{key}"; } } diff --git a/osu.Game/Screens/Edit/DiscardUnsavedChangesDialog.cs b/osu.Game/Screens/Edit/DiscardUnsavedChangesDialog.cs new file mode 100644 index 0000000000..1867b48830 --- /dev/null +++ b/osu.Game/Screens/Edit/DiscardUnsavedChangesDialog.cs @@ -0,0 +1,33 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using osu.Framework.Graphics.Sprites; +using osu.Game.Localisation; +using osu.Game.Overlays.Dialog; + +namespace osu.Game.Screens.Edit +{ + public partial class DiscardUnsavedChangesDialog : PopupDialog + { + public DiscardUnsavedChangesDialog(Action exit) + { + HeaderText = EditorDialogsStrings.DiscardUnsavedChangesDialogHeader; + + Icon = FontAwesome.Solid.Trash; + + Buttons = new PopupDialogButton[] + { + new PopupDialogDangerousButton + { + Text = EditorDialogsStrings.ForgetAllChanges, + Action = exit + }, + new PopupDialogCancelButton + { + Text = EditorDialogsStrings.ContinueEditing, + }, + }; + } + } +} diff --git a/osu.Game/Screens/Edit/Editor.cs b/osu.Game/Screens/Edit/Editor.cs index 219e14861f..bf254093b3 100644 --- a/osu.Game/Screens/Edit/Editor.cs +++ b/osu.Game/Screens/Edit/Editor.cs @@ -164,6 +164,7 @@ namespace osu.Game.Screens.Edit private bool switchingDifficulty; private string lastSavedHash; + private EditorMenuItem discardChangesMenuItem; private ScreenContainer screenContainer; @@ -391,6 +392,10 @@ namespace osu.Game.Screens.Edit { undoMenuItem = new EditorMenuItem(CommonStrings.Undo, MenuItemType.Standard, Undo) { Hotkey = new Hotkey(PlatformAction.Undo) }, redoMenuItem = new EditorMenuItem(CommonStrings.Redo, MenuItemType.Standard, Redo) { Hotkey = new Hotkey(PlatformAction.Redo) }, + discardChangesMenuItem = new EditorMenuItem("Discard unsaved changes", MenuItemType.Destructive, DiscardUnsavedChanges) + { + Hotkey = new Hotkey(GlobalAction.EditorDiscardUnsavedChanges) + }, new OsuMenuItemSpacer(), cutMenuItem = new EditorMenuItem(CommonStrings.Cut, MenuItemType.Standard, Cut) { Hotkey = new Hotkey(PlatformAction.Cut) }, copyMenuItem = new EditorMenuItem(CommonStrings.Copy, MenuItemType.Standard, Copy) { Hotkey = new Hotkey(PlatformAction.Copy) }, @@ -607,6 +612,8 @@ namespace osu.Game.Screens.Edit { base.Update(); clock.ProcessFrame(); + + discardChangesMenuItem.Action.Disabled = !HasUnsavedChanges; } public bool OnPressed(KeyBindingPressEvent e) @@ -821,6 +828,10 @@ namespace osu.Game.Screens.Edit case GlobalAction.EditorTestGameplay: bottomBar.TestGameplayButton.TriggerClick(); return true; + + case GlobalAction.EditorDiscardUnsavedChanges: + DiscardUnsavedChanges(); + return true; } return false; @@ -1008,6 +1019,20 @@ namespace osu.Game.Screens.Edit protected void Redo() => changeHandler?.RestoreState(1); + protected void DiscardUnsavedChanges() + { + if (!HasUnsavedChanges) + return; + + // we're not doing this via `changeHandler` because `changeHandler` has limited number of undo actions + // and therefore there's no guarantee that it even *has* the beatmap's last saved state in its history still. + dialogOverlay.Push(new DiscardUnsavedChangesDialog(() => + { + updateLastSavedHash(); // without this a second dialog will show (the standard "save unsaved changes" one that shows on exit). + SwitchToDifficulty(editorBeatmap.BeatmapInfo); + })); + } + protected void SetPreviewPointToCurrentTime() { editorBeatmap.PreviewTime.Value = (int)clock.CurrentTime; @@ -1510,11 +1535,11 @@ namespace osu.Game.Screens.Edit loader?.CancelPendingDifficultySwitch(); } - public Task Reload() + public Task SaveAndReload() { var tcs = new TaskCompletionSource(); - dialogOverlay.Push(new ReloadEditorDialog( + dialogOverlay.Push(new SaveAndReloadEditorDialog( reload: () => { bool reloadedSuccessfully = attemptMutationOperation(() => diff --git a/osu.Game/Screens/Edit/ReloadEditorDialog.cs b/osu.Game/Screens/Edit/SaveAndReloadEditorDialog.cs similarity index 86% rename from osu.Game/Screens/Edit/ReloadEditorDialog.cs rename to osu.Game/Screens/Edit/SaveAndReloadEditorDialog.cs index 72a9f81347..b73c7cfff8 100644 --- a/osu.Game/Screens/Edit/ReloadEditorDialog.cs +++ b/osu.Game/Screens/Edit/SaveAndReloadEditorDialog.cs @@ -8,9 +8,9 @@ using osu.Game.Localisation; namespace osu.Game.Screens.Edit { - public partial class ReloadEditorDialog : PopupDialog + public partial class SaveAndReloadEditorDialog : PopupDialog { - public ReloadEditorDialog(Action reload, Action cancel) + public SaveAndReloadEditorDialog(Action reload, Action cancel) { HeaderText = EditorDialogsStrings.EditorReloadDialogHeader; From 5feddae6c75f8f8020196362ea40646c7a08460e Mon Sep 17 00:00:00 2001 From: Zihad Date: Wed, 5 Mar 2025 21:35:24 +0600 Subject: [PATCH 1215/1275] Revert "Update window title in input thread" This reverts commit 14b5c0bf10389b924fa8ca515c2e27457fdcc119. This is not necessary as the title update is already scheduled on the correct thread by the framework. --- osu.Game/OsuGame.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs index 2b9e2cb9cd..e070e89c19 100644 --- a/osu.Game/OsuGame.cs +++ b/osu.Game/OsuGame.cs @@ -837,7 +837,7 @@ namespace osu.Game if (beatmap.NewValue != null && beatmap.NewValue is not DummyWorkingBeatmap) newTitle = $"{Name} - {beatmap.NewValue.BeatmapInfo.GetDisplayTitleRomanisable(true, false)}"; - Host.InputThread.Scheduler.AddOnce(s => Host.Window.Title = s, newTitle); + Host.Window.Title = newTitle; } private void modsChanged(ValueChangedEvent> mods) From 4ae5f239cb3c342812bef639f43971ccca7d3a71 Mon Sep 17 00:00:00 2001 From: Zihad Date: Wed, 5 Mar 2025 21:41:11 +0600 Subject: [PATCH 1216/1275] Remove unnecessary comment --- osu.Game/OsuGame.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs index e070e89c19..abe5ce21c6 100644 --- a/osu.Game/OsuGame.cs +++ b/osu.Game/OsuGame.cs @@ -832,7 +832,6 @@ namespace osu.Game if (Host.Window == null) return; - // prevent weird window title saying please load a beatmap string newTitle = Name; if (beatmap.NewValue != null && beatmap.NewValue is not DummyWorkingBeatmap) newTitle = $"{Name} - {beatmap.NewValue.BeatmapInfo.GetDisplayTitleRomanisable(true, false)}"; From d33a8dfc3b57d3888c09a232e6d8fa3fb70c6dca Mon Sep 17 00:00:00 2001 From: Zihad Date: Wed, 5 Mar 2025 22:47:39 +0600 Subject: [PATCH 1217/1275] Skip updating window title for protected mapsets --- osu.Game/OsuGame.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs index abe5ce21c6..37ff70ccb7 100644 --- a/osu.Game/OsuGame.cs +++ b/osu.Game/OsuGame.cs @@ -833,7 +833,7 @@ namespace osu.Game return; string newTitle = Name; - if (beatmap.NewValue != null && beatmap.NewValue is not DummyWorkingBeatmap) + if (beatmap.NewValue?.BeatmapSetInfo?.Protected == false && beatmap.NewValue is not DummyWorkingBeatmap) newTitle = $"{Name} - {beatmap.NewValue.BeatmapInfo.GetDisplayTitleRomanisable(true, false)}"; Host.Window.Title = newTitle; From 02d19eaa55c05fe9149cf7771ca40342bc689bbd Mon Sep 17 00:00:00 2001 From: Zihad Date: Thu, 6 Mar 2025 01:36:59 +0600 Subject: [PATCH 1218/1275] Update window title changes to match osu! stable It shows beatmap metadata during gameplay, spectating, and watching replays but shows beatmap filename during editng --- osu.Game/OsuGame.cs | 33 +++++++++++++++++++++++++++++++-- 1 file changed, 31 insertions(+), 2 deletions(-) diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs index 37ff70ccb7..ed71f357a5 100644 --- a/osu.Game/OsuGame.cs +++ b/osu.Game/OsuGame.cs @@ -421,6 +421,7 @@ namespace osu.Game SelectedMods.BindValueChanged(modsChanged); Beatmap.BindValueChanged(beatmapChanged, true); + configUserActivity.BindValueChanged(userActivityChanged); applySafeAreaConsiderations = LocalConfig.GetBindable(OsuSetting.SafeAreaConsiderations); applySafeAreaConsiderations.BindValueChanged(apply => SafeAreaContainer.SafeAreaOverrideEdges = apply.NewValue ? SafeAreaOverrideEdges : Edges.All, true); @@ -828,13 +829,41 @@ namespace osu.Game { beatmap.OldValue?.CancelAsyncLoad(); beatmap.NewValue?.BeginAsyncLoad(); + updateWindowTitle(); + } + private void userActivityChanged(ValueChangedEvent userActivity) + { + updateWindowTitle(); + } + + private void updateWindowTitle() + { if (Host.Window == null) return; + if (Beatmap.Value?.BeatmapSetInfo?.Protected != false || Beatmap.Value is DummyWorkingBeatmap) + { + Host.Window.Title = Name; + return; + } + string newTitle = Name; - if (beatmap.NewValue?.BeatmapSetInfo?.Protected == false && beatmap.NewValue is not DummyWorkingBeatmap) - newTitle = $"{Name} - {beatmap.NewValue.BeatmapInfo.GetDisplayTitleRomanisable(true, false)}"; + + switch (configUserActivity.Value) + { + case UserActivity.InGame: + case UserActivity.TestingBeatmap: + case UserActivity.WatchingReplay: + newTitle = $"{Name} - {Beatmap.Value.BeatmapInfo.GetDisplayTitleRomanisable(true, false)}"; + break; + + case UserActivity.EditingBeatmap: + if (Beatmap.Value.BeatmapInfo.Path != null) + newTitle = $"{Name} - {Beatmap.Value.BeatmapInfo.Path}"; + + break; + } Host.Window.Title = newTitle; } From 574f2363fff982d21d7ab42eaf130cc89000f5cb Mon Sep 17 00:00:00 2001 From: schiavoanto Date: Wed, 5 Mar 2025 23:31:35 +0100 Subject: [PATCH 1219/1275] Add localisation for skin management buttons in settings --- osu.Game/Localisation/CommonStrings.cs | 10 ++++++++++ osu.Game/Overlays/Settings/Sections/SkinSection.cs | 6 +++--- 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/osu.Game/Localisation/CommonStrings.cs b/osu.Game/Localisation/CommonStrings.cs index 243a100029..26e344ec71 100644 --- a/osu.Game/Localisation/CommonStrings.cs +++ b/osu.Game/Localisation/CommonStrings.cs @@ -39,11 +39,21 @@ namespace osu.Game.Localisation /// public static LocalisableString Default => new TranslatableString(getKey(@"default"), @"Default"); + /// + /// "Rename" + /// + public static LocalisableString Rename => new TranslatableString(getKey(@"rename"), @"Rename"); + /// /// "Export" /// public static LocalisableString Export => new TranslatableString(getKey(@"export"), @"Export"); + /// + /// "Delete" + /// + public static LocalisableString Delete => new TranslatableString(getKey(@"delete"), @"Delete"); + /// /// "Width" /// diff --git a/osu.Game/Overlays/Settings/Sections/SkinSection.cs b/osu.Game/Overlays/Settings/Sections/SkinSection.cs index a89d5e2f4a..1f220138de 100644 --- a/osu.Game/Overlays/Settings/Sections/SkinSection.cs +++ b/osu.Game/Overlays/Settings/Sections/SkinSection.cs @@ -165,7 +165,7 @@ namespace osu.Game.Overlays.Settings.Sections [BackgroundDependencyLoader] private void load() { - Text = "Rename"; + Text = CommonStrings.Rename; Action = this.ShowPopover; } @@ -193,7 +193,7 @@ namespace osu.Game.Overlays.Settings.Sections [BackgroundDependencyLoader] private void load() { - Text = "Export"; + Text = CommonStrings.Export; Action = export; } @@ -231,7 +231,7 @@ namespace osu.Game.Overlays.Settings.Sections [BackgroundDependencyLoader] private void load() { - Text = "Delete"; + Text = CommonStrings.Delete; Action = delete; } From ee2615da53da7f537e5c920869464ce2bd13ffab Mon Sep 17 00:00:00 2001 From: schiavoanto Date: Wed, 5 Mar 2025 23:51:29 +0100 Subject: [PATCH 1220/1275] Use osu-web delete localisation --- osu.Game/Overlays/Settings/Sections/SkinSection.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/osu.Game/Overlays/Settings/Sections/SkinSection.cs b/osu.Game/Overlays/Settings/Sections/SkinSection.cs index 1f220138de..84767c8619 100644 --- a/osu.Game/Overlays/Settings/Sections/SkinSection.cs +++ b/osu.Game/Overlays/Settings/Sections/SkinSection.cs @@ -27,6 +27,7 @@ using osu.Game.Screens.Select; using osu.Game.Skinning; using osuTK; using Realms; +using WebCommonStrings = osu.Game.Resources.Localisation.Web.CommonStrings; namespace osu.Game.Overlays.Settings.Sections { @@ -231,7 +232,7 @@ namespace osu.Game.Overlays.Settings.Sections [BackgroundDependencyLoader] private void load() { - Text = CommonStrings.Delete; + Text = WebCommonStrings.ButtonsDelete; Action = delete; } From 5c3695673b49fee5570f1fd0bcc0588cc2654d37 Mon Sep 17 00:00:00 2001 From: schiavoanto Date: Thu, 6 Mar 2025 00:22:47 +0100 Subject: [PATCH 1221/1275] Remove delete string from CommonStrings --- osu.Game/Localisation/CommonStrings.cs | 5 ----- 1 file changed, 5 deletions(-) diff --git a/osu.Game/Localisation/CommonStrings.cs b/osu.Game/Localisation/CommonStrings.cs index 26e344ec71..f9d0feb5e2 100644 --- a/osu.Game/Localisation/CommonStrings.cs +++ b/osu.Game/Localisation/CommonStrings.cs @@ -49,11 +49,6 @@ namespace osu.Game.Localisation /// public static LocalisableString Export => new TranslatableString(getKey(@"export"), @"Export"); - /// - /// "Delete" - /// - public static LocalisableString Delete => new TranslatableString(getKey(@"delete"), @"Delete"); - /// /// "Width" /// From 50c4f9098320a6e7b46e5bcea425c96b03f1f07d Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Thu, 6 Mar 2025 12:55:59 +0900 Subject: [PATCH 1222/1275] Fix intermittent playlists results screen tests --- .../TestScenePlaylistsResultsScreen.cs | 22 +++++++++---------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsResultsScreen.cs b/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsResultsScreen.cs index 469f7c8b74..6b73f1a5f4 100644 --- a/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsResultsScreen.cs +++ b/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsResultsScreen.cs @@ -156,13 +156,13 @@ namespace osu.Game.Tests.Visual.Playlists AddStep("get panel count", () => beforePanelCount = this.ChildrenOfType().Count()); AddStep("scroll to right", () => resultsScreen.ChildrenOfType().Single().ChildrenOfType().Single().ScrollToEnd(false)); - AddAssert("right loading spinner shown", () => + AddUntilStep("right loading spinner shown", () => resultsScreen.ChildrenOfType().Single(l => l.Anchor == Anchor.CentreRight).State.Value == Visibility.Visible); waitForDisplay(); AddAssert($"count increased by {scores_per_result}", () => this.ChildrenOfType().Count() == beforePanelCount + scores_per_result); - AddAssert("right loading spinner hidden", () => + AddUntilStep("right loading spinner hidden", () => resultsScreen.ChildrenOfType().Single(l => l.Anchor == Anchor.CentreRight).State.Value == Visibility.Hidden); } } @@ -180,26 +180,26 @@ namespace osu.Game.Tests.Visual.Playlists AddStep("get panel count", () => beforePanelCount = this.ChildrenOfType().Count()); AddStep("scroll to right", () => resultsScreen.ChildrenOfType().Single().ChildrenOfType().Single().ScrollToEnd(false)); - AddAssert("right loading spinner shown", () => + AddUntilStep("right loading spinner shown", () => resultsScreen.ChildrenOfType().Single(l => l.Anchor == Anchor.CentreRight).State.Value == Visibility.Visible); waitForDisplay(); AddAssert($"count increased by {scores_per_result}", () => this.ChildrenOfType().Count() == beforePanelCount + scores_per_result); - AddAssert("right loading spinner hidden", () => + AddUntilStep("right loading spinner hidden", () => resultsScreen.ChildrenOfType().Single(l => l.Anchor == Anchor.CentreRight).State.Value == Visibility.Hidden); AddStep("get panel count", () => beforePanelCount = this.ChildrenOfType().Count()); AddStep("bind delayed handler with no scores", () => bindHandler(delayed: true, noScores: true)); AddStep("scroll to right", () => resultsScreen.ChildrenOfType().Single().ChildrenOfType().Single().ScrollToEnd(false)); - AddAssert("right loading spinner shown", () => + AddUntilStep("right loading spinner shown", () => resultsScreen.ChildrenOfType().Single(l => l.Anchor == Anchor.CentreRight).State.Value == Visibility.Visible); waitForDisplay(); AddAssert("count not increased", () => this.ChildrenOfType().Count() == beforePanelCount); - AddAssert("right loading spinner hidden", () => + AddUntilStep("right loading spinner hidden", () => resultsScreen.ChildrenOfType().Single(l => l.Anchor == Anchor.CentreRight).State.Value == Visibility.Hidden); AddAssert("no placeholders shown", () => this.ChildrenOfType().Count(), () => Is.Zero); @@ -222,13 +222,13 @@ namespace osu.Game.Tests.Visual.Playlists AddStep("get panel count", () => beforePanelCount = this.ChildrenOfType().Count()); AddStep("scroll to left", () => resultsScreen.ChildrenOfType().Single().ChildrenOfType().Single().ScrollToStart(false)); - AddAssert("left loading spinner shown", () => + AddUntilStep("left loading spinner shown", () => resultsScreen.ChildrenOfType().Single(l => l.Anchor == Anchor.CentreLeft).State.Value == Visibility.Visible); waitForDisplay(); AddAssert($"count increased by {scores_per_result}", () => this.ChildrenOfType().Count() == beforePanelCount + scores_per_result); - AddAssert("left loading spinner hidden", () => + AddUntilStep("left loading spinner hidden", () => resultsScreen.ChildrenOfType().Single(l => l.Anchor == Anchor.CentreLeft).State.Value == Visibility.Hidden); } } @@ -242,7 +242,7 @@ namespace osu.Game.Tests.Visual.Playlists AddStep("bind user score info handler", () => bindHandler(noScores: true)); createUserBestResults(); AddAssert("no scores visible", () => !resultsScreen.ChildrenOfType().Single().GetScorePanels().Any()); - AddAssert("placeholder shown", () => this.ChildrenOfType().Count(), () => Is.EqualTo(1)); + AddUntilStep("placeholder shown", () => this.ChildrenOfType().Count(), () => Is.EqualTo(1)); } [Test] @@ -261,12 +261,12 @@ namespace osu.Game.Tests.Visual.Playlists AddStep("simulate user falling down ranking", () => userScore.Position += 2); AddStep("scroll to left", () => resultsScreen.ChildrenOfType().Single().ChildrenOfType().Single().ScrollToStart(false)); - AddAssert("left loading spinner shown", () => + AddUntilStep("left loading spinner shown", () => resultsScreen.ChildrenOfType().Single(l => l.Anchor == Anchor.CentreLeft).State.Value == Visibility.Visible); waitForDisplay(); - AddAssert("left loading spinner hidden", () => + AddUntilStep("left loading spinner hidden", () => resultsScreen.ChildrenOfType().Single(l => l.Anchor == Anchor.CentreLeft).State.Value == Visibility.Hidden); } From 975f4e4c7df982ff2762b0223b2ef51c19d2070e Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 6 Mar 2025 15:45:11 +0900 Subject: [PATCH 1223/1275] Simplify code and don't set title if already correct --- osu.Game/OsuGame.cs | 26 +++++++++----------------- 1 file changed, 9 insertions(+), 17 deletions(-) diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs index ed71f357a5..4a9154f14b 100644 --- a/osu.Game/OsuGame.cs +++ b/osu.Game/OsuGame.cs @@ -421,7 +421,7 @@ namespace osu.Game SelectedMods.BindValueChanged(modsChanged); Beatmap.BindValueChanged(beatmapChanged, true); - configUserActivity.BindValueChanged(userActivityChanged); + configUserActivity.BindValueChanged(_ => updateWindowTitle()); applySafeAreaConsiderations = LocalConfig.GetBindable(OsuSetting.SafeAreaConsiderations); applySafeAreaConsiderations.BindValueChanged(apply => SafeAreaContainer.SafeAreaOverrideEdges = apply.NewValue ? SafeAreaOverrideEdges : Edges.All, true); @@ -832,26 +832,19 @@ namespace osu.Game updateWindowTitle(); } - private void userActivityChanged(ValueChangedEvent userActivity) - { - updateWindowTitle(); - } - private void updateWindowTitle() { if (Host.Window == null) return; - if (Beatmap.Value?.BeatmapSetInfo?.Protected != false || Beatmap.Value is DummyWorkingBeatmap) - { - Host.Window.Title = Name; - return; - } - - string newTitle = Name; + string newTitle; switch (configUserActivity.Value) { + default: + newTitle = Name; + break; + case UserActivity.InGame: case UserActivity.TestingBeatmap: case UserActivity.WatchingReplay: @@ -859,13 +852,12 @@ namespace osu.Game break; case UserActivity.EditingBeatmap: - if (Beatmap.Value.BeatmapInfo.Path != null) - newTitle = $"{Name} - {Beatmap.Value.BeatmapInfo.Path}"; - + newTitle = $"{Name} - {Beatmap.Value.BeatmapInfo.Path ?? "new beatmap"}"; break; } - Host.Window.Title = newTitle; + if (newTitle != Host.Window.Title) + Host.Window.Title = newTitle; } private void modsChanged(ValueChangedEvent> mods) From bdd2808fb598360fd710520f92eb5e107ff97cea Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 6 Mar 2025 16:05:51 +0900 Subject: [PATCH 1224/1275] Bump difficulty calculator versions in preparation for release --- osu.Game.Rulesets.Catch/Difficulty/CatchDifficultyCalculator.cs | 2 +- osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs | 2 +- osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Game.Rulesets.Catch/Difficulty/CatchDifficultyCalculator.cs b/osu.Game.Rulesets.Catch/Difficulty/CatchDifficultyCalculator.cs index 6434adb63c..14a8ff31c5 100644 --- a/osu.Game.Rulesets.Catch/Difficulty/CatchDifficultyCalculator.cs +++ b/osu.Game.Rulesets.Catch/Difficulty/CatchDifficultyCalculator.cs @@ -24,7 +24,7 @@ namespace osu.Game.Rulesets.Catch.Difficulty private float halfCatcherWidth; - public override int Version => 20220701; + public override int Version => 20250306; public CatchDifficultyCalculator(IRulesetInfo ruleset, IWorkingBeatmap beatmap) : base(ruleset, beatmap) diff --git a/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs b/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs index 30339fbaa7..eb2cb95972 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs @@ -22,7 +22,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty { private const double difficulty_multiplier = 0.0675; - public override int Version => 20241007; + public override int Version => 20250306; public OsuDifficultyCalculator(IRulesetInfo ruleset, IWorkingBeatmap beatmap) : base(ruleset, beatmap) diff --git a/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs b/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs index 7bc050d2df..e0bc0e177c 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs @@ -33,7 +33,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty private bool isConvert; - public override int Version => 20241007; + public override int Version => 20250306; public TaikoDifficultyCalculator(IRulesetInfo ruleset, IWorkingBeatmap beatmap) : base(ruleset, beatmap) From 0f0dd58b698df3f30baca8988fac285c5c87401a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 6 Mar 2025 09:45:44 +0100 Subject: [PATCH 1225/1275] Fix differential submission process crashing when no files have changed Closes https://github.com/ppy/osu/issues/32247. --- osu.Game/Screens/Edit/Submission/BeatmapSubmissionScreen.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/Edit/Submission/BeatmapSubmissionScreen.cs b/osu.Game/Screens/Edit/Submission/BeatmapSubmissionScreen.cs index 13981bcb69..2ea710d3ab 100644 --- a/osu.Game/Screens/Edit/Submission/BeatmapSubmissionScreen.cs +++ b/osu.Game/Screens/Edit/Submission/BeatmapSubmissionScreen.cs @@ -304,7 +304,7 @@ namespace osu.Game.Screens.Edit.Submission Logger.Log($"Beatmap submission failed on upload: {ex}"); allowExit(); }; - patchRequest.Progressed += (current, total) => uploadStep.SetInProgress((float)current / total); + patchRequest.Progressed += (current, total) => uploadStep.SetInProgress(total > 0 ? (float)current / total : null); api.Queue(patchRequest); uploadStep.SetInProgress(); From 64830e2c31dba046b8753b13aed37fa9596ef413 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 6 Mar 2025 18:51:32 +0900 Subject: [PATCH 1226/1275] Use localisation --- osu.Game/Screens/Edit/Editor.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/Edit/Editor.cs b/osu.Game/Screens/Edit/Editor.cs index bf254093b3..80e1a656de 100644 --- a/osu.Game/Screens/Edit/Editor.cs +++ b/osu.Game/Screens/Edit/Editor.cs @@ -392,7 +392,7 @@ namespace osu.Game.Screens.Edit { undoMenuItem = new EditorMenuItem(CommonStrings.Undo, MenuItemType.Standard, Undo) { Hotkey = new Hotkey(PlatformAction.Undo) }, redoMenuItem = new EditorMenuItem(CommonStrings.Redo, MenuItemType.Standard, Redo) { Hotkey = new Hotkey(PlatformAction.Redo) }, - discardChangesMenuItem = new EditorMenuItem("Discard unsaved changes", MenuItemType.Destructive, DiscardUnsavedChanges) + discardChangesMenuItem = new EditorMenuItem(GlobalActionKeyBindingStrings.EditorDiscardUnsavedChanges, MenuItemType.Destructive, DiscardUnsavedChanges) { Hotkey = new Hotkey(GlobalAction.EditorDiscardUnsavedChanges) }, From 6e387761307fe5c03b83c5551f8286a974b4fae4 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Thu, 6 Mar 2025 18:55:40 +0900 Subject: [PATCH 1227/1275] Fix initial multiplayer room items not having freestyle --- osu.Game/Online/Rooms/MultiplayerPlaylistItem.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/osu.Game/Online/Rooms/MultiplayerPlaylistItem.cs b/osu.Game/Online/Rooms/MultiplayerPlaylistItem.cs index 4c4a3d97f2..3234e28166 100644 --- a/osu.Game/Online/Rooms/MultiplayerPlaylistItem.cs +++ b/osu.Game/Online/Rooms/MultiplayerPlaylistItem.cs @@ -80,6 +80,7 @@ namespace osu.Game.Online.Rooms PlaylistOrder = item.PlaylistOrder ?? 0; PlayedAt = item.PlayedAt; StarRating = item.Beatmap.StarRating; + Freestyle = item.Freestyle; } } } From d9b7d034ba34e587189adfb5a9c8930b5e1ef8ce Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 6 Mar 2025 19:34:20 +0900 Subject: [PATCH 1228/1275] Move to file menu --- osu.Game/Screens/Edit/Editor.cs | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/osu.Game/Screens/Edit/Editor.cs b/osu.Game/Screens/Edit/Editor.cs index 80e1a656de..f56380a34d 100644 --- a/osu.Game/Screens/Edit/Editor.cs +++ b/osu.Game/Screens/Edit/Editor.cs @@ -392,10 +392,6 @@ namespace osu.Game.Screens.Edit { undoMenuItem = new EditorMenuItem(CommonStrings.Undo, MenuItemType.Standard, Undo) { Hotkey = new Hotkey(PlatformAction.Undo) }, redoMenuItem = new EditorMenuItem(CommonStrings.Redo, MenuItemType.Standard, Redo) { Hotkey = new Hotkey(PlatformAction.Redo) }, - discardChangesMenuItem = new EditorMenuItem(GlobalActionKeyBindingStrings.EditorDiscardUnsavedChanges, MenuItemType.Destructive, DiscardUnsavedChanges) - { - Hotkey = new Hotkey(GlobalAction.EditorDiscardUnsavedChanges) - }, new OsuMenuItemSpacer(), cutMenuItem = new EditorMenuItem(CommonStrings.Cut, MenuItemType.Standard, Cut) { Hotkey = new Hotkey(PlatformAction.Cut) }, copyMenuItem = new EditorMenuItem(CommonStrings.Copy, MenuItemType.Standard, Copy) { Hotkey = new Hotkey(PlatformAction.Copy) }, @@ -1273,6 +1269,11 @@ namespace osu.Game.Screens.Edit saveRelatedMenuItems.Add(save); yield return save; + yield return discardChangesMenuItem = new EditorMenuItem(GlobalActionKeyBindingStrings.EditorDiscardUnsavedChanges, MenuItemType.Destructive, DiscardUnsavedChanges) + { + Hotkey = new Hotkey(GlobalAction.EditorDiscardUnsavedChanges) + }; + if (RuntimeInfo.OS != RuntimeInfo.Platform.Android) { var export = createExportMenu(); From e39b551b484ea6689ebf581e58a9324361bb894f Mon Sep 17 00:00:00 2001 From: GAMIS65 Date: Thu, 6 Mar 2025 22:48:08 +0100 Subject: [PATCH 1229/1275] Use localisation from osu web for the report button --- osu.Game/Overlays/Chat/DrawableChatUsername.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/osu.Game/Overlays/Chat/DrawableChatUsername.cs b/osu.Game/Overlays/Chat/DrawableChatUsername.cs index 67191f6836..9cdad507a6 100644 --- a/osu.Game/Overlays/Chat/DrawableChatUsername.cs +++ b/osu.Game/Overlays/Chat/DrawableChatUsername.cs @@ -26,6 +26,7 @@ using osu.Game.Resources.Localisation.Web; using osuTK; using osuTK.Graphics; using ChatStrings = osu.Game.Localisation.ChatStrings; +using WebUsersStrings = osu.Game.Resources.Localisation.Web.UsersStrings; namespace osu.Game.Overlays.Chat { @@ -178,7 +179,7 @@ namespace osu.Game.Overlays.Chat } if (!user.Equals(api.LocalUser.Value)) - items.Add(new OsuMenuItem("Report", MenuItemType.Destructive, ReportRequested)); + items.Add(new OsuMenuItem(WebUsersStrings.ReportButtonText, MenuItemType.Destructive, ReportRequested)); return items.ToArray(); } From 81d35a7ebfc7a7b144112e62cf4d899522c6a9e5 Mon Sep 17 00:00:00 2001 From: GAMIS65 Date: Thu, 6 Mar 2025 23:02:22 +0100 Subject: [PATCH 1230/1275] Use UsersStrings instead --- osu.Game/Overlays/Chat/DrawableChatUsername.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/osu.Game/Overlays/Chat/DrawableChatUsername.cs b/osu.Game/Overlays/Chat/DrawableChatUsername.cs index 9cdad507a6..83f67d1a8a 100644 --- a/osu.Game/Overlays/Chat/DrawableChatUsername.cs +++ b/osu.Game/Overlays/Chat/DrawableChatUsername.cs @@ -26,7 +26,6 @@ using osu.Game.Resources.Localisation.Web; using osuTK; using osuTK.Graphics; using ChatStrings = osu.Game.Localisation.ChatStrings; -using WebUsersStrings = osu.Game.Resources.Localisation.Web.UsersStrings; namespace osu.Game.Overlays.Chat { @@ -179,7 +178,7 @@ namespace osu.Game.Overlays.Chat } if (!user.Equals(api.LocalUser.Value)) - items.Add(new OsuMenuItem(WebUsersStrings.ReportButtonText, MenuItemType.Destructive, ReportRequested)); + items.Add(new OsuMenuItem(UsersStrings.ReportButtonText, MenuItemType.Destructive, ReportRequested)); return items.ToArray(); } From 18aa168a00f1bdfb019844e5e024a3b0d606dac3 Mon Sep 17 00:00:00 2001 From: Jamie Taylor Date: Fri, 7 Mar 2025 15:45:27 +0900 Subject: [PATCH 1231/1275] Allow kiai/star-fountain SFX to be skinnable --- osu.Game/Screens/Menu/KiaiMenuFountains.cs | 12 ++++++++---- osu.Game/Screens/Play/KiaiGameplayFountains.cs | 12 ++++++++---- 2 files changed, 16 insertions(+), 8 deletions(-) diff --git a/osu.Game/Screens/Menu/KiaiMenuFountains.cs b/osu.Game/Screens/Menu/KiaiMenuFountains.cs index dbbff4a9f5..b103d9e573 100644 --- a/osu.Game/Screens/Menu/KiaiMenuFountains.cs +++ b/osu.Game/Screens/Menu/KiaiMenuFountains.cs @@ -3,11 +3,12 @@ using System; using osu.Framework.Allocation; -using osu.Framework.Audio; using osu.Framework.Audio.Sample; using osu.Framework.Graphics; using osu.Framework.Utils; +using osu.Game.Audio; using osu.Game.Graphics.Containers; +using osu.Game.Skinning; namespace osu.Game.Screens.Menu { @@ -16,11 +17,14 @@ namespace osu.Game.Screens.Menu private StarFountain leftFountain = null!; private StarFountain rightFountain = null!; - private Sample? sample; + [Resolved] + private ISkinSource skin { get; set; } = null!; + + private ISample? sample; private SampleChannel? sampleChannel; [BackgroundDependencyLoader] - private void load(AudioManager audio) + private void load() { RelativeSizeAxes = Axes.Both; @@ -40,7 +44,7 @@ namespace osu.Game.Screens.Menu }, }; - sample = audio.Samples.Get(@"Gameplay/fountain-shoot"); + sample = skin.GetSample(new SampleInfo(@"Gameplay/fountain-shoot")); } private bool isTriggered; diff --git a/osu.Game/Screens/Play/KiaiGameplayFountains.cs b/osu.Game/Screens/Play/KiaiGameplayFountains.cs index 7e09f50133..c8dcee2580 100644 --- a/osu.Game/Screens/Play/KiaiGameplayFountains.cs +++ b/osu.Game/Screens/Play/KiaiGameplayFountains.cs @@ -3,14 +3,15 @@ using System; using osu.Framework.Allocation; -using osu.Framework.Audio; using osu.Framework.Audio.Sample; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Utils; +using osu.Game.Audio; using osu.Game.Configuration; using osu.Game.Graphics.Containers; using osu.Game.Screens.Menu; +using osu.Game.Skinning; namespace osu.Game.Screens.Play { @@ -21,11 +22,14 @@ namespace osu.Game.Screens.Play private Bindable kiaiStarFountains = null!; - private Sample? sample; + [Resolved] + private ISkinSource skin { get; set; } = null!; + + private ISample? sample; private SampleChannel? sampleChannel; [BackgroundDependencyLoader] - private void load(OsuConfigManager config, AudioManager audio) + private void load(OsuConfigManager config) { kiaiStarFountains = config.GetBindable(OsuSetting.StarFountains); @@ -47,7 +51,7 @@ namespace osu.Game.Screens.Play }, }; - sample = audio.Samples.Get(@"Gameplay/fountain-shoot"); + sample = skin.GetSample(new SampleInfo(@"Gameplay/fountain-shoot")); } private bool isTriggered; From efe1089003c8f1c35c33b9364403b90c13413b76 Mon Sep 17 00:00:00 2001 From: Jamie Taylor Date: Fri, 7 Mar 2025 15:46:10 +0900 Subject: [PATCH 1232/1275] Don't play kiai sfx when game is in background --- osu.Game/Screens/Menu/KiaiMenuFountains.cs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/osu.Game/Screens/Menu/KiaiMenuFountains.cs b/osu.Game/Screens/Menu/KiaiMenuFountains.cs index b103d9e573..e62ef31278 100644 --- a/osu.Game/Screens/Menu/KiaiMenuFountains.cs +++ b/osu.Game/Screens/Menu/KiaiMenuFountains.cs @@ -5,6 +5,7 @@ using System; using osu.Framework.Allocation; using osu.Framework.Audio.Sample; using osu.Framework.Graphics; +using osu.Framework.Platform; using osu.Framework.Utils; using osu.Game.Audio; using osu.Game.Graphics.Containers; @@ -17,6 +18,9 @@ namespace osu.Game.Screens.Menu private StarFountain leftFountain = null!; private StarFountain rightFountain = null!; + [Resolved] + private GameHost host { get; set; } = null!; + [Resolved] private ISkinSource skin { get; set; } = null!; @@ -85,6 +89,9 @@ namespace osu.Game.Screens.Menu break; } + // Don't play SFX when game is in background + if (!host.IsActive.Value) return; + // Track sample channel to avoid overlapping playback sampleChannel?.Stop(); sampleChannel = sample?.GetChannel(); From 33dccfcec8f9db04b6d098a64ca28048bce2cf0a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 7 Mar 2025 08:51:55 +0100 Subject: [PATCH 1233/1275] Add visual test coverage --- .../Visual/SongSelect/TestSceneBeatmapLeaderboard.cs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapLeaderboard.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapLeaderboard.cs index bfb835cad1..62ca8bf831 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapLeaderboard.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapLeaderboard.cs @@ -181,6 +181,11 @@ namespace osu.Game.Tests.Visual.SongSelect { AddStep(@"Set scope", () => leaderboard.Scope = BeatmapLeaderboardScope.Global); AddStep(@"New Scores", () => leaderboard.SetScores(generateSampleScores(new BeatmapInfo()))); + AddStep(@"New Scores with teams", () => leaderboard.SetScores(generateSampleScores(new BeatmapInfo()).Select(s => + { + s.User.Team = new APITeam(); + return s; + }))); } [Test] @@ -473,7 +478,7 @@ namespace osu.Game.Tests.Visual.SongSelect Accuracy = 0.5140, MaxCombo = 244, TotalScore = 1707827, - Date = DateTime.Now.AddMonths(-3), + Date = DateTime.Now.AddMonths(-10), Mods = new Mod[] { new OsuModHidden(), new OsuModHardRock(), }, BeatmapInfo = beatmapInfo, BeatmapHash = beatmapInfo.Hash, From 4acdd3365aeac6570b1a420aed34877a028a6ca5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 7 Mar 2025 08:55:40 +0100 Subject: [PATCH 1234/1275] Fix leaderboard date text being cut off sometimes Closes https://github.com/ppy/osu/issues/32256. --- osu.Game/Online/Leaderboards/LeaderboardScore.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Online/Leaderboards/LeaderboardScore.cs b/osu.Game/Online/Leaderboards/LeaderboardScore.cs index 28b20c0c05..fb5bb225c0 100644 --- a/osu.Game/Online/Leaderboards/LeaderboardScore.cs +++ b/osu.Game/Online/Leaderboards/LeaderboardScore.cs @@ -190,7 +190,7 @@ namespace osu.Game.Online.Leaderboards RelativeSizeAxes = Axes.Y, Direction = FillDirection.Horizontal, Spacing = new Vector2(5f, 0f), - Width = 114f, + Width = 130f, Masking = true, Children = new Drawable[] { From 6d22502739bf5b69a26692da0a996eac90032a8c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 7 Mar 2025 09:20:50 +0100 Subject: [PATCH 1235/1275] Fix precise movement popover crashing if selection bounding box exceeds playfield size Closes https://github.com/ppy/osu/issues/32252. --- .../Edit/PreciseMovementPopover.cs | 25 ++++++++++++++++--- 1 file changed, 21 insertions(+), 4 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Edit/PreciseMovementPopover.cs b/osu.Game.Rulesets.Osu/Edit/PreciseMovementPopover.cs index f2cb8794b5..04d6afc925 100644 --- a/osu.Game.Rulesets.Osu/Edit/PreciseMovementPopover.cs +++ b/osu.Game.Rulesets.Osu/Edit/PreciseMovementPopover.cs @@ -1,6 +1,7 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System; using System.Collections.Generic; using System.Diagnostics; using System.Linq; @@ -127,8 +128,11 @@ namespace osu.Game.Rulesets.Osu.Edit if (relativeCheckbox.Current.Value) { - (xBindable.MinValue, xBindable.MaxValue) = (0 - initialSurroundingQuad.TopLeft.X, OsuPlayfield.BASE_SIZE.X - initialSurroundingQuad.BottomRight.X); - (yBindable.MinValue, yBindable.MaxValue) = (0 - initialSurroundingQuad.TopLeft.Y, OsuPlayfield.BASE_SIZE.Y - initialSurroundingQuad.BottomRight.Y); + xBindable.MinValue = 0 - Math.Max(initialSurroundingQuad.TopLeft.X, 0); + xBindable.MaxValue = OsuPlayfield.BASE_SIZE.X - Math.Min(initialSurroundingQuad.BottomRight.X, OsuPlayfield.BASE_SIZE.X); + + yBindable.MinValue = 0 - Math.Max(initialSurroundingQuad.TopLeft.Y, 0); + yBindable.MaxValue = OsuPlayfield.BASE_SIZE.Y - Math.Min(initialSurroundingQuad.BottomRight.Y, OsuPlayfield.BASE_SIZE.Y); xBindable.Default = yBindable.Default = 0; @@ -146,8 +150,21 @@ namespace osu.Game.Rulesets.Osu.Edit var quadRelativeToPosition = new RectangleF(initialSurroundingQuad.Location - initialPosition, initialSurroundingQuad.Size); - (xBindable.MinValue, xBindable.MaxValue) = (0 - quadRelativeToPosition.TopLeft.X, OsuPlayfield.BASE_SIZE.X - quadRelativeToPosition.BottomRight.X); - (yBindable.MinValue, yBindable.MaxValue) = (0 - quadRelativeToPosition.TopLeft.Y, OsuPlayfield.BASE_SIZE.Y - quadRelativeToPosition.BottomRight.Y); + if (initialSurroundingQuad.Width < OsuPlayfield.BASE_SIZE.X) + { + xBindable.MinValue = 0 - quadRelativeToPosition.TopLeft.X; + xBindable.MaxValue = OsuPlayfield.BASE_SIZE.X - quadRelativeToPosition.BottomRight.X; + } + else + xBindable.MinValue = xBindable.MaxValue = initialPosition.X; + + if (initialSurroundingQuad.Height < OsuPlayfield.BASE_SIZE.Y) + { + yBindable.MinValue = 0 - quadRelativeToPosition.TopLeft.Y; + yBindable.MaxValue = OsuPlayfield.BASE_SIZE.Y - quadRelativeToPosition.BottomRight.Y; + } + else + yBindable.MinValue = yBindable.MaxValue = initialPosition.Y; xBindable.Default = initialPosition.X; yBindable.Default = initialPosition.Y; From 12fa96de252f5d1b186fd7666570dd6c5f21b901 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 7 Mar 2025 10:43:22 +0100 Subject: [PATCH 1236/1275] Ensure that star rating reprocessing does not incur online lookup requests Yesterday after the lazer release there was a bit of a spike in the number of osu-web requests pointed at `/api/v2/beatmaps/lookup` specifically. The most likely reason for this is that prior to this commit, the star rating recalculation was fully performed by the `BeatmapUpdater.Process()` flow. This process does full metadata lookups, and while it *will* attempt to use the local `online.db` metadata cache, it *will* also fall back to API requests if the local metadata fetch fails. While that means that the local cache likely saved us from a doomsday scenario here, it *also* is the case that all of that metadata lookup stuff is *entirely unnecessary* when wanting to just update star ratings. Therefore, this splits out only the part relevant to star ratings as a separate background process, so that it can run completely locally. --- .../Database/BackgroundDataStoreProcessor.cs | 88 ++++++++++++++++--- 1 file changed, 75 insertions(+), 13 deletions(-) diff --git a/osu.Game/Database/BackgroundDataStoreProcessor.cs b/osu.Game/Database/BackgroundDataStoreProcessor.cs index 1512b6be93..5053ab9a4c 100644 --- a/osu.Game/Database/BackgroundDataStoreProcessor.cs +++ b/osu.Game/Database/BackgroundDataStoreProcessor.cs @@ -76,8 +76,9 @@ namespace osu.Game.Database { Logger.Log("Beginning background data store processing.."); - checkForOutdatedStarRatings(); - processBeatmapSetsWithMissingMetrics(); + clearOutdatedStarRatings(); + populateMissingStarRatings(); + processOnlineBeatmapSetsWithNoUpdate(); // Note that the previous method will also update these on a fresh run. processBeatmapsWithMissingObjectCounts(); processScoresWithMissingStatistics(); @@ -100,7 +101,7 @@ namespace osu.Game.Database /// Check whether the databased difficulty calculation version matches the latest ruleset provided version. /// If it doesn't, clear out any existing difficulties so they can be incrementally recalculated. /// - private void checkForOutdatedStarRatings() + private void clearOutdatedStarRatings() { foreach (var ruleset in rulesetStore.AvailableRulesets) { @@ -132,7 +133,74 @@ namespace osu.Game.Database } } - private void processBeatmapSetsWithMissingMetrics() + /// + /// This is split out from as a separate process to prevent high server-side load + /// from the firing online requests as part of the update. + /// Star rating recalculations can be ran strictly locally. + /// + private void populateMissingStarRatings() + { + HashSet beatmapIds = new HashSet(); + + Logger.Log("Querying for beatmaps with missing star ratings..."); + + realmAccess.Run(r => + { + foreach (var b in r.All().Where(b => b.StarRating < 0 && b.BeatmapSet != null)) + beatmapIds.Add(b.ID); + }); + + if (beatmapIds.Count == 0) + return; + + Logger.Log($"Found {beatmapIds.Count} beatmaps which require star rating reprocessing."); + + var notification = showProgressNotification(beatmapIds.Count, "Reprocessing star rating for beatmaps", "beatmaps' star ratings have been updated"); + + int processedCount = 0; + int failedCount = 0; + + foreach (var id in beatmapIds) + { + if (notification?.State == ProgressNotificationState.Cancelled) + break; + + updateNotificationProgress(notification, processedCount, beatmapIds.Count); + + sleepIfRequired(); + + realmAccess.Write(r => + { + var beatmap = r.Find(id); + + if (beatmap == null) + return; + + try + { + var working = beatmapManager.GetWorkingBeatmap(beatmap); + var ruleset = working.BeatmapInfo.Ruleset.CreateInstance(); + + Debug.Assert(ruleset != null); + + var calculator = ruleset.CreateDifficultyCalculator(working); + + beatmap.StarRating = calculator.Calculate().StarRating; + ((IWorkingBeatmapCache)beatmapManager).Invalidate(beatmap); + ++processedCount; + } + catch (Exception e) + { + Logger.Log($"Background processing failed on {beatmap}: {e}"); + ++failedCount; + } + }); + } + + completeNotification(notification, processedCount, beatmapIds.Count, failedCount); + } + + private void processOnlineBeatmapSetsWithNoUpdate() { HashSet beatmapSetIds = new HashSet(); @@ -148,12 +216,7 @@ namespace osu.Game.Database // of other possible ways), but for now avoid queueing if the user isn't logged in at startup. if (api.IsLoggedIn) { - foreach (var b in r.All().Where(b => (b.StarRating < 0 || (b.OnlineID > 0 && b.LastOnlineUpdate == null)) && b.BeatmapSet != null)) - beatmapSetIds.Add(b.BeatmapSet!.ID); - } - else - { - foreach (var b in r.All().Where(b => b.StarRating < 0 && b.BeatmapSet != null)) + foreach (var b in r.All().Where(b => b.OnlineID > 0 && b.LastOnlineUpdate == null && b.BeatmapSet != null)) beatmapSetIds.Add(b.BeatmapSet!.ID); } }); @@ -161,10 +224,9 @@ namespace osu.Game.Database if (beatmapSetIds.Count == 0) return; - Logger.Log($"Found {beatmapSetIds.Count} beatmap sets which require reprocessing."); + Logger.Log($"Found {beatmapSetIds.Count} beatmap sets which require online updates."); - // Technically this is doing more than just star ratings, but easier for the end user to understand. - var notification = showProgressNotification(beatmapSetIds.Count, "Reprocessing star rating for beatmaps", "beatmaps' star ratings have been updated"); + var notification = showProgressNotification(beatmapSetIds.Count, "Updating online data for beatmaps", "beatmaps' online data have been updated"); int processedCount = 0; int failedCount = 0; From 1a7774cd196a725cc98e154fe73fbb80e29605a3 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 7 Mar 2025 21:33:27 +0900 Subject: [PATCH 1237/1275] Update resources --- osu.Game/osu.Game.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj index e35eaf5645..1fe29f2a21 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -36,7 +36,7 @@ - + From 2272ca1ae5420f020cf158d5de9418b9cea249fe Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sat, 8 Mar 2025 22:18:08 +0900 Subject: [PATCH 1238/1275] Fix namespace --- osu.Game/Online/API/Requests/RemoveBeatmapTagRequest.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Online/API/Requests/RemoveBeatmapTagRequest.cs b/osu.Game/Online/API/Requests/RemoveBeatmapTagRequest.cs index 8090dd2cb0..4ac00e28f4 100644 --- a/osu.Game/Online/API/Requests/RemoveBeatmapTagRequest.cs +++ b/osu.Game/Online/API/Requests/RemoveBeatmapTagRequest.cs @@ -4,7 +4,7 @@ using System.Net.Http; using osu.Framework.IO.Network; -namespace osu.Game.Online.API.Requests.Responses +namespace osu.Game.Online.API.Requests { public class RemoveBeatmapTagRequest : APIRequest { From f845ea19b5dd16369fc1f14bb7f23b759112fc1f Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Sun, 9 Mar 2025 09:57:04 +0900 Subject: [PATCH 1239/1275] Fix initial multiplayer room settings not applied --- .../Match/MultiplayerMatchSettingsOverlay.cs | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerMatchSettingsOverlay.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerMatchSettingsOverlay.cs index f74de26f1f..42d240c60e 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerMatchSettingsOverlay.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerMatchSettingsOverlay.cs @@ -365,8 +365,6 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match updateRoomMaxParticipants(); updateRoomAutoStartDuration(); updateRoomPlaylist(); - - drawablePlaylist.Items.BindCollectionChanged((_, __) => room.Playlist = drawablePlaylist.Items.ToArray()); } private void onRoomPropertyChanged(object? sender, PropertyChangedEventArgs e) @@ -470,6 +468,14 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match } else { + room.Name = NameField.Text; + room.Password = PasswordTextBox.Text; + room.Type = TypePicker.Current.Value; + room.QueueMode = QueueModeDropdown.Current.Value; + room.AutoStartDuration = TimeSpan.FromSeconds((int)startModeDropdown.Current.Value); + room.AutoSkip = AutoSkipCheckbox.Current.Value; + room.Playlist = drawablePlaylist.Items.ToArray(); + client.CreateRoom(room).ContinueWith(t => Schedule(() => { if (t.IsCompletedSuccessfully) @@ -505,10 +511,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match const string not_found_prefix = "beatmaps not found:"; if (message.StartsWith(not_found_prefix, StringComparison.Ordinal)) - { ErrorText.Text = "The selected beatmap is not available online."; - room.Playlist.SingleOrDefault()?.MarkInvalid(); - } else ErrorText.Text = message; From 7fdadbd852ef2dbb3e7a0b09315d2d485414a5e9 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Sun, 9 Mar 2025 10:16:28 +0900 Subject: [PATCH 1240/1275] Fix error message on invalid room password --- .../Multiplayer/InvalidPasswordException.cs | 4 ++++ .../Multiplayer/MultiplayerLoungeSubScreen.cs | 15 ++++++++++----- 2 files changed, 14 insertions(+), 5 deletions(-) diff --git a/osu.Game/Online/Multiplayer/InvalidPasswordException.cs b/osu.Game/Online/Multiplayer/InvalidPasswordException.cs index b76a1cc05d..860fb90258 100644 --- a/osu.Game/Online/Multiplayer/InvalidPasswordException.cs +++ b/osu.Game/Online/Multiplayer/InvalidPasswordException.cs @@ -9,5 +9,9 @@ namespace osu.Game.Online.Multiplayer [Serializable] public class InvalidPasswordException : HubException { + public InvalidPasswordException() + : base("Invalid password") + { + } } } diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerLoungeSubScreen.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerLoungeSubScreen.cs index 93552670e9..54aa2003fa 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerLoungeSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerLoungeSubScreen.cs @@ -84,12 +84,17 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer onSuccess(room); else { - const string message = "Failed to join multiplayer room."; + Exception? exception = result.Exception?.AsSingular(); - if (result.Exception != null) - Logger.Error(result.Exception, message); - - onFailure.Invoke(result.Exception?.AsSingular().Message ?? message); + if (exception?.GetHubExceptionMessage() is string message) + onFailure(message); + else + { + const string generic_failure_message = "Failed to join multiplayer room."; + if (result.Exception != null) + Logger.Error(result.Exception, generic_failure_message); + onFailure(generic_failure_message); + } } }); } From 0a6c2121536f0dddcfe840a18c3f1126d8f83aca Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sun, 9 Mar 2025 23:47:29 +0900 Subject: [PATCH 1241/1275] Use `SkinnableSound` to ensure samples track active skin --- osu.Game/Screens/Menu/KiaiMenuFountains.cs | 22 +++++-------------- .../Screens/Play/KiaiGameplayFountains.cs | 17 ++++---------- 2 files changed, 10 insertions(+), 29 deletions(-) diff --git a/osu.Game/Screens/Menu/KiaiMenuFountains.cs b/osu.Game/Screens/Menu/KiaiMenuFountains.cs index e62ef31278..b57012eaf7 100644 --- a/osu.Game/Screens/Menu/KiaiMenuFountains.cs +++ b/osu.Game/Screens/Menu/KiaiMenuFountains.cs @@ -3,7 +3,6 @@ using System; using osu.Framework.Allocation; -using osu.Framework.Audio.Sample; using osu.Framework.Graphics; using osu.Framework.Platform; using osu.Framework.Utils; @@ -21,18 +20,14 @@ namespace osu.Game.Screens.Menu [Resolved] private GameHost host { get; set; } = null!; - [Resolved] - private ISkinSource skin { get; set; } = null!; - - private ISample? sample; - private SampleChannel? sampleChannel; + private SkinnableSound? sample; [BackgroundDependencyLoader] private void load() { RelativeSizeAxes = Axes.Both; - Children = new[] + Children = new Drawable[] { leftFountain = new StarFountain { @@ -46,9 +41,8 @@ namespace osu.Game.Screens.Menu Origin = Anchor.BottomRight, X = -250, }, + sample = new SkinnableSound(new SampleInfo("Gameplay/fountain-shoot")) }; - - sample = skin.GetSample(new SampleInfo(@"Gameplay/fountain-shoot")); } private bool isTriggered; @@ -89,13 +83,9 @@ namespace osu.Game.Screens.Menu break; } - // Don't play SFX when game is in background - if (!host.IsActive.Value) return; - - // Track sample channel to avoid overlapping playback - sampleChannel?.Stop(); - sampleChannel = sample?.GetChannel(); - sampleChannel?.Play(); + // Don't play SFX when game is in background as it can be a bit noisy. + if (host.IsActive.Value) + sample?.Play(); } } } diff --git a/osu.Game/Screens/Play/KiaiGameplayFountains.cs b/osu.Game/Screens/Play/KiaiGameplayFountains.cs index c8dcee2580..017e66253f 100644 --- a/osu.Game/Screens/Play/KiaiGameplayFountains.cs +++ b/osu.Game/Screens/Play/KiaiGameplayFountains.cs @@ -3,7 +3,6 @@ using System; using osu.Framework.Allocation; -using osu.Framework.Audio.Sample; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Utils; @@ -22,11 +21,7 @@ namespace osu.Game.Screens.Play private Bindable kiaiStarFountains = null!; - [Resolved] - private ISkinSource skin { get; set; } = null!; - - private ISample? sample; - private SampleChannel? sampleChannel; + private SkinnableSound? sample; [BackgroundDependencyLoader] private void load(OsuConfigManager config) @@ -35,7 +30,7 @@ namespace osu.Game.Screens.Play RelativeSizeAxes = Axes.Both; - Children = new[] + Children = new Drawable[] { leftFountain = new GameplayStarFountain { @@ -49,9 +44,8 @@ namespace osu.Game.Screens.Play Origin = Anchor.BottomRight, X = -75, }, + sample = new SkinnableSound(new SampleInfo("Gameplay/fountain-shoot")) }; - - sample = skin.GetSample(new SampleInfo(@"Gameplay/fountain-shoot")); } private bool isTriggered; @@ -78,10 +72,7 @@ namespace osu.Game.Screens.Play leftFountain.Shoot(1); rightFountain.Shoot(-1); - // Track sample channel to avoid overlapping playback - sampleChannel?.Stop(); - sampleChannel = sample?.GetChannel(); - sampleChannel?.Play(); + sample?.Play(); } public partial class GameplayStarFountain : StarFountain From bbd2c33934520e34fd5601b14d1499c3b37daa3d Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 10 Mar 2025 14:45:36 +0900 Subject: [PATCH 1242/1275] Allow grid spacing setting up to 256 pixels Addresses https://github.com/ppy/osu/discussions/29713. I think there's valid uses of this apart from just hiding (ie values between 128 and 256) so let's just get this in. --- osu.Game.Rulesets.Osu/Edit/OsuGridToolboxGroup.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu/Edit/OsuGridToolboxGroup.cs b/osu.Game.Rulesets.Osu/Edit/OsuGridToolboxGroup.cs index 6220fa66b1..991d42c7b4 100644 --- a/osu.Game.Rulesets.Osu/Edit/OsuGridToolboxGroup.cs +++ b/osu.Game.Rulesets.Osu/Edit/OsuGridToolboxGroup.cs @@ -57,7 +57,7 @@ namespace osu.Game.Rulesets.Osu.Edit public BindableFloat Spacing { get; } = new BindableFloat(4f) { MinValue = 4f, - MaxValue = 128f, + MaxValue = 256f, Precision = 0.01f, }; From 3cb32c38adaaf66e401a5265eb234b9e22470c22 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 10 Mar 2025 15:19:34 +0900 Subject: [PATCH 1243/1275] Disable user customisation of spectator list font / colour It's all a bit weird so let's just disable it for now. For instance, this is exposed as "text" font / colour but only affects the header. Also, no other headers are cusotmisable in similar components. --- .../Visual/Gameplay/TestSceneSpectatorList.cs | 4 ++-- osu.Game/Screens/Play/HUD/SpectatorList.cs | 13 ++++--------- osu.Game/Skinning/TrianglesSkin.cs | 2 +- 3 files changed, 7 insertions(+), 12 deletions(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSpectatorList.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSpectatorList.cs index bd1e15d06d..1445e872b5 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneSpectatorList.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSpectatorList.cs @@ -75,8 +75,8 @@ namespace osu.Game.Tests.Visual.Gameplay AddRepeatStep("remove random user", () => ((ISpectatorClient)spectatorClient).UserEndedWatching( spectatorClient.WatchingUsers[RNG.Next(spectatorClient.WatchingUsers.Count)].OnlineID), 5); - AddStep("change font to venera", () => list.Font.Value = Typeface.Venera); - AddStep("change font to torus", () => list.Font.Value = Typeface.Torus); + AddStep("change font to venera", () => list.HeaderFont.Value = Typeface.Venera); + AddStep("change font to torus", () => list.HeaderFont.Value = Typeface.Torus); AddStep("change header colour", () => list.HeaderColour.Value = new Colour4(RNG.NextSingle(), RNG.NextSingle(), RNG.NextSingle(), 1)); AddStep("enter break", () => playingState.Value = LocalUserPlayingState.Break); diff --git a/osu.Game/Screens/Play/HUD/SpectatorList.cs b/osu.Game/Screens/Play/HUD/SpectatorList.cs index 4297c62712..0cc4076313 100644 --- a/osu.Game/Screens/Play/HUD/SpectatorList.cs +++ b/osu.Game/Screens/Play/HUD/SpectatorList.cs @@ -13,12 +13,10 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Colour; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Pooling; -using osu.Game.Configuration; using osu.Game.Graphics; using osu.Game.Graphics.Sprites; -using osu.Game.Online.Chat; using osu.Game.Localisation.HUD; -using osu.Game.Localisation.SkinComponents; +using osu.Game.Online.Chat; using osu.Game.Online.Multiplayer; using osu.Game.Online.Spectator; using osu.Game.Skinning; @@ -31,10 +29,7 @@ namespace osu.Game.Screens.Play.HUD { private const int max_spectators_displayed = 10; - [SettingSource(typeof(SkinnableComponentStrings), nameof(SkinnableComponentStrings.Font), nameof(SkinnableComponentStrings.FontDescription))] - public Bindable Font { get; } = new Bindable(Typeface.Torus); - - [SettingSource(typeof(SkinnableComponentStrings), nameof(SkinnableComponentStrings.TextColour), nameof(SkinnableComponentStrings.TextColourDescription))] + public Bindable HeaderFont { get; } = new Bindable(Typeface.Torus); public BindableColour4 HeaderColour { get; } = new BindableColour4(Colour4.White); private BindableList watchingUsers { get; } = new BindableList(); @@ -97,7 +92,7 @@ namespace osu.Game.Screens.Play.HUD watchingUsers.BindCollectionChanged(onSpectatorsChanged, true); userPlayingState.BindValueChanged(_ => updateVisibility()); - Font.BindValueChanged(_ => updateAppearance()); + HeaderFont.BindValueChanged(_ => updateAppearance()); HeaderColour.BindValueChanged(_ => updateAppearance(), true); FinishTransforms(true); @@ -198,7 +193,7 @@ namespace osu.Game.Screens.Play.HUD private void updateAppearance() { - header.Font = OsuFont.GetFont(Font.Value, 12, FontWeight.Bold); + header.Font = OsuFont.GetFont(HeaderFont.Value, 12, FontWeight.Bold); header.Colour = HeaderColour.Value; Width = header.DrawWidth; diff --git a/osu.Game/Skinning/TrianglesSkin.cs b/osu.Game/Skinning/TrianglesSkin.cs index 06fe1c80ee..a4a967bed9 100644 --- a/osu.Game/Skinning/TrianglesSkin.cs +++ b/osu.Game/Skinning/TrianglesSkin.cs @@ -158,7 +158,7 @@ namespace osu.Game.Skinning if (spectatorList != null) { - spectatorList.Font.Value = Typeface.Venera; + spectatorList.HeaderFont.Value = Typeface.Venera; spectatorList.HeaderColour.Value = new OsuColour().BlueLighter; spectatorList.Anchor = Anchor.BottomLeft; spectatorList.Origin = Anchor.BottomLeft; From 7ca9d8392d4854ae9ee21a537eda6692ee35a147 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 10 Mar 2025 17:17:20 +0900 Subject: [PATCH 1244/1275] Cache ruleset instance to avoid instantiation per beatmap processed --- osu.Game/Database/BackgroundDataStoreProcessor.cs | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/osu.Game/Database/BackgroundDataStoreProcessor.cs b/osu.Game/Database/BackgroundDataStoreProcessor.cs index 5053ab9a4c..5a1c4a4721 100644 --- a/osu.Game/Database/BackgroundDataStoreProcessor.cs +++ b/osu.Game/Database/BackgroundDataStoreProcessor.cs @@ -160,7 +160,17 @@ namespace osu.Game.Database int processedCount = 0; int failedCount = 0; - foreach (var id in beatmapIds) + Dictionary rulesetCache = new Dictionary(); + + Ruleset getRuleset(RulesetInfo rulesetInfo) + { + if (!rulesetCache.TryGetValue(rulesetInfo.ShortName, out var ruleset)) + ruleset = rulesetCache[rulesetInfo.ShortName] = rulesetInfo.CreateInstance(); + + return ruleset; + } + + foreach (Guid id in beatmapIds) { if (notification?.State == ProgressNotificationState.Cancelled) break; @@ -179,7 +189,7 @@ namespace osu.Game.Database try { var working = beatmapManager.GetWorkingBeatmap(beatmap); - var ruleset = working.BeatmapInfo.Ruleset.CreateInstance(); + var ruleset = getRuleset(working.BeatmapInfo.Ruleset); Debug.Assert(ruleset != null); From 27ead5a383dd2bf9884d6a33ac9697909a693592 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 10 Mar 2025 09:18:49 +0100 Subject: [PATCH 1245/1275] Use `CurrentMatchPlayingUserIds` instead of `RoomUpdated` --- osu.Game/Screens/Play/HUD/SpectatorList.cs | 19 +++++++------------ 1 file changed, 7 insertions(+), 12 deletions(-) diff --git a/osu.Game/Screens/Play/HUD/SpectatorList.cs b/osu.Game/Screens/Play/HUD/SpectatorList.cs index 98b3ede874..17e77f5238 100644 --- a/osu.Game/Screens/Play/HUD/SpectatorList.cs +++ b/osu.Game/Screens/Play/HUD/SpectatorList.cs @@ -8,7 +8,6 @@ using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Extensions.LocalisationExtensions; -using osu.Framework.Extensions.ObjectExtensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Colour; using osu.Framework.Graphics.Containers; @@ -37,7 +36,8 @@ namespace osu.Game.Screens.Play.HUD [SettingSource(typeof(SkinnableComponentStrings), nameof(SkinnableComponentStrings.TextColour), nameof(SkinnableComponentStrings.TextColourDescription))] public BindableColour4 HeaderColour { get; } = new BindableColour4(Colour4.White); - private BindableList watchingUsers { get; } = new BindableList(); + private IBindableList watchingUsers { get; } = new BindableList(); + private IBindableList multiplayerPlayers { get; } = new BindableList(); private BindableList actualSpectators { get; } = new BindableList(); private Bindable userPlayingState { get; } = new Bindable(); @@ -92,11 +92,14 @@ namespace osu.Game.Screens.Play.HUD { base.LoadComplete(); - ((IBindableList)watchingUsers).BindTo(client.WatchingUsers); ((IBindable)userPlayingState).BindTo(gameplayState.PlayingState); + multiplayerPlayers.BindTo(multiplayerClient.CurrentMatchPlayingUserIds); + multiplayerPlayers.BindCollectionChanged((_, _) => removePlayersFromMultiplayerRoom()); + + watchingUsers.BindTo(client.WatchingUsers); watchingUsers.BindCollectionChanged(onWatchingUsersChanged, true); - multiplayerClient.RoomUpdated += removePlayersFromMultiplayerRoom; + actualSpectators.BindCollectionChanged(onSpectatorsChanged, true); userPlayingState.BindValueChanged(_ => updateVisibility()); @@ -236,14 +239,6 @@ namespace osu.Game.Screens.Play.HUD Width = header.DrawWidth; } - protected override void Dispose(bool isDisposing) - { - base.Dispose(isDisposing); - - if (multiplayerClient.IsNotNull()) - multiplayerClient.RoomUpdated -= removePlayersFromMultiplayerRoom; - } - private partial class SpectatorListEntry : PoolableDrawable { public Bindable Current { get; } = new Bindable(); From 25108beae3fb470c123af1d2ffb1cc7fcf808269 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 10 Mar 2025 09:47:44 +0100 Subject: [PATCH 1246/1275] Actually use the proper list --- osu.Game/Screens/Play/HUD/SpectatorList.cs | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/osu.Game/Screens/Play/HUD/SpectatorList.cs b/osu.Game/Screens/Play/HUD/SpectatorList.cs index 17e77f5238..6479956601 100644 --- a/osu.Game/Screens/Play/HUD/SpectatorList.cs +++ b/osu.Game/Screens/Play/HUD/SpectatorList.cs @@ -145,16 +145,12 @@ namespace osu.Game.Screens.Play.HUD private void removePlayersFromMultiplayerRoom() { - if (multiplayerClient.Room == null) - return; - // the multiplayer gameplay leaderboard relies on calling `SpectatorClient.WatchUser()` to get updates on users' total scores. // this has an unfortunate side effect of other players showing up in `SpectatorClient.WatchingUsers`. // // we do not generally wish to display other players in the room as spectators due to that implementation detail, // therefore this code is intended to filter out those players on the client side. - var excludedUserIds = multiplayerClient.Room.Users.Where(u => u.State != MultiplayerUserState.Spectating).Select(u => u.UserID).ToHashSet(); - actualSpectators.RemoveAll(s => excludedUserIds.Contains(s.OnlineID)); + actualSpectators.RemoveAll(s => multiplayerPlayers.Contains(s.OnlineID)); } private void onSpectatorsChanged(object? sender, NotifyCollectionChangedEventArgs e) From 6b1472b0705486a250fe3d84320ac57f2560e6d7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 10 Mar 2025 10:36:19 +0100 Subject: [PATCH 1247/1275] Pull actual diffcalc out of realm transaction --- .../Database/BackgroundDataStoreProcessor.cs | 46 ++++++++++--------- 1 file changed, 24 insertions(+), 22 deletions(-) diff --git a/osu.Game/Database/BackgroundDataStoreProcessor.cs b/osu.Game/Database/BackgroundDataStoreProcessor.cs index 5a1c4a4721..4e813fa2c7 100644 --- a/osu.Game/Database/BackgroundDataStoreProcessor.cs +++ b/osu.Game/Database/BackgroundDataStoreProcessor.cs @@ -179,32 +179,34 @@ namespace osu.Game.Database sleepIfRequired(); - realmAccess.Write(r => + var beatmap = realmAccess.Run(r => r.Find(id)?.Detach()); + + if (beatmap == null) + return; + + try { - var beatmap = r.Find(id); + var working = beatmapManager.GetWorkingBeatmap(beatmap); + var ruleset = getRuleset(working.BeatmapInfo.Ruleset); - if (beatmap == null) - return; + Debug.Assert(ruleset != null); - try + var calculator = ruleset.CreateDifficultyCalculator(working); + + double starRating = calculator.Calculate().StarRating; + realmAccess.Write(r => { - var working = beatmapManager.GetWorkingBeatmap(beatmap); - var ruleset = getRuleset(working.BeatmapInfo.Ruleset); - - Debug.Assert(ruleset != null); - - var calculator = ruleset.CreateDifficultyCalculator(working); - - beatmap.StarRating = calculator.Calculate().StarRating; - ((IWorkingBeatmapCache)beatmapManager).Invalidate(beatmap); - ++processedCount; - } - catch (Exception e) - { - Logger.Log($"Background processing failed on {beatmap}: {e}"); - ++failedCount; - } - }); + if (r.Find(id) is BeatmapInfo liveBeatmapInfo) + liveBeatmapInfo.StarRating = starRating; + }); + ((IWorkingBeatmapCache)beatmapManager).Invalidate(beatmap); + ++processedCount; + } + catch (Exception e) + { + Logger.Log($"Background processing failed on {beatmap}: {e}"); + ++failedCount; + } } completeNotification(notification, processedCount, beatmapIds.Count, failedCount); From 3d4dd8507723fcd3a048442834336486debb9732 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 10 Mar 2025 10:44:22 +0100 Subject: [PATCH 1248/1275] Move back tag to extra if reached zero votes --- osu.Game/Screens/Ranking/UserTagControl.cs | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/osu.Game/Screens/Ranking/UserTagControl.cs b/osu.Game/Screens/Ranking/UserTagControl.cs index 6b7d22a7c2..b11dc1588b 100644 --- a/osu.Game/Screens/Ranking/UserTagControl.cs +++ b/osu.Game/Screens/Ranking/UserTagControl.cs @@ -172,7 +172,7 @@ namespace osu.Game.Screens.Ranking var tag = (UserTag)e.NewItems[i]!; var drawableTag = new DrawableUserTag(tag); tagFlow.Insert(tagFlow.Count, drawableTag); - tag.VoteCount.BindValueChanged(sortTags, true); + tag.VoteCount.BindValueChanged(voteCountChanged, true); layout.Invalidate(); } @@ -184,7 +184,7 @@ namespace osu.Game.Screens.Ranking for (int i = 0; i < e.OldItems!.Count; i++) { var tag = (UserTag)e.OldItems[i]!; - tag.VoteCount.ValueChanged -= sortTags; + tag.VoteCount.ValueChanged -= voteCountChanged; tagFlow.Remove(oldItems[e.OldStartingIndex + i], true); } @@ -199,7 +199,18 @@ namespace osu.Game.Screens.Ranking } } - private void sortTags(ValueChangedEvent _) => layout.Invalidate(); + private void voteCountChanged(ValueChangedEvent _) + { + var tagsWithNoVotes = displayedTags.Where(t => t.VoteCount.Value == 0).ToArray(); + + foreach (var tag in tagsWithNoVotes) + { + displayedTags.Remove(tag); + extraTags.Add(tag); + } + + layout.Invalidate(); + } protected override void Update() { From 00127b363d532ed4f51ec03de13f06e0478d5920 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 10 Mar 2025 10:52:24 +0100 Subject: [PATCH 1249/1275] Add search box to user tag control --- osu.Game/Screens/Ranking/UserTagControl.cs | 56 ++++++++++++++++------ 1 file changed, 42 insertions(+), 14 deletions(-) diff --git a/osu.Game/Screens/Ranking/UserTagControl.cs b/osu.Game/Screens/Ranking/UserTagControl.cs index b11dc1588b..57b05f078c 100644 --- a/osu.Game/Screens/Ranking/UserTagControl.cs +++ b/osu.Game/Screens/Ranking/UserTagControl.cs @@ -17,6 +17,7 @@ using osu.Framework.Graphics.Effects; using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.UserInterface; +using osu.Framework.Localisation; using osu.Game.Beatmaps; using osu.Game.Configuration; using osu.Game.Extensions; @@ -447,6 +448,9 @@ namespace osu.Game.Screens.Ranking private partial class ExtraTagsPopover : OsuPopover { + private SearchTextBox searchBox = null!; + private SearchContainer searchContainer = null!; + public BindableList ExtraTags { get; } = new BindableList(); public Action? OnSelected { get; set; } @@ -457,28 +461,43 @@ namespace osu.Game.Screens.Ranking Child = new OsuScrollContainer { Width = 250, - Height = 200, + Height = 250, ScrollbarOverlapsContent = false, - Child = new FillFlowContainer + Children = new Drawable[] { - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, - Padding = new MarginPadding { Right = 5 }, - Spacing = new Vector2(10), - ChildrenEnumerable = ExtraTags.Select(tag => new DrawableExtraTag(tag) + searchBox = new SearchTextBox { - Action = () => + RelativeSizeAxes = Axes.X, + }, + searchContainer = new SearchContainer + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Direction = FillDirection.Vertical, + Padding = new MarginPadding { Right = 5, Top = 50, }, + Spacing = new Vector2(10), + ChildrenEnumerable = ExtraTags.Select(tag => new DrawableExtraTag(tag) { - OnSelected?.Invoke(tag); - this.HidePopover(); - } - }) - } + Action = () => + { + OnSelected?.Invoke(tag); + this.HidePopover(); + } + }) + } + }, }; } + + protected override void LoadComplete() + { + base.LoadComplete(); + + searchBox.Current.BindValueChanged(_ => searchContainer.SearchTerm = searchBox.Current.Value, true); + } } - private partial class DrawableExtraTag : OsuAnimatedButton + private partial class DrawableExtraTag : OsuAnimatedButton, IFilterable { private readonly UserTag tag; @@ -527,6 +546,15 @@ namespace osu.Game.Screens.Ranking } }); } + + public IEnumerable FilterTerms => [tag.Name, tag.Description]; + + public bool MatchingFilter + { + set => Alpha = value ? 1 : 0; + } + + public bool FilteringActive { set { } } } } From afad2cf278cdbc142a8edd56e9b5a69f98cd3acf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 10 Mar 2025 11:52:09 +0100 Subject: [PATCH 1250/1275] Apply more granular copying from database when retrieving working beatmap --- osu.Game/Beatmaps/WorkingBeatmap.cs | 17 ++++++++++++----- osu.Game/Beatmaps/WorkingBeatmapCache.cs | 19 +++++++++++++++++-- 2 files changed, 29 insertions(+), 7 deletions(-) diff --git a/osu.Game/Beatmaps/WorkingBeatmap.cs b/osu.Game/Beatmaps/WorkingBeatmap.cs index fd40097c4e..8df57fd0c8 100644 --- a/osu.Game/Beatmaps/WorkingBeatmap.cs +++ b/osu.Game/Beatmaps/WorkingBeatmap.cs @@ -235,11 +235,18 @@ namespace osu.Game.Beatmaps // Todo: Handle cancellation during beatmap parsing var b = GetBeatmap() ?? new Beatmap(); - // The original beatmap version needs to be preserved as the database doesn't contain it - BeatmapInfo.BeatmapVersion = b.BeatmapInfo.BeatmapVersion; - - // Use the database-backed info for more up-to-date values (beatmap id, ranked status, etc) - b.BeatmapInfo = BeatmapInfo; + // Copy across values of key properties for which the database-backed model has data that the decoded beatmap isn't going to. + b.BeatmapInfo.ID = BeatmapInfo.ID; + b.BeatmapInfo.UserSettings = BeatmapInfo.UserSettings; + b.BeatmapInfo.BeatmapSet = BeatmapInfo.BeatmapSet; + b.BeatmapInfo.Status = BeatmapInfo.Status; + b.BeatmapInfo.OnlineID = BeatmapInfo.OnlineID; + b.BeatmapInfo.OnlineMD5Hash = BeatmapInfo.OnlineMD5Hash; + b.BeatmapInfo.LastLocalUpdate = BeatmapInfo.LastLocalUpdate; + b.BeatmapInfo.LastOnlineUpdate = BeatmapInfo.LastOnlineUpdate; + b.BeatmapInfo.LastPlayed = BeatmapInfo.LastPlayed; + b.BeatmapInfo.EditorTimestamp = BeatmapInfo.EditorTimestamp; + b.BeatmapInfo.StarRating = BeatmapInfo.StarRating; // this could be recomputed in the decoding process but it's a bit annoying to do. return b; }, loadCancellationSource.Token, TaskCreationOptions.LongRunning, TaskScheduler.Default); diff --git a/osu.Game/Beatmaps/WorkingBeatmapCache.cs b/osu.Game/Beatmaps/WorkingBeatmapCache.cs index 8af74d11d8..352012106a 100644 --- a/osu.Game/Beatmaps/WorkingBeatmapCache.cs +++ b/osu.Game/Beatmaps/WorkingBeatmapCache.cs @@ -21,6 +21,7 @@ using osu.Framework.Statistics; using osu.Game.Beatmaps.Formats; using osu.Game.Database; using osu.Game.IO; +using osu.Game.Rulesets.Objects.Types; using osu.Game.Skinning; using osu.Game.Storyboards; @@ -152,14 +153,28 @@ namespace osu.Game.Beatmaps return null; } - if (stream.ComputeMD5Hash() != BeatmapInfo.MD5Hash) + string streamMD5 = stream.ComputeMD5Hash(); + string streamSHA2 = stream.ComputeSHA2Hash(); + + if (streamMD5 != BeatmapInfo.MD5Hash) { Logger.Log($"Beatmap failed to load (file {BeatmapInfo.Path} does not have the expected hash).", level: LogLevel.Error); return null; } using (var reader = new LineBufferedReader(stream)) - return Decoder.GetDecoder(reader).Decode(reader); + { + var beatmap = Decoder.GetDecoder(reader).Decode(reader); + + beatmap.BeatmapInfo.MD5Hash = streamMD5; + beatmap.BeatmapInfo.Hash = streamSHA2; + beatmap.BeatmapInfo.Length = beatmap.CalculatePlayableLength(); + beatmap.BeatmapInfo.BPM = 60000 / beatmap.GetMostCommonBeatLength(); + beatmap.BeatmapInfo.EndTimeObjectCount = beatmap.HitObjects.Count(h => h is IHasDuration); + beatmap.BeatmapInfo.TotalObjectCount = beatmap.HitObjects.Count; + + return beatmap; + } } catch (Exception e) { From a78868712ca0ea25d60826f3e0c9fcb78fb95fd6 Mon Sep 17 00:00:00 2001 From: Arthur Araujo Date: Mon, 10 Mar 2025 16:09:18 -0300 Subject: [PATCH 1251/1275] Change amount from 0.9f to 0.6f --- .../Compose/Components/Timeline/TimelineBlueprintContainer.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineBlueprintContainer.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineBlueprintContainer.cs index 011ff17b30..0f1d3716e2 100644 --- a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineBlueprintContainer.cs +++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineBlueprintContainer.cs @@ -90,7 +90,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline { placementBlueprint = CreateBlueprintFor(obj.NewValue).AsNonNull(); - placementBlueprint.Colour = OsuColour.Gray(0.9f); + placementBlueprint.Colour = OsuColour.Gray(0.6f); // TODO: this is out of order, causing incorrect stacking height. SelectionBlueprints.Add(placementBlueprint); From 75bd101c9e9f822b6dadb9904185516fc8aeab8a Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 11 Mar 2025 15:47:45 +0900 Subject: [PATCH 1252/1275] Ensure realm database file is touched on startup Closes https://github.com/ppy/osu/discussions/32304. --- osu.Game/Database/RealmAccess.cs | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/osu.Game/Database/RealmAccess.cs b/osu.Game/Database/RealmAccess.cs index 5cc143f4e2..3212e17b7b 100644 --- a/osu.Game/Database/RealmAccess.cs +++ b/osu.Game/Database/RealmAccess.cs @@ -315,6 +315,17 @@ namespace osu.Game.Database attemptRecoverFromFile(newerVersionFilename); } + try + { + // Some platforms' realm implementation (including windows) don't update modified time on open. + // Let's do this explicitly as some users may depend on it roughly aligning to usage expectations. + string fullPath = storage.GetFullPath(Filename); + var fi = new FileInfo(fullPath); + if (fi.Exists) + fi.LastWriteTime = DateTime.Now; + } + catch { } + try { return getRealmInstance(); From c962210b4f250fab62c31f082fedf34dbe26a8e4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 10 Mar 2025 13:21:27 +0100 Subject: [PATCH 1253/1275] Fix placement blueprint tests --- .../Editor/CatchPlacementBlueprintTestScene.cs | 2 ++ .../Editor/ManiaPlacementBlueprintTestScene.cs | 2 ++ .../Editor/TestSceneHitCirclePlacementBlueprint.cs | 1 + .../Editor/TestSceneSliderPlacementBlueprint.cs | 2 ++ .../Editor/TestSceneSpinnerPlacementBlueprint.cs | 2 ++ osu.Game/Tests/Visual/PlacementBlueprintTestScene.cs | 4 +++- 6 files changed, 12 insertions(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Catch.Tests/Editor/CatchPlacementBlueprintTestScene.cs b/osu.Game.Rulesets.Catch.Tests/Editor/CatchPlacementBlueprintTestScene.cs index a327e6d4c9..a5713feda3 100644 --- a/osu.Game.Rulesets.Catch.Tests/Editor/CatchPlacementBlueprintTestScene.cs +++ b/osu.Game.Rulesets.Catch.Tests/Editor/CatchPlacementBlueprintTestScene.cs @@ -22,6 +22,8 @@ namespace osu.Game.Rulesets.Catch.Tests.Editor { public abstract partial class CatchPlacementBlueprintTestScene : PlacementBlueprintTestScene { + protected sealed override Ruleset CreateRuleset() => new CatchRuleset(); + protected const double TIME_SNAP = 100; protected DrawableCatchHitObject LastObject; diff --git a/osu.Game.Rulesets.Mania.Tests/Editor/ManiaPlacementBlueprintTestScene.cs b/osu.Game.Rulesets.Mania.Tests/Editor/ManiaPlacementBlueprintTestScene.cs index 0f913a6a7d..83070c3e29 100644 --- a/osu.Game.Rulesets.Mania.Tests/Editor/ManiaPlacementBlueprintTestScene.cs +++ b/osu.Game.Rulesets.Mania.Tests/Editor/ManiaPlacementBlueprintTestScene.cs @@ -22,6 +22,8 @@ namespace osu.Game.Rulesets.Mania.Tests.Editor { public abstract partial class ManiaPlacementBlueprintTestScene : PlacementBlueprintTestScene { + protected sealed override Ruleset CreateRuleset() => new ManiaRuleset(); + private readonly Column column; [Cached(typeof(IReadOnlyList))] diff --git a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneHitCirclePlacementBlueprint.cs b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneHitCirclePlacementBlueprint.cs index a105d860bf..5bce97d7b8 100644 --- a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneHitCirclePlacementBlueprint.cs +++ b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneHitCirclePlacementBlueprint.cs @@ -13,6 +13,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor { public partial class TestSceneHitCirclePlacementBlueprint : PlacementBlueprintTestScene { + protected sealed override Ruleset CreateRuleset() => new OsuRuleset(); protected override DrawableHitObject CreateHitObject(HitObject hitObject) => new DrawableHitCircle((HitCircle)hitObject); protected override HitObjectPlacementBlueprint CreateBlueprint() => new HitCirclePlacementBlueprint(); } diff --git a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderPlacementBlueprint.cs b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderPlacementBlueprint.cs index 5831cc0a8a..8835254c48 100644 --- a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderPlacementBlueprint.cs +++ b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderPlacementBlueprint.cs @@ -23,6 +23,8 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor { public partial class TestSceneSliderPlacementBlueprint : PlacementBlueprintTestScene { + protected sealed override Ruleset CreateRuleset() => new OsuRuleset(); + [SetUp] public void Setup() => Schedule(() => { diff --git a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSpinnerPlacementBlueprint.cs b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSpinnerPlacementBlueprint.cs index d7b5cc73be..18834ef847 100644 --- a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSpinnerPlacementBlueprint.cs +++ b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSpinnerPlacementBlueprint.cs @@ -13,6 +13,8 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor { public partial class TestSceneSpinnerPlacementBlueprint : PlacementBlueprintTestScene { + protected sealed override Ruleset CreateRuleset() => new OsuRuleset(); + protected override DrawableHitObject CreateHitObject(HitObject hitObject) => new DrawableSpinner((Spinner)hitObject); protected override HitObjectPlacementBlueprint CreateBlueprint() => new SpinnerPlacementBlueprint(); diff --git a/osu.Game/Tests/Visual/PlacementBlueprintTestScene.cs b/osu.Game/Tests/Visual/PlacementBlueprintTestScene.cs index baf614d1c8..a644936a16 100644 --- a/osu.Game/Tests/Visual/PlacementBlueprintTestScene.cs +++ b/osu.Game/Tests/Visual/PlacementBlueprintTestScene.cs @@ -51,7 +51,9 @@ namespace osu.Game.Tests.Visual protected virtual IBeatmap GetPlayableBeatmap() { - var playable = Beatmap.Value.GetPlayableBeatmap(Beatmap.Value.BeatmapInfo.Ruleset); + var rulesetInfo = CreateRuleset()!.RulesetInfo; + var playable = Beatmap.Value.GetPlayableBeatmap(rulesetInfo); + playable.BeatmapInfo.Ruleset = rulesetInfo; playable.Difficulty.CircleSize = 2; return playable; } From d4f0fc0fdee85accf84de96d6388e0a9ba2ecd0d Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 11 Mar 2025 16:12:32 +0900 Subject: [PATCH 1254/1275] Disallow adjusting slider repeats with more lenient check condition --- .../Compose/Components/Timeline/TimelineHitObjectBlueprint.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineHitObjectBlueprint.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineHitObjectBlueprint.cs index 6c0d5af247..f60d1b023b 100644 --- a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineHitObjectBlueprint.cs +++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineHitObjectBlueprint.cs @@ -441,7 +441,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline double lengthOfOneRepeat = repeatHitObject.Duration / (repeatHitObject.RepeatCount + 1); int proposedCount = Math.Max(0, (int)Math.Round(proposedDuration / lengthOfOneRepeat) - 1); - if (proposedCount == repeatHitObject.RepeatCount || Precision.AlmostEquals(lengthOfOneRepeat, 0)) + if (proposedCount == repeatHitObject.RepeatCount || Precision.AlmostEquals(lengthOfOneRepeat, 0, 1)) return; repeatHitObject.RepeatCount = proposedCount; From 23891b1994c296d7e6761010deeb334f6c1af103 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 11 Mar 2025 16:17:44 +0900 Subject: [PATCH 1255/1275] Fix edge case allowing almost-zero-length sliders to be placed during distance snapping --- .../Edit/Blueprints/Components/SelectionEditablePath.cs | 2 +- .../Sliders/Components/PathControlPointVisualiser.cs | 2 +- .../Edit/Blueprints/Sliders/SliderPlacementBlueprint.cs | 2 +- .../Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs | 2 +- osu.Game.Rulesets.Osu/Edit/OsuSelectionScaleHandler.cs | 2 +- osu.Game/Rulesets/Objects/SliderPath.cs | 5 ++++- 6 files changed, 9 insertions(+), 6 deletions(-) diff --git a/osu.Game.Rulesets.Catch/Edit/Blueprints/Components/SelectionEditablePath.cs b/osu.Game.Rulesets.Catch/Edit/Blueprints/Components/SelectionEditablePath.cs index 26b26641d3..654ef006a5 100644 --- a/osu.Game.Rulesets.Catch/Edit/Blueprints/Components/SelectionEditablePath.cs +++ b/osu.Game.Rulesets.Catch/Edit/Blueprints/Components/SelectionEditablePath.cs @@ -143,7 +143,7 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints.Components { base.UpdateHitObjectFromPath(hitObject); - if (hitObject.Path.ControlPoints.Count <= 1 || !hitObject.Path.HasValidLength) + if (hitObject.Path.ControlPoints.Count <= 1 || !hitObject.Path.HasValidLengthForPlacement) EditorBeatmap?.Remove(hitObject); } } diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs index bc3d27fd68..5ae9b194be 100644 --- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs +++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs @@ -484,7 +484,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components // Snap the path to the current beat divisor before checking length validity. hitObject.SnapTo(distanceSnapProvider); - if (!hitObject.Path.HasValidLength) + if (!hitObject.Path.HasValidLengthForPlacement) { for (int i = 0; i < hitObject.Path.ControlPoints.Count; i++) hitObject.Path.ControlPoints[i].Position = oldControlPoints[i]; diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderPlacementBlueprint.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderPlacementBlueprint.cs index a747d4fce8..d934eb5a9e 100644 --- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderPlacementBlueprint.cs +++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderPlacementBlueprint.cs @@ -58,7 +58,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders private readonly IncrementalBSplineBuilder bSplineBuilder = new IncrementalBSplineBuilder { Degree = 4 }; - protected override bool IsValidForPlacement => HitObject.Path.HasValidLength; + protected override bool IsValidForPlacement => HitObject.Path.HasValidLengthForPlacement; public SliderPlacementBlueprint() : base(new Slider()) diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs index 9978c46027..d6150f85db 100644 --- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs +++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs @@ -476,7 +476,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders HitObject.SnapTo(distanceSnapProvider); // If there are 0 or 1 remaining control points, or the slider has an invalid length, it is in a degenerate form and should be deleted - if (controlPoints.Count <= 1 || !HitObject.Path.HasValidLength) + if (controlPoints.Count <= 1 || !HitObject.Path.HasValidLengthForPlacement) { placementHandler?.Delete(HitObject); return; diff --git a/osu.Game.Rulesets.Osu/Edit/OsuSelectionScaleHandler.cs b/osu.Game.Rulesets.Osu/Edit/OsuSelectionScaleHandler.cs index 4c3db207f2..9a5d3c3bc1 100644 --- a/osu.Game.Rulesets.Osu/Edit/OsuSelectionScaleHandler.cs +++ b/osu.Game.Rulesets.Osu/Edit/OsuSelectionScaleHandler.cs @@ -180,7 +180,7 @@ namespace osu.Game.Rulesets.Osu.Edit Quad scaledQuad = GeometryUtils.GetSurroundingQuad(new OsuHitObject[] { slider }); (bool xInBounds, bool yInBounds) = isQuadInBounds(scaledQuad); - if (xInBounds && yInBounds && slider.Path.HasValidLength) + if (xInBounds && yInBounds && slider.Path.HasValidLengthForPlacement) return; for (int i = 0; i < slider.Path.ControlPoints.Count; i++) diff --git a/osu.Game/Rulesets/Objects/SliderPath.cs b/osu.Game/Rulesets/Objects/SliderPath.cs index 5550815370..eb591ec530 100644 --- a/osu.Game/Rulesets/Objects/SliderPath.cs +++ b/osu.Game/Rulesets/Objects/SliderPath.cs @@ -31,7 +31,10 @@ namespace osu.Game.Rulesets.Objects /// public readonly Bindable ExpectedDistance = new Bindable(); - public bool HasValidLength => Precision.DefinitelyBigger(Distance, 0); + /// + /// Should be used to check whether placement can continue after a user editor operation. + /// + public bool HasValidLengthForPlacement => Precision.DefinitelyBigger(Distance, 0, 1); /// /// The control points of the path. From 5ef2479e24d8001ee82b32c7bde832347b981747 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 11 Mar 2025 16:29:47 +0900 Subject: [PATCH 1256/1275] Remove previous version of local cache lookup handling --- .../LocalCachedBeatmapMetadataSource.cs | 41 ------------------- 1 file changed, 41 deletions(-) diff --git a/osu.Game/Beatmaps/LocalCachedBeatmapMetadataSource.cs b/osu.Game/Beatmaps/LocalCachedBeatmapMetadataSource.cs index a1744f74b3..1412d3234c 100644 --- a/osu.Game/Beatmaps/LocalCachedBeatmapMetadataSource.cs +++ b/osu.Game/Beatmaps/LocalCachedBeatmapMetadataSource.cs @@ -104,11 +104,6 @@ namespace osu.Game.Beatmaps switch (getCacheVersion(db)) { - case 1: - // will eventually become irrelevant due to the monthly recycling of local caches - // can be removed 20250221 - return queryCacheVersion1(db, beatmapInfo, out onlineMetadata); - case 2: return queryCacheVersion2(db, beatmapInfo, out onlineMetadata); } @@ -270,42 +265,6 @@ namespace osu.Game.Beatmaps } } - private bool queryCacheVersion1(SqliteConnection db, BeatmapInfo beatmapInfo, out OnlineBeatmapMetadata? onlineMetadata) - { - Debug.Assert(beatmapInfo.BeatmapSet != null); - - using var cmd = db.CreateCommand(); - - cmd.CommandText = - @"SELECT beatmapset_id, beatmap_id, approved, user_id, checksum, last_update FROM osu_beatmaps WHERE checksum = @MD5Hash OR filename = @Path"; - - cmd.Parameters.Add(new SqliteParameter(@"@MD5Hash", beatmapInfo.MD5Hash)); - cmd.Parameters.Add(new SqliteParameter(@"@Path", beatmapInfo.Path)); - - using var reader = cmd.ExecuteReader(); - - if (reader.Read()) - { - logForModel(beatmapInfo.BeatmapSet, $@"Cached local retrieval for {beatmapInfo} (cache version 1)."); - - onlineMetadata = new OnlineBeatmapMetadata - { - BeatmapSetID = reader.GetInt32(0), - BeatmapID = reader.GetInt32(1), - BeatmapStatus = (BeatmapOnlineStatus)reader.GetByte(2), - BeatmapSetStatus = (BeatmapOnlineStatus)reader.GetByte(2), - AuthorID = reader.GetInt32(3), - MD5Hash = reader.GetString(4), - LastUpdated = reader.GetDateTimeOffset(5), - // TODO: DateSubmitted and DateRanked are not provided by local cache in this version. - }; - return true; - } - - onlineMetadata = null; - return false; - } - private bool queryCacheVersion2(SqliteConnection db, BeatmapInfo beatmapInfo, out OnlineBeatmapMetadata? onlineMetadata) { Debug.Assert(beatmapInfo.BeatmapSet != null); From 8d83dfede7a2c7a2818da4f5cc97165524f4237b Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 11 Mar 2025 16:37:46 +0900 Subject: [PATCH 1257/1275] Ensure only ranked/approved/loved lookups occur on local cached source --- osu.Game/Beatmaps/LocalCachedBeatmapMetadataSource.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/osu.Game/Beatmaps/LocalCachedBeatmapMetadataSource.cs b/osu.Game/Beatmaps/LocalCachedBeatmapMetadataSource.cs index 1412d3234c..0b4f4f1700 100644 --- a/osu.Game/Beatmaps/LocalCachedBeatmapMetadataSource.cs +++ b/osu.Game/Beatmaps/LocalCachedBeatmapMetadataSource.cs @@ -277,7 +277,11 @@ namespace osu.Game.Beatmaps FROM `osu_beatmaps` AS `b` JOIN `osu_beatmapsets` AS `s` ON `s`.`beatmapset_id` = `b`.`beatmapset_id` WHERE `b`.`checksum` = @MD5Hash OR `b`.`filename` = @Path + AND `b`.`approved` in (1, 2, 4) """; + // approved conditional can theoretically be removed as it was fixed in + // https://github.com/ppy/osu-onlinedb-generator/commit/489ac000775c3ff63bc914efb83cad0f6fbde261 + // but it's also safe to leave it (should not affect performance). cmd.Parameters.Add(new SqliteParameter(@"@MD5Hash", beatmapInfo.MD5Hash)); cmd.Parameters.Add(new SqliteParameter(@"@Path", beatmapInfo.Path)); From be5c89c2e40321a1c10d80abb3e523686d7734f0 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 11 Mar 2025 17:03:06 +0900 Subject: [PATCH 1258/1275] Add basic helper method to update beatmap statistics --- osu.Game/Beatmaps/BeatmapInfoExtensions.cs | 13 +++++++++++++ osu.Game/Beatmaps/BeatmapUpdater.cs | 7 ++----- osu.Game/Beatmaps/WorkingBeatmapCache.cs | 6 +----- 3 files changed, 16 insertions(+), 10 deletions(-) diff --git a/osu.Game/Beatmaps/BeatmapInfoExtensions.cs b/osu.Game/Beatmaps/BeatmapInfoExtensions.cs index 16b4b04ce4..25f98c812c 100644 --- a/osu.Game/Beatmaps/BeatmapInfoExtensions.cs +++ b/osu.Game/Beatmaps/BeatmapInfoExtensions.cs @@ -1,15 +1,28 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System.Linq; using osu.Framework.Localisation; using osu.Game.Online.API; using osu.Game.Rulesets; +using osu.Game.Rulesets.Objects.Types; using osu.Game.Screens.Select; namespace osu.Game.Beatmaps { public static class BeatmapInfoExtensions { + /// + /// Given an , update length, BPM and object counts. + /// + public static void UpdateStatisticsFromBeatmap(this BeatmapInfo beatmapInfo, IBeatmap beatmap) + { + beatmapInfo.Length = beatmap.CalculatePlayableLength(); + beatmapInfo.BPM = 60000 / beatmap.GetMostCommonBeatLength(); + beatmapInfo.EndTimeObjectCount = beatmap.HitObjects.Count(h => h is IHasDuration); + beatmapInfo.TotalObjectCount = beatmap.HitObjects.Count; + } + /// /// A user-presentable display title representing this beatmap. /// diff --git a/osu.Game/Beatmaps/BeatmapUpdater.cs b/osu.Game/Beatmaps/BeatmapUpdater.cs index efb432b84e..64ac69bb07 100644 --- a/osu.Game/Beatmaps/BeatmapUpdater.cs +++ b/osu.Game/Beatmaps/BeatmapUpdater.cs @@ -51,7 +51,7 @@ namespace osu.Game.Beatmaps if (lookupScope != MetadataLookupScope.None) metadataLookup.Update(beatmapSet, lookupScope == MetadataLookupScope.OnlineFirst); - foreach (var beatmap in beatmapSet.Beatmaps) + foreach (BeatmapInfo beatmap in beatmapSet.Beatmaps) { difficultyCache.Invalidate(beatmap); @@ -63,10 +63,7 @@ namespace osu.Game.Beatmaps var calculator = ruleset.CreateDifficultyCalculator(working); beatmap.StarRating = calculator.Calculate().StarRating; - beatmap.Length = working.Beatmap.CalculatePlayableLength(); - beatmap.BPM = 60000 / working.Beatmap.GetMostCommonBeatLength(); - beatmap.EndTimeObjectCount = working.Beatmap.HitObjects.Count(h => h is IHasDuration); - beatmap.TotalObjectCount = working.Beatmap.HitObjects.Count; + beatmap.UpdateStatisticsFromBeatmap(working.Beatmap); } // And invalidate again afterwards as re-fetching the most up-to-date database metadata will be required. diff --git a/osu.Game/Beatmaps/WorkingBeatmapCache.cs b/osu.Game/Beatmaps/WorkingBeatmapCache.cs index 352012106a..fdeb840977 100644 --- a/osu.Game/Beatmaps/WorkingBeatmapCache.cs +++ b/osu.Game/Beatmaps/WorkingBeatmapCache.cs @@ -21,7 +21,6 @@ using osu.Framework.Statistics; using osu.Game.Beatmaps.Formats; using osu.Game.Database; using osu.Game.IO; -using osu.Game.Rulesets.Objects.Types; using osu.Game.Skinning; using osu.Game.Storyboards; @@ -168,10 +167,7 @@ namespace osu.Game.Beatmaps beatmap.BeatmapInfo.MD5Hash = streamMD5; beatmap.BeatmapInfo.Hash = streamSHA2; - beatmap.BeatmapInfo.Length = beatmap.CalculatePlayableLength(); - beatmap.BeatmapInfo.BPM = 60000 / beatmap.GetMostCommonBeatLength(); - beatmap.BeatmapInfo.EndTimeObjectCount = beatmap.HitObjects.Count(h => h is IHasDuration); - beatmap.BeatmapInfo.TotalObjectCount = beatmap.HitObjects.Count; + beatmap.BeatmapInfo.UpdateStatisticsFromBeatmap(beatmap); return beatmap; } From 914a230446da83db38d46752381f66e37fe272ed Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 11 Mar 2025 18:17:18 +0900 Subject: [PATCH 1259/1275] Add brackets to ensure correct lookups MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Bartłomiej Dach --- osu.Game/Beatmaps/LocalCachedBeatmapMetadataSource.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Beatmaps/LocalCachedBeatmapMetadataSource.cs b/osu.Game/Beatmaps/LocalCachedBeatmapMetadataSource.cs index 0b4f4f1700..d876ba55b2 100644 --- a/osu.Game/Beatmaps/LocalCachedBeatmapMetadataSource.cs +++ b/osu.Game/Beatmaps/LocalCachedBeatmapMetadataSource.cs @@ -276,7 +276,7 @@ namespace osu.Game.Beatmaps SELECT `b`.`beatmapset_id`, `b`.`beatmap_id`, `b`.`approved`, `b`.`user_id`, `b`.`checksum`, `b`.`last_update`, `s`.`submit_date`, `s`.`approved_date` FROM `osu_beatmaps` AS `b` JOIN `osu_beatmapsets` AS `s` ON `s`.`beatmapset_id` = `b`.`beatmapset_id` - WHERE `b`.`checksum` = @MD5Hash OR `b`.`filename` = @Path + WHERE (`b`.`checksum` = @MD5Hash OR `b`.`filename` = @Path) AND `b`.`approved` in (1, 2, 4) """; // approved conditional can theoretically be removed as it was fixed in From 770291b4623f7818ea76c6928de7e69768389ca8 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 11 Mar 2025 18:35:10 +0900 Subject: [PATCH 1260/1275] Show border instead of adjusting dim --- .../Components/Timeline/TimelineBlueprintContainer.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineBlueprintContainer.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineBlueprintContainer.cs index 0f1d3716e2..c149a8f73a 100644 --- a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineBlueprintContainer.cs +++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineBlueprintContainer.cs @@ -18,6 +18,7 @@ using osu.Framework.Input.Events; using osu.Framework.Utils; using osu.Game.Audio; using osu.Game.Graphics; +using osu.Game.Graphics.UserInterface; using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects.Types; @@ -90,7 +91,9 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline { placementBlueprint = CreateBlueprintFor(obj.NewValue).AsNonNull(); - placementBlueprint.Colour = OsuColour.Gray(0.6f); + // just to show the border. using the selection state doesn't seem to backfire. + // if it does then we'll probably want to just make `new` object above rather than rely on `CreateBlueprintFor`. + placementBlueprint.State = SelectionState.Selected; // TODO: this is out of order, causing incorrect stacking height. SelectionBlueprints.Add(placementBlueprint); From ee723aef6811656fe09aa4f670a98163c648e5fb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 11 Mar 2025 10:43:02 +0100 Subject: [PATCH 1261/1275] Update framework --- osu.Android.props | 2 +- osu.Game/osu.Game.csproj | 2 +- osu.iOS.props | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Android.props b/osu.Android.props index d4b49e492a..8f219ea426 100644 --- a/osu.Android.props +++ b/osu.Android.props @@ -10,7 +10,7 @@ true - + diff --git a/osu.iOS.props b/osu.iOS.props index d10a3d649a..8045009621 100644 --- a/osu.iOS.props +++ b/osu.iOS.props @@ -17,6 +17,6 @@ -all - + From e2e2383c504282a9ef29e7c4803b185d9eb5d2e9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 4 Mar 2025 15:02:18 +0100 Subject: [PATCH 1262/1275] Adjust text flow usages to framework changes --- osu.Game/Graphics/Containers/LinkFlowContainer.cs | 13 +++++++++---- osu.Game/Overlays/BeatmapSet/BeatmapAvailability.cs | 1 - osu.Game/Overlays/Changelog/ChangelogEntry.cs | 1 - osu.Game/Overlays/Chat/ChatLine.cs | 2 +- osu.Game/Overlays/Music/PlaylistItem.cs | 1 - .../Header/Components/PreviousUsernamesDisplay.cs | 1 - 6 files changed, 10 insertions(+), 9 deletions(-) diff --git a/osu.Game/Graphics/Containers/LinkFlowContainer.cs b/osu.Game/Graphics/Containers/LinkFlowContainer.cs index aa72996fff..6022ea6bd6 100644 --- a/osu.Game/Graphics/Containers/LinkFlowContainer.cs +++ b/osu.Game/Graphics/Containers/LinkFlowContainer.cs @@ -134,9 +134,14 @@ namespace osu.Game.Graphics.Containers protected virtual DrawableLinkCompiler CreateLinkCompiler(ITextPart textPart) => new DrawableLinkCompiler(textPart); - // We want the compilers to always be visible no matter where they are, so RelativeSizeAxes is used. - // However due to https://github.com/ppy/osu-framework/issues/2073, it's possible for the compilers to be relative size in the flow's auto-size axes - an unsupported operation. - // Since the compilers don't display any content and don't affect the layout, it's simplest to exclude them from the flow. - public override IEnumerable FlowingChildren => base.FlowingChildren.Where(c => !(c is DrawableLinkCompiler)); + protected override FillFlowContainer CreateFlow() => new LinkFlow(); + + private partial class LinkFlow : InnerFlow + { + // We want the compilers to always be visible no matter where they are, so RelativeSizeAxes is used. + // However due to https://github.com/ppy/osu-framework/issues/2073, it's possible for the compilers to be relative size in the flow's auto-size axes - an unsupported operation. + // Since the compilers don't display any content and don't affect the layout, it's simplest to exclude them from the flow. + public override IEnumerable FlowingChildren => base.FlowingChildren.Where(c => !(c is DrawableLinkCompiler)); + } } } diff --git a/osu.Game/Overlays/BeatmapSet/BeatmapAvailability.cs b/osu.Game/Overlays/BeatmapSet/BeatmapAvailability.cs index d18e1c93c9..c9783d42dc 100644 --- a/osu.Game/Overlays/BeatmapSet/BeatmapAvailability.cs +++ b/osu.Game/Overlays/BeatmapSet/BeatmapAvailability.cs @@ -39,7 +39,6 @@ namespace osu.Game.Overlays.BeatmapSet }, textContainer = new LinkFlowContainer(t => t.Font = OsuFont.GetFont(size: 14)) { - Direction = FillDirection.Full, RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y, Padding = new MarginPadding(10), diff --git a/osu.Game/Overlays/Changelog/ChangelogEntry.cs b/osu.Game/Overlays/Changelog/ChangelogEntry.cs index 9c40440778..d6021972c6 100644 --- a/osu.Game/Overlays/Changelog/ChangelogEntry.cs +++ b/osu.Game/Overlays/Changelog/ChangelogEntry.cs @@ -82,7 +82,6 @@ namespace osu.Game.Overlays.Changelog }, title = new LinkFlowContainer { - Direction = FillDirection.Full, RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y, TextAnchor = Anchor.BottomLeft, diff --git a/osu.Game/Overlays/Chat/ChatLine.cs b/osu.Game/Overlays/Chat/ChatLine.cs index e386f2ac09..20c3b26b8b 100644 --- a/osu.Game/Overlays/Chat/ChatLine.cs +++ b/osu.Game/Overlays/Chat/ChatLine.cs @@ -46,7 +46,7 @@ namespace osu.Game.Overlays.Chat } } - public IReadOnlyCollection DrawableContentFlow => drawableContentFlow; + public IEnumerable DrawableContentFlow => drawableContentFlow.Children; private const float font_size = 13; diff --git a/osu.Game/Overlays/Music/PlaylistItem.cs b/osu.Game/Overlays/Music/PlaylistItem.cs index 90fdfd0491..01b0472172 100644 --- a/osu.Game/Overlays/Music/PlaylistItem.cs +++ b/osu.Game/Overlays/Music/PlaylistItem.cs @@ -63,7 +63,6 @@ namespace osu.Game.Overlays.Music { sprite.Font = OsuFont.GetFont(size: 14, weight: FontWeight.Bold); sprite.Colour = colours.Gray9; - sprite.Padding = new MarginPadding { Top = 1 }; }); SelectedSet.BindValueChanged(set => updateSelectionState(set.NewValue)); diff --git a/osu.Game/Overlays/Profile/Header/Components/PreviousUsernamesDisplay.cs b/osu.Game/Overlays/Profile/Header/Components/PreviousUsernamesDisplay.cs index dce5c84d12..1cd09566fb 100644 --- a/osu.Game/Overlays/Profile/Header/Components/PreviousUsernamesDisplay.cs +++ b/osu.Game/Overlays/Profile/Header/Components/PreviousUsernamesDisplay.cs @@ -85,7 +85,6 @@ namespace osu.Game.Overlays.Profile.Header.Components { RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y, - Direction = FillDirection.Full, // Prevents the tooltip of having a sudden size reduction and flickering when the text is being faded out. // Also prevents a potential OnHover/HoverLost feedback loop. AlwaysPresent = true, From 749df665d161fd27b253247980f7e441a528f6ef Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 11 Mar 2025 18:47:16 +0900 Subject: [PATCH 1263/1275] Focus search box immediately --- osu.Game/Screens/Ranking/UserTagControl.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/osu.Game/Screens/Ranking/UserTagControl.cs b/osu.Game/Screens/Ranking/UserTagControl.cs index 57b05f078c..a643bd6206 100644 --- a/osu.Game/Screens/Ranking/UserTagControl.cs +++ b/osu.Game/Screens/Ranking/UserTagControl.cs @@ -467,6 +467,7 @@ namespace osu.Game.Screens.Ranking { searchBox = new SearchTextBox { + HoldFocus = true, RelativeSizeAxes = Axes.X, }, searchContainer = new SearchContainer From 345f565b90b947eb6d353381a2cc5fc1d7a38a7e Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 11 Mar 2025 18:47:28 +0900 Subject: [PATCH 1264/1275] Allow using `Enter` key to select a single match --- osu.Game/Screens/Ranking/UserTagControl.cs | 39 ++++++++++++++++------ 1 file changed, 29 insertions(+), 10 deletions(-) diff --git a/osu.Game/Screens/Ranking/UserTagControl.cs b/osu.Game/Screens/Ranking/UserTagControl.cs index a643bd6206..2e559ff534 100644 --- a/osu.Game/Screens/Ranking/UserTagControl.cs +++ b/osu.Game/Screens/Ranking/UserTagControl.cs @@ -17,6 +17,7 @@ using osu.Framework.Graphics.Effects; using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.UserInterface; +using osu.Framework.Input.Events; using osu.Framework.Localisation; using osu.Game.Beatmaps; using osu.Game.Configuration; @@ -30,6 +31,7 @@ using osu.Game.Online.API; using osu.Game.Online.API.Requests; using osu.Game.Online.API.Requests.Responses; using osuTK; +using osuTK.Input; namespace osu.Game.Screens.Ranking { @@ -479,32 +481,49 @@ namespace osu.Game.Screens.Ranking Spacing = new Vector2(10), ChildrenEnumerable = ExtraTags.Select(tag => new DrawableExtraTag(tag) { - Action = () => - { - OnSelected?.Invoke(tag); - this.HidePopover(); - } + Action = () => select(tag) }) } }, }; } + private void select(UserTag tag) + { + OnSelected?.Invoke(tag); + this.HidePopover(); + } + protected override void LoadComplete() { base.LoadComplete(); searchBox.Current.BindValueChanged(_ => searchContainer.SearchTerm = searchBox.Current.Value, true); } + + protected override bool OnKeyDown(KeyDownEvent e) + { + var visibleItems = searchContainer.OfType().Where(d => d.IsPresent).ToArray(); + + if (e.Key == Key.Enter) + { + if (visibleItems.Length == 1) + select(visibleItems.Single().Tag); + + return true; + } + + return base.OnKeyDown(e); + } } private partial class DrawableExtraTag : OsuAnimatedButton, IFilterable { - private readonly UserTag tag; + public readonly UserTag Tag; public DrawableExtraTag(UserTag tag) { - this.tag = tag; + Tag = tag; RelativeSizeAxes = Axes.X; AutoSizeAxes = Axes.Y; @@ -535,20 +554,20 @@ namespace osu.Game.Screens.Ranking { RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y, - Text = tag.Name, + Text = Tag.Name, }, new OsuTextFlowContainer(t => t.Font = OsuFont.Default.With(size: 14)) { RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y, - Text = tag.Description, + Text = Tag.Description, } } } }); } - public IEnumerable FilterTerms => [tag.Name, tag.Description]; + public IEnumerable FilterTerms => [Tag.Name, Tag.Description]; public bool MatchingFilter { From e6fe6206475106d73801c9801498119189566022 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 11 Mar 2025 10:51:12 +0100 Subject: [PATCH 1265/1275] Improve tip threshold for click slider copy & tooltip --- osu.Game/Localisation/TabletSettingsStrings.cs | 5 +++++ osu.Game/Overlays/Settings/Sections/Input/TabletSettings.cs | 4 ++-- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/osu.Game/Localisation/TabletSettingsStrings.cs b/osu.Game/Localisation/TabletSettingsStrings.cs index 6c2e3c1f9c..ff0ced457f 100644 --- a/osu.Game/Localisation/TabletSettingsStrings.cs +++ b/osu.Game/Localisation/TabletSettingsStrings.cs @@ -59,6 +59,11 @@ namespace osu.Game.Localisation /// public static LocalisableString LockAspectRatio => new TranslatableString(getKey(@"lock_aspect_ratio"), @"Lock aspect ratio"); + /// + /// "Tip pressure for click" + /// + public static LocalisableString TipPressureForClick => new TranslatableString(getKey(@"tip_pressure_for_click"), "Tip pressure for click"); + private static string getKey(string key) => $@"{prefix}:{key}"; } } diff --git a/osu.Game/Overlays/Settings/Sections/Input/TabletSettings.cs b/osu.Game/Overlays/Settings/Sections/Input/TabletSettings.cs index 9d70e49659..e104bb7e39 100644 --- a/osu.Game/Overlays/Settings/Sections/Input/TabletSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/Input/TabletSettings.cs @@ -215,10 +215,10 @@ namespace osu.Game.Overlays.Settings.Sections.Input Current = sizeY, CanBeShown = { BindTarget = enabled } }, - new SettingsSlider + new SettingsPercentageSlider { TransferValueOnCommit = true, - LabelText = "Tip Threshold", + LabelText = TabletSettingsStrings.TipPressureForClick, Current = pressureThreshold, CanBeShown = { BindTarget = enabled } }, From 61e1234e0aeaa3fc30902d593a283ff786ed0d06 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 11 Mar 2025 10:52:03 +0100 Subject: [PATCH 1266/1275] Fix compile failure --- osu.Game.Tests/Visual/Settings/TestSceneTabletSettings.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/osu.Game.Tests/Visual/Settings/TestSceneTabletSettings.cs b/osu.Game.Tests/Visual/Settings/TestSceneTabletSettings.cs index 5ca08e0bba..95a134e204 100644 --- a/osu.Game.Tests/Visual/Settings/TestSceneTabletSettings.cs +++ b/osu.Game.Tests/Visual/Settings/TestSceneTabletSettings.cs @@ -135,6 +135,7 @@ namespace osu.Game.Tests.Visual.Settings public Bindable AreaSize { get; } = new Bindable(); public Bindable Rotation { get; } = new Bindable(); + public BindableFloat PressureThreshold { get; } = new BindableFloat(); public IBindable Tablet => tablet; From bcdc49e248b826b6dce387242d84c4710762dd1c Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 11 Mar 2025 18:54:17 +0900 Subject: [PATCH 1267/1275] Adjust naming and subclassing --- osu.Game/Screens/Ranking/UserTag.cs | 25 ++++ osu.Game/Screens/Ranking/UserTagControl.cs | 144 +++++++++------------ 2 files changed, 87 insertions(+), 82 deletions(-) create mode 100644 osu.Game/Screens/Ranking/UserTag.cs diff --git a/osu.Game/Screens/Ranking/UserTag.cs b/osu.Game/Screens/Ranking/UserTag.cs new file mode 100644 index 0000000000..d44e531330 --- /dev/null +++ b/osu.Game/Screens/Ranking/UserTag.cs @@ -0,0 +1,25 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Bindables; +using osu.Game.Online.API.Requests.Responses; + +namespace osu.Game.Screens.Ranking +{ + public record UserTag + { + public long Id { get; } + public string Name { get; } + public string Description { get; } + + public BindableInt VoteCount { get; } = new BindableInt(); + public BindableBool Voted { get; } = new BindableBool(); + + public UserTag(APITag tag) + { + Id = tag.Id; + Name = tag.Name; + Description = tag.Description; + } + } +} diff --git a/osu.Game/Screens/Ranking/UserTagControl.cs b/osu.Game/Screens/Ranking/UserTagControl.cs index 2e559ff534..7600d0aaae 100644 --- a/osu.Game/Screens/Ranking/UserTagControl.cs +++ b/osu.Game/Screens/Ranking/UserTagControl.cs @@ -79,12 +79,12 @@ namespace osu.Game.Screens.Ranking LayoutEasing = Easing.OutQuint, Spacing = new Vector2(4), }, - new ExtraTagsButton + new AddTagsButton { Anchor = Anchor.TopCentre, Origin = Anchor.TopCentre, OnTagSelected = onExtraTagSelected, - ExtraTags = { BindTarget = extraTags }, + AvailableTags = { BindTarget = extraTags }, }, }, }, @@ -420,13 +420,13 @@ namespace osu.Game.Screens.Ranking } } - private partial class ExtraTagsButton : GrayButton, IHasPopover + private partial class AddTagsButton : GrayButton, IHasPopover { - public BindableList ExtraTags { get; } = new BindableList(); + public BindableList AvailableTags { get; } = new BindableList(); public Action? OnTagSelected { get; set; } - public ExtraTagsButton() + public AddTagsButton() : base(FontAwesome.Solid.Plus) { Size = new Vector2(30); @@ -438,22 +438,22 @@ namespace osu.Game.Screens.Ranking { base.LoadComplete(); - ExtraTags.BindCollectionChanged((_, _) => Enabled.Value = ExtraTags.Count > 0, true); + AvailableTags.BindCollectionChanged((_, _) => Enabled.Value = AvailableTags.Count > 0, true); } - public Popover GetPopover() => new ExtraTagsPopover + public Popover GetPopover() => new AddTagsPopover { - ExtraTags = { BindTarget = ExtraTags }, + AvailableTags = { BindTarget = AvailableTags }, OnSelected = OnTagSelected, }; } - private partial class ExtraTagsPopover : OsuPopover + private partial class AddTagsPopover : OsuPopover { private SearchTextBox searchBox = null!; private SearchContainer searchContainer = null!; - public BindableList ExtraTags { get; } = new BindableList(); + public BindableList AvailableTags { get; } = new BindableList(); public Action? OnSelected { get; set; } @@ -479,7 +479,7 @@ namespace osu.Game.Screens.Ranking Direction = FillDirection.Vertical, Padding = new MarginPadding { Right = 5, Top = 50, }, Spacing = new Vector2(10), - ChildrenEnumerable = ExtraTags.Select(tag => new DrawableExtraTag(tag) + ChildrenEnumerable = AvailableTags.Select(tag => new DrawableAddableTag(tag) { Action = () => select(tag) }) @@ -488,12 +488,6 @@ namespace osu.Game.Screens.Ranking }; } - private void select(UserTag tag) - { - OnSelected?.Invoke(tag); - this.HidePopover(); - } - protected override void LoadComplete() { base.LoadComplete(); @@ -503,7 +497,7 @@ namespace osu.Game.Screens.Ranking protected override bool OnKeyDown(KeyDownEvent e) { - var visibleItems = searchContainer.OfType().Where(d => d.IsPresent).ToArray(); + var visibleItems = searchContainer.OfType().Where(d => d.IsPresent).ToArray(); if (e.Key == Key.Enter) { @@ -515,82 +509,68 @@ namespace osu.Game.Screens.Ranking return base.OnKeyDown(e); } - } - private partial class DrawableExtraTag : OsuAnimatedButton, IFilterable - { - public readonly UserTag Tag; - - public DrawableExtraTag(UserTag tag) + private void select(UserTag tag) { - Tag = tag; - - RelativeSizeAxes = Axes.X; - AutoSizeAxes = Axes.Y; - Anchor = Origin = Anchor.Centre; + OnSelected?.Invoke(tag); + this.HidePopover(); } - [BackgroundDependencyLoader] - private void load(OsuColour colours) + private partial class DrawableAddableTag : OsuAnimatedButton, IFilterable { - Content.AddRange(new Drawable[] + public readonly UserTag Tag; + + public DrawableAddableTag(UserTag tag) { - new Box + Tag = tag; + + RelativeSizeAxes = Axes.X; + AutoSizeAxes = Axes.Y; + Anchor = Origin = Anchor.Centre; + } + + [BackgroundDependencyLoader] + private void load(OsuColour colours) + { + Content.AddRange(new Drawable[] { - RelativeSizeAxes = Axes.Both, - Colour = colours.GreySeaFoamDark, - Depth = float.MaxValue, - }, - new FillFlowContainer - { - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, - Direction = FillDirection.Vertical, - Spacing = new Vector2(2), - Padding = new MarginPadding(5), - Children = new Drawable[] + new Box { - new OsuTextFlowContainer(t => t.Font = OsuFont.Default.With(weight: FontWeight.Bold)) + RelativeSizeAxes = Axes.Both, + Colour = colours.GreySeaFoamDark, + Depth = float.MaxValue, + }, + new FillFlowContainer + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Direction = FillDirection.Vertical, + Spacing = new Vector2(2), + Padding = new MarginPadding(5), + Children = new Drawable[] { - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, - Text = Tag.Name, - }, - new OsuTextFlowContainer(t => t.Font = OsuFont.Default.With(size: 14)) - { - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, - Text = Tag.Description, + new OsuTextFlowContainer(t => t.Font = OsuFont.Default.With(weight: FontWeight.Bold)) + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Text = Tag.Name, + }, + new OsuTextFlowContainer(t => t.Font = OsuFont.Default.With(size: 14)) + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Text = Tag.Description, + } } } - } - }); + }); + } + + public IEnumerable FilterTerms => [Tag.Name, Tag.Description]; + + public bool MatchingFilter { set => Alpha = value ? 1 : 0; } + public bool FilteringActive { set { } } } - - public IEnumerable FilterTerms => [Tag.Name, Tag.Description]; - - public bool MatchingFilter - { - set => Alpha = value ? 1 : 0; - } - - public bool FilteringActive { set { } } - } - } - - public record UserTag - { - public long Id { get; } - public string Name { get; } - public string Description { get; set; } - public BindableInt VoteCount { get; } = new BindableInt(); - public BindableBool Voted { get; } = new BindableBool(); - - public UserTag(APITag tag) - { - Id = tag.Id; - Name = tag.Name; - Description = tag.Description; } } } From c99448939258b8d7ec7c39b3d1f17b71d5dec38b Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 11 Mar 2025 21:28:35 +0900 Subject: [PATCH 1268/1275] Fix silly test failures --- osu.Game.Tests/Visual/Settings/TestSceneTabletSettings.cs | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/osu.Game.Tests/Visual/Settings/TestSceneTabletSettings.cs b/osu.Game.Tests/Visual/Settings/TestSceneTabletSettings.cs index 95a134e204..9f0dc75f84 100644 --- a/osu.Game.Tests/Visual/Settings/TestSceneTabletSettings.cs +++ b/osu.Game.Tests/Visual/Settings/TestSceneTabletSettings.cs @@ -135,7 +135,13 @@ namespace osu.Game.Tests.Visual.Settings public Bindable AreaSize { get; } = new Bindable(); public Bindable Rotation { get; } = new Bindable(); - public BindableFloat PressureThreshold { get; } = new BindableFloat(); + + public BindableFloat PressureThreshold { get; } = new BindableFloat + { + MinValue = 0f, + MaxValue = 1f, + Precision = 0.005f, + }; public IBindable Tablet => tablet; From 54d7a91cabc04e63873e53ec8ccefd69662e36f1 Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Wed, 12 Mar 2025 00:36:28 -0400 Subject: [PATCH 1269/1275] Fix osu!taiko mobile scaling not accurate --- .../UI/TaikoPlayfieldAdjustmentContainer.cs | 15 +-------------- 1 file changed, 1 insertion(+), 14 deletions(-) diff --git a/osu.Game.Rulesets.Taiko/UI/TaikoPlayfieldAdjustmentContainer.cs b/osu.Game.Rulesets.Taiko/UI/TaikoPlayfieldAdjustmentContainer.cs index 6a9e5789de..07fda13c8c 100644 --- a/osu.Game.Rulesets.Taiko/UI/TaikoPlayfieldAdjustmentContainer.cs +++ b/osu.Game.Rulesets.Taiko/UI/TaikoPlayfieldAdjustmentContainer.cs @@ -2,7 +2,6 @@ // See the LICENCE file in the repository root for full licence text. using System; -using osu.Framework; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; @@ -60,19 +59,7 @@ namespace osu.Game.Rulesets.Taiko.UI // Limit the maximum relative height of the playfield to one-third of available area to avoid it masking out on extreme resolutions. relativeHeight = Math.Min(relativeHeight, 1f / 3f); - Scale = new Vector2(Math.Max((Parent!.ChildSize.Y / 768f) * (relativeHeight / base_relative_height), 1f)); - - // on mobile platforms where the base aspect ratio is wider, the taiko playfield - // needs to be scaled down to remain playable. - if (RuntimeInfo.IsMobile && osuGame != null) - { - const float base_aspect_ratio = 1024f / 768f; - float gameAspectRatio = osuGame.ScalingContainerTargetDrawSize.X / osuGame.ScalingContainerTargetDrawSize.Y; - // this magic scale is unexplainable, but required so the playfield doesn't become too zoomed out as the aspect ratio increases. - const float magic_scale = 1.25f; - Scale *= magic_scale * new Vector2(base_aspect_ratio / gameAspectRatio); - } - + Scale = new Vector2(Parent!.ChildSize.Y / 768f * (relativeHeight / base_relative_height)); Width = 1 / Scale.X; } From 65cdcb469603ca22ec36872d20e07ad8f9fd563f Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 12 Mar 2025 14:01:11 +0900 Subject: [PATCH 1270/1275] Fix default beatmap not being correctly set after aborting new beatmap creation Closes https://github.com/ppy/osu/issues/32337. --- osu.Game.Tests/Visual/Editing/TestSceneEditorBeatmapCreation.cs | 2 ++ osu.Game/Beatmaps/WorkingBeatmapCache.cs | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/osu.Game.Tests/Visual/Editing/TestSceneEditorBeatmapCreation.cs b/osu.Game.Tests/Visual/Editing/TestSceneEditorBeatmapCreation.cs index 996e87ff8a..2758954907 100644 --- a/osu.Game.Tests/Visual/Editing/TestSceneEditorBeatmapCreation.cs +++ b/osu.Game.Tests/Visual/Editing/TestSceneEditorBeatmapCreation.cs @@ -94,6 +94,8 @@ namespace osu.Game.Tests.Visual.Editing AddStep("release", () => InputManager.ReleaseButton(MouseButton.Left)); AddAssert("new beatmap not persisted", () => beatmapManager.QueryBeatmapSet(s => s.ID == editorBeatmap.BeatmapInfo.BeatmapSet.AsNonNull().ID)?.Value.DeletePending == true); + + AddUntilStep("wait for default beatmap", () => Editor.Beatmap.Value is DummyWorkingBeatmap); } [Test] diff --git a/osu.Game/Beatmaps/WorkingBeatmapCache.cs b/osu.Game/Beatmaps/WorkingBeatmapCache.cs index fdeb840977..bd125deddf 100644 --- a/osu.Game/Beatmaps/WorkingBeatmapCache.cs +++ b/osu.Game/Beatmaps/WorkingBeatmapCache.cs @@ -89,7 +89,7 @@ namespace osu.Game.Beatmaps public virtual WorkingBeatmap GetWorkingBeatmap([CanBeNull] BeatmapInfo beatmapInfo) { - if (beatmapInfo?.BeatmapSet == null) + if (beatmapInfo?.ID == DefaultBeatmap.BeatmapInfo.ID || beatmapInfo?.BeatmapSet == null) return DefaultBeatmap; lock (workingCache) From 7f4f92dedf35e6933a2a3f242484eb81c2279e88 Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Wed, 12 Mar 2025 01:14:31 -0400 Subject: [PATCH 1271/1275] Remove unnecessary DI property --- .../UI/TaikoPlayfieldAdjustmentContainer.cs | 4 ---- 1 file changed, 4 deletions(-) diff --git a/osu.Game.Rulesets.Taiko/UI/TaikoPlayfieldAdjustmentContainer.cs b/osu.Game.Rulesets.Taiko/UI/TaikoPlayfieldAdjustmentContainer.cs index 07fda13c8c..9f821ee93d 100644 --- a/osu.Game.Rulesets.Taiko/UI/TaikoPlayfieldAdjustmentContainer.cs +++ b/osu.Game.Rulesets.Taiko/UI/TaikoPlayfieldAdjustmentContainer.cs @@ -2,7 +2,6 @@ // See the LICENCE file in the repository root for full licence text. using System; -using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Game.Rulesets.Taiko.Beatmaps; @@ -20,9 +19,6 @@ namespace osu.Game.Rulesets.Taiko.UI public readonly IBindable LockPlayfieldAspectRange = new BindableBool(true); - [Resolved] - private OsuGame? osuGame { get; set; } - public TaikoPlayfieldAdjustmentContainer() { RelativeSizeAxes = Axes.X; From 72854d0ae64c0c867b9b9bf2a601bfba6c115a1e Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 12 Mar 2025 14:20:13 +0900 Subject: [PATCH 1272/1275] Fix storyboard letterbox hiding HUD elements Addresses https://github.com/ppy/osu/discussions/29788. --- osu.Game.Tests/Visual/Gameplay/TestSceneBreakTracker.cs | 2 +- osu.Game/Screens/Play/BreakOverlay.cs | 8 +------- osu.Game/Screens/Play/Player.cs | 9 ++++++++- 3 files changed, 10 insertions(+), 9 deletions(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneBreakTracker.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneBreakTracker.cs index 21b6495865..9fc1ce3027 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneBreakTracker.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneBreakTracker.cs @@ -40,7 +40,7 @@ namespace osu.Game.Tests.Visual.Gameplay RelativeSizeAxes = Axes.Both, }, breakTracker = new TestBreakTracker(), - breakOverlay = new BreakOverlay(true, new ScoreProcessor(new OsuRuleset())) + breakOverlay = new BreakOverlay(new ScoreProcessor(new OsuRuleset())) { ProcessCustomClock = false, BreakTracker = breakTracker, diff --git a/osu.Game/Screens/Play/BreakOverlay.cs b/osu.Game/Screens/Play/BreakOverlay.cs index 550d29965f..49b7067c8d 100644 --- a/osu.Game/Screens/Play/BreakOverlay.cs +++ b/osu.Game/Screens/Play/BreakOverlay.cs @@ -46,7 +46,7 @@ namespace osu.Game.Screens.Play private readonly IBindable currentPeriod = new Bindable(); - public BreakOverlay(bool letterboxing, ScoreProcessor scoreProcessor) + public BreakOverlay(ScoreProcessor scoreProcessor) { this.scoreProcessor = scoreProcessor; RelativeSizeAxes = Axes.Both; @@ -63,12 +63,6 @@ namespace osu.Game.Screens.Play RelativeSizeAxes = Axes.Both, Children = new Drawable[] { - new LetterboxOverlay - { - Alpha = letterboxing ? 1 : 0, - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - }, new CircularContainer { Anchor = Anchor.Centre, diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs index 92c483b24a..29b54c8699 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -34,6 +34,7 @@ using osu.Game.Rulesets.UI; using osu.Game.Rulesets.UI.Scrolling; using osu.Game.Scoring; using osu.Game.Scoring.Legacy; +using osu.Game.Screens.Play.Break; using osu.Game.Screens.Play.HUD; using osu.Game.Screens.Ranking; using osu.Game.Skinning; @@ -450,6 +451,12 @@ namespace osu.Game.Screens.Play Children = new[] { DimmableStoryboard.OverlayLayerContainer.CreateProxy(), + new LetterboxOverlay + { + Alpha = working.Beatmap.LetterboxInBreaks ? 1 : 0, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + }, HUDOverlay = new HUDOverlay(DrawableRuleset, GameplayState.Mods, Configuration.AlwaysShowLeaderboard) { HoldToQuit = @@ -468,7 +475,7 @@ namespace osu.Game.Screens.Play Anchor = Anchor.Centre, Origin = Anchor.Centre }, - BreakOverlay = new BreakOverlay(working.Beatmap.LetterboxInBreaks, ScoreProcessor) + BreakOverlay = new BreakOverlay(ScoreProcessor) { Clock = DrawableRuleset.FrameStableClock, ProcessCustomClock = false, From 16afd5f1179f02a791949e3e35dafa8773c1e9fc Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 12 Mar 2025 17:18:30 +0900 Subject: [PATCH 1273/1275] Use reference check rather than `Guid` comparison --- osu.Game/Beatmaps/WorkingBeatmapCache.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Beatmaps/WorkingBeatmapCache.cs b/osu.Game/Beatmaps/WorkingBeatmapCache.cs index bd125deddf..30bbbbc1fe 100644 --- a/osu.Game/Beatmaps/WorkingBeatmapCache.cs +++ b/osu.Game/Beatmaps/WorkingBeatmapCache.cs @@ -89,7 +89,7 @@ namespace osu.Game.Beatmaps public virtual WorkingBeatmap GetWorkingBeatmap([CanBeNull] BeatmapInfo beatmapInfo) { - if (beatmapInfo?.ID == DefaultBeatmap.BeatmapInfo.ID || beatmapInfo?.BeatmapSet == null) + if (beatmapInfo == null || ReferenceEquals(beatmapInfo, DefaultBeatmap.BeatmapInfo)) return DefaultBeatmap; lock (workingCache) From 77aac2922f5135b9a9eac5a163bb79aae1c95391 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 12 Mar 2025 17:31:27 +0900 Subject: [PATCH 1274/1275] Fix `LetterboxOverlay` not handling its own visibility --- .../Visual/Gameplay/TestSceneBreakTracker.cs | 7 +- .../Gameplay/TestSceneLetterboxOverlay.cs | 24 ------ .../Screens/Play/Break/LetterboxOverlay.cs | 42 ---------- osu.Game/Screens/Play/BreakOverlay.cs | 4 +- osu.Game/Screens/Play/LetterboxOverlay.cs | 82 +++++++++++++++++++ osu.Game/Screens/Play/Player.cs | 6 +- 6 files changed, 93 insertions(+), 72 deletions(-) delete mode 100644 osu.Game.Tests/Visual/Gameplay/TestSceneLetterboxOverlay.cs delete mode 100644 osu.Game/Screens/Play/Break/LetterboxOverlay.cs create mode 100644 osu.Game/Screens/Play/LetterboxOverlay.cs diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneBreakTracker.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneBreakTracker.cs index 9fc1ce3027..844f5cba01 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneBreakTracker.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneBreakTracker.cs @@ -44,7 +44,12 @@ namespace osu.Game.Tests.Visual.Gameplay { ProcessCustomClock = false, BreakTracker = breakTracker, - } + }, + new LetterboxOverlay + { + ProcessCustomClock = false, + BreakTracker = breakTracker, + }, }; } diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneLetterboxOverlay.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneLetterboxOverlay.cs deleted file mode 100644 index ce93837925..0000000000 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneLetterboxOverlay.cs +++ /dev/null @@ -1,24 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -using osu.Framework.Graphics; -using osu.Framework.Graphics.Shapes; -using osu.Game.Screens.Play.Break; - -namespace osu.Game.Tests.Visual.Gameplay -{ - public partial class TestSceneLetterboxOverlay : OsuTestScene - { - public TestSceneLetterboxOverlay() - { - AddRange(new Drawable[] - { - new Box - { - RelativeSizeAxes = Axes.Both - }, - new LetterboxOverlay() - }); - } - } -} diff --git a/osu.Game/Screens/Play/Break/LetterboxOverlay.cs b/osu.Game/Screens/Play/Break/LetterboxOverlay.cs deleted file mode 100644 index 9308a02b07..0000000000 --- a/osu.Game/Screens/Play/Break/LetterboxOverlay.cs +++ /dev/null @@ -1,42 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -using osu.Framework.Graphics; -using osu.Framework.Graphics.Colour; -using osu.Framework.Graphics.Containers; -using osu.Framework.Graphics.Shapes; -using osuTK.Graphics; - -namespace osu.Game.Screens.Play.Break -{ - public partial class LetterboxOverlay : CompositeDrawable - { - private static readonly Color4 transparent_black = new Color4(0, 0, 0, 0); - - public LetterboxOverlay() - { - const int height = 150; - - RelativeSizeAxes = Axes.Both; - InternalChildren = new Drawable[] - { - new Box - { - Anchor = Anchor.TopLeft, - Origin = Anchor.TopLeft, - RelativeSizeAxes = Axes.X, - Height = height, - Colour = ColourInfo.GradientVertical(Color4.Black, transparent_black), - }, - new Box - { - Anchor = Anchor.BottomLeft, - Origin = Anchor.BottomLeft, - RelativeSizeAxes = Axes.X, - Height = height, - Colour = ColourInfo.GradientVertical(transparent_black, Color4.Black), - } - }; - } - } -} diff --git a/osu.Game/Screens/Play/BreakOverlay.cs b/osu.Game/Screens/Play/BreakOverlay.cs index 49b7067c8d..2ae66a6dc4 100644 --- a/osu.Game/Screens/Play/BreakOverlay.cs +++ b/osu.Game/Screens/Play/BreakOverlay.cs @@ -35,7 +35,7 @@ namespace osu.Game.Screens.Play public override bool RemoveCompletedTransforms => false; - public BreakTracker BreakTracker { get; init; } = null!; + public required BreakTracker BreakTracker { get; init; } private readonly Container remainingTimeAdjustmentBox; private readonly Container remainingTimeBox; @@ -159,7 +159,7 @@ namespace osu.Game.Screens.Play if (currentPeriod.Value == null) return; - float timeBoxTargetWidth = (float)Math.Max(0, (remainingTimeForCurrentPeriod - timingPoint.BeatLength / currentPeriod.Value.Value.Duration)); + float timeBoxTargetWidth = (float)Math.Max(0, remainingTimeForCurrentPeriod - timingPoint.BeatLength / currentPeriod.Value.Value.Duration); remainingTimeBox.ResizeWidthTo(timeBoxTargetWidth, timingPoint.BeatLength * 3.5, Easing.OutQuint); } diff --git a/osu.Game/Screens/Play/LetterboxOverlay.cs b/osu.Game/Screens/Play/LetterboxOverlay.cs new file mode 100644 index 0000000000..168c707c3b --- /dev/null +++ b/osu.Game/Screens/Play/LetterboxOverlay.cs @@ -0,0 +1,82 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Bindables; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Colour; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; +using osu.Game.Utils; +using osuTK.Graphics; + +namespace osu.Game.Screens.Play +{ + public partial class LetterboxOverlay : CompositeDrawable + { + public required BreakTracker BreakTracker { get; init; } + + private readonly Container fadeContainer; + + private readonly IBindable currentPeriod = new Bindable(); + + private static readonly Color4 transparent_black = new Color4(0, 0, 0, 0); + + public LetterboxOverlay() + { + const int letterbox_height = 150; + + RelativeSizeAxes = Axes.Both; + + InternalChild = fadeContainer = new Container + { + RelativeSizeAxes = Axes.Both, + Alpha = 0, + Children = new Drawable[] + { + new Box + { + Anchor = Anchor.TopLeft, + Origin = Anchor.TopLeft, + RelativeSizeAxes = Axes.X, + Height = letterbox_height, + Colour = ColourInfo.GradientVertical(Color4.Black, transparent_black), + }, + new Box + { + Anchor = Anchor.BottomLeft, + Origin = Anchor.BottomLeft, + RelativeSizeAxes = Axes.X, + Height = letterbox_height, + Colour = ColourInfo.GradientVertical(transparent_black, Color4.Black), + } + } + }; + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + currentPeriod.BindTo(BreakTracker.CurrentPeriod); + currentPeriod.BindValueChanged(updateDisplay, true); + } + + private void updateDisplay(ValueChangedEvent period) + { + FinishTransforms(true); + Scheduler.CancelDelayedTasks(); + + if (period.NewValue == null) + return; + + var b = period.NewValue.Value; + + using (BeginAbsoluteSequence(b.Start)) + { + fadeContainer.FadeIn(BreakOverlay.BREAK_FADE_DURATION); + using (BeginDelayedSequence(b.Duration - BreakOverlay.BREAK_FADE_DURATION)) + fadeContainer.FadeOut(BreakOverlay.BREAK_FADE_DURATION); + } + } + } +} diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs index 29b54c8699..b27e0b7477 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -34,7 +34,6 @@ using osu.Game.Rulesets.UI; using osu.Game.Rulesets.UI.Scrolling; using osu.Game.Scoring; using osu.Game.Scoring.Legacy; -using osu.Game.Screens.Play.Break; using osu.Game.Screens.Play.HUD; using osu.Game.Screens.Ranking; using osu.Game.Skinning; @@ -453,9 +452,10 @@ namespace osu.Game.Screens.Play DimmableStoryboard.OverlayLayerContainer.CreateProxy(), new LetterboxOverlay { + Clock = DrawableRuleset.FrameStableClock, + ProcessCustomClock = false, + BreakTracker = breakTracker, Alpha = working.Beatmap.LetterboxInBreaks ? 1 : 0, - Anchor = Anchor.Centre, - Origin = Anchor.Centre, }, HUDOverlay = new HUDOverlay(DrawableRuleset, GameplayState.Mods, Configuration.AlwaysShowLeaderboard) { From 8ef5a01bc15e5ec88c43020cdd5cf78a32e699bf Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 12 Mar 2025 17:46:05 +0900 Subject: [PATCH 1275/1275] Adjust visuals to match stable Was never a huge fan of the gradient we had. --- osu.Game/Screens/Play/LetterboxOverlay.cs | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/osu.Game/Screens/Play/LetterboxOverlay.cs b/osu.Game/Screens/Play/LetterboxOverlay.cs index 168c707c3b..4c934f56cd 100644 --- a/osu.Game/Screens/Play/LetterboxOverlay.cs +++ b/osu.Game/Screens/Play/LetterboxOverlay.cs @@ -3,7 +3,6 @@ using osu.Framework.Bindables; using osu.Framework.Graphics; -using osu.Framework.Graphics.Colour; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; using osu.Game.Utils; @@ -23,9 +22,8 @@ namespace osu.Game.Screens.Play public LetterboxOverlay() { - const int letterbox_height = 150; - RelativeSizeAxes = Axes.Both; + const float letterbox_height = 0.125f; InternalChild = fadeContainer = new Container { @@ -37,17 +35,17 @@ namespace osu.Game.Screens.Play { Anchor = Anchor.TopLeft, Origin = Anchor.TopLeft, - RelativeSizeAxes = Axes.X, + RelativeSizeAxes = Axes.Both, Height = letterbox_height, - Colour = ColourInfo.GradientVertical(Color4.Black, transparent_black), + Colour = Color4.Black, }, new Box { Anchor = Anchor.BottomLeft, Origin = Anchor.BottomLeft, - RelativeSizeAxes = Axes.X, + RelativeSizeAxes = Axes.Both, Height = letterbox_height, - Colour = ColourInfo.GradientVertical(transparent_black, Color4.Black), + Colour = Color4.Black, } } };