From 91fb59ee15caf75b08a43bd6508a4edf3f49e8f5 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Sun, 11 Feb 2024 07:37:13 +0300 Subject: [PATCH 01/84] 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 02/84] 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 03/84] 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 04/84] 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 05/84] 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 2fd495228c21f9bd8e8700aefdb1b93c69027d3e Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Fri, 25 Oct 2024 02:25:32 -0400 Subject: [PATCH 06/84] 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 07/84] 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 08/84] 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 09/84] 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 10/84] 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 4a628287e260e0120adc2dd102fcfc8c81930a78 Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Sun, 17 Nov 2024 18:13:37 -0500 Subject: [PATCH 11/84] 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 12/84] 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 13/84] 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 14/84] 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 15/84] 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 16/84] 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 74daf85e489bb5402504729303b0f2dd1fa14e2f Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Mon, 18 Nov 2024 06:40:14 -0500 Subject: [PATCH 17/84] 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 18/84] 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 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 19/84] 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 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 20/84] 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 21/84] 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 22/84] 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 c844d65a81da4606eee240ad58774d38953cedb4 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 22 Nov 2024 19:11:14 +0900 Subject: [PATCH 23/84] 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 24/84] 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 62837c7e53de8e9130c216b3b6c3158ef2504d78 Mon Sep 17 00:00:00 2001 From: Joseph Madamba Date: Sat, 9 Nov 2024 10:27:23 -0800 Subject: [PATCH 25/84] 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 eed02c2ab143b0fd1973906aa5a3d629f581eb49 Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Sat, 23 Nov 2024 15:45:29 -0500 Subject: [PATCH 26/84] 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 631bfadd68f2a58ef51f7d17c10271bfddc34de5 Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Sun, 24 Nov 2024 04:10:01 -0500 Subject: [PATCH 27/84] 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 aa1358b2b4576b2d9d08365503cb610328068255 Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Sun, 24 Nov 2024 04:33:03 -0500 Subject: [PATCH 28/84] 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 53b390667a844ca98dd0d7f1d1be0ebffd6c2133 Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Sun, 24 Nov 2024 06:04:36 -0500 Subject: [PATCH 29/84] 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 30/84] 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 a1916d12db3be7e55fd7ac1d184a1725cca4266d Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 25 Nov 2024 18:41:50 +0900 Subject: [PATCH 31/84] 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 32/84] 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 33/84] 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 5668258182537bdfaa6d6419e75a3d6f497e1d86 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 25 Nov 2024 19:43:42 +0900 Subject: [PATCH 34/84] 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 35/84] 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 36/84] 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 37/84] 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 38/84] 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 39/84] 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 40/84] 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 41/84] 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 d150aeef2b019ecf1cad5f4e3f5b16ea1473297b Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Tue, 26 Nov 2024 01:01:59 -0500 Subject: [PATCH 42/84] 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 43/84] 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 44/84] 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 b76460f1003a269d2893d2466a71dd8e8a4a3201 Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Tue, 26 Nov 2024 01:26:44 -0500 Subject: [PATCH 45/84] 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 46/84] 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 e0199386a38ef6581a8e78168e7080b1b34f9b0c Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 26 Nov 2024 17:33:39 +0900 Subject: [PATCH 47/84] 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 48/84] 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 49/84] 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 312336de24e730247b64af3b601bbe114d2d563f Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 26 Nov 2024 18:12:26 +0900 Subject: [PATCH 50/84] 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 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 51/84] 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 bd1f978138c2dc6d02f084f49be91c99b3902366 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Tue, 26 Nov 2024 21:35:10 +0900 Subject: [PATCH 52/84] 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 53/84] 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 54/84] 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 55/84] 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 56/84] 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 16d8b1138562d4350726ea0b3ed3053d73f805f0 Mon Sep 17 00:00:00 2001 From: HenintsoaSky Date: Wed, 27 Nov 2024 00:53:22 +0300 Subject: [PATCH 57/84] 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 58/84] 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 59/84] 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 60/84] 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 61/84] 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 62/84] 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 63/84] 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 64/84] 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 24c0799680c30223bdc1326bc3735807da950178 Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Wed, 27 Nov 2024 16:54:51 -0500 Subject: [PATCH 65/84] 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 66/84] 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 70eee8882a0ed4df045c0ea8f30764cd93cee88c Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 28 Nov 2024 15:42:37 +0900 Subject: [PATCH 67/84] 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 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 68/84] 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 69/84] 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 70/84] 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 66093872e85d8d08dba3860393fabbe87cf9a660 Mon Sep 17 00:00:00 2001 From: Hiviexd Date: Thu, 28 Nov 2024 12:49:30 +0100 Subject: [PATCH 71/84] 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 72/84] 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 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 73/84] 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 74/84] 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 75/84] 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 078d62fe093fc1ba9587cb6f3cdd4e4fec02e1f7 Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Thu, 28 Nov 2024 17:54:03 -0500 Subject: [PATCH 76/84] 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 77/84] 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 276c37bcf77b19762c84b9d4c89251597a492ea8 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 29 Nov 2024 13:47:56 +0900 Subject: [PATCH 78/84] 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 79/84] 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 80/84] 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 81/84] 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 82/84] 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 0e1b62ef8521d3cec72b0c60fbc557d25e94762a Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 29 Nov 2024 21:10:27 +0900 Subject: [PATCH 83/84] 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 84/84] 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);