From 0209129618e63f5ca6d22bc9dd283f7735fb3466 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 5 Mar 2025 12:25:57 +0100 Subject: [PATCH 001/164] Extract leaderboard fetch logic from song select beatmap leaderboard drawable RFC. Another attempt at this. - Supersedes https://github.com/ppy/osu/pull/31881 - Supersedes / closes https://github.com/ppy/osu/pull/31355 - Closes https://github.com/ppy/osu/issues/29861 This is a weird diff because I am feeling rather boxed in by all the constraints, namely that: - Leaderboard state should be global state - But the global state is essentially managed by song select and namely `BeatmapLeaderboard` itself. That's because trying to e.g. not have `BeatmapLeaderboard` pass the beatmap and the ruleset to the global leaderboard manager is worse, as it essentially introduces two parallel paths of execution that need to be somehow merged into one (as in I'd have to somehow sync `LeaderboardManager` responding to beatmap/ruleset changes with `BeatmapLeaderboard` which is inheritance hell) - Also local leaderboard fetching is data-push (as in the scores can change under the leaderboard manager), and online leaderboard fetching is data-pull (as in the scores do not change unless the leaderboard manager does something). Also online leaderboard fetching can fail. Which is why I need to still have the weird setup wherein there's a `FetchWithCriteriaAsync()` (because I need to be able to respond to online requests taking time, or failing), but also the `BeatmapLeaderboard` only uses the public `Scores` bindable to actually read the scores (because it needs to respond to new local scores arriving). - Another thing to think about here is what happens when a retrieval fails because e.g. the user requested friend leaderboards without having supporter. With how this diff is written, that special condition is handled to `BeatmapLeaderboard`, and `LeaderboardManager`'s state will remain as whatever it was before that scope change was requested, which may be considered good or it may not (I imagine it's better to show scores in gameplay than not in this case, but maybe I'm wrong?) --- .../SongSelect/TestSceneBeatmapLeaderboard.cs | 3 + .../Online/Leaderboards/LeaderboardManager.cs | 162 ++++++++++++++++++ osu.Game/OsuGameBase.cs | 5 + .../Select/Leaderboards/BeatmapLeaderboard.cs | 120 +++---------- 4 files changed, 192 insertions(+), 98 deletions(-) create mode 100644 osu.Game/Online/Leaderboards/LeaderboardManager.cs diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapLeaderboard.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapLeaderboard.cs index 474d2ee6e3..ebeba23123 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapLeaderboard.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapLeaderboard.cs @@ -42,6 +42,7 @@ namespace osu.Game.Tests.Visual.SongSelect private RulesetStore rulesetStore = null!; private BeatmapManager beatmapManager = null!; private PlaySongSelect songSelect = null!; + private LeaderboardManager leaderboardManager = null!; protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent) { @@ -52,6 +53,7 @@ namespace osu.Game.Tests.Visual.SongSelect dependencies.Cache(scoreManager = new ScoreManager(rulesetStore, () => beatmapManager, LocalStorage, Realm, API)); dependencies.CacheAs(songSelect = new PlaySongSelect()); Dependencies.Cache(Realm); + dependencies.Cache(leaderboardManager = new LeaderboardManager()); return dependencies; } @@ -60,6 +62,7 @@ namespace osu.Game.Tests.Visual.SongSelect private void load() { LoadComponent(songSelect); + LoadComponent(leaderboardManager); } public TestSceneBeatmapLeaderboard() diff --git a/osu.Game/Online/Leaderboards/LeaderboardManager.cs b/osu.Game/Online/Leaderboards/LeaderboardManager.cs new file mode 100644 index 0000000000..9104c83c02 --- /dev/null +++ b/osu.Game/Online/Leaderboards/LeaderboardManager.cs @@ -0,0 +1,162 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Threading.Tasks; +using osu.Framework.Allocation; +using osu.Framework.Bindables; +using osu.Framework.Graphics; +using osu.Game.Beatmaps; +using osu.Game.Database; +using osu.Game.Online.API; +using osu.Game.Online.API.Requests; +using osu.Game.Rulesets; +using osu.Game.Rulesets.Mods; +using osu.Game.Scoring; +using osu.Game.Screens.Select.Leaderboards; +using Realms; + +namespace osu.Game.Online.Leaderboards +{ + public partial class LeaderboardManager : Component + { + public IBindable Scores => scores; + private readonly Bindable scores = new Bindable(); + + private LeaderboardCriteria? criteria; + + private IDisposable? localScoreSubscription; + private TaskCompletionSource? localFetchCompletionSource; + private TaskCompletionSource? lastFetchCompletionSource; + private GetScoresRequest? inFlightOnlineRequest; + + [Resolved] + private IAPIProvider api { get; set; } = null!; + + [Resolved] + private RealmAccess realm { get; set; } = null!; + + [Resolved] + private RulesetStore rulesets { get; set; } = null!; + + public LeaderboardManager() + { + scores.BindValueChanged(_ => + { + if (localFetchCompletionSource != null && localFetchCompletionSource == lastFetchCompletionSource && scores.Value != null) + { + localFetchCompletionSource.SetResult(scores.Value); + localFetchCompletionSource = null; + } + }); + } + + public Task FetchWithCriteriaAsync(LeaderboardCriteria newCriteria) + { + if (criteria?.Equals(newCriteria) == true && lastFetchCompletionSource?.Task.IsFaulted == false) + return lastFetchCompletionSource?.Task ?? Task.FromResult(Scores.Value); + + criteria = newCriteria; + localScoreSubscription?.Dispose(); + inFlightOnlineRequest?.Cancel(); + lastFetchCompletionSource?.TrySetCanceled(); + scores.Value = null; + + switch (newCriteria.Scope) + { + case BeatmapLeaderboardScope.Local: + { + lastFetchCompletionSource = localFetchCompletionSource = new TaskCompletionSource(); + localScoreSubscription = realm.RegisterForNotifications(r => + r.All().Filter($"{nameof(ScoreInfo.BeatmapInfo)}.{nameof(BeatmapInfo.ID)} == $0" + + $" AND {nameof(ScoreInfo.BeatmapInfo)}.{nameof(BeatmapInfo.Hash)} == {nameof(ScoreInfo.BeatmapHash)}" + + $" AND {nameof(ScoreInfo.Ruleset)}.{nameof(RulesetInfo.ShortName)} == $1" + + $" AND {nameof(ScoreInfo.DeletePending)} == false" + , newCriteria.Beatmap.ID, newCriteria.Ruleset.ShortName), localScoresChanged); + return localFetchCompletionSource.Task; + } + + default: + { + var onlineFetchCompletionSource = new TaskCompletionSource(); + lastFetchCompletionSource = onlineFetchCompletionSource; + + IReadOnlyList? requestMods = null; + + if (newCriteria.ExactMods != null) + { + if (!newCriteria.ExactMods.Any()) + // add nomod for the request + requestMods = new Mod[] { new ModNoMod() }; + else + requestMods = newCriteria.ExactMods; + } + + var newRequest = new GetScoresRequest(newCriteria.Beatmap, newCriteria.Ruleset, newCriteria.Scope, requestMods); + newRequest.Success += response => + { + if (inFlightOnlineRequest != null && !newRequest.Equals(inFlightOnlineRequest)) + return; + + var result = new LeaderboardScores + ( + response.Scores.Select(s => s.ToScoreInfo(rulesets, newCriteria.Beatmap)).OrderByTotalScore(), + response.UserScore?.CreateScoreInfo(rulesets, newCriteria.Beatmap) + ); + inFlightOnlineRequest = null; + if (onlineFetchCompletionSource.TrySetResult(result)) + scores.Value = result; + }; + newRequest.Failure += ex => onlineFetchCompletionSource.TrySetException(ex); + api.Queue(inFlightOnlineRequest = newRequest); + return onlineFetchCompletionSource.Task; + } + } + } + + private void localScoresChanged(IRealmCollection sender, ChangeSet? changes) + { + Debug.Assert(criteria != null); + + // This subscription may fire from changes to linked beatmaps, which we don't care about. + // It's currently not possible for a score to be modified after insertion, so we can safely ignore callbacks with only modifications. + if (changes?.HasCollectionChanges() == false) + return; + + var newScores = sender.AsEnumerable(); + + if (criteria.ExactMods != null) + { + if (!criteria.ExactMods.Any()) + { + // we need to filter out all scores that have any mods to get all local nomod scores + newScores = newScores.Where(s => !s.Mods.Any()); + } + else + { + // otherwise find all the scores that have all of the currently selected mods (similar to how web applies mod filters) + // we're creating and using a string HashSet representation of selected mods so that it can be translated into the DB query itself + var selectedMods = criteria.ExactMods.Select(m => m.Acronym).ToHashSet(); + + newScores = newScores.Where(s => selectedMods.SetEquals(s.Mods.Select(m => m.Acronym))); + } + } + + newScores = newScores.Detach().OrderByTotalScore(); + + scores.Value = new LeaderboardScores(newScores, null); + } + } + + public record LeaderboardCriteria( + BeatmapInfo Beatmap, + RulesetInfo Ruleset, + BeatmapLeaderboardScope Scope, + Mod[]? ExactMods + ); + + public record LeaderboardScores(IEnumerable TopScores, ScoreInfo? UserScore); +} diff --git a/osu.Game/OsuGameBase.cs b/osu.Game/OsuGameBase.cs index 4087a8b71e..fb28b8c5a4 100644 --- a/osu.Game/OsuGameBase.cs +++ b/osu.Game/OsuGameBase.cs @@ -49,6 +49,7 @@ using osu.Game.Localisation; using osu.Game.Online; using osu.Game.Online.API; using osu.Game.Online.Chat; +using osu.Game.Online.Leaderboards; using osu.Game.Online.Metadata; using osu.Game.Online.Multiplayer; using osu.Game.Online.Spectator; @@ -203,6 +204,7 @@ namespace osu.Game private UserLookupCache userCache; private BeatmapLookupCache beatmapCache; + private LeaderboardManager leaderboardManager; private RulesetConfigCache rulesetConfigCache; @@ -365,6 +367,9 @@ namespace osu.Game dependencies.CacheAs>(Beatmap); dependencies.CacheAs(Beatmap); + dependencies.Cache(leaderboardManager = new LeaderboardManager()); + base.Content.Add(leaderboardManager); + // add api components to hierarchy. if (API is APIAccess apiAccess) base.Content.Add(apiAccess); diff --git a/osu.Game/Screens/Select/Leaderboards/BeatmapLeaderboard.cs b/osu.Game/Screens/Select/Leaderboards/BeatmapLeaderboard.cs index 46705aaa28..e435554b03 100644 --- a/osu.Game/Screens/Select/Leaderboards/BeatmapLeaderboard.cs +++ b/osu.Game/Screens/Select/Leaderboards/BeatmapLeaderboard.cs @@ -3,21 +3,17 @@ using System; using System.Collections.Generic; -using System.Diagnostics; using System.Linq; using System.Threading; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.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.Leaderboards; using osu.Game.Rulesets; using osu.Game.Rulesets.Mods; using osu.Game.Scoring; -using Realms; namespace osu.Game.Screens.Select.Leaderboards { @@ -67,6 +63,8 @@ namespace osu.Game.Screens.Select.Leaderboards } } + private readonly IBindable fetchedScores = new Bindable(); + [Resolved] private IBindable ruleset { get; set; } = null!; @@ -77,14 +75,7 @@ namespace osu.Game.Screens.Select.Leaderboards private IAPIProvider api { get; set; } = null!; [Resolved] - private RulesetStore rulesets { get; set; } = null!; - - [Resolved] - private RealmAccess realm { get; set; } = null!; - - private IDisposable? scoreSubscription; - - private GetScoresRequest? scoreRetrievalRequest; + private LeaderboardManager leaderboardManager { get; set; } = null!; [BackgroundDependencyLoader] private void load() @@ -95,15 +86,23 @@ namespace osu.Game.Screens.Select.Leaderboards if (filterMods) RefetchScores(); }; + fetchedScores.BindTo(leaderboardManager.Scores); + } + + protected override void LoadComplete() + { + base.LoadComplete(); + fetchedScores.BindValueChanged(_ => + { + if (fetchedScores.Value != null) + SetScores(fetchedScores.Value.TopScores, fetchedScores.Value.UserScore); + }); } protected override bool IsOnlineScope => Scope != BeatmapLeaderboardScope.Local; protected override APIRequest? FetchScores(CancellationToken cancellationToken) { - scoreRetrievalRequest?.Cancel(); - scoreRetrievalRequest = null; - var fetchBeatmapInfo = BeatmapInfo; if (fetchBeatmapInfo == null) @@ -114,12 +113,6 @@ namespace osu.Game.Screens.Select.Leaderboards var fetchRuleset = ruleset.Value ?? fetchBeatmapInfo.Ruleset; - if (Scope == BeatmapLeaderboardScope.Local) - { - subscribeToLocalScores(fetchBeatmapInfo, cancellationToken); - return null; - } - if (!api.IsLoggedIn) { SetErrorState(LeaderboardState.NotLoggedIn); @@ -132,7 +125,7 @@ namespace osu.Game.Screens.Select.Leaderboards return null; } - if (fetchBeatmapInfo.OnlineID <= 0 || fetchBeatmapInfo.Status <= BeatmapOnlineStatus.Pending) + if ((fetchBeatmapInfo.OnlineID <= 0 || fetchBeatmapInfo.Status <= BeatmapOnlineStatus.Pending) && IsOnlineScope) { SetErrorState(LeaderboardState.BeatmapUnavailable); return null; @@ -150,29 +143,14 @@ namespace osu.Game.Screens.Select.Leaderboards return null; } - IReadOnlyList? requestMods = null; + leaderboardManager.FetchWithCriteriaAsync(new LeaderboardCriteria(fetchBeatmapInfo, fetchRuleset, Scope, filterMods ? mods.Value.ToArray() : null)) + .ContinueWith(t => + { + if (t.Exception != null && !t.IsCanceled) + Schedule(() => SetErrorState(LeaderboardState.NetworkFailure)); + }, cancellationToken); - if (filterMods && !mods.Value.Any()) - // add nomod for the request - requestMods = new Mod[] { new ModNoMod() }; - else if (filterMods) - requestMods = mods.Value; - - var newRequest = new GetScoresRequest(fetchBeatmapInfo, fetchRuleset, Scope, requestMods); - newRequest.Success += response => Schedule(() => - { - // Request may have changed since fetch request. - // Can't rely on request cancellation due to Schedule inside SetScores so let's play it safe. - if (!newRequest.Equals(scoreRetrievalRequest)) - return; - - SetScores( - response.Scores.Select(s => s.ToScoreInfo(rulesets, fetchBeatmapInfo)).OrderByTotalScore(), - response.UserScore?.CreateScoreInfo(rulesets, fetchBeatmapInfo) - ); - }); - - return scoreRetrievalRequest = newRequest; + return null; } protected override LeaderboardScore CreateDrawableScore(ScoreInfo model, int index) => new LeaderboardScore(model, index, IsOnlineScope, Scope != BeatmapLeaderboardScope.Friend) @@ -184,59 +162,5 @@ namespace osu.Game.Screens.Select.Leaderboards { Action = () => ScoreSelected?.Invoke(model) }; - - private void subscribeToLocalScores(BeatmapInfo beatmapInfo, CancellationToken cancellationToken) - { - Debug.Assert(beatmapInfo != null); - - scoreSubscription?.Dispose(); - scoreSubscription = null; - - scoreSubscription = realm.RegisterForNotifications(r => - r.All().Filter($"{nameof(ScoreInfo.BeatmapInfo)}.{nameof(BeatmapInfo.ID)} == $0" - + $" AND {nameof(ScoreInfo.BeatmapInfo)}.{nameof(BeatmapInfo.Hash)} == {nameof(ScoreInfo.BeatmapHash)}" - + $" AND {nameof(ScoreInfo.Ruleset)}.{nameof(RulesetInfo.ShortName)} == $1" - + $" AND {nameof(ScoreInfo.DeletePending)} == false" - , beatmapInfo.ID, ruleset.Value.ShortName), localScoresChanged); - - void localScoresChanged(IRealmCollection sender, ChangeSet? changes) - { - if (cancellationToken.IsCancellationRequested) - return; - - // This subscription may fire from changes to linked beatmaps, which we don't care about. - // It's currently not possible for a score to be modified after insertion, so we can safely ignore callbacks with only modifications. - if (changes?.HasCollectionChanges() == false) - return; - - var scores = sender.AsEnumerable(); - - if (filterMods && !mods.Value.Any()) - { - // we need to filter out all scores that have any mods to get all local nomod scores - scores = scores.Where(s => !s.Mods.Any()); - } - else if (filterMods) - { - // otherwise find all the scores that have all of the currently selected mods (similar to how web applies mod filters) - // we're creating and using a string HashSet representation of selected mods so that it can be translated into the DB query itself - var selectedMods = mods.Value.Select(m => m.Acronym).ToHashSet(); - - scores = scores.Where(s => selectedMods.SetEquals(s.Mods.Select(m => m.Acronym))); - } - - scores = scores.Detach().OrderByTotalScore(); - - SetScores(scores); - } - } - - protected override void Dispose(bool isDisposing) - { - base.Dispose(isDisposing); - - scoreSubscription?.Dispose(); - scoreRetrievalRequest?.Cancel(); - } } } From afce72896f437d0000ed9610be3925ccf13f2f47 Mon Sep 17 00:00:00 2001 From: Zihad Date: Thu, 27 Mar 2025 22:37:37 +0600 Subject: [PATCH 002/164] Implement blocking users from context menu --- osu.Game/Online/API/APIAccess.cs | 31 +++++++++++++++ osu.Game/Online/API/DummyAPIAccess.cs | 6 +++ osu.Game/Online/API/IAPIProvider.cs | 10 +++++ .../Online/API/Requests/BlockUserRequest.cs | 30 +++++++++++++++ .../Online/API/Requests/GetBlocksRequest.cs | 13 +++++++ .../Online/API/Requests/UnblockUserRequest.cs | 27 +++++++++++++ osu.Game/Users/UserPanel.cs | 38 +++++++++++++++++++ 7 files changed, 155 insertions(+) create mode 100644 osu.Game/Online/API/Requests/BlockUserRequest.cs create mode 100644 osu.Game/Online/API/Requests/GetBlocksRequest.cs create mode 100644 osu.Game/Online/API/Requests/UnblockUserRequest.cs diff --git a/osu.Game/Online/API/APIAccess.cs b/osu.Game/Online/API/APIAccess.cs index 36712fbdaa..51fadb521a 100644 --- a/osu.Game/Online/API/APIAccess.cs +++ b/osu.Game/Online/API/APIAccess.cs @@ -58,6 +58,7 @@ namespace osu.Game.Online.API public IBindable LocalUser => localUser; public IBindableList Friends => friends; + public IBindableList Blocks => blocks; public INotificationsClient NotificationsClient { get; } @@ -66,6 +67,7 @@ namespace osu.Game.Online.API private Bindable localUser { get; } = new Bindable(createGuestUser()); private BindableList friends { get; } = new BindableList(); + private BindableList blocks { get; } = new BindableList(); protected bool HasLogin => authentication.Token.Value != null || (!string.IsNullOrEmpty(ProvidedUsername) && !string.IsNullOrEmpty(password)); @@ -638,6 +640,35 @@ namespace osu.Game.Online.API Queue(friendsReq); } + public void UpdateLocalBlocks() + { + if (!IsLoggedIn) + return; + + var blocksReq = new GetBlocksRequest(); + blocksReq.Failure += ex => + { + if (ex is not WebRequestFlushedException) + state.Value = APIState.Failing; + }; + blocksReq.Success += res => + { + var existingBlocks = blocks.Select(f => f.TargetID).ToHashSet(); + var updatedBlocks = res.Select(f => f.TargetID).ToHashSet(); + + // Add new blocked users to local list. + blocks.AddRange(res.Where(r => !existingBlocks.Contains(r.TargetID))); + + // Remove non-blocked users from local list. + blocks.RemoveAll(b => !updatedBlocks.Contains(b.TargetID)); + + // Remove friends who got blocked since last check. + friends.RemoveAll(f => updatedBlocks.Contains(f.TargetID)); + }; + + Queue(blocksReq); + } + private static APIUser createGuestUser() => new GuestUser(); protected override void Dispose(bool isDisposing) diff --git a/osu.Game/Online/API/DummyAPIAccess.cs b/osu.Game/Online/API/DummyAPIAccess.cs index f9649cdd88..0c2ed9903c 100644 --- a/osu.Game/Online/API/DummyAPIAccess.cs +++ b/osu.Game/Online/API/DummyAPIAccess.cs @@ -26,6 +26,7 @@ namespace osu.Game.Online.API }); public BindableList Friends { get; } = new BindableList(); + public BindableList Blocks { get; } = new BindableList(); public DummyNotificationsClient NotificationsClient { get; } = new DummyNotificationsClient(); INotificationsClient IAPIProvider.NotificationsClient => NotificationsClient; @@ -180,6 +181,10 @@ namespace osu.Game.Online.API { } + public void UpdateLocalBlocks() + { + } + public IHubClientConnector? GetHubConnector(string clientName, string endpoint, bool preferMessagePack) => null; public IChatClient GetChatClient() => new TestChatClientConnector(this); @@ -194,6 +199,7 @@ namespace osu.Game.Online.API IBindable IAPIProvider.LocalUser => LocalUser; IBindableList IAPIProvider.Friends => Friends; + IBindableList IAPIProvider.Blocks => Blocks; /// /// Skip 2FA requirement for next login. diff --git a/osu.Game/Online/API/IAPIProvider.cs b/osu.Game/Online/API/IAPIProvider.cs index 54eaaaafc2..3ab985e41f 100644 --- a/osu.Game/Online/API/IAPIProvider.cs +++ b/osu.Game/Online/API/IAPIProvider.cs @@ -23,6 +23,11 @@ namespace osu.Game.Online.API /// IBindableList Friends { get; } + /// + /// The users blocked by the local user. + /// + IBindableList Blocks { get; } + /// /// The language supplied by this provider to API requests. /// @@ -118,6 +123,11 @@ namespace osu.Game.Online.API /// void UpdateLocalFriends(); + /// + /// Update the list of users blocked by the current user. + /// + void UpdateLocalBlocks(); + /// /// Schedule a callback to run on the update thread. /// diff --git a/osu.Game/Online/API/Requests/BlockUserRequest.cs b/osu.Game/Online/API/Requests/BlockUserRequest.cs new file mode 100644 index 0000000000..bfcce075eb --- /dev/null +++ b/osu.Game/Online/API/Requests/BlockUserRequest.cs @@ -0,0 +1,30 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Net.Http; +using osu.Framework.IO.Network; + +namespace osu.Game.Online.API.Requests +{ + public class BlockUserRequest : APIRequest + { + public readonly int TargetId; + + public BlockUserRequest(int targetId) + { + TargetId = targetId; + } + + protected override WebRequest CreateWebRequest() + { + var req = base.CreateWebRequest(); + + req.Method = HttpMethod.Post; + req.AddParameter("target", TargetId.ToString(), RequestParameterType.Query); + + return req; + } + + protected override string Target => @"blocks"; + } +} diff --git a/osu.Game/Online/API/Requests/GetBlocksRequest.cs b/osu.Game/Online/API/Requests/GetBlocksRequest.cs new file mode 100644 index 0000000000..c16c256870 --- /dev/null +++ b/osu.Game/Online/API/Requests/GetBlocksRequest.cs @@ -0,0 +1,13 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Collections.Generic; +using osu.Game.Online.API.Requests.Responses; + +namespace osu.Game.Online.API.Requests +{ + public class GetBlocksRequest : APIRequest> + { + protected override string Target => @"blocks"; + } +} diff --git a/osu.Game/Online/API/Requests/UnblockUserRequest.cs b/osu.Game/Online/API/Requests/UnblockUserRequest.cs new file mode 100644 index 0000000000..5f88631776 --- /dev/null +++ b/osu.Game/Online/API/Requests/UnblockUserRequest.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 UnblockUserRequest : APIRequest + { + public readonly int TargetId; + + public UnblockUserRequest(int targetId) + { + TargetId = targetId; + } + + protected override WebRequest CreateWebRequest() + { + var req = base.CreateWebRequest(); + req.Method = HttpMethod.Delete; + return req; + } + + protected override string Target => @$"blocks/{TargetId}"; + } +} diff --git a/osu.Game/Users/UserPanel.cs b/osu.Game/Users/UserPanel.cs index 1010234e1f..76b7894a9e 100644 --- a/osu.Game/Users/UserPanel.cs +++ b/osu.Game/Users/UserPanel.cs @@ -14,6 +14,7 @@ using osu.Game.Overlays; using osu.Framework.Graphics.UserInterface; using osu.Game.Graphics.UserInterface; using osu.Framework.Graphics.Cursor; +using osu.Framework.Graphics.Sprites; using osu.Framework.Localisation; using osu.Framework.Screens; using osu.Game.Graphics.Containers; @@ -22,8 +23,10 @@ using osu.Game.Online.API.Requests.Responses; using osu.Game.Online.Chat; using osu.Game.Resources.Localisation.Web; using osu.Game.Localisation; +using osu.Game.Online.API.Requests; using osu.Game.Online.Metadata; using osu.Game.Online.Multiplayer; +using osu.Game.Overlays.Notifications; using osu.Game.Screens; using osu.Game.Screens.Play; using osu.Game.Users.Drawables; @@ -80,6 +83,11 @@ namespace osu.Game.Users [Resolved] private MetadataClient? metadataClient { get; set; } + [Resolved] + private INotificationOverlay? notifications { get; set; } + + private LoadingLayer loading { get; set; } = null!; + [BackgroundDependencyLoader] private void load() { @@ -96,6 +104,7 @@ namespace osu.Game.Users Add(background); Add(CreateLayout()); + Add(loading = new LoadingLayer(true)); base.Action = ViewProfile = () => { @@ -157,6 +166,10 @@ namespace osu.Game.Users chatOverlay?.Show(); })); + items.Add(isUserBlocked() + ? new OsuMenuItem(UsersStrings.BlocksButtonUnblock, MenuItemType.Standard, () => blockUser(false)) + : new OsuMenuItem(UsersStrings.BlocksButtonBlock, MenuItemType.Destructive, () => blockUser(true))); + if (isUserOnline()) { items.Add(new OsuMenuItem(ContextMenuStrings.SpectatePlayer, MenuItemType.Standard, () => @@ -179,9 +192,34 @@ namespace osu.Game.Users bool isUserOnline() => metadataClient?.GetPresence(User.OnlineID) != null; bool canInviteUser() => isUserOnline() && multiplayerClient?.Room?.Users.All(u => u.UserID != User.Id) == true; + bool isUserBlocked() => api.Blocks.Any(b => b.TargetID == User.OnlineID); } } + private void blockUser(bool block) + { + loading.Show(); + APIRequest req = block ? new BlockUserRequest(User.OnlineID) : new UnblockUserRequest(User.OnlineID); + + req.Success += () => + { + api.UpdateLocalBlocks(); + loading.Hide(); + }; + + req.Failure += e => + { + notifications?.Post(new SimpleNotification + { + Text = e.Message, + Icon = FontAwesome.Solid.Times, + }); + loading.Hide(); + }; + + api.Queue(req); + } + public IEnumerable FilterTerms => [User.Username]; public bool MatchingFilter From fbdea8f99019779e15babf2e41b9dda6da84aa70 Mon Sep 17 00:00:00 2001 From: Zihad Date: Thu, 27 Mar 2025 22:53:51 +0600 Subject: [PATCH 003/164] Add failing test --- .../Online/TestSceneCurrentlyOnlineDisplay.cs | 82 +++++++++++++++++++ 1 file changed, 82 insertions(+) diff --git a/osu.Game.Tests/Visual/Online/TestSceneCurrentlyOnlineDisplay.cs b/osu.Game.Tests/Visual/Online/TestSceneCurrentlyOnlineDisplay.cs index a1d0d40811..8e99212bcb 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneCurrentlyOnlineDisplay.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneCurrentlyOnlineDisplay.cs @@ -9,6 +9,7 @@ using NUnit.Framework; using osu.Framework.Graphics; using osu.Framework.Testing; using osu.Game.Database; +using osu.Game.Online.API; using osu.Game.Online.API.Requests.Responses; using osu.Game.Online.Metadata; using osu.Game.Online.Spectator; @@ -99,6 +100,87 @@ namespace osu.Game.Tests.Visual.Online AddStep("End watching user presence", () => token.Dispose()); } + [Test] + public void TestBlockedUsersHidden() + { + IDisposable token = null!; + + AddStep("Clear blocks", () => + { + DummyAPIAccess api = (DummyAPIAccess)API; + api.Blocks.Clear(); + }); + + AddStep("Begin watching user presence", () => token = metadataClient.BeginWatchingUserPresence()); + AddStep("Add online user", () => metadataClient.UserPresenceUpdated(streamingUser.Id, new UserPresence { Status = UserStatus.Online, Activity = new UserActivity.InSoloGame() })); + AddUntilStep("Panel loaded", () => currentlyOnline.ChildrenOfType().FirstOrDefault()?.User.Id == streamingUser.Id); + + AddStep("Block online user", () => + { + DummyAPIAccess api = (DummyAPIAccess)API; + api.Blocks.Add(new APIRelation() + { + RelationType = RelationType.Block, + TargetUser = streamingUser, + TargetID = streamingUser.Id + }); + }); + + AddAssert("Blocked user not shown", () => currentlyOnline.ChildrenOfType().All(p => p.User.Id != streamingUser.Id)); + + AddStep("Unblock online user", () => + { + DummyAPIAccess api = (DummyAPIAccess)API; + api.Blocks.RemoveAll(b => b.TargetID == streamingUser.Id); + }); + + AddAssert("Unblocked user shown again", () => currentlyOnline.ChildrenOfType().Any(p => p.User.Id == streamingUser.Id)); + + AddStep("Remove playing user", () => metadataClient.UserPresenceUpdated(streamingUser.Id, null)); + AddStep("End watching user presence", () => token.Dispose()); + } + + [Test] + public void TestUnblockedOfflineUsersHidden() + { + IDisposable token = null!; + + AddStep("Clear blocks", () => + { + DummyAPIAccess api = (DummyAPIAccess)API; + api.Blocks.Clear(); + }); + + AddStep("Begin watching user presence", () => token = metadataClient.BeginWatchingUserPresence()); + AddStep("Add online user", () => metadataClient.UserPresenceUpdated(streamingUser.Id, new UserPresence { Status = UserStatus.Online, Activity = new UserActivity.InSoloGame() })); + AddUntilStep("Panel loaded", () => currentlyOnline.ChildrenOfType().FirstOrDefault()?.User.Id == streamingUser.Id); + + AddStep("Block online user", () => + { + DummyAPIAccess api = (DummyAPIAccess)API; + api.Blocks.Add(new APIRelation() + { + RelationType = RelationType.Block, + TargetUser = streamingUser, + TargetID = streamingUser.Id + }); + }); + + AddAssert("Blocked user not shown", () => currentlyOnline.ChildrenOfType().All(p => p.User.Id != streamingUser.Id)); + + AddStep("Remove playing user", () => metadataClient.UserPresenceUpdated(streamingUser.Id, null)); + + AddStep("Unblock offline user", () => + { + DummyAPIAccess api = (DummyAPIAccess)API; + api.Blocks.RemoveAll(b => b.TargetID == streamingUser.Id); + }); + + AddAssert("Unblocked offline user not shown", () => currentlyOnline.ChildrenOfType().All(p => p.User.Id != streamingUser.Id)); + + AddStep("End watching user presence", () => token.Dispose()); + } + internal partial class TestUserLookupCache : UserLookupCache { private static readonly string[] usernames = From 7ca3a1895a412092da45c7db10c3c7babec59b40 Mon Sep 17 00:00:00 2001 From: Zihad Date: Thu, 27 Mar 2025 23:10:11 +0600 Subject: [PATCH 004/164] Hide blocked users from currently online --- .../Dashboard/CurrentlyOnlineDisplay.cs | 64 +++++++++++++++++-- 1 file changed, 57 insertions(+), 7 deletions(-) diff --git a/osu.Game/Overlays/Dashboard/CurrentlyOnlineDisplay.cs b/osu.Game/Overlays/Dashboard/CurrentlyOnlineDisplay.cs index 39df3ba22c..bda23078d9 100644 --- a/osu.Game/Overlays/Dashboard/CurrentlyOnlineDisplay.cs +++ b/osu.Game/Overlays/Dashboard/CurrentlyOnlineDisplay.cs @@ -2,7 +2,9 @@ // See the LICENCE file in the repository root for full licence text. using System.Collections.Generic; +using System.Collections.Specialized; using System.Diagnostics; +using System.Linq; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Extensions; @@ -14,6 +16,7 @@ using osu.Framework.Localisation; using osu.Framework.Screens; using osu.Game.Database; using osu.Game.Graphics.UserInterface; +using osu.Game.Online.API; using osu.Game.Online.API.Requests.Responses; using osu.Game.Online.Metadata; using osu.Game.Resources.Localisation.Web; @@ -31,6 +34,7 @@ namespace osu.Game.Overlays.Dashboard private const float padding = 10; private readonly IBindableDictionary onlineUserPresences = new BindableDictionary(); + private readonly IBindableList blockedUsers = new BindableList(); private readonly Dictionary userPanels = new Dictionary(); private SearchContainer userFlow = null!; @@ -42,6 +46,9 @@ namespace osu.Game.Overlays.Dashboard [Resolved] private UserLookupCache users { get; set; } = null!; + [Resolved] + private IAPIProvider api { get; set; } = null!; + [BackgroundDependencyLoader] private void load(OverlayColourProvider colourProvider) { @@ -95,6 +102,8 @@ namespace osu.Game.Overlays.Dashboard onlineUserPresences.BindTo(metadataClient.UserPresences); onlineUserPresences.BindCollectionChanged(onUserPresenceUpdated, true); + blockedUsers.BindTo(api.Blocks); + blockedUsers.BindCollectionChanged(onBlocksUpdated); } protected override void OnFocus(FocusEvent e) @@ -104,6 +113,36 @@ namespace osu.Game.Overlays.Dashboard searchTextBox.TakeFocus(); } + private void onBlocksUpdated(object? sender, NotifyCollectionChangedEventArgs e) + { + switch (e.Action) + { + case NotifyCollectionChangedAction.Add: + Debug.Assert(e.NewItems != null); + + foreach (APIRelation block in e.NewItems.Cast()) + { + int userId = block.TargetID; + removeUserPanel(userId); + } + + break; + + case NotifyCollectionChangedAction.Remove: + Debug.Assert(e.OldItems != null); + + foreach (APIRelation block in e.OldItems) + { + int userId = block.TargetID; + if (!onlineUserPresences.ContainsKey(userId)) continue; + + addUserPanel(userId); + } + + break; + } + } + private void onUserPresenceUpdated(object? sender, NotifyDictionaryChangedEventArgs e) => Schedule(() => { switch (e.Action) @@ -114,12 +153,9 @@ namespace osu.Game.Overlays.Dashboard foreach (var kvp in e.NewItems) { int userId = kvp.Key; + if (blockedUsers.Any(b => b.TargetID == userId)) continue; - users.GetUserAsync(userId).ContinueWith(task => - { - if (task.GetResultSafely() is APIUser user) - Schedule(() => userFlow.Add(userPanels[userId] = createUserPanel(user))); - }); + addUserPanel(userId); } break; @@ -130,14 +166,28 @@ namespace osu.Game.Overlays.Dashboard foreach (var kvp in e.OldItems) { int userId = kvp.Key; - if (userPanels.Remove(userId, out var userPanel)) - userPanel.Expire(); + removeUserPanel(userId); } break; } }); + private void addUserPanel(int userId) + { + users.GetUserAsync(userId).ContinueWith(task => + { + if (task.GetResultSafely() is APIUser user) + Schedule(() => userFlow.Add(userPanels[userId] = createUserPanel(user))); + }); + } + + private void removeUserPanel(int userId) + { + if (userPanels.Remove(userId, out var userPanel)) + userPanel.Expire(); + } + private OnlineUserPanel createUserPanel(APIUser user) => new OnlineUserPanel(user).With(panel => { From 68b3687315228dae283eb8ad1591e423bae166fb Mon Sep 17 00:00:00 2001 From: Zihad Date: Fri, 28 Mar 2025 16:06:49 +0600 Subject: [PATCH 005/164] Revert "Hide blocked users from currently online" This reverts commits 7ca3a1895a412092da45c7db10c3c7babec59b40 and fbdea8f99019779e15babf2e41b9dda6da84aa70. --- .../Online/TestSceneCurrentlyOnlineDisplay.cs | 82 ------------------- .../Dashboard/CurrentlyOnlineDisplay.cs | 64 ++------------- 2 files changed, 7 insertions(+), 139 deletions(-) diff --git a/osu.Game.Tests/Visual/Online/TestSceneCurrentlyOnlineDisplay.cs b/osu.Game.Tests/Visual/Online/TestSceneCurrentlyOnlineDisplay.cs index 8e99212bcb..a1d0d40811 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneCurrentlyOnlineDisplay.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneCurrentlyOnlineDisplay.cs @@ -9,7 +9,6 @@ using NUnit.Framework; using osu.Framework.Graphics; using osu.Framework.Testing; using osu.Game.Database; -using osu.Game.Online.API; using osu.Game.Online.API.Requests.Responses; using osu.Game.Online.Metadata; using osu.Game.Online.Spectator; @@ -100,87 +99,6 @@ namespace osu.Game.Tests.Visual.Online AddStep("End watching user presence", () => token.Dispose()); } - [Test] - public void TestBlockedUsersHidden() - { - IDisposable token = null!; - - AddStep("Clear blocks", () => - { - DummyAPIAccess api = (DummyAPIAccess)API; - api.Blocks.Clear(); - }); - - AddStep("Begin watching user presence", () => token = metadataClient.BeginWatchingUserPresence()); - AddStep("Add online user", () => metadataClient.UserPresenceUpdated(streamingUser.Id, new UserPresence { Status = UserStatus.Online, Activity = new UserActivity.InSoloGame() })); - AddUntilStep("Panel loaded", () => currentlyOnline.ChildrenOfType().FirstOrDefault()?.User.Id == streamingUser.Id); - - AddStep("Block online user", () => - { - DummyAPIAccess api = (DummyAPIAccess)API; - api.Blocks.Add(new APIRelation() - { - RelationType = RelationType.Block, - TargetUser = streamingUser, - TargetID = streamingUser.Id - }); - }); - - AddAssert("Blocked user not shown", () => currentlyOnline.ChildrenOfType().All(p => p.User.Id != streamingUser.Id)); - - AddStep("Unblock online user", () => - { - DummyAPIAccess api = (DummyAPIAccess)API; - api.Blocks.RemoveAll(b => b.TargetID == streamingUser.Id); - }); - - AddAssert("Unblocked user shown again", () => currentlyOnline.ChildrenOfType().Any(p => p.User.Id == streamingUser.Id)); - - AddStep("Remove playing user", () => metadataClient.UserPresenceUpdated(streamingUser.Id, null)); - AddStep("End watching user presence", () => token.Dispose()); - } - - [Test] - public void TestUnblockedOfflineUsersHidden() - { - IDisposable token = null!; - - AddStep("Clear blocks", () => - { - DummyAPIAccess api = (DummyAPIAccess)API; - api.Blocks.Clear(); - }); - - AddStep("Begin watching user presence", () => token = metadataClient.BeginWatchingUserPresence()); - AddStep("Add online user", () => metadataClient.UserPresenceUpdated(streamingUser.Id, new UserPresence { Status = UserStatus.Online, Activity = new UserActivity.InSoloGame() })); - AddUntilStep("Panel loaded", () => currentlyOnline.ChildrenOfType().FirstOrDefault()?.User.Id == streamingUser.Id); - - AddStep("Block online user", () => - { - DummyAPIAccess api = (DummyAPIAccess)API; - api.Blocks.Add(new APIRelation() - { - RelationType = RelationType.Block, - TargetUser = streamingUser, - TargetID = streamingUser.Id - }); - }); - - AddAssert("Blocked user not shown", () => currentlyOnline.ChildrenOfType().All(p => p.User.Id != streamingUser.Id)); - - AddStep("Remove playing user", () => metadataClient.UserPresenceUpdated(streamingUser.Id, null)); - - AddStep("Unblock offline user", () => - { - DummyAPIAccess api = (DummyAPIAccess)API; - api.Blocks.RemoveAll(b => b.TargetID == streamingUser.Id); - }); - - AddAssert("Unblocked offline user not shown", () => currentlyOnline.ChildrenOfType().All(p => p.User.Id != streamingUser.Id)); - - AddStep("End watching user presence", () => token.Dispose()); - } - internal partial class TestUserLookupCache : UserLookupCache { private static readonly string[] usernames = diff --git a/osu.Game/Overlays/Dashboard/CurrentlyOnlineDisplay.cs b/osu.Game/Overlays/Dashboard/CurrentlyOnlineDisplay.cs index bda23078d9..39df3ba22c 100644 --- a/osu.Game/Overlays/Dashboard/CurrentlyOnlineDisplay.cs +++ b/osu.Game/Overlays/Dashboard/CurrentlyOnlineDisplay.cs @@ -2,9 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using System.Collections.Generic; -using System.Collections.Specialized; using System.Diagnostics; -using System.Linq; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Extensions; @@ -16,7 +14,6 @@ using osu.Framework.Localisation; using osu.Framework.Screens; using osu.Game.Database; using osu.Game.Graphics.UserInterface; -using osu.Game.Online.API; using osu.Game.Online.API.Requests.Responses; using osu.Game.Online.Metadata; using osu.Game.Resources.Localisation.Web; @@ -34,7 +31,6 @@ namespace osu.Game.Overlays.Dashboard private const float padding = 10; private readonly IBindableDictionary onlineUserPresences = new BindableDictionary(); - private readonly IBindableList blockedUsers = new BindableList(); private readonly Dictionary userPanels = new Dictionary(); private SearchContainer userFlow = null!; @@ -46,9 +42,6 @@ namespace osu.Game.Overlays.Dashboard [Resolved] private UserLookupCache users { get; set; } = null!; - [Resolved] - private IAPIProvider api { get; set; } = null!; - [BackgroundDependencyLoader] private void load(OverlayColourProvider colourProvider) { @@ -102,8 +95,6 @@ namespace osu.Game.Overlays.Dashboard onlineUserPresences.BindTo(metadataClient.UserPresences); onlineUserPresences.BindCollectionChanged(onUserPresenceUpdated, true); - blockedUsers.BindTo(api.Blocks); - blockedUsers.BindCollectionChanged(onBlocksUpdated); } protected override void OnFocus(FocusEvent e) @@ -113,36 +104,6 @@ namespace osu.Game.Overlays.Dashboard searchTextBox.TakeFocus(); } - private void onBlocksUpdated(object? sender, NotifyCollectionChangedEventArgs e) - { - switch (e.Action) - { - case NotifyCollectionChangedAction.Add: - Debug.Assert(e.NewItems != null); - - foreach (APIRelation block in e.NewItems.Cast()) - { - int userId = block.TargetID; - removeUserPanel(userId); - } - - break; - - case NotifyCollectionChangedAction.Remove: - Debug.Assert(e.OldItems != null); - - foreach (APIRelation block in e.OldItems) - { - int userId = block.TargetID; - if (!onlineUserPresences.ContainsKey(userId)) continue; - - addUserPanel(userId); - } - - break; - } - } - private void onUserPresenceUpdated(object? sender, NotifyDictionaryChangedEventArgs e) => Schedule(() => { switch (e.Action) @@ -153,9 +114,12 @@ namespace osu.Game.Overlays.Dashboard foreach (var kvp in e.NewItems) { int userId = kvp.Key; - if (blockedUsers.Any(b => b.TargetID == userId)) continue; - addUserPanel(userId); + users.GetUserAsync(userId).ContinueWith(task => + { + if (task.GetResultSafely() is APIUser user) + Schedule(() => userFlow.Add(userPanels[userId] = createUserPanel(user))); + }); } break; @@ -166,28 +130,14 @@ namespace osu.Game.Overlays.Dashboard foreach (var kvp in e.OldItems) { int userId = kvp.Key; - removeUserPanel(userId); + if (userPanels.Remove(userId, out var userPanel)) + userPanel.Expire(); } break; } }); - private void addUserPanel(int userId) - { - users.GetUserAsync(userId).ContinueWith(task => - { - if (task.GetResultSafely() is APIUser user) - Schedule(() => userFlow.Add(userPanels[userId] = createUserPanel(user))); - }); - } - - private void removeUserPanel(int userId) - { - if (userPanels.Remove(userId, out var userPanel)) - userPanel.Expire(); - } - private OnlineUserPanel createUserPanel(APIUser user) => new OnlineUserPanel(user).With(panel => { From a675a5cfd569b873723de343d1f3b6054cf23b9c Mon Sep 17 00:00:00 2001 From: Zihad Date: Fri, 28 Mar 2025 16:13:04 +0600 Subject: [PATCH 006/164] Fix failing tests Now that `UserPanel` also has a `LoadingSpinner`, we need to use `.First` instead of `.Single` here. --- osu.Game.Tests/Visual/Online/TestSceneFriendDisplay.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Tests/Visual/Online/TestSceneFriendDisplay.cs b/osu.Game.Tests/Visual/Online/TestSceneFriendDisplay.cs index 25611cf8d5..52905fe5da 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneFriendDisplay.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneFriendDisplay.cs @@ -218,7 +218,7 @@ namespace osu.Game.Tests.Visual.Online } private void waitForLoad() - => AddUntilStep("wait for panels to load", () => this.ChildrenOfType().Single().State.Value, () => Is.EqualTo(Visibility.Hidden)); + => AddUntilStep("wait for panels to load", () => this.ChildrenOfType().First().State.Value, () => Is.EqualTo(Visibility.Hidden)); private void assertVisiblePanelCount(int expectedVisible) where T : UserPanel From 40e792b55817c5601f94a9819fa687c07483f2a0 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Fri, 4 Apr 2025 21:04:46 +0900 Subject: [PATCH 007/164] Allow viewing results of historical multiplayer items --- .../Match/Playlist/MultiplayerHistoryList.cs | 1 + .../Match/Playlist/MultiplayerPlaylist.cs | 8 ++++++- .../Multiplayer/MultiplayerMatchSubScreen.cs | 17 +++++++++++++- .../Playlists/PlaylistsRoomSubScreen.cs | 22 ++++++++++++++----- 4 files changed, 40 insertions(+), 8 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/Playlist/MultiplayerHistoryList.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/Playlist/MultiplayerHistoryList.cs index d18bb011f0..14b1aa38be 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/Playlist/MultiplayerHistoryList.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/Playlist/MultiplayerHistoryList.cs @@ -18,6 +18,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match.Playlist public MultiplayerHistoryList() { ShowItemOwners = true; + AllowShowingResults = true; } protected override FillFlowContainer> CreateListFillFlowContainer() => new HistoryFillFlowContainer diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/Playlist/MultiplayerPlaylist.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/Playlist/MultiplayerPlaylist.cs index fba3acc32a..5af0fed48f 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/Playlist/MultiplayerPlaylist.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/Playlist/MultiplayerPlaylist.cs @@ -21,10 +21,15 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match.Playlist public readonly Bindable DisplayMode = new Bindable(); /// - /// Invoked when an item requests to be edited. + /// Invoked when the user requests to edit an item. /// public Action? RequestEdit; + /// + /// Invoked when the user requests to view the results for an item. + /// + public Action? RequestResults; + [Resolved] private MultiplayerClient client { get; set; } = null!; @@ -62,6 +67,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match.Playlist { RelativeSizeAxes = Axes.Both, Alpha = 0, + RequestResults = item => RequestResults?.Invoke(item) } } } diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs index d464362fda..b22851052b 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs @@ -34,6 +34,7 @@ using osu.Game.Screens.OnlinePlay.Multiplayer.Match; using osu.Game.Screens.OnlinePlay.Multiplayer.Match.Playlist; using osu.Game.Screens.OnlinePlay.Multiplayer.Participants; using osu.Game.Screens.OnlinePlay.Multiplayer.Spectate; +using osu.Game.Screens.OnlinePlay.Playlists; using osu.Game.Users; using osu.Game.Utils; using osuTK; @@ -272,7 +273,8 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer new MultiplayerPlaylist { RelativeSizeAxes = Axes.Both, - RequestEdit = ShowSongSelect + RequestEdit = ShowSongSelect, + RequestResults = showResults } }, new Drawable[] @@ -670,6 +672,19 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer this.Push(new MultiplayerMatchFreestyleSelect(room, new PlaylistItem(item))); } + /// + /// Shows the results screen for a playlist item. + /// + private void showResults(PlaylistItem item) + { + if (!this.IsCurrentScreen() || client.Room == null || client.LocalUser == null) + return; + + // fallback is to allow this class to operate when there is no parent OnlineScreen (testing purposes). + var targetScreen = (Screen?)parentScreen ?? this; + targetScreen.Push(new PlaylistItemUserBestResultsScreen(client.Room.RoomID, item, client.LocalUser.UserID)); + } + public override void OnEntering(ScreenTransitionEvent e) { base.OnEntering(e); diff --git a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs index 305a81bdbe..91723fbec3 100644 --- a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs @@ -250,12 +250,7 @@ namespace osu.Game.Screens.OnlinePlay.Playlists SelectedItem = { BindTarget = SelectedItem }, AllowSelection = true, AllowShowingResults = true, - RequestResults = item => - { - Debug.Assert(room.RoomID != null); - parentScreen?.Push(new PlaylistItemUserBestResultsScreen(room.RoomID.Value, item, - api.LocalUser.Value.Id)); - } + RequestResults = showResults } }, new Drawable[] @@ -689,6 +684,21 @@ namespace osu.Game.Screens.OnlinePlay.Playlists }); } + /// + /// Shows the results screen for a playlist item. + /// + private void showResults(PlaylistItem item) + { + if (!this.IsCurrentScreen()) + return; + + Debug.Assert(room.RoomID != null); + + // fallback is to allow this class to operate when there is no parent OnlineScreen (testing purposes). + var targetScreen = (Screen?)parentScreen ?? this; + targetScreen.Push(new PlaylistItemUserBestResultsScreen(room.RoomID.Value, item, api.LocalUser.Value.OnlineID)); + } + /// /// May be invoked by the owner of the room to permanently close the room ahead of its intended end date. /// From 2aaadc1a900d8d5518b7b3e4c699e1c7b70cf8f9 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Fri, 4 Apr 2025 21:11:32 +0900 Subject: [PATCH 008/164] Remove dimming from expired panels --- osu.Game/Screens/OnlinePlay/DrawableRoomPlaylistItem.cs | 3 --- 1 file changed, 3 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/DrawableRoomPlaylistItem.cs b/osu.Game/Screens/OnlinePlay/DrawableRoomPlaylistItem.cs index 1e1e79d256..9e585d584d 100644 --- a/osu.Game/Screens/OnlinePlay/DrawableRoomPlaylistItem.cs +++ b/osu.Game/Screens/OnlinePlay/DrawableRoomPlaylistItem.cs @@ -128,9 +128,6 @@ namespace osu.Game.Screens.OnlinePlay Item = item; valid.BindTo(item.Valid); - - if (item.Expired) - Colour = OsuColour.Gray(0.5f); } [BackgroundDependencyLoader] From 3a231debee500acf7ab3c1a80e72071f78d45cbb Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Fri, 4 Apr 2025 21:17:04 +0900 Subject: [PATCH 009/164] Don't select items when viewing results --- osu.Game/Screens/OnlinePlay/DrawableRoomPlaylist.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/osu.Game/Screens/OnlinePlay/DrawableRoomPlaylist.cs b/osu.Game/Screens/OnlinePlay/DrawableRoomPlaylist.cs index c9d8365852..423c956d1c 100644 --- a/osu.Game/Screens/OnlinePlay/DrawableRoomPlaylist.cs +++ b/osu.Game/Screens/OnlinePlay/DrawableRoomPlaylist.cs @@ -165,7 +165,8 @@ namespace osu.Game.Screens.OnlinePlay d.RequestDeletion = i => RequestDeletion?.Invoke(i); d.RequestResults = i => { - SelectedItem.Value = i; + if (AllowSelection) + SelectedItem.Value = i; RequestResults?.Invoke(i); }; d.RequestEdit = i => RequestEdit?.Invoke(i); From 965f16ef8a4485ec366980a9ef97c0caf70a688a Mon Sep 17 00:00:00 2001 From: sineplusx <112003827+sineplusx@users.noreply.github.com> Date: Mon, 7 Apr 2025 21:14:42 +0200 Subject: [PATCH 010/164] Use actual keybind in multiplayer chat hint --- osu.Game/Localisation/ChatStrings.cs | 4 ++-- .../OnlinePlay/Multiplayer/GameplayChatDisplay.cs | 10 +++++++--- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/osu.Game/Localisation/ChatStrings.cs b/osu.Game/Localisation/ChatStrings.cs index 6841e7d938..b14cfd6729 100644 --- a/osu.Game/Localisation/ChatStrings.cs +++ b/osu.Game/Localisation/ChatStrings.cs @@ -25,9 +25,9 @@ namespace osu.Game.Localisation public static LocalisableString MentionUser => new TranslatableString(getKey(@"mention_user"), @"Mention"); /// - /// "press enter to chat..." + /// "press {0} to chat..." /// - public static LocalisableString InGameInputPlaceholder => new TranslatableString(getKey(@"in_game_input_placeholder"), @"press enter to chat..."); + public static LocalisableString InGameInputPlaceholder(LocalisableString keyBind) => new TranslatableString(getKey(@"in_game_input_placeholder"), @"press {0} to chat...", keyBind); private static string getKey(string key) => $@"{prefix}:{key}"; } diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/GameplayChatDisplay.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/GameplayChatDisplay.cs index befaf115ae..83c94ab534 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/GameplayChatDisplay.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/GameplayChatDisplay.cs @@ -6,6 +6,7 @@ using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Input.Bindings; using osu.Framework.Input.Events; +using osu.Game.Configuration; using osu.Game.Input.Bindings; using osu.Game.Localisation; using osu.Game.Online.Rooms; @@ -37,14 +38,17 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer : base(room, leaveChannelOnDispose: false) { RelativeSizeAxes = Axes.X; - Background.Alpha = 0.2f; + } - TextBox.PlaceholderText = ChatStrings.InGameInputPlaceholder; + [BackgroundDependencyLoader] + private void load(OsuConfigManager config) + { + TextBox.PlaceholderText = ChatStrings.InGameInputPlaceholder(config.LookupKeyBindings(GlobalAction.ToggleChatFocus)); TextBox.Focus = () => TextBox.PlaceholderText = Resources.Localisation.Web.ChatStrings.InputPlaceholder; TextBox.FocusLost = () => { - TextBox.PlaceholderText = ChatStrings.InGameInputPlaceholder; + TextBox.PlaceholderText = ChatStrings.InGameInputPlaceholder(config.LookupKeyBindings(GlobalAction.ToggleChatFocus)); expandedFromTextBoxFocus.Value = false; }; } From 4b038c37627bcabce7b299f8d60995dbb455be40 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 8 Apr 2025 14:12:58 +0900 Subject: [PATCH 011/164] Tidy up flow for retrieving key binding representations --- osu.Game/Configuration/OsuConfigManager.cs | 17 ++++++++--------- osu.Game/OsuGame.cs | 4 ++-- .../Multiplayer/GameplayChatDisplay.cs | 6 ++++-- 3 files changed, 14 insertions(+), 13 deletions(-) diff --git a/osu.Game/Configuration/OsuConfigManager.cs b/osu.Game/Configuration/OsuConfigManager.cs index 76d06f3665..d464c97621 100644 --- a/osu.Game/Configuration/OsuConfigManager.cs +++ b/osu.Game/Configuration/OsuConfigManager.cs @@ -2,7 +2,6 @@ // See the LICENCE file in the repository root for full licence text. using System; -using System.Diagnostics; using osu.Framework; using osu.Framework.Bindables; using osu.Framework.Configuration; @@ -35,6 +34,11 @@ namespace osu.Game.Configuration Migrate(); } + /// + /// For a given , return a human-readable string representing the bindings bound to the action. + /// + public LocalisableString LookupKeyBindings(GlobalAction action) => LookupKeyBindingsFunc(action); + protected override void InitialiseDefaults() { // UI/selection defaults @@ -263,10 +267,6 @@ namespace osu.Game.Configuration public override TrackedSettings CreateTrackedSettings() { - // these need to be assigned in normal game startup scenarios. - Debug.Assert(LookupKeyBindings != null); - Debug.Assert(LookupSkinName != null); - return new TrackedSettings { new TrackedSetting(OsuSetting.ShowFpsDisplay, state => new SettingDescription( @@ -308,7 +308,7 @@ namespace osu.Game.Configuration string skinName = string.Empty; if (Guid.TryParse(skin, out var id)) - skinName = LookupSkinName(id); + skinName = LookupSkinNameFunc(id); return new SettingDescription( rawValue: skinName, @@ -329,9 +329,8 @@ namespace osu.Game.Configuration }; } - public Func LookupSkinName { private get; set; } = _ => @"unknown"; - - public Func LookupKeyBindings { get; set; } = _ => @"unknown"; + public Func LookupSkinNameFunc { private get; set; } = _ => @"unknown"; + public Func LookupKeyBindingsFunc { private get; set; } = _ => @"unknown"; IBindable IGameplaySettings.ComboColourNormalisationAmount => GetOriginalBindable(OsuSetting.ComboColourNormalisationAmount); IBindable IGameplaySettings.PositionalHitsoundsLevel => GetOriginalBindable(OsuSetting.PositionalHitsoundsLevel); diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs index 3381553970..558242b37b 100644 --- a/osu.Game/OsuGame.cs +++ b/osu.Game/OsuGame.cs @@ -979,9 +979,9 @@ namespace osu.Game // make config aware of how to lookup skins for on-screen display purposes. // if this becomes a more common thing, tracked settings should be reconsidered to allow local DI. - LocalConfig.LookupSkinName = id => SkinManager.Query(s => s.ID == id)?.ToString() ?? "Unknown"; + LocalConfig.LookupSkinNameFunc = id => SkinManager.Query(s => s.ID == id)?.ToString() ?? "Unknown"; - LocalConfig.LookupKeyBindings = l => + LocalConfig.LookupKeyBindingsFunc = l => { var combinations = KeyBindingStore.GetReadableKeyCombinationsFor(l); diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/GameplayChatDisplay.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/GameplayChatDisplay.cs index 83c94ab534..7b9a4d34ca 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/GameplayChatDisplay.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/GameplayChatDisplay.cs @@ -44,13 +44,15 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer [BackgroundDependencyLoader] private void load(OsuConfigManager config) { - TextBox.PlaceholderText = ChatStrings.InGameInputPlaceholder(config.LookupKeyBindings(GlobalAction.ToggleChatFocus)); + resetPlaceholderText(); TextBox.Focus = () => TextBox.PlaceholderText = Resources.Localisation.Web.ChatStrings.InputPlaceholder; TextBox.FocusLost = () => { - TextBox.PlaceholderText = ChatStrings.InGameInputPlaceholder(config.LookupKeyBindings(GlobalAction.ToggleChatFocus)); + resetPlaceholderText(); expandedFromTextBoxFocus.Value = false; }; + + void resetPlaceholderText() => TextBox.PlaceholderText = ChatStrings.InGameInputPlaceholder(config.LookupKeyBindings(GlobalAction.ToggleChatFocus)); } protected override bool OnHover(HoverEvent e) => true; // use UI mouse cursor. From 5fc80dbddbe692eff5a61071b50c94205ae8b89c Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 8 Apr 2025 14:15:42 +0900 Subject: [PATCH 012/164] Hook up `LocalConfig` functions in `OsuGameBase` to make work in tests --- osu.Game/OsuGame.cs | 14 -------------- osu.Game/OsuGameBase.cs | 13 +++++++++++++ 2 files changed, 13 insertions(+), 14 deletions(-) diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs index 558242b37b..19b80bfba4 100644 --- a/osu.Game/OsuGame.cs +++ b/osu.Game/OsuGame.cs @@ -977,20 +977,6 @@ namespace osu.Game MultiplayerClient.PostNotification = n => Notifications.Post(n); MultiplayerClient.PresentMatch = PresentMultiplayerMatch; - // make config aware of how to lookup skins for on-screen display purposes. - // if this becomes a more common thing, tracked settings should be reconsidered to allow local DI. - LocalConfig.LookupSkinNameFunc = id => SkinManager.Query(s => s.ID == id)?.ToString() ?? "Unknown"; - - LocalConfig.LookupKeyBindingsFunc = l => - { - var combinations = KeyBindingStore.GetReadableKeyCombinationsFor(l); - - if (combinations.Count == 0) - return ToastStrings.NoKeyBound; - - return string.Join(" / ", combinations); - }; - ScreenFooter.BackReceptor backReceptor; dependencies.CacheAs(idleTracker = new GameIdleTracker(6000)); diff --git a/osu.Game/OsuGameBase.cs b/osu.Game/OsuGameBase.cs index 4087a8b71e..28a02e0dc2 100644 --- a/osu.Game/OsuGameBase.cs +++ b/osu.Game/OsuGameBase.cs @@ -421,6 +421,19 @@ namespace osu.Game Ruleset.BindValueChanged(onRulesetChanged); Beatmap.BindValueChanged(onBeatmapChanged); + + // make config aware of how to lookup skins for on-screen display purposes. + // if this becomes a more common thing, tracked settings should be reconsidered to allow local DI. + LocalConfig.LookupSkinNameFunc = id => SkinManager.Query(s => s.ID == id)?.ToString() ?? "Unknown"; + LocalConfig.LookupKeyBindingsFunc = l => + { + var combinations = KeyBindingStore.GetReadableKeyCombinationsFor(l); + + if (combinations.Count == 0) + return ToastStrings.NoKeyBound; + + return string.Join(" / ", combinations); + }; } private void updateLanguage() => CurrentLanguage.Value = LanguageExtensions.GetLanguageFor(frameworkLocale.Value, localisationParameters.Value); From e42301058f92b455dc8a1fc337644089d086fe98 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 8 Apr 2025 14:28:48 +0900 Subject: [PATCH 013/164] Fix mouse down actions being leaked through buttons --- osu.Game/Graphics/UserInterface/OsuAnimatedButton.cs | 2 +- osu.Game/Screens/Edit/Compose/Components/BeatDivisorControl.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Graphics/UserInterface/OsuAnimatedButton.cs b/osu.Game/Graphics/UserInterface/OsuAnimatedButton.cs index 48d225de41..ddabd6c9eb 100644 --- a/osu.Game/Graphics/UserInterface/OsuAnimatedButton.cs +++ b/osu.Game/Graphics/UserInterface/OsuAnimatedButton.cs @@ -122,7 +122,7 @@ namespace osu.Game.Graphics.UserInterface protected override bool OnMouseDown(MouseDownEvent e) { Content.ScaleTo(ScaleOnMouseDown, 2000, Easing.OutQuint); - return base.OnMouseDown(e); + return true; } protected override void OnMouseUp(MouseUpEvent e) diff --git a/osu.Game/Screens/Edit/Compose/Components/BeatDivisorControl.cs b/osu.Game/Screens/Edit/Compose/Components/BeatDivisorControl.cs index 22df917992..c09014f2ba 100644 --- a/osu.Game/Screens/Edit/Compose/Components/BeatDivisorControl.cs +++ b/osu.Game/Screens/Edit/Compose/Components/BeatDivisorControl.cs @@ -470,7 +470,7 @@ namespace osu.Game.Screens.Edit.Compose.Components { marker.Active = true; handleMouseInput(e.ScreenSpaceMousePosition); - return base.OnMouseDown(e); + return true; } protected override void OnMouseUp(MouseUpEvent e) From f3d9f21e71da17ed4035fa1936f82cc0ebca320b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 8 Apr 2025 13:02:14 +0200 Subject: [PATCH 014/164] Fix test failures --- osu.Game/Online/Leaderboards/LeaderboardManager.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Online/Leaderboards/LeaderboardManager.cs b/osu.Game/Online/Leaderboards/LeaderboardManager.cs index 9104c83c02..4ca090f8a0 100644 --- a/osu.Game/Online/Leaderboards/LeaderboardManager.cs +++ b/osu.Game/Online/Leaderboards/LeaderboardManager.cs @@ -49,7 +49,7 @@ namespace osu.Game.Online.Leaderboards if (localFetchCompletionSource != null && localFetchCompletionSource == lastFetchCompletionSource && scores.Value != null) { localFetchCompletionSource.SetResult(scores.Value); - localFetchCompletionSource = null; + localFetchCompletionSource = lastFetchCompletionSource = null; } }); } From 9aaa1fe5f42a8749ab9493c279e11df62fb37526 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 8 Apr 2025 13:08:11 +0200 Subject: [PATCH 015/164] Simplify code further --- .../Online/Leaderboards/LeaderboardManager.cs | 20 ++++++++----------- 1 file changed, 8 insertions(+), 12 deletions(-) diff --git a/osu.Game/Online/Leaderboards/LeaderboardManager.cs b/osu.Game/Online/Leaderboards/LeaderboardManager.cs index 4ca090f8a0..7ca5de6f21 100644 --- a/osu.Game/Online/Leaderboards/LeaderboardManager.cs +++ b/osu.Game/Online/Leaderboards/LeaderboardManager.cs @@ -42,18 +42,6 @@ namespace osu.Game.Online.Leaderboards [Resolved] private RulesetStore rulesets { get; set; } = null!; - public LeaderboardManager() - { - scores.BindValueChanged(_ => - { - if (localFetchCompletionSource != null && localFetchCompletionSource == lastFetchCompletionSource && scores.Value != null) - { - localFetchCompletionSource.SetResult(scores.Value); - localFetchCompletionSource = lastFetchCompletionSource = null; - } - }); - } - public Task FetchWithCriteriaAsync(LeaderboardCriteria newCriteria) { if (criteria?.Equals(newCriteria) == true && lastFetchCompletionSource?.Task.IsFaulted == false) @@ -69,6 +57,8 @@ namespace osu.Game.Online.Leaderboards { case BeatmapLeaderboardScope.Local: { + // this task completion source will be marked completed in the `localScoresChanged()` below. + // yes it's twisty, but such are the costs of trying to reconcile data-push / subscription and data-pull / explicit fetch flows. lastFetchCompletionSource = localFetchCompletionSource = new TaskCompletionSource(); localScoreSubscription = realm.RegisterForNotifications(r => r.All().Filter($"{nameof(ScoreInfo.BeatmapInfo)}.{nameof(BeatmapInfo.ID)} == $0" @@ -148,6 +138,12 @@ namespace osu.Game.Online.Leaderboards newScores = newScores.Detach().OrderByTotalScore(); scores.Value = new LeaderboardScores(newScores, null); + + if (localFetchCompletionSource != null && localFetchCompletionSource == lastFetchCompletionSource) + { + localFetchCompletionSource.SetResult(scores.Value); + localFetchCompletionSource = lastFetchCompletionSource = null; + } } } From 68b5f9a314b420ab520745fdf3ff12b1d2f28dcd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 8 Apr 2025 13:35:24 +0200 Subject: [PATCH 016/164] Fix various weirdness around refetching --- .../Select/Leaderboards/BeatmapLeaderboard.cs | 24 +++++++++---------- 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/osu.Game/Screens/Select/Leaderboards/BeatmapLeaderboard.cs b/osu.Game/Screens/Select/Leaderboards/BeatmapLeaderboard.cs index e435554b03..c52cd61c42 100644 --- a/osu.Game/Screens/Select/Leaderboards/BeatmapLeaderboard.cs +++ b/osu.Game/Screens/Select/Leaderboards/BeatmapLeaderboard.cs @@ -63,7 +63,7 @@ namespace osu.Game.Screens.Select.Leaderboards } } - private readonly IBindable fetchedScores = new Bindable(); + private readonly Bindable fetchedScores = new Bindable(); [Resolved] private IBindable ruleset { get; set; } = null!; @@ -86,17 +86,7 @@ namespace osu.Game.Screens.Select.Leaderboards if (filterMods) RefetchScores(); }; - fetchedScores.BindTo(leaderboardManager.Scores); - } - - protected override void LoadComplete() - { - base.LoadComplete(); - fetchedScores.BindValueChanged(_ => - { - if (fetchedScores.Value != null) - SetScores(fetchedScores.Value.TopScores, fetchedScores.Value.UserScore); - }); + ((IBindable)fetchedScores).BindTo(leaderboardManager.Scores); } protected override bool IsOnlineScope => Scope != BeatmapLeaderboardScope.Local; @@ -147,7 +137,17 @@ namespace osu.Game.Screens.Select.Leaderboards .ContinueWith(t => { if (t.Exception != null && !t.IsCanceled) + { Schedule(() => SetErrorState(LeaderboardState.NetworkFailure)); + return; + } + + fetchedScores.UnbindEvents(); + fetchedScores.BindValueChanged(scores => + { + if (scores.NewValue != null) + Schedule(() => SetScores(scores.NewValue.TopScores, scores.NewValue.UserScore)); + }, true); }, cancellationToken); return null; From 4978903ba581d0007cf52530e3f8d81c6fd0bfbd Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Wed, 9 Apr 2025 14:55:38 +0900 Subject: [PATCH 017/164] Fix difficulty stats not showing in playlists --- osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs index 305a81bdbe..4d3a8616d7 100644 --- a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs @@ -442,6 +442,7 @@ namespace osu.Game.Screens.OnlinePlay.Playlists { SelectedItem = { BindTarget = SelectedItem }, SelectedMods = { BindTarget = UserMods }, + Beatmap = { BindTarget = Beatmap }, IsValidMod = _ => false }); } From 27edee477da2d03c11d90893334b9813a5a4c38b Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 9 Apr 2025 14:56:30 +0900 Subject: [PATCH 018/164] Block all mouse events at `BeatDivisorControl` and revert animated button change --- osu.Game/Graphics/UserInterface/OsuAnimatedButton.cs | 2 +- .../Screens/Edit/Compose/Components/BeatDivisorControl.cs | 5 ++++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/osu.Game/Graphics/UserInterface/OsuAnimatedButton.cs b/osu.Game/Graphics/UserInterface/OsuAnimatedButton.cs index ddabd6c9eb..48d225de41 100644 --- a/osu.Game/Graphics/UserInterface/OsuAnimatedButton.cs +++ b/osu.Game/Graphics/UserInterface/OsuAnimatedButton.cs @@ -122,7 +122,7 @@ namespace osu.Game.Graphics.UserInterface protected override bool OnMouseDown(MouseDownEvent e) { Content.ScaleTo(ScaleOnMouseDown, 2000, Easing.OutQuint); - return true; + return base.OnMouseDown(e); } protected override void OnMouseUp(MouseUpEvent e) diff --git a/osu.Game/Screens/Edit/Compose/Components/BeatDivisorControl.cs b/osu.Game/Screens/Edit/Compose/Components/BeatDivisorControl.cs index c09014f2ba..5386b39190 100644 --- a/osu.Game/Screens/Edit/Compose/Components/BeatDivisorControl.cs +++ b/osu.Game/Screens/Edit/Compose/Components/BeatDivisorControl.cs @@ -167,6 +167,9 @@ namespace osu.Game.Screens.Edit.Compose.Components }, true); } + protected override bool OnMouseDown(MouseDownEvent e) => true; + protected override bool OnClick(ClickEvent e) => true; + private void cycleDivisorType(int direction) { int totalTypes = Enum.GetValues().Length; @@ -470,7 +473,7 @@ namespace osu.Game.Screens.Edit.Compose.Components { marker.Active = true; handleMouseInput(e.ScreenSpaceMousePosition); - return true; + return base.OnMouseDown(e); } protected override void OnMouseUp(MouseUpEvent e) From 3fe967e4d77c47aaad127bf55cec94c1ae8da0f6 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Wed, 9 Apr 2025 15:12:02 +0900 Subject: [PATCH 019/164] Fix exception cancelling disposed cancellation token --- osu.Game/Screens/OnlinePlay/Lounge/Components/RoomPanel.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/osu.Game/Screens/OnlinePlay/Lounge/Components/RoomPanel.cs b/osu.Game/Screens/OnlinePlay/Lounge/Components/RoomPanel.cs index 6aeaf01c45..acbf5d8462 100644 --- a/osu.Game/Screens/OnlinePlay/Lounge/Components/RoomPanel.cs +++ b/osu.Game/Screens/OnlinePlay/Lounge/Components/RoomPanel.cs @@ -317,6 +317,7 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components beatmapLookupCancellation?.Cancel(); beatmapLookupCancellation?.Dispose(); + beatmapLookupCancellation = null; if (item.NewValue?.Beatmap == null) { From 9e90f80ab533e538cefdb6fb08bf6d5e811ffe60 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 9 Apr 2025 15:23:55 +0900 Subject: [PATCH 020/164] Move key binding string retrieval to `RealmKeyBindingStore` --- osu.Game/Configuration/OsuConfigManager.cs | 11 +++-------- osu.Game/Input/RealmKeyBindingStore.cs | 15 +++++++++++++++ osu.Game/OsuGameBase.cs | 12 ++---------- osu.Game/Overlays/Music/MusicKeyBindingHandler.cs | 6 +++--- osu.Game/Overlays/OSD/SpeedChangeToast.cs | 6 +++--- .../Rulesets/Edit/ComposerDistanceSnapProvider.cs | 6 +++--- .../OnlinePlay/Multiplayer/GameplayChatDisplay.cs | 10 +++++----- osu.Game/Screens/Play/HUDOverlay.cs | 5 +++-- osu.Game/Screens/Select/ModSpeedHotkeyHandler.cs | 7 ++++--- 9 files changed, 41 insertions(+), 37 deletions(-) diff --git a/osu.Game/Configuration/OsuConfigManager.cs b/osu.Game/Configuration/OsuConfigManager.cs index d464c97621..0399f50ded 100644 --- a/osu.Game/Configuration/OsuConfigManager.cs +++ b/osu.Game/Configuration/OsuConfigManager.cs @@ -34,11 +34,6 @@ namespace osu.Game.Configuration Migrate(); } - /// - /// For a given , return a human-readable string representing the bindings bound to the action. - /// - public LocalisableString LookupKeyBindings(GlobalAction action) => LookupKeyBindingsFunc(action); - protected override void InitialiseDefaults() { // UI/selection defaults @@ -308,7 +303,7 @@ namespace osu.Game.Configuration string skinName = string.Empty; if (Guid.TryParse(skin, out var id)) - skinName = LookupSkinNameFunc(id); + skinName = LookupSkinName(id); return new SettingDescription( rawValue: skinName, @@ -329,8 +324,8 @@ namespace osu.Game.Configuration }; } - public Func LookupSkinNameFunc { private get; set; } = _ => @"unknown"; - public Func LookupKeyBindingsFunc { private get; set; } = _ => @"unknown"; + public Func LookupSkinName { private get; set; } = _ => @"unknown"; + public Func LookupKeyBindings { private get; set; } = _ => @"unknown"; IBindable IGameplaySettings.ComboColourNormalisationAmount => GetOriginalBindable(OsuSetting.ComboColourNormalisationAmount); IBindable IGameplaySettings.PositionalHitsoundsLevel => GetOriginalBindable(OsuSetting.PositionalHitsoundsLevel); diff --git a/osu.Game/Input/RealmKeyBindingStore.cs b/osu.Game/Input/RealmKeyBindingStore.cs index 48ace58235..7209d3851b 100644 --- a/osu.Game/Input/RealmKeyBindingStore.cs +++ b/osu.Game/Input/RealmKeyBindingStore.cs @@ -5,8 +5,10 @@ using System.Collections.Generic; using System.Linq; using osu.Framework.Input; using osu.Framework.Input.Bindings; +using osu.Framework.Localisation; using osu.Game.Database; using osu.Game.Input.Bindings; +using osu.Game.Localisation; using osu.Game.Rulesets; using Realms; @@ -23,6 +25,19 @@ namespace osu.Game.Input this.keyCombinationProvider = keyCombinationProvider; } + /// + /// For a given , return a human-readable string representing the bindings bound to the action. + /// + public LocalisableString GetBindingsStringFor(GlobalAction globalAction) + { + var combinations = GetReadableKeyCombinationsFor(globalAction); + + if (combinations.Count == 0) + return ToastStrings.NoKeyBound; + + return string.Join(" / ", combinations); + } + /// /// Retrieve all user-defined key combinations (in a format that can be displayed) for a specific action. /// diff --git a/osu.Game/OsuGameBase.cs b/osu.Game/OsuGameBase.cs index 28a02e0dc2..3eaabd9d2a 100644 --- a/osu.Game/OsuGameBase.cs +++ b/osu.Game/OsuGameBase.cs @@ -424,16 +424,8 @@ namespace osu.Game // make config aware of how to lookup skins for on-screen display purposes. // if this becomes a more common thing, tracked settings should be reconsidered to allow local DI. - LocalConfig.LookupSkinNameFunc = id => SkinManager.Query(s => s.ID == id)?.ToString() ?? "Unknown"; - LocalConfig.LookupKeyBindingsFunc = l => - { - var combinations = KeyBindingStore.GetReadableKeyCombinationsFor(l); - - if (combinations.Count == 0) - return ToastStrings.NoKeyBound; - - return string.Join(" / ", combinations); - }; + LocalConfig.LookupSkinName = id => SkinManager.Query(s => s.ID == id)?.ToString() ?? "Unknown"; + LocalConfig.LookupKeyBindings = l => KeyBindingStore.GetBindingsStringFor(l); } private void updateLanguage() => CurrentLanguage.Value = LanguageExtensions.GetLanguageFor(frameworkLocale.Value, localisationParameters.Value); diff --git a/osu.Game/Overlays/Music/MusicKeyBindingHandler.cs b/osu.Game/Overlays/Music/MusicKeyBindingHandler.cs index 78de76b981..8cec85b748 100644 --- a/osu.Game/Overlays/Music/MusicKeyBindingHandler.cs +++ b/osu.Game/Overlays/Music/MusicKeyBindingHandler.cs @@ -9,7 +9,7 @@ using osu.Framework.Input.Bindings; using osu.Framework.Input.Events; using osu.Framework.Localisation; using osu.Game.Beatmaps; -using osu.Game.Configuration; +using osu.Game.Input; using osu.Game.Input.Bindings; using osu.Game.Localisation; using osu.Game.Overlays.OSD; @@ -92,9 +92,9 @@ namespace osu.Game.Overlays.Music } [BackgroundDependencyLoader] - private void load(OsuConfigManager config) + private void load(RealmKeyBindingStore keyBindingStore) { - ShortcutText.Text = config.LookupKeyBindings(action).ToUpper(); + ShortcutText.Text = keyBindingStore.GetBindingsStringFor(action).ToUpper(); } } } diff --git a/osu.Game/Overlays/OSD/SpeedChangeToast.cs b/osu.Game/Overlays/OSD/SpeedChangeToast.cs index 49d3985b04..652c043357 100644 --- a/osu.Game/Overlays/OSD/SpeedChangeToast.cs +++ b/osu.Game/Overlays/OSD/SpeedChangeToast.cs @@ -1,7 +1,7 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using osu.Game.Configuration; +using osu.Game.Input; using osu.Game.Input.Bindings; using osu.Game.Localisation; @@ -9,8 +9,8 @@ namespace osu.Game.Overlays.OSD { public partial class SpeedChangeToast : Toast { - public SpeedChangeToast(OsuConfigManager config, double newSpeed) - : base(ModSelectOverlayStrings.ModCustomisation, ToastStrings.SpeedChangedTo(newSpeed), config.LookupKeyBindings(GlobalAction.IncreaseModSpeed) + " / " + config.LookupKeyBindings(GlobalAction.DecreaseModSpeed)) + public SpeedChangeToast(RealmKeyBindingStore keyBindingStore, double newSpeed) + : base(ModSelectOverlayStrings.ModCustomisation, ToastStrings.SpeedChangedTo(newSpeed), keyBindingStore.GetBindingsStringFor(GlobalAction.IncreaseModSpeed) + " / " + keyBindingStore.GetBindingsStringFor(GlobalAction.DecreaseModSpeed)) { } } diff --git a/osu.Game/Rulesets/Edit/ComposerDistanceSnapProvider.cs b/osu.Game/Rulesets/Edit/ComposerDistanceSnapProvider.cs index 4129a6fb2c..2d6e09b3fd 100644 --- a/osu.Game/Rulesets/Edit/ComposerDistanceSnapProvider.cs +++ b/osu.Game/Rulesets/Edit/ComposerDistanceSnapProvider.cs @@ -14,9 +14,9 @@ using osu.Framework.Input.Bindings; using osu.Framework.Input.Events; using osu.Framework.Localisation; using osu.Framework.Utils; -using osu.Game.Configuration; using osu.Game.Graphics; using osu.Game.Graphics.UserInterface; +using osu.Game.Input; using osu.Game.Input.Bindings; using osu.Game.Overlays; using osu.Game.Overlays.OSD; @@ -312,9 +312,9 @@ namespace osu.Game.Rulesets.Edit } [BackgroundDependencyLoader] - private void load(OsuConfigManager config) + private void load(RealmKeyBindingStore keyBindingStore) { - ShortcutText.Text = config.LookupKeyBindings(getAction(change)).ToUpper(); + ShortcutText.Text = keyBindingStore.GetBindingsStringFor(getAction(change)).ToUpper(); } private static GlobalAction getAction(ValueChangedEvent change) => change.NewValue - change.OldValue > 0 diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/GameplayChatDisplay.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/GameplayChatDisplay.cs index 7b9a4d34ca..65f667b929 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/GameplayChatDisplay.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/GameplayChatDisplay.cs @@ -6,10 +6,10 @@ using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Input.Bindings; using osu.Framework.Input.Events; -using osu.Game.Configuration; +using osu.Game.Input; using osu.Game.Input.Bindings; -using osu.Game.Localisation; using osu.Game.Online.Rooms; +using osu.Game.Resources.Localisation.Web; using osu.Game.Screens.OnlinePlay.Match.Components; using osu.Game.Screens.Play; @@ -42,17 +42,17 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer } [BackgroundDependencyLoader] - private void load(OsuConfigManager config) + private void load(RealmKeyBindingStore keyBindingStore) { resetPlaceholderText(); - TextBox.Focus = () => TextBox.PlaceholderText = Resources.Localisation.Web.ChatStrings.InputPlaceholder; + TextBox.Focus = () => TextBox.PlaceholderText = ChatStrings.InputPlaceholder; TextBox.FocusLost = () => { resetPlaceholderText(); expandedFromTextBoxFocus.Value = false; }; - void resetPlaceholderText() => TextBox.PlaceholderText = ChatStrings.InGameInputPlaceholder(config.LookupKeyBindings(GlobalAction.ToggleChatFocus)); + void resetPlaceholderText() => TextBox.PlaceholderText = Localisation.ChatStrings.InGameInputPlaceholder(keyBindingStore.GetBindingsStringFor(GlobalAction.ToggleChatFocus)); } protected override bool OnHover(HoverEvent e) => true; // use UI mouse cursor. diff --git a/osu.Game/Screens/Play/HUDOverlay.cs b/osu.Game/Screens/Play/HUDOverlay.cs index 75a28a4240..733675dfb1 100644 --- a/osu.Game/Screens/Play/HUDOverlay.cs +++ b/osu.Game/Screens/Play/HUDOverlay.cs @@ -14,6 +14,7 @@ using osu.Framework.Graphics.Primitives; using osu.Framework.Input.Bindings; using osu.Framework.Input.Events; using osu.Game.Configuration; +using osu.Game.Input; using osu.Game.Input.Bindings; using osu.Game.Localisation; using osu.Game.Overlays; @@ -195,7 +196,7 @@ namespace osu.Game.Screens.Play } [BackgroundDependencyLoader(true)] - private void load(OsuConfigManager config, INotificationOverlay notificationOverlay) + private void load(OsuConfigManager config, RealmKeyBindingStore keyBindingStore, INotificationOverlay notificationOverlay) { if (drawableRuleset != null) { @@ -214,7 +215,7 @@ namespace osu.Game.Screens.Play notificationOverlay?.Post(new SimpleNotification { - Text = NotificationsStrings.ScoreOverlayDisabled(config.LookupKeyBindings(GlobalAction.ToggleInGameInterface)) + Text = NotificationsStrings.ScoreOverlayDisabled(keyBindingStore.GetBindingsStringFor(GlobalAction.ToggleInGameInterface)) }); } diff --git a/osu.Game/Screens/Select/ModSpeedHotkeyHandler.cs b/osu.Game/Screens/Select/ModSpeedHotkeyHandler.cs index c4cd44705e..998f94849c 100644 --- a/osu.Game/Screens/Select/ModSpeedHotkeyHandler.cs +++ b/osu.Game/Screens/Select/ModSpeedHotkeyHandler.cs @@ -8,6 +8,7 @@ using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Utils; using osu.Game.Configuration; +using osu.Game.Input; using osu.Game.Overlays; using osu.Game.Overlays.OSD; using osu.Game.Rulesets.Mods; @@ -21,7 +22,7 @@ namespace osu.Game.Screens.Select private Bindable> selectedMods { get; set; } = null!; [Resolved] - private OsuConfigManager config { get; set; } = null!; + private RealmKeyBindingStore keyBindingStore { get; set; } = null!; [Resolved] private OnScreenDisplay? onScreenDisplay { get; set; } @@ -55,7 +56,7 @@ namespace osu.Game.Screens.Select if (Precision.AlmostEquals(targetSpeed, 1, 0.005)) { selectedMods.Value = selectedMods.Value.Where(m => m is not ModRateAdjust).ToList(); - onScreenDisplay?.Display(new SpeedChangeToast(config, targetSpeed)); + onScreenDisplay?.Display(new SpeedChangeToast(keyBindingStore, targetSpeed)); return true; } @@ -108,7 +109,7 @@ namespace osu.Game.Screens.Select return false; selectedMods.Value = intendedMods; - onScreenDisplay?.Display(new SpeedChangeToast(config, targetMod.SpeedChange.Value)); + onScreenDisplay?.Display(new SpeedChangeToast(keyBindingStore, targetMod.SpeedChange.Value)); return true; } } From 7ab428f18c5ace02dfe7ce39cf90ded64a774aae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 9 Apr 2025 10:20:22 +0200 Subject: [PATCH 021/164] Fix editor click-through issues on bottom bar too --- osu.Game/Screens/Edit/BottomBar.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/osu.Game/Screens/Edit/BottomBar.cs b/osu.Game/Screens/Edit/BottomBar.cs index 6af8217d41..49f3d704bc 100644 --- a/osu.Game/Screens/Edit/BottomBar.cs +++ b/osu.Game/Screens/Edit/BottomBar.cs @@ -7,6 +7,7 @@ using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Effects; +using osu.Framework.Input.Events; using osu.Framework.Testing; using osu.Game.Rulesets.Edit; using osu.Game.Screens.Edit.Components; @@ -92,5 +93,8 @@ namespace osu.Game.Screens.Edit } }, true); } + + protected override bool OnMouseDown(MouseDownEvent e) => true; + protected override bool OnClick(ClickEvent e) => true; } } From 0c7c791fb5c40235c6033ddf1f68811f0e4325a8 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 9 Apr 2025 19:34:01 +0900 Subject: [PATCH 022/164] Fix `StarRatingDisplay` not using fixed width text --- osu.Game/Beatmaps/Drawables/StarRatingDisplay.cs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/osu.Game/Beatmaps/Drawables/StarRatingDisplay.cs b/osu.Game/Beatmaps/Drawables/StarRatingDisplay.cs index 4119ffb636..dbd4918c81 100644 --- a/osu.Game/Beatmaps/Drawables/StarRatingDisplay.cs +++ b/osu.Game/Beatmaps/Drawables/StarRatingDisplay.cs @@ -127,9 +127,8 @@ namespace osu.Game.Beatmaps.Drawables Anchor = Anchor.Centre, Origin = Anchor.Centre, Margin = new MarginPadding { Bottom = 1.5f }, - // todo: this should be size: 12f, but to match up with the design, it needs to be 14.4f - // see https://github.com/ppy/osu-framework/issues/3271. - Font = OsuFont.Torus.With(size: 14.4f, weight: FontWeight.Bold), + Spacing = new Vector2(-1.4f), + Font = OsuFont.Torus.With(size: 14.4f, weight: FontWeight.Bold, fixedWidth: true), Shadow = false, }, }, From 962edbdf78f7447776f03b8ea23da8aa6f52e7a9 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 10 Apr 2025 01:46:44 +0900 Subject: [PATCH 023/164] Remove manual handling of autosize now that we are using fixed width display --- .../Beatmaps/Drawables/StarRatingDisplay.cs | 23 ++++++------------- 1 file changed, 7 insertions(+), 16 deletions(-) diff --git a/osu.Game/Beatmaps/Drawables/StarRatingDisplay.cs b/osu.Game/Beatmaps/Drawables/StarRatingDisplay.cs index dbd4918c81..9eea53d621 100644 --- a/osu.Game/Beatmaps/Drawables/StarRatingDisplay.cs +++ b/osu.Game/Beatmaps/Drawables/StarRatingDisplay.cs @@ -119,18 +119,14 @@ namespace osu.Game.Beatmaps.Drawables Size = new Vector2(8f), }, Empty(), - textContainer = new Container + starsText = new OsuSpriteText { - AutoSizeAxes = Axes.Y, - Child = starsText = new OsuSpriteText - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Margin = new MarginPadding { Bottom = 1.5f }, - Spacing = new Vector2(-1.4f), - Font = OsuFont.Torus.With(size: 14.4f, weight: FontWeight.Bold, fixedWidth: true), - Shadow = false, - }, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Margin = new MarginPadding { Bottom = 1.5f }, + Spacing = new Vector2(-1.4f), + Font = OsuFont.Torus.With(size: 14.4f, weight: FontWeight.Bold, fixedWidth: true), + Shadow = false, }, } } @@ -161,11 +157,6 @@ namespace osu.Game.Beatmaps.Drawables starIcon.Colour = s.NewValue >= 6.5 ? colours.Orange1 : colourProvider?.Background5 ?? Color4Extensions.FromHex("303d47"); starsText.Colour = s.NewValue >= 6.5 ? colours.Orange1 : colourProvider?.Background5 ?? Color4.Black.Opacity(0.75f); - - // In order to avoid autosize throwing the width of these displays all over the place, - // let's lock in some sane defaults for the text width based on how many digits we're - // displaying. - textContainer.Width = 24 + Math.Max(starsText.Text.ToString().Length - 4, 0) * 6; }, true); } } From bfc6d61e5dc82dbef4090c36983bbe98ff00da8c Mon Sep 17 00:00:00 2001 From: Joseph Madamba Date: Wed, 9 Apr 2025 11:41:20 -0700 Subject: [PATCH 024/164] Fix success rate / points of failure not showing for unranked beatmaps on beatmap info overlay --- osu.Game/Overlays/BeatmapSet/Info.cs | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/osu.Game/Overlays/BeatmapSet/Info.cs b/osu.Game/Overlays/BeatmapSet/Info.cs index 37741b63ce..96e6622507 100644 --- a/osu.Game/Overlays/BeatmapSet/Info.cs +++ b/osu.Game/Overlays/BeatmapSet/Info.cs @@ -10,9 +10,7 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; using osu.Game.Beatmaps; -using osu.Game.Graphics; using osu.Game.Graphics.Containers; -using osu.Game.Graphics.Sprites; using osu.Game.Online.API.Requests.Responses; using osu.Game.Overlays.BeatmapListing; @@ -38,7 +36,6 @@ namespace osu.Game.Overlays.BeatmapSet MetadataSection source, mapperTags; MetadataSectionGenre genre; MetadataSectionLanguage language; - OsuSpriteText notRankedPlaceholder; RelativeSizeAxes = Axes.X; Height = base_height; @@ -109,14 +106,6 @@ namespace osu.Game.Overlays.BeatmapSet RelativeSizeAxes = Axes.Both, Padding = new MarginPadding { Top = 20, Horizontal = 15 }, }, - notRankedPlaceholder = new OsuSpriteText - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Alpha = 0, - Text = "This beatmap is not ranked", - Font = OsuFont.GetFont(size: 12) - }, }, }, }, @@ -131,9 +120,6 @@ namespace osu.Game.Overlays.BeatmapSet updateUserTags(); genre.Metadata = b.NewValue?.Genre ?? new BeatmapSetOnlineGenre { Id = (int)SearchGenre.Unspecified }; language.Metadata = b.NewValue?.Language ?? new BeatmapSetOnlineLanguage { Id = (int)SearchLanguage.Unspecified }; - bool setHasLeaderboard = b.NewValue?.Status > 0; - successRate.Alpha = setHasLeaderboard ? 1 : 0; - notRankedPlaceholder.Alpha = setHasLeaderboard ? 0 : 1; }); Beatmap.BindValueChanged(b => { From fb326c6afd0d0a8b6efd532af7a9fbcbbf62f52e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 10 Apr 2025 07:47:31 +0200 Subject: [PATCH 025/164] Fix code quality --- osu.Game/Beatmaps/Drawables/StarRatingDisplay.cs | 3 --- 1 file changed, 3 deletions(-) diff --git a/osu.Game/Beatmaps/Drawables/StarRatingDisplay.cs b/osu.Game/Beatmaps/Drawables/StarRatingDisplay.cs index 9eea53d621..050a78a6b4 100644 --- a/osu.Game/Beatmaps/Drawables/StarRatingDisplay.cs +++ b/osu.Game/Beatmaps/Drawables/StarRatingDisplay.cs @@ -1,7 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using System; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Extensions.Color4Extensions; @@ -39,8 +38,6 @@ namespace osu.Game.Beatmaps.Drawables private readonly Bindable displayedStars = new BindableDouble(); - private readonly Container textContainer; - /// /// The currently displayed stars of this display wrapped in a bindable. /// This bindable gets transformed on change rather than instantaneous, if animation is enabled. From 92c0b3cae267b8d12c8e3e659cfc6e30b900938e Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Thu, 10 Apr 2025 15:52:49 +0900 Subject: [PATCH 026/164] Fix progressively worsening performance on taiko argon skin --- .../Skinning/Argon/ArgonCirclePiece.cs | 9 +++++++++ osu.Game.Rulesets.Taiko/Skinning/Default/CirclePiece.cs | 9 +++++++++ 2 files changed, 18 insertions(+) diff --git a/osu.Game.Rulesets.Taiko/Skinning/Argon/ArgonCirclePiece.cs b/osu.Game.Rulesets.Taiko/Skinning/Argon/ArgonCirclePiece.cs index cecb99c690..d94031380b 100644 --- a/osu.Game.Rulesets.Taiko/Skinning/Argon/ArgonCirclePiece.cs +++ b/osu.Game.Rulesets.Taiko/Skinning/Argon/ArgonCirclePiece.cs @@ -3,6 +3,7 @@ using osu.Framework.Allocation; using osu.Framework.Audio.Track; +using osu.Framework.Extensions.ObjectExtensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Colour; using osu.Framework.Graphics.Shapes; @@ -112,5 +113,13 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Argon .FadeOut(timingPoint.BeatLength * 0.75, Easing.OutSine); } } + + protected override void Dispose(bool isDisposing) + { + base.Dispose(isDisposing); + + if (drawableHitObject.IsNotNull()) + drawableHitObject.ApplyCustomUpdateState -= updateStateTransforms; + } } } diff --git a/osu.Game.Rulesets.Taiko/Skinning/Default/CirclePiece.cs b/osu.Game.Rulesets.Taiko/Skinning/Default/CirclePiece.cs index b3833d372c..a7b1b9c4b6 100644 --- a/osu.Game.Rulesets.Taiko/Skinning/Default/CirclePiece.cs +++ b/osu.Game.Rulesets.Taiko/Skinning/Default/CirclePiece.cs @@ -4,6 +4,7 @@ using osu.Framework.Allocation; using osu.Framework.Audio.Track; using osu.Framework.Extensions.Color4Extensions; +using osu.Framework.Extensions.ObjectExtensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Effects; @@ -202,5 +203,13 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Default .Then() .FadeEdgeEffectTo(edge_alpha_kiai, duration, Easing.OutQuint); } + + protected override void Dispose(bool isDisposing) + { + base.Dispose(isDisposing); + + if (drawableHitObject.IsNotNull()) + drawableHitObject.ApplyCustomUpdateState -= updateStateTransforms; + } } } From 05dc113c703a2aaa2c3f58adff0611ae38fc9fc7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 10 Apr 2025 08:24:53 +0200 Subject: [PATCH 027/164] Use more distinctive window title for tournament client This is in response to feedback in https://discord.com/channels/188630481301012481/1097318920991559880/1359117234257268747. The long and short of it is that without a unique title it's a bit hard to tell in software like OBS which window is the game and which window is the tournament client if wanting to run both, which I can agree with. --- osu.Game.Tournament/TournamentGameBase.cs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/osu.Game.Tournament/TournamentGameBase.cs b/osu.Game.Tournament/TournamentGameBase.cs index eecd097a97..2be7c4aff3 100644 --- a/osu.Game.Tournament/TournamentGameBase.cs +++ b/osu.Game.Tournament/TournamentGameBase.cs @@ -53,6 +53,14 @@ namespace osu.Game.Tournament return new ProductionEndpointConfiguration(); } + public override void SetHost(GameHost host) + { + base.SetHost(host); + + if (host.Window != null) + host.Window.Title = $"{Name} [tournament client]"; + } + private TournamentSpriteText initialisationText = null!; [BackgroundDependencyLoader] From d0dddcde7847da8f0a16ca9e015efebf923a2321 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Thu, 10 Apr 2025 16:09:47 +0900 Subject: [PATCH 028/164] Fix another missing disposal This one probably doesn't matter as much because it's used as a single instance in `TaikoPlayfield` (so its lifetime should end around the same time as the `HealthProcessor`). --- .../Skinning/Legacy/LegacyKiaiGlow.cs | 21 ++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacyKiaiGlow.cs b/osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacyKiaiGlow.cs index 9877efa127..58830f7492 100644 --- a/osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacyKiaiGlow.cs +++ b/osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacyKiaiGlow.cs @@ -17,12 +17,14 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy { internal partial class LegacyKiaiGlow : BeatSyncedContainer { - private bool isKiaiActive; + [Resolved] + private HealthProcessor? healthProcessor { get; set; } + private bool isKiaiActive; private Sprite sprite = null!; - [BackgroundDependencyLoader(true)] - private void load(ISkinSource skin, HealthProcessor? healthProcessor) + [BackgroundDependencyLoader] + private void load(ISkinSource skin) { Child = sprite = new Sprite { @@ -33,6 +35,11 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy Scale = new Vector2(TaikoLegacyHitTarget.SCALE), Colour = new Colour4(255, 228, 0, 255), }; + } + + protected override void LoadComplete() + { + base.LoadComplete(); if (healthProcessor != null) healthProcessor.NewJudgement += onNewJudgement; @@ -61,5 +68,13 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy sprite.ScaleTo(TaikoLegacyHitTarget.SCALE + 0.15f).Then() .ScaleTo(TaikoLegacyHitTarget.SCALE, 80, Easing.OutQuad); } + + protected override void Dispose(bool isDisposing) + { + base.Dispose(isDisposing); + + if (healthProcessor != null) + healthProcessor.NewJudgement -= onNewJudgement; + } } } From 42ff312ece51e9956a3c539ac9ec9c4576059bc7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 8 Apr 2025 13:53:07 +0200 Subject: [PATCH 029/164] Actually use leaderboard manager in players This was supposed to be the case all along, but I guess in all of the rewrite attempts I forgot? With this, https://github.com/ppy/osu/issues/29861 is actually fixed again - and additionally, so is https://github.com/ppy/osu/issues/26716. --- .../Online/Leaderboards/LeaderboardManager.cs | 16 ++++++++++- osu.Game/Screens/Play/ReplayPlayer.cs | 25 +++++++++++++++-- osu.Game/Screens/Play/SoloPlayer.cs | 28 ++++++++++++++++--- osu.Game/Screens/Select/PlaySongSelect.cs | 10 ++----- 4 files changed, 63 insertions(+), 16 deletions(-) diff --git a/osu.Game/Online/Leaderboards/LeaderboardManager.cs b/osu.Game/Online/Leaderboards/LeaderboardManager.cs index 7ca5de6f21..314705eb02 100644 --- a/osu.Game/Online/Leaderboards/LeaderboardManager.cs +++ b/osu.Game/Online/Leaderboards/LeaderboardManager.cs @@ -11,6 +11,7 @@ using osu.Framework.Bindables; using osu.Framework.Graphics; 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.Rulesets; @@ -154,5 +155,18 @@ namespace osu.Game.Online.Leaderboards Mod[]? ExactMods ); - public record LeaderboardScores(IEnumerable TopScores, ScoreInfo? UserScore); + public record LeaderboardScores(IEnumerable TopScores, ScoreInfo? UserScore) + { + public IEnumerable AllScores + { + get + { + foreach (var score in TopScores) + yield return score; + + if (UserScore != null && TopScores.All(topScore => !topScore.Equals(UserScore) && !topScore.MatchesOnlineID(UserScore))) + yield return UserScore; + } + } + } } diff --git a/osu.Game/Screens/Play/ReplayPlayer.cs b/osu.Game/Screens/Play/ReplayPlayer.cs index ba572f6014..a5952f3ff3 100644 --- a/osu.Game/Screens/Play/ReplayPlayer.cs +++ b/osu.Game/Screens/Play/ReplayPlayer.cs @@ -14,6 +14,7 @@ using osu.Framework.Input.Events; using osu.Game.Beatmaps; using osu.Game.Configuration; using osu.Game.Input.Bindings; +using osu.Game.Online.Leaderboards; using osu.Game.Rulesets.Mods; using osu.Game.Scoring; using osu.Game.Screens.Play.HUD; @@ -59,6 +60,12 @@ namespace osu.Game.Screens.Play this.createScore = createScore; } + [Resolved] + private LeaderboardManager leaderboardManager { get; set; } = null!; + + private readonly IBindable globalScores = new Bindable(); + private readonly BindableList localScores = new BindableList(); + /// /// Add a settings group to the HUD overlay. Intended to be used by rulesets to add replay-specific settings. /// @@ -87,6 +94,20 @@ namespace osu.Game.Screens.Play HUDOverlay.PlayerSettingsOverlay.AddAtStart(playbackSettings); } + protected override void LoadComplete() + { + base.LoadComplete(); + + globalScores.BindTo(leaderboardManager.Scores); + globalScores.BindValueChanged(_ => + { + localScores.Clear(); + + if (globalScores.Value is LeaderboardScores g) + localScores.AddRange(g.AllScores.OrderByTotalScore()); + }, true); + } + protected override void PrepareReplay() { DrawableRuleset?.SetReplayScore(Score); @@ -97,13 +118,11 @@ namespace osu.Game.Screens.Play // Don't re-import replay scores as they're already present in the database. protected override Task ImportScore(Score score) => Task.CompletedTask; - public readonly BindableList LeaderboardScores = new BindableList(); - protected override GameplayLeaderboard CreateGameplayLeaderboard() => new SoloGameplayLeaderboard(Score.ScoreInfo.User) { AlwaysVisible = { Value = true }, - Scores = { BindTarget = LeaderboardScores } + Scores = { BindTarget = localScores } }; protected override ResultsScreen CreateResults(ScoreInfo score) => new SoloResultsScreen(score) diff --git a/osu.Game/Screens/Play/SoloPlayer.cs b/osu.Game/Screens/Play/SoloPlayer.cs index f4cf2da364..ed5dea98cd 100644 --- a/osu.Game/Screens/Play/SoloPlayer.cs +++ b/osu.Game/Screens/Play/SoloPlayer.cs @@ -6,10 +6,12 @@ using System; using System.Diagnostics; using System.Threading.Tasks; +using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Game.Beatmaps; using osu.Game.Extensions; using osu.Game.Online.API; +using osu.Game.Online.Leaderboards; using osu.Game.Online.Rooms; using osu.Game.Online.Solo; using osu.Game.Scoring; @@ -29,6 +31,26 @@ namespace osu.Game.Screens.Play { } + [Resolved] + private LeaderboardManager leaderboardManager { get; set; } = null!; + + private readonly IBindable globalScores = new Bindable(); + private readonly BindableList localScores = new BindableList(); + + protected override void LoadComplete() + { + base.LoadComplete(); + + globalScores.BindTo(leaderboardManager.Scores); + globalScores.BindValueChanged(_ => + { + localScores.Clear(); + + if (globalScores.Value is LeaderboardScores g) + localScores.AddRange(g.AllScores.OrderByTotalScore()); + }, true); + } + protected override APIRequest CreateTokenRequest() { int beatmapId = Beatmap.Value.BeatmapInfo.OnlineID; @@ -43,13 +65,11 @@ namespace osu.Game.Screens.Play return new CreateSoloScoreRequest(Beatmap.Value.BeatmapInfo, rulesetId, Game.VersionHash); } - public readonly BindableList LeaderboardScores = new BindableList(); - protected override GameplayLeaderboard CreateGameplayLeaderboard() => new SoloGameplayLeaderboard(Score.ScoreInfo.User) { AlwaysVisible = { Value = false }, - Scores = { BindTarget = LeaderboardScores } + Scores = { BindTarget = localScores } }; protected override bool ShouldExitOnTokenRetrievalFailure(Exception exception) => false; @@ -59,7 +79,7 @@ namespace osu.Game.Screens.Play // Before importing a score, stop binding the leaderboard with its score source. // This avoids a case where the imported score may cause a leaderboard refresh // (if the leaderboard's source is local). - LeaderboardScores.UnbindBindings(); + globalScores.UnbindBindings(); return base.ImportScore(score); } diff --git a/osu.Game/Screens/Select/PlaySongSelect.cs b/osu.Game/Screens/Select/PlaySongSelect.cs index 7b1479f392..c49b7c2ef2 100644 --- a/osu.Game/Screens/Select/PlaySongSelect.cs +++ b/osu.Game/Screens/Select/PlaySongSelect.cs @@ -129,17 +129,11 @@ namespace osu.Game.Screens.Select if (replayGeneratingMod != null) { - player = new ReplayPlayer((beatmap, mods) => replayGeneratingMod.CreateScoreFromReplayData(beatmap, mods)) - { - LeaderboardScores = { BindTarget = playBeatmapDetailArea.Leaderboard.Scores } - }; + player = new ReplayPlayer((beatmap, mods) => replayGeneratingMod.CreateScoreFromReplayData(beatmap, mods)); } else { - player = new SoloPlayer - { - LeaderboardScores = { BindTarget = playBeatmapDetailArea.Leaderboard.Scores } - }; + player = new SoloPlayer(); } return player; From 17bcc2842902a5befc90ce3249338493286d832b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 10 Apr 2025 09:45:25 +0200 Subject: [PATCH 030/164] Update scores request to move away from old endpoint --- osu.Game/Online/API/Requests/GetScoresRequest.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Online/API/Requests/GetScoresRequest.cs b/osu.Game/Online/API/Requests/GetScoresRequest.cs index f2a2daccb5..ed26c77dd9 100644 --- a/osu.Game/Online/API/Requests/GetScoresRequest.cs +++ b/osu.Game/Online/API/Requests/GetScoresRequest.cs @@ -36,7 +36,7 @@ namespace osu.Game.Online.API.Requests this.mods = mods ?? Array.Empty(); } - protected override string Target => $@"beatmaps/{beatmapInfo.OnlineID}/solo-scores{createQueryParameters()}"; + protected override string Target => $@"beatmaps/{beatmapInfo.OnlineID}/scores{createQueryParameters()}"; private string createQueryParameters() { From 31b98ac7b99414870c6b02fb472d5ab39d64fa44 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 10 Apr 2025 10:19:04 +0200 Subject: [PATCH 031/164] Ensure correct global leaderboard state when presenting scores With `ReplayPlayer` now consuming the `LeaderboardManager`'s global state, flows such as presenting a score need to set the global state up correctly to avoid accidentally showing a leaderboard from a completely different score. This also incidentally closes https://github.com/ppy/osu/issues/27609. --- .../Online/Leaderboards/LeaderboardManager.cs | 14 ++++++------- osu.Game/OsuGame.cs | 20 +++++++++++++++++++ osu.Game/OsuGameBase.cs | 6 +++--- 3 files changed, 30 insertions(+), 10 deletions(-) diff --git a/osu.Game/Online/Leaderboards/LeaderboardManager.cs b/osu.Game/Online/Leaderboards/LeaderboardManager.cs index 314705eb02..ff3fe39a96 100644 --- a/osu.Game/Online/Leaderboards/LeaderboardManager.cs +++ b/osu.Game/Online/Leaderboards/LeaderboardManager.cs @@ -27,7 +27,7 @@ namespace osu.Game.Online.Leaderboards public IBindable Scores => scores; private readonly Bindable scores = new Bindable(); - private LeaderboardCriteria? criteria; + public LeaderboardCriteria? CurrentCriteria { get; private set; } private IDisposable? localScoreSubscription; private TaskCompletionSource? localFetchCompletionSource; @@ -45,10 +45,10 @@ namespace osu.Game.Online.Leaderboards public Task FetchWithCriteriaAsync(LeaderboardCriteria newCriteria) { - if (criteria?.Equals(newCriteria) == true && lastFetchCompletionSource?.Task.IsFaulted == false) + if (CurrentCriteria?.Equals(newCriteria) == true && lastFetchCompletionSource?.Task.IsFaulted == false) return lastFetchCompletionSource?.Task ?? Task.FromResult(Scores.Value); - criteria = newCriteria; + CurrentCriteria = newCriteria; localScoreSubscription?.Dispose(); inFlightOnlineRequest?.Cancel(); lastFetchCompletionSource?.TrySetCanceled(); @@ -110,7 +110,7 @@ namespace osu.Game.Online.Leaderboards private void localScoresChanged(IRealmCollection sender, ChangeSet? changes) { - Debug.Assert(criteria != null); + Debug.Assert(CurrentCriteria != null); // This subscription may fire from changes to linked beatmaps, which we don't care about. // It's currently not possible for a score to be modified after insertion, so we can safely ignore callbacks with only modifications. @@ -119,9 +119,9 @@ namespace osu.Game.Online.Leaderboards var newScores = sender.AsEnumerable(); - if (criteria.ExactMods != null) + if (CurrentCriteria.ExactMods != null) { - if (!criteria.ExactMods.Any()) + if (!CurrentCriteria.ExactMods.Any()) { // we need to filter out all scores that have any mods to get all local nomod scores newScores = newScores.Where(s => !s.Mods.Any()); @@ -130,7 +130,7 @@ namespace osu.Game.Online.Leaderboards { // otherwise find all the scores that have all of the currently selected mods (similar to how web applies mod filters) // we're creating and using a string HashSet representation of selected mods so that it can be translated into the DB query itself - var selectedMods = criteria.ExactMods.Select(m => m.Acronym).ToHashSet(); + var selectedMods = CurrentCriteria.ExactMods.Select(m => m.Acronym).ToHashSet(); newScores = newScores.Where(s => selectedMods.SetEquals(s.Mods.Select(m => m.Acronym))); } diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs index 3381553970..76d370b4dc 100644 --- a/osu.Game/OsuGame.cs +++ b/osu.Game/OsuGame.cs @@ -47,6 +47,7 @@ using osu.Game.IO; using osu.Game.Localisation; using osu.Game.Online; using osu.Game.Online.Chat; +using osu.Game.Online.Leaderboards; using osu.Game.Online.Rooms; using osu.Game.Overlays; using osu.Game.Overlays.BeatmapListing; @@ -67,6 +68,7 @@ using osu.Game.Screens.OnlinePlay.Multiplayer; using osu.Game.Screens.Play; using osu.Game.Screens.Ranking; using osu.Game.Screens.Select; +using osu.Game.Screens.Select.Leaderboards; using osu.Game.Seasonal; using osu.Game.Skinning; using osu.Game.Updater; @@ -784,6 +786,24 @@ namespace osu.Game if (!Beatmap.Value.BeatmapInfo.Equals(databasedBeatmap)) Beatmap.Value = BeatmapManager.GetWorkingBeatmap(databasedBeatmap); + var currentLeaderboard = LeaderboardManager.CurrentCriteria; + + bool leaderboardBeatmapMatches = currentLeaderboard != null && databasedBeatmap.Equals(currentLeaderboard.Beatmap); + bool leaderboardRulesetMatches = currentLeaderboard != null && databasedScore.ScoreInfo.Ruleset.Equals(currentLeaderboard.Ruleset); + + if (!leaderboardBeatmapMatches || !leaderboardRulesetMatches) + { + var newLeaderboard = currentLeaderboard != null + ? currentLeaderboard with { Beatmap = databasedBeatmap, Ruleset = databasedScore.ScoreInfo.Ruleset } + : new LeaderboardCriteria(databasedBeatmap, databasedScore.ScoreInfo.Ruleset, BeatmapLeaderboardScope.Global, null); + LeaderboardManager.FetchWithCriteriaAsync(newLeaderboard) + .ContinueWith(t => + { + if (t.Exception != null) + Logger.Log($@"Failed to fetch leaderboards when displaying results: {t.Exception}", LoggingTarget.Network); + }); + } + switch (presentType) { case ScorePresentType.Gameplay: diff --git a/osu.Game/OsuGameBase.cs b/osu.Game/OsuGameBase.cs index fb28b8c5a4..9a8a127886 100644 --- a/osu.Game/OsuGameBase.cs +++ b/osu.Game/OsuGameBase.cs @@ -204,7 +204,7 @@ namespace osu.Game private UserLookupCache userCache; private BeatmapLookupCache beatmapCache; - private LeaderboardManager leaderboardManager; + protected LeaderboardManager LeaderboardManager { get; private set; } private RulesetConfigCache rulesetConfigCache; @@ -367,8 +367,8 @@ namespace osu.Game dependencies.CacheAs>(Beatmap); dependencies.CacheAs(Beatmap); - dependencies.Cache(leaderboardManager = new LeaderboardManager()); - base.Content.Add(leaderboardManager); + dependencies.Cache(LeaderboardManager = new LeaderboardManager()); + base.Content.Add(LeaderboardManager); // add api components to hierarchy. if (API is APIAccess apiAccess) From 34180c62eb7d4f46131d4f3763f108aebf17de6a Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Thu, 10 Apr 2025 17:47:39 +0900 Subject: [PATCH 032/164] Add display to show completed playlist items --- .../TestSceneDrawableRoomPlaylist.cs | 13 +++ .../DrawableRoomPlaylistItemStrings.cs | 19 ++++ osu.Game/Online/Rooms/ItemAttemptsCount.cs | 12 +++ osu.Game/Online/Rooms/PlaylistItem.cs | 7 ++ .../OnlinePlay/DrawableRoomPlaylistItem.cs | 90 ++++++++++++++++++- .../Playlists/PlaylistsRoomSubScreen.cs | 25 ++++++ 6 files changed, 163 insertions(+), 3 deletions(-) create mode 100644 osu.Game/Localisation/DrawableRoomPlaylistItemStrings.cs diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneDrawableRoomPlaylist.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneDrawableRoomPlaylist.cs index 18cd720bf2..7e19f45a00 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneDrawableRoomPlaylist.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneDrawableRoomPlaylist.cs @@ -105,6 +105,19 @@ namespace osu.Game.Tests.Visual.Multiplayer AddAssert("no item selected", () => playlist.SelectedItem.Value == null); } + [Test] + public void TestMarkCompleted() + { + createPlaylist(); + AddStep("mark some items as complete", () => + { + playlist.Items[0].MarkCompleted(); + playlist.Items[2].MarkCompleted(); + playlist.Items[3].MarkCompleted(); + playlist.Items[5].MarkCompleted(); + }); + } + [Test] public void TestSelectable() { diff --git a/osu.Game/Localisation/DrawableRoomPlaylistItemStrings.cs b/osu.Game/Localisation/DrawableRoomPlaylistItemStrings.cs new file mode 100644 index 0000000000..44616c03ca --- /dev/null +++ b/osu.Game/Localisation/DrawableRoomPlaylistItemStrings.cs @@ -0,0 +1,19 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Localisation; + +namespace osu.Game.Localisation +{ + public static class DrawableRoomPlaylistItemStrings + { + private const string prefix = @"osu.Game.Resources.Localisation.DrawableRoomPlaylistItem"; + + /// + /// "You have completed this beatmap" + /// + public static LocalisableString CompletedTooltip => new TranslatableString(getKey(@"completed_tooltip"), @"You have completed this beatmap"); + + private static string getKey(string key) => $@"{prefix}:{key}"; + } +} diff --git a/osu.Game/Online/Rooms/ItemAttemptsCount.cs b/osu.Game/Online/Rooms/ItemAttemptsCount.cs index dc86897660..17b7f093f4 100644 --- a/osu.Game/Online/Rooms/ItemAttemptsCount.cs +++ b/osu.Game/Online/Rooms/ItemAttemptsCount.cs @@ -10,10 +10,22 @@ namespace osu.Game.Online.Rooms /// public class ItemAttemptsCount { + /// + /// The playlist item this object describes. + /// [JsonProperty("id")] public int PlaylistItemID { get; set; } + /// + /// The number of times the user attempted the playlist item. + /// [JsonProperty("attempts")] public int Attempts { get; set; } + + /// + /// Whether the user has a passing score on the playlist item. + /// + [JsonProperty("completed")] + public bool Completed { get; set; } } } diff --git a/osu.Game/Online/Rooms/PlaylistItem.cs b/osu.Game/Online/Rooms/PlaylistItem.cs index 427f31fc64..8ba62fd0e2 100644 --- a/osu.Game/Online/Rooms/PlaylistItem.cs +++ b/osu.Game/Online/Rooms/PlaylistItem.cs @@ -85,6 +85,11 @@ namespace osu.Game.Online.Rooms private readonly Bindable valid = new BindableBool(true); + [JsonIgnore] + public IBindable Completed => completed; + + private readonly Bindable completed = new BindableBool(false); + [JsonConstructor] private PlaylistItem() : this(new APIBeatmap()) @@ -118,6 +123,8 @@ namespace osu.Game.Online.Rooms public void MarkInvalid() => valid.Value = false; + public void MarkCompleted() => completed.Value = true; + #region Newtonsoft.Json implicit ShouldSerialize() methods // The properties in this region are used implicitly by Newtonsoft.Json to not serialise certain fields in some cases. diff --git a/osu.Game/Screens/OnlinePlay/DrawableRoomPlaylistItem.cs b/osu.Game/Screens/OnlinePlay/DrawableRoomPlaylistItem.cs index 9e585d584d..0afeaa9532 100644 --- a/osu.Game/Screens/OnlinePlay/DrawableRoomPlaylistItem.cs +++ b/osu.Game/Screens/OnlinePlay/DrawableRoomPlaylistItem.cs @@ -31,13 +31,14 @@ using osu.Game.Online.Chat; using osu.Game.Online.Rooms; using osu.Game.Overlays; using osu.Game.Overlays.BeatmapSet; -using osu.Game.Resources.Localisation.Web; using osu.Game.Rulesets; using osu.Game.Rulesets.Mods; using osu.Game.Screens.Play.HUD; using osu.Game.Users.Drawables; using osuTK; using osuTK.Graphics; +using osu.Game.Localisation; +using WebLocalisation = osu.Game.Resources.Localisation.Web; namespace osu.Game.Screens.OnlinePlay { @@ -76,6 +77,7 @@ namespace osu.Game.Screens.OnlinePlay private readonly DelayedLoadWrapper onScreenLoader; private readonly IBindable valid = new Bindable(); + private readonly IBindable completed = new Bindable(); private IBeatmapInfo? beatmap; private IRulesetInfo? ruleset; @@ -128,6 +130,7 @@ namespace osu.Game.Screens.OnlinePlay Item = item; valid.BindTo(item.Valid); + completed.BindTo(item.Completed); } [BackgroundDependencyLoader] @@ -525,9 +528,27 @@ namespace osu.Game.Screens.OnlinePlay private IEnumerable createButtons() => new[] { - beatmap == null ? Empty() : new PlaylistDownloadButton(beatmap), + new CompletionIcon + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Visible = { BindTarget = completed } + }, + beatmap == null + ? Empty().With(d => + { + d.Anchor = Anchor.Centre; + d.Origin = Anchor.Centre; + }) + : new PlaylistDownloadButton(beatmap) + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + }, showResultsButton = new GrayButton(FontAwesome.Solid.ChartPie) { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, Size = new Vector2(30, 30), Action = () => RequestResults?.Invoke(Item), Alpha = AllowShowingResults ? 1 : 0, @@ -535,13 +556,17 @@ namespace osu.Game.Screens.OnlinePlay }, editButton = new PlaylistEditButton { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, Size = new Vector2(30, 30), Alpha = AllowEditing ? 1 : 0, Action = () => RequestEdit?.Invoke(Item), - TooltipText = CommonStrings.ButtonsEdit + TooltipText = WebLocalisation.CommonStrings.ButtonsEdit }, removeButton = new PlaylistRemoveButton { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, Size = new Vector2(30, 30), Alpha = AllowDeletion ? 1 : 0, Action = () => RequestDeletion?.Invoke(Item), @@ -768,5 +793,64 @@ namespace osu.Game.Screens.OnlinePlay this.allowInteraction = allowInteraction; } } + + private partial class CompletionIcon : CompositeDrawable, IHasTooltip + { + public readonly BindableBool Visible = new BindableBool(); + + [Resolved] + private OsuColour colours { get; set; } = null!; + + [BackgroundDependencyLoader] + private void load() + { + InternalChild = new CircularContainer + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Size = new Vector2(16), + Masking = true, + Colour = colours.Lime0, + Children = new Drawable[] + { + new Box + { + RelativeSizeAxes = Axes.Both + }, + new SpriteIcon + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + RelativeSizeAxes = Axes.Both, + Scale = new Vector2(0.5f), + Colour = OsuColour.Gray(0.5f), + Icon = FontAwesome.Solid.Check + } + } + }; + } + + protected override void LoadComplete() + { + base.LoadComplete(); + Visible.BindValueChanged(onVisibleChanged, true); + } + + private void onVisibleChanged(ValueChangedEvent visible) + { + if (visible.NewValue) + { + Size = new Vector2(16); + Alpha = 1; + } + else + { + Size = Vector2.Zero; + Alpha = 0; + } + } + + public LocalisableString TooltipText => DrawableRoomPlaylistItemStrings.CompletedTooltip; + } } } diff --git a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs index 053e3b97af..47219e42cb 100644 --- a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs @@ -465,6 +465,7 @@ namespace osu.Game.Screens.OnlinePlay.Playlists }); updateSetupState(); + updateUserScore(); updateGameplayState(); } @@ -480,6 +481,10 @@ namespace osu.Game.Screens.OnlinePlay.Playlists case nameof(Room.RoomID): updateSetupState(); break; + + case nameof(Room.UserScore): + updateUserScore(); + break; } } @@ -507,12 +512,32 @@ namespace osu.Game.Screens.OnlinePlay.Playlists progressSection.Alpha = room.MaxAttempts != null ? 1 : 0; drawablePlaylist.Items.ReplaceRange(0, drawablePlaylist.Items.Count, room.Playlist); + updateUserScore(); + // Select an initial item for the user to help them get into a playable state quicker. SelectedItem.Value = room.Playlist.FirstOrDefault(); }); } } + /// + /// Responds to changes in to mark playlist items as completed. + /// + private void updateUserScore() + { + if (room.UserScore == null) + return; + + if (drawablePlaylist.Items.Count == 0) + return; + + foreach (var item in room.UserScore.PlaylistItemAttempts) + { + if (item.Completed) + drawablePlaylist.Items.Single(i => i.ID == item.PlaylistItemID).MarkCompleted(); + } + } + /// /// Adjusts the rate at which the is updated. /// From 3dc8a4c1ed26e5cb95d146bcccb333eb9e7d09ae Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Thu, 10 Apr 2025 18:44:44 +0900 Subject: [PATCH 033/164] Rename to `passed` --- osu.Game/Online/Rooms/ItemAttemptsCount.cs | 4 ++-- .../Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Game/Online/Rooms/ItemAttemptsCount.cs b/osu.Game/Online/Rooms/ItemAttemptsCount.cs index 17b7f093f4..9ea2235500 100644 --- a/osu.Game/Online/Rooms/ItemAttemptsCount.cs +++ b/osu.Game/Online/Rooms/ItemAttemptsCount.cs @@ -25,7 +25,7 @@ namespace osu.Game.Online.Rooms /// /// Whether the user has a passing score on the playlist item. /// - [JsonProperty("completed")] - public bool Completed { get; set; } + [JsonProperty("passed")] + public bool Passed { get; set; } } } diff --git a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs index 47219e42cb..9834598ac0 100644 --- a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs @@ -533,7 +533,7 @@ namespace osu.Game.Screens.OnlinePlay.Playlists foreach (var item in room.UserScore.PlaylistItemAttempts) { - if (item.Completed) + if (item.Passed) drawablePlaylist.Items.Single(i => i.ID == item.PlaylistItemID).MarkCompleted(); } } From 69035ef48f772c468f0e7dcc4e770dd306660a84 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 10 Apr 2025 19:59:20 +0900 Subject: [PATCH 034/164] Fix status pill animating from zero height --- .../TestSceneBeatmapSetOnlineStatusPill.cs | 1 - .../Drawables/BeatmapSetOnlineStatusPill.cs | 28 ++++++++++--------- .../Cards/BeatmapCardExtraInfoRow.cs | 1 - .../BeatmapSet/BeatmapSetHeaderContent.cs | 1 - osu.Game/Screens/Select/BeatmapInfoWedge.cs | 1 - .../Select/Carousel/SetPanelContent.cs | 1 - osu.Game/Screens/SelectV2/PanelBeatmapSet.cs | 1 - .../SelectV2/PanelBeatmapStandalone.cs | 1 - 8 files changed, 15 insertions(+), 20 deletions(-) diff --git a/osu.Game.Tests/Visual/Beatmaps/TestSceneBeatmapSetOnlineStatusPill.cs b/osu.Game.Tests/Visual/Beatmaps/TestSceneBeatmapSetOnlineStatusPill.cs index dcc4654437..2b95d7a554 100644 --- a/osu.Game.Tests/Visual/Beatmaps/TestSceneBeatmapSetOnlineStatusPill.cs +++ b/osu.Game.Tests/Visual/Beatmaps/TestSceneBeatmapSetOnlineStatusPill.cs @@ -28,7 +28,6 @@ namespace osu.Game.Tests.Visual.Beatmaps Spacing = new Vector2(0, 10), ChildrenEnumerable = Enum.GetValues(typeof(BeatmapOnlineStatus)).Cast().Select(status => new BeatmapSetOnlineStatusPill { - AutoSizeAxes = Axes.Both, Anchor = Anchor.Centre, Origin = Anchor.Centre, Status = status diff --git a/osu.Game/Beatmaps/Drawables/BeatmapSetOnlineStatusPill.cs b/osu.Game/Beatmaps/Drawables/BeatmapSetOnlineStatusPill.cs index 7b99ad40de..2a1dd536b8 100644 --- a/osu.Game/Beatmaps/Drawables/BeatmapSetOnlineStatusPill.cs +++ b/osu.Game/Beatmaps/Drawables/BeatmapSetOnlineStatusPill.cs @@ -19,10 +19,6 @@ namespace osu.Game.Beatmaps.Drawables { public partial class BeatmapSetOnlineStatusPill : CircularContainer, IHasTooltip { - private const double animation_duration = 400; - - private BeatmapOnlineStatus status; - public BeatmapOnlineStatus Status { get => status; @@ -34,30 +30,27 @@ namespace osu.Game.Beatmaps.Drawables status = value; if (IsLoaded) - { - AutoSizeDuration = (float)animation_duration; - AutoSizeEasing = Easing.OutQuint; - updateState(); - } } } + private BeatmapOnlineStatus status; + public float TextSize { - get => statusText.Font.Size; - set => statusText.Font = statusText.Font.With(size: value); + init => statusText.Font = statusText.Font.With(size: value); } public MarginPadding TextPadding { - get => statusText.Padding; - set => statusText.Padding = value; + init => statusText.Padding = value; } private readonly OsuSpriteText statusText; private readonly Box background; + private const double animation_duration = 400; + [Resolved] private OsuColour colours { get; set; } = null!; @@ -66,6 +59,7 @@ namespace osu.Game.Beatmaps.Drawables public BeatmapSetOnlineStatusPill() { + AutoSizeAxes = Axes.Both; Masking = true; Alpha = 0; @@ -105,6 +99,14 @@ namespace osu.Game.Beatmaps.Drawables return; } + // Only animate resizing if we already have a size. + // This avoids animating height from zero. + if (Width > 0) + { + AutoSizeDuration = (float)animation_duration; + AutoSizeEasing = Easing.OutQuint; + } + this.FadeIn(animation_duration, Easing.OutQuint); Color4 statusTextColour; diff --git a/osu.Game/Beatmaps/Drawables/Cards/BeatmapCardExtraInfoRow.cs b/osu.Game/Beatmaps/Drawables/Cards/BeatmapCardExtraInfoRow.cs index 41513ec7a2..ee2f682708 100644 --- a/osu.Game/Beatmaps/Drawables/Cards/BeatmapCardExtraInfoRow.cs +++ b/osu.Game/Beatmaps/Drawables/Cards/BeatmapCardExtraInfoRow.cs @@ -30,7 +30,6 @@ namespace osu.Game.Beatmaps.Drawables.Cards { new BeatmapSetOnlineStatusPill { - AutoSizeAxes = Axes.Both, Status = beatmapSet.Status, Anchor = Anchor.CentreLeft, Origin = Anchor.CentreLeft, diff --git a/osu.Game/Overlays/BeatmapSet/BeatmapSetHeaderContent.cs b/osu.Game/Overlays/BeatmapSet/BeatmapSetHeaderContent.cs index c72c2a6698..9b10f6156d 100644 --- a/osu.Game/Overlays/BeatmapSet/BeatmapSetHeaderContent.cs +++ b/osu.Game/Overlays/BeatmapSet/BeatmapSetHeaderContent.cs @@ -177,7 +177,6 @@ namespace osu.Game.Overlays.BeatmapSet { onlineStatusPill = new BeatmapSetOnlineStatusPill { - AutoSizeAxes = Axes.Both, Anchor = Anchor.TopRight, Origin = Anchor.TopRight, TextSize = 14, diff --git a/osu.Game/Screens/Select/BeatmapInfoWedge.cs b/osu.Game/Screens/Select/BeatmapInfoWedge.cs index fd1c944689..5a09780943 100644 --- a/osu.Game/Screens/Select/BeatmapInfoWedge.cs +++ b/osu.Game/Screens/Select/BeatmapInfoWedge.cs @@ -263,7 +263,6 @@ namespace osu.Game.Screens.Select }, new BeatmapSetOnlineStatusPill { - AutoSizeAxes = Axes.Both, Anchor = Anchor.TopRight, Origin = Anchor.TopRight, Shear = -wedged_container_shear, diff --git a/osu.Game/Screens/Select/Carousel/SetPanelContent.cs b/osu.Game/Screens/Select/Carousel/SetPanelContent.cs index 8d6fbbf256..c3ded16bd2 100644 --- a/osu.Game/Screens/Select/Carousel/SetPanelContent.cs +++ b/osu.Game/Screens/Select/Carousel/SetPanelContent.cs @@ -77,7 +77,6 @@ namespace osu.Game.Screens.Select.Carousel }, new BeatmapSetOnlineStatusPill { - AutoSizeAxes = Axes.Both, Origin = Anchor.CentreLeft, Anchor = Anchor.CentreLeft, TextSize = 11, diff --git a/osu.Game/Screens/SelectV2/PanelBeatmapSet.cs b/osu.Game/Screens/SelectV2/PanelBeatmapSet.cs index c599c3e534..9e9ef612ea 100644 --- a/osu.Game/Screens/SelectV2/PanelBeatmapSet.cs +++ b/osu.Game/Screens/SelectV2/PanelBeatmapSet.cs @@ -97,7 +97,6 @@ namespace osu.Game.Screens.SelectV2 }, statusPill = new BeatmapSetOnlineStatusPill { - AutoSizeAxes = Axes.Both, Origin = Anchor.CentreLeft, Anchor = Anchor.CentreLeft, TextSize = 11, diff --git a/osu.Game/Screens/SelectV2/PanelBeatmapStandalone.cs b/osu.Game/Screens/SelectV2/PanelBeatmapStandalone.cs index 948311a86e..f893bb0caf 100644 --- a/osu.Game/Screens/SelectV2/PanelBeatmapStandalone.cs +++ b/osu.Game/Screens/SelectV2/PanelBeatmapStandalone.cs @@ -117,7 +117,6 @@ namespace osu.Game.Screens.SelectV2 }, statusPill = new BeatmapSetOnlineStatusPill { - AutoSizeAxes = Axes.Both, Origin = Anchor.CentreLeft, Anchor = Anchor.CentreLeft, TextSize = 11, From 57033fc1801b7243a97687cebc98caa41d96b6b4 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 10 Apr 2025 20:10:30 +0900 Subject: [PATCH 035/164] Allow displaying "unknown" status in status pill --- .../TestSceneBeatmapSetOnlineStatusPill.cs | 30 +++++++++++++------ osu.Game/Beatmaps/BeatmapOnlineStatus.cs | 2 +- .../Drawables/BeatmapSetOnlineStatusPill.cs | 7 ++++- osu.Game/Graphics/OsuColour.cs | 3 ++ 4 files changed, 31 insertions(+), 11 deletions(-) diff --git a/osu.Game.Tests/Visual/Beatmaps/TestSceneBeatmapSetOnlineStatusPill.cs b/osu.Game.Tests/Visual/Beatmaps/TestSceneBeatmapSetOnlineStatusPill.cs index 2b95d7a554..82e02a9b6f 100644 --- a/osu.Game.Tests/Visual/Beatmaps/TestSceneBeatmapSetOnlineStatusPill.cs +++ b/osu.Game.Tests/Visual/Beatmaps/TestSceneBeatmapSetOnlineStatusPill.cs @@ -19,6 +19,8 @@ namespace osu.Game.Tests.Visual.Beatmaps { public partial class TestSceneBeatmapSetOnlineStatusPill : ThemeComparisonTestScene { + private bool showUnknownStatus; + protected override Drawable CreateContent() => new FillFlowContainer { AutoSizeAxes = Axes.Both, @@ -26,11 +28,20 @@ namespace osu.Game.Tests.Visual.Beatmaps Origin = Anchor.Centre, Direction = FillDirection.Vertical, Spacing = new Vector2(0, 10), - ChildrenEnumerable = Enum.GetValues(typeof(BeatmapOnlineStatus)).Cast().Select(status => new BeatmapSetOnlineStatusPill + ChildrenEnumerable = Enum.GetValues(typeof(BeatmapOnlineStatus)).Cast().Select(status => new Container { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Status = status + RelativeSizeAxes = Axes.X, + Height = 20, + Children = new Drawable[] + { + new BeatmapSetOnlineStatusPill + { + ShowUnknownStatus = showUnknownStatus, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Status = status + } + } }) }; @@ -47,6 +58,12 @@ namespace osu.Game.Tests.Visual.Beatmaps pill.Width = 90; })); + AddStep("toggle show unknown", () => + { + showUnknownStatus = !showUnknownStatus; + CreateThemedContent(OverlayColourScheme.Red); + }); + AddStep("unset fixed width", () => statusPills.ForEach(pill => pill.AutoSizeAxes = Axes.Both)); } @@ -64,11 +81,6 @@ namespace osu.Game.Tests.Visual.Beatmaps pill.Status = BeatmapOnlineStatus.LocallyModified; break; - // skip none - case BeatmapOnlineStatus.LocallyModified: - pill.Status = BeatmapOnlineStatus.Graveyard; - break; - default: pill.Status = (pill.Status + 1); break; diff --git a/osu.Game/Beatmaps/BeatmapOnlineStatus.cs b/osu.Game/Beatmaps/BeatmapOnlineStatus.cs index 41393a8a39..d489aeda3f 100644 --- a/osu.Game/Beatmaps/BeatmapOnlineStatus.cs +++ b/osu.Game/Beatmaps/BeatmapOnlineStatus.cs @@ -14,10 +14,10 @@ namespace osu.Game.Beatmaps /// This is a special status given when local changes are made via the editor. /// Once in this state, online status changes should be ignored unless the beatmap is reverted or submitted. /// - [Description("Local")] [LocalisableDescription(typeof(SongSelectStrings), nameof(SongSelectStrings.LocallyModified))] LocallyModified = -4, + [Description("Unknown")] None = -3, [LocalisableDescription(typeof(BeatmapsetsStrings), nameof(BeatmapsetsStrings.ShowStatusGraveyard))] diff --git a/osu.Game/Beatmaps/Drawables/BeatmapSetOnlineStatusPill.cs b/osu.Game/Beatmaps/Drawables/BeatmapSetOnlineStatusPill.cs index 2a1dd536b8..83b385bb8e 100644 --- a/osu.Game/Beatmaps/Drawables/BeatmapSetOnlineStatusPill.cs +++ b/osu.Game/Beatmaps/Drawables/BeatmapSetOnlineStatusPill.cs @@ -19,6 +19,11 @@ namespace osu.Game.Beatmaps.Drawables { public partial class BeatmapSetOnlineStatusPill : CircularContainer, IHasTooltip { + /// + /// Whether to show as "unknownn" instead of fading out. + /// + public bool ShowUnknownStatus { get; init; } + public BeatmapOnlineStatus Status { get => status; @@ -93,7 +98,7 @@ namespace osu.Game.Beatmaps.Drawables private void updateState() { - if (Status == BeatmapOnlineStatus.None) + if (Status == BeatmapOnlineStatus.None && !ShowUnknownStatus) { Hide(); return; diff --git a/osu.Game/Graphics/OsuColour.cs b/osu.Game/Graphics/OsuColour.cs index 2c43876fb2..bc3047e624 100644 --- a/osu.Game/Graphics/OsuColour.cs +++ b/osu.Game/Graphics/OsuColour.cs @@ -120,6 +120,9 @@ namespace osu.Game.Graphics { switch (status) { + case BeatmapOnlineStatus.None: + return Color4.RosyBrown; + case BeatmapOnlineStatus.LocallyModified: return Color4.OrangeRed; From f9112066d3638477b088e0502665bbb97ede9a3f Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 10 Apr 2025 17:42:17 +0900 Subject: [PATCH 036/164] Fix carousel handling of bleed areas The idea of specifying "bleed" is to make the carousel aware of its vertical display area. The top bleed is under the filter control; bottom beneath the toolbar. At the end of the day, the point of panel X offset incursion, and the scroll target for current selection, should be at the centre of the screen. The fixes match code which already exists in the previous implementation. Basically, without incorporating `BleedTop` into calculations a second time, the centre position would not match expectations (of being the centre including bleed). --- .../SongSelect/BeatmapCarouselV2TestScene.cs | 2 ++ osu.Game/Screens/SelectV2/Carousel.cs | 14 +++++++++----- 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/osu.Game.Tests/Visual/SongSelect/BeatmapCarouselV2TestScene.cs b/osu.Game.Tests/Visual/SongSelect/BeatmapCarouselV2TestScene.cs index 2c902a466f..ad8004304a 100644 --- a/osu.Game.Tests/Visual/SongSelect/BeatmapCarouselV2TestScene.cs +++ b/osu.Game.Tests/Visual/SongSelect/BeatmapCarouselV2TestScene.cs @@ -96,6 +96,8 @@ namespace osu.Game.Tests.Visual.SongSelect { Carousel = new BeatmapCarousel { + BleedTop = 200, + BleedBottom = 200, Anchor = Anchor.Centre, Origin = Anchor.Centre, Width = 800, diff --git a/osu.Game/Screens/SelectV2/Carousel.cs b/osu.Game/Screens/SelectV2/Carousel.cs index 5339b5358b..21310b76a1 100644 --- a/osu.Game/Screens/SelectV2/Carousel.cs +++ b/osu.Game/Screens/SelectV2/Carousel.cs @@ -505,7 +505,7 @@ namespace osu.Game.Screens.SelectV2 private void scrollToSelection() { if (currentKeyboardSelection.CarouselItem != null) - Scroll.ScrollTo(currentKeyboardSelection.CarouselItem.CarouselYPosition - visibleHalfHeight); + Scroll.ScrollTo(currentKeyboardSelection.CarouselItem.CarouselYPosition - visibleHalfHeight + BleedTop); } #endregion @@ -519,17 +519,17 @@ namespace osu.Game.Screens.SelectV2 /// /// The position of the lower visible bound with respect to the current scroll position. /// - private float visibleBottomBound => (float)(Scroll.Current + DrawHeight + BleedBottom); + private float visibleBottomBound; /// /// The position of the upper visible bound with respect to the current scroll position. /// - private float visibleUpperBound => (float)(Scroll.Current - BleedTop); + private float visibleUpperBound; /// /// Half the height of the visible content. /// - private float visibleHalfHeight => (DrawHeight + BleedBottom + BleedTop) / 2; + private float visibleHalfHeight; protected override void Update() { @@ -538,6 +538,10 @@ namespace osu.Game.Screens.SelectV2 if (carouselItems == null) return; + visibleBottomBound = (float)(Scroll.Current + DrawHeight + BleedBottom); + visibleUpperBound = (float)(Scroll.Current - BleedTop); + visibleHalfHeight = (DrawHeight + BleedBottom + BleedTop) / 2; + if (!selectionValid.IsValid) { refreshAfterSelection(); @@ -582,7 +586,7 @@ namespace osu.Game.Screens.SelectV2 protected virtual float GetPanelXOffset(Drawable panel) { Vector2 posInScroll = Scroll.ToLocalSpace(panel.ScreenSpaceDrawQuad.Centre); - float dist = Math.Abs(1f - posInScroll.Y / visibleHalfHeight); + float dist = Math.Abs(1f - (posInScroll.Y + BleedTop) / visibleHalfHeight); return offsetX(dist, visibleHalfHeight); } From 430b22b383686441bf6dfae8a3419f620c93c0dd Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Thu, 20 Mar 2025 11:41:05 -0400 Subject: [PATCH 037/164] Remove previous beatmap wedge implementation --- .../SongSelectV2/TestSceneBeatmapInfoWedge.cs | 213 ----------- .../TestSceneDifficultyNameContent.cs | 44 --- .../Screens/SelectV2/BeatmapInfoWedgeV2.cs | 330 ------------------ .../SelectV2/Wedge/DifficultyNameContent.cs | 88 ----- .../Wedge/LocalDifficultyNameContent.cs | 34 -- 5 files changed, 709 deletions(-) delete mode 100644 osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapInfoWedge.cs delete mode 100644 osu.Game.Tests/Visual/SongSelectV2/TestSceneDifficultyNameContent.cs delete mode 100644 osu.Game/Screens/SelectV2/BeatmapInfoWedgeV2.cs delete mode 100644 osu.Game/Screens/SelectV2/Wedge/DifficultyNameContent.cs delete mode 100644 osu.Game/Screens/SelectV2/Wedge/LocalDifficultyNameContent.cs diff --git a/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapInfoWedge.cs b/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapInfoWedge.cs deleted file mode 100644 index 5b717887e2..0000000000 --- a/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapInfoWedge.cs +++ /dev/null @@ -1,213 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -using System.Collections.Generic; -using System.Linq; -using NUnit.Framework; -using osu.Framework.Allocation; -using osu.Framework.Graphics; -using osu.Framework.Graphics.Containers; -using osu.Framework.Graphics.Shapes; -using osu.Framework.Graphics.UserInterface; -using osu.Framework.Testing; -using osu.Game.Beatmaps; -using osu.Game.Rulesets; -using osu.Game.Rulesets.Objects; -using osu.Game.Rulesets.Objects.Legacy; -using osu.Game.Screens.Select; -using osu.Game.Screens.SelectV2; - -namespace osu.Game.Tests.Visual.SongSelectV2 -{ - public partial class TestSceneBeatmapInfoWedge : SongSelectComponentsTestScene - { - private RulesetStore rulesets = null!; - private TestBeatmapInfoWedgeV2 infoWedge = null!; - private readonly List beatmaps = new List(); - - [BackgroundDependencyLoader] - private void load(RulesetStore rulesets) - { - this.rulesets = rulesets; - } - - public override void SetUpSteps() - { - base.SetUpSteps(); - - AddStep("reset mods", () => SelectedMods.SetDefault()); - } - - protected override void LoadComplete() - { - base.LoadComplete(); - - AddRange(new Drawable[] - { - // This exists only to make the wedge more visible in the test scene - new Box - { - Y = -20, - Colour = Colour4.Cornsilk.Darken(0.2f), - Height = BeatmapInfoWedgeV2.WEDGE_HEIGHT + 40, - Width = 0.65f, - RelativeSizeAxes = Axes.X, - Margin = new MarginPadding { Top = 20, Left = -10 } - }, - new Container - { - RelativeSizeAxes = Axes.Both, - Padding = new MarginPadding { Top = 20 }, - Child = infoWedge = new TestBeatmapInfoWedgeV2 - { - Width = 0.6f, - RelativeSizeAxes = Axes.X, - }, - } - }); - - AddSliderStep("change star difficulty", 0, 11.9, 5.55, v => - { - foreach (var hasCurrentValue in infoWedge.ChildrenOfType>()) - hasCurrentValue.Current.Value = new StarDifficulty(v, 0); - }); - } - - [Test] - public void TestRulesetChange() - { - selectBeatmap(Beatmap.Value.Beatmap); - - AddWaitStep("wait for select", 3); - - foreach (var rulesetInfo in rulesets.AvailableRulesets) - { - var instance = rulesetInfo.CreateInstance(); - var testBeatmap = createTestBeatmap(rulesetInfo); - - beatmaps.Add(testBeatmap); - - setRuleset(rulesetInfo); - - selectBeatmap(testBeatmap); - - testBeatmapLabels(instance); - } - } - - [Test] - public void TestWedgeVisibility() - { - AddStep("hide", () => { infoWedge.Hide(); }); - AddWaitStep("wait for hide", 3); - AddAssert("check visibility", () => infoWedge.Alpha == 0); - AddStep("show", () => { infoWedge.Show(); }); - AddWaitStep("wait for show", 1); - AddAssert("check visibility", () => infoWedge.Alpha > 0); - } - - private void testBeatmapLabels(Ruleset ruleset) - { - AddAssert("check title", () => infoWedge.Info!.TitleLabel.Current.Value == $"{ruleset.ShortName}Title"); - AddAssert("check artist", () => infoWedge.Info!.ArtistLabel.Current.Value == $"{ruleset.ShortName}Artist"); - } - - [Test] - public void TestTruncation() - { - selectBeatmap(createLongMetadata()); - } - - [Test] - public void TestNullBeatmapWithBackground() - { - selectBeatmap(null); - AddAssert("check default title", () => infoWedge.Info!.TitleLabel.Current.Value == Beatmap.Default.BeatmapInfo.Metadata.Title); - AddAssert("check default artist", () => infoWedge.Info!.ArtistLabel.Current.Value == Beatmap.Default.BeatmapInfo.Metadata.Artist); - AddAssert("check no info labels", () => !infoWedge.Info.ChildrenOfType().Any()); - } - - private void setRuleset(RulesetInfo rulesetInfo) - { - Container? containerBefore = null; - - AddStep("set ruleset", () => - { - // wedge content is only refreshed if the ruleset changes, so only wait for load in that case. - if (!rulesetInfo.Equals(Ruleset.Value)) - containerBefore = infoWedge.DisplayedContent; - - Ruleset.Value = rulesetInfo; - }); - - AddUntilStep("wait for async load", () => infoWedge.DisplayedContent != containerBefore); - } - - private void selectBeatmap(IBeatmap? b) - { - Container? containerBefore = null; - - AddStep($"select {b?.Metadata.Title ?? "null"} beatmap", () => - { - containerBefore = infoWedge.DisplayedContent; - infoWedge.Beatmap = Beatmap.Value = b == null ? Beatmap.Default : CreateWorkingBeatmap(b); - infoWedge.Show(); - }); - - AddUntilStep("wait for async load", () => infoWedge.DisplayedContent != containerBefore); - } - - private IBeatmap createTestBeatmap(RulesetInfo ruleset) - { - List objects = new List(); - for (double i = 0; i < 50000; i += 1000) - objects.Add(new TestHitObject { StartTime = i }); - - return new Beatmap - { - BeatmapInfo = new BeatmapInfo - { - Metadata = new BeatmapMetadata - { - Author = { Username = $"{ruleset.ShortName}Author" }, - Artist = $"{ruleset.ShortName}Artist", - Source = $"{ruleset.ShortName}Source", - Title = $"{ruleset.ShortName}Title" - }, - Ruleset = ruleset, - StarRating = 6, - DifficultyName = $"{ruleset.ShortName}Version", - Difficulty = new BeatmapDifficulty() - }, - HitObjects = objects - }; - } - - private IBeatmap createLongMetadata() - { - return new Beatmap - { - BeatmapInfo = new BeatmapInfo - { - Metadata = new BeatmapMetadata - { - Author = { Username = "WWWWWWWWWWWWWWW" }, - Artist = "Verrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrry long Artist", - Source = "Verrrrry long Source", - Title = "Verrrrry long Title" - }, - DifficultyName = "Verrrrrrrrrrrrrrrrrrrrrrrrrrrrry long Version", - Status = BeatmapOnlineStatus.Graveyard, - }, - }; - } - - private partial class TestBeatmapInfoWedgeV2 : BeatmapInfoWedgeV2 - { - public new Container? DisplayedContent => base.DisplayedContent; - public new WedgeInfoText? Info => base.Info; - } - - private class TestHitObject : ConvertHitObject; - } -} diff --git a/osu.Game.Tests/Visual/SongSelectV2/TestSceneDifficultyNameContent.cs b/osu.Game.Tests/Visual/SongSelectV2/TestSceneDifficultyNameContent.cs deleted file mode 100644 index 49e7e2bc1a..0000000000 --- a/osu.Game.Tests/Visual/SongSelectV2/TestSceneDifficultyNameContent.cs +++ /dev/null @@ -1,44 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -using System.Linq; -using NUnit.Framework; -using osu.Framework.Localisation; -using osu.Framework.Testing; -using osu.Game.Beatmaps; -using osu.Game.Graphics.Containers; -using osu.Game.Graphics.Sprites; -using osu.Game.Screens.SelectV2.Wedge; - -namespace osu.Game.Tests.Visual.SongSelectV2 -{ - public partial class TestSceneDifficultyNameContent : SongSelectComponentsTestScene - { - private DifficultyNameContent? difficultyNameContent; - - [Test] - public void TestLocalBeatmap() - { - AddStep("set component", () => Child = difficultyNameContent = new LocalDifficultyNameContent()); - - AddAssert("difficulty name is not set", () => LocalisableString.IsNullOrEmpty(difficultyNameContent.ChildrenOfType().Single().Text)); - AddAssert("author is not set", () => LocalisableString.IsNullOrEmpty(difficultyNameContent.ChildrenOfType().Single().ChildrenOfType().Single().Text)); - - AddStep("set beatmap", () => Beatmap.Value = CreateWorkingBeatmap(new Beatmap - { - BeatmapInfo = new BeatmapInfo - { - DifficultyName = "really long difficulty name that gets truncated", - Metadata = new BeatmapMetadata - { - Author = { Username = "really long username that is autosized" }, - }, - OnlineID = 1, - } - })); - - AddAssert("difficulty name is set", () => !LocalisableString.IsNullOrEmpty(difficultyNameContent.ChildrenOfType().Single().Text)); - AddAssert("author is set", () => !LocalisableString.IsNullOrEmpty(difficultyNameContent.ChildrenOfType().Single().ChildrenOfType().Single().Text)); - } - } -} diff --git a/osu.Game/Screens/SelectV2/BeatmapInfoWedgeV2.cs b/osu.Game/Screens/SelectV2/BeatmapInfoWedgeV2.cs deleted file mode 100644 index b294896c77..0000000000 --- a/osu.Game/Screens/SelectV2/BeatmapInfoWedgeV2.cs +++ /dev/null @@ -1,330 +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.Threading; -using osu.Framework.Allocation; -using osu.Framework.Bindables; -using osu.Framework.Graphics; -using osu.Framework.Graphics.Containers; -using osu.Framework.Graphics.Effects; -using osu.Framework.Graphics.Shapes; -using osu.Framework.Localisation; -using osu.Game.Beatmaps; -using osu.Game.Beatmaps.Drawables; -using osu.Game.Graphics; -using osu.Game.Graphics.Containers; -using osu.Game.Graphics.Sprites; -using osu.Game.Graphics.UserInterface; -using osu.Game.Rulesets; -using osu.Game.Screens.Select; -using osuTK; - -namespace osu.Game.Screens.SelectV2 -{ - public partial class BeatmapInfoWedgeV2 : VisibilityContainer - { - public const float WEDGE_HEIGHT = 120; - private const float shear_width = 21; - private const float transition_duration = 250; - private const float corner_radius = 10; - private const float colour_bar_width = 30; - - /// Todo: move this const out to song select when more new design elements are implemented for the beatmap details area, since it applies to text alignment of various elements - private const float text_margin = 62; - - private static readonly Vector2 wedged_container_shear = new Vector2(shear_width / WEDGE_HEIGHT, 0); - - [Resolved] - private IBindable ruleset { get; set; } = null!; - - [Resolved] - private OsuColour colours { get; set; } = null!; - - [Resolved] - private BeatmapDifficultyCache difficultyCache { get; set; } = null!; - - protected Container? DisplayedContent { get; private set; } - - protected WedgeInfoText? Info { get; private set; } - - private Container difficultyColourBar = null!; - private StarCounter starCounter = null!; - private StarRatingDisplay starRatingDisplay = null!; - private BeatmapSetOnlineStatusPill statusPill = null!; - private Container content = null!; - - private IBindable? starDifficulty; - private CancellationTokenSource? cancellationSource; - - public BeatmapInfoWedgeV2() - { - Height = WEDGE_HEIGHT; - Shear = wedged_container_shear; - Masking = true; - Margin = new MarginPadding { Left = -corner_radius }; - EdgeEffect = new EdgeEffectParameters - { - Colour = Colour4.Black.Opacity(0.2f), - Type = EdgeEffectType.Shadow, - Radius = 3, - }; - CornerRadius = corner_radius; - } - - [BackgroundDependencyLoader] - private void load() - { - Child = content = new Container - { - RelativeSizeAxes = Axes.Both, - Children = new Drawable[] - { - // These elements can't be grouped with the rest of the content, due to being present either outside or under the backgrounds area - difficultyColourBar = new Container - { - Colour = Colour4.Transparent, - Depth = float.MaxValue, - Anchor = Anchor.TopRight, - Origin = Anchor.TopRight, - RelativeSizeAxes = Axes.Y, - - // By limiting the width we avoid this box showing up as an outline around the drawables that are on top of it. - Width = colour_bar_width + corner_radius, - Child = new Box { RelativeSizeAxes = Axes.Both } - }, - new Container - { - // Applying the shear to this container and nesting the starCounter inside avoids - // the deformation that occurs if the shear is applied to the starCounter whilst rotated - Shear = -wedged_container_shear, - X = -colour_bar_width / 2, - Anchor = Anchor.CentreRight, - Origin = Anchor.Centre, - RelativeSizeAxes = Axes.Y, - Width = colour_bar_width, - Child = starCounter = new StarCounter - { - Rotation = (float)(Math.Atan(shear_width / WEDGE_HEIGHT) * (180 / Math.PI)), - Colour = Colour4.Transparent, - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Scale = new Vector2(0.35f), - Direction = FillDirection.Vertical - } - }, - new FillFlowContainer - { - Name = "Topright-aligned metadata", - Anchor = Anchor.TopRight, - Origin = Anchor.TopRight, - Direction = FillDirection.Vertical, - Padding = new MarginPadding { Top = 3, Right = colour_bar_width + 8 }, - AutoSizeAxes = Axes.Both, - Spacing = new Vector2(0, 5), - Depth = float.MinValue, - Children = new Drawable[] - { - starRatingDisplay = new StarRatingDisplay(default, animated: true) - { - Anchor = Anchor.TopRight, - Origin = Anchor.TopRight, - Shear = -wedged_container_shear, - Alpha = 0, - }, - statusPill = new BeatmapSetOnlineStatusPill - { - AutoSizeAxes = Axes.Both, - Anchor = Anchor.TopRight, - Origin = Anchor.TopRight, - Shear = -wedged_container_shear, - TextSize = 11, - TextPadding = new MarginPadding { Horizontal = 8, Vertical = 2 }, - Alpha = 0, - } - } - }, - } - }; - } - - protected override void LoadComplete() - { - base.LoadComplete(); - - ruleset.BindValueChanged(_ => updateDisplay()); - - starRatingDisplay.Current.BindValueChanged(s => - { - // use actual stars as star counter has its own animation - starCounter.Current = (float)s.NewValue.Stars; - }, true); - - starRatingDisplay.DisplayedStars.BindValueChanged(s => - { - // sync color with star rating display - starCounter.Colour = s.NewValue >= 6.5 ? colours.Orange1 : Colour4.Black.Opacity(0.75f); - difficultyColourBar.FadeColour(colours.ForStarDifficulty(s.NewValue)); - }, true); - } - - private const double animation_duration = 600; - - protected override void PopIn() - { - this.MoveToX(0, animation_duration, Easing.OutQuint); - this.FadeIn(200, Easing.In); - } - - protected override void PopOut() - { - this.MoveToX(-150, animation_duration, Easing.OutQuint); - this.FadeOut(200, Easing.OutQuint); - } - - private WorkingBeatmap beatmap = null!; - - public WorkingBeatmap Beatmap - { - get => beatmap; - set - { - if (beatmap == value) return; - - beatmap = value; - - updateDisplay(); - } - } - - private Container? loadingInfo; - - private void updateDisplay() - { - statusPill.Status = beatmap.BeatmapInfo.Status; - - starDifficulty = difficultyCache.GetBindableDifficulty(beatmap.BeatmapInfo, (cancellationSource = new CancellationTokenSource()).Token); - - starDifficulty.BindValueChanged(s => - { - starRatingDisplay.Current.Value = s.NewValue ?? default; - - starRatingDisplay.FadeIn(transition_duration); - }); - - Scheduler.AddOnce(() => - { - LoadComponentAsync(loadingInfo = new Container - { - Padding = new MarginPadding { Right = colour_bar_width }, - RelativeSizeAxes = Axes.Both, - Depth = DisplayedContent?.Depth + 1 ?? 0, - Child = new Container - { - Masking = true, - CornerRadius = corner_radius, - RelativeSizeAxes = Axes.Both, - Children = new Drawable[] - { - // TODO: New wedge design uses a coloured horizontal gradient for its background, however this lacks implementation information in the figma draft. - // pending https://www.figma.com/file/DXKwqZhD5yyb1igc3mKo1P?node-id=2980:3361#340801912 being answered. - new BeatmapInfoWedgeBackground(beatmap) { Shear = -Shear }, - Info = new WedgeInfoText(beatmap) { Shear = -Shear } - } - } - }, d => - { - // Ensure we are the most recent loaded wedge. - if (d != loadingInfo) return; - - removeOldInfo(); - content.Add(DisplayedContent = d); - }); - }); - - void removeOldInfo() - { - DisplayedContent?.FadeOut(transition_duration); - DisplayedContent?.Expire(); - DisplayedContent = null; - } - } - - protected override void Dispose(bool isDisposing) - { - base.Dispose(isDisposing); - - cancellationSource?.Cancel(); - } - - public partial class WedgeInfoText : Container - { - public OsuSpriteText TitleLabel { get; private set; } = null!; - public OsuSpriteText ArtistLabel { get; private set; } = null!; - - private readonly WorkingBeatmap working; - - public WedgeInfoText(WorkingBeatmap working) - { - this.working = working; - - RelativeSizeAxes = Axes.Both; - } - - [BackgroundDependencyLoader] - private void load(SongSelect? songSelect, LocalisationManager localisation) - { - var metadata = working.Metadata; - - var titleText = new RomanisableString(metadata.TitleUnicode, metadata.Title); - var artistText = new RomanisableString(metadata.ArtistUnicode, metadata.Artist); - - Child = new FillFlowContainer - { - Name = "Top-left aligned metadata", - Direction = FillDirection.Vertical, - Padding = new MarginPadding { Left = text_margin, Top = 12 }, - AutoSizeAxes = Axes.Y, - RelativeSizeAxes = Axes.X, - Children = new Drawable[] - { - new OsuHoverContainer - { - AutoSizeAxes = Axes.Both, - Action = () => songSelect?.Search(titleText.GetPreferred(localisation.CurrentParameters.Value.PreferOriginalScript)), - Child = TitleLabel = new TruncatingSpriteText - { - Shadow = true, - Text = titleText, - Font = OsuFont.TorusAlternate.With(size: 40, weight: FontWeight.SemiBold), - }, - }, - new OsuHoverContainer - { - AutoSizeAxes = Axes.Both, - Action = () => songSelect?.Search(artistText.GetPreferred(localisation.CurrentParameters.Value.PreferOriginalScript)), - Child = ArtistLabel = new TruncatingSpriteText - { - // TODO : figma design has a diffused shadow, instead of the solid one present here, not possible currently as far as i'm aware. - Shadow = true, - Text = artistText, - // Not sure if this should be semi bold or medium - Font = OsuFont.Torus.With(size: 20, weight: FontWeight.SemiBold), - }, - }, - } - }; - } - - protected override void UpdateAfterChildren() - { - base.UpdateAfterChildren(); - - // best effort to confine the auto-sized text to wedge bounds - // the artist label doesn't have an extra text_margin as it doesn't touch the right metadata - TitleLabel.MaxWidth = DrawWidth - text_margin * 2 - shear_width; - ArtistLabel.MaxWidth = DrawWidth - text_margin - shear_width; - } - } - } -} diff --git a/osu.Game/Screens/SelectV2/Wedge/DifficultyNameContent.cs b/osu.Game/Screens/SelectV2/Wedge/DifficultyNameContent.cs deleted file mode 100644 index 4a3dc34cf9..0000000000 --- a/osu.Game/Screens/SelectV2/Wedge/DifficultyNameContent.cs +++ /dev/null @@ -1,88 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -using System; -using osu.Framework.Allocation; -using osu.Framework.Graphics; -using osu.Framework.Graphics.Containers; -using osu.Game.Graphics; -using osu.Game.Graphics.Containers; -using osu.Game.Graphics.Sprites; -using osu.Game.Localisation; -using osu.Game.Overlays; - -namespace osu.Game.Screens.SelectV2.Wedge -{ - public abstract partial class DifficultyNameContent : CompositeDrawable - { - protected OsuSpriteText DifficultyName = null!; - private OsuSpriteText mappedByLabel = null!; - protected OsuHoverContainer MapperLink = null!; - protected OsuSpriteText MapperName = null!; - - protected DifficultyNameContent() - { - RelativeSizeAxes = Axes.X; - AutoSizeAxes = Axes.Y; - } - - [BackgroundDependencyLoader] - private void load() - { - InternalChild = new FillFlowContainer - { - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, - Direction = FillDirection.Horizontal, - Children = new Drawable[] - { - DifficultyName = new TruncatingSpriteText - { - Anchor = Anchor.BottomLeft, - Origin = Anchor.BottomLeft, - Font = OsuFont.GetFont(weight: FontWeight.SemiBold), - }, - mappedByLabel = new OsuSpriteText - { - Anchor = Anchor.BottomLeft, - Origin = Anchor.BottomLeft, - // TODO: better null display? beatmap carousel panels also just show this text currently. - Text = " mapped by ", - Font = OsuFont.GetFont(size: 14), - }, - // This is not a `LinkFlowContainer` as there are single-frame layout issues when Update() - // is being used for layout, see https://github.com/ppy/osu-framework/issues/3369. - MapperLink = new MapperLinkContainer - { - Anchor = Anchor.BottomLeft, - Origin = Anchor.BottomLeft, - AutoSizeAxes = Axes.Both, - Child = MapperName = new OsuSpriteText - { - Font = OsuFont.GetFont(weight: FontWeight.SemiBold, size: 14), - } - }, - } - }; - } - - protected override void Update() - { - base.Update(); - - // truncate difficulty name when width exceeds bounds, prioritizing mapper name display - DifficultyName.MaxWidth = Math.Max(DrawWidth - mappedByLabel.DrawWidth - - MapperName.DrawWidth, 0); - } - - private partial class MapperLinkContainer : OsuHoverContainer - { - [BackgroundDependencyLoader] - private void load(OverlayColourProvider? overlayColourProvider, OsuColour colours) - { - TooltipText = ContextMenuStrings.ViewProfile; - IdleColour = overlayColourProvider?.Light2 ?? colours.Blue; - } - } - } -} diff --git a/osu.Game/Screens/SelectV2/Wedge/LocalDifficultyNameContent.cs b/osu.Game/Screens/SelectV2/Wedge/LocalDifficultyNameContent.cs deleted file mode 100644 index 66f8cb02b2..0000000000 --- a/osu.Game/Screens/SelectV2/Wedge/LocalDifficultyNameContent.cs +++ /dev/null @@ -1,34 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -using osu.Framework.Allocation; -using osu.Framework.Bindables; -using osu.Game.Beatmaps; -using osu.Game.Online; -using osu.Game.Online.Chat; - -namespace osu.Game.Screens.SelectV2.Wedge -{ - public partial class LocalDifficultyNameContent : DifficultyNameContent - { - [Resolved] - private IBindable beatmap { get; set; } = null!; - - [Resolved] - private ILinkHandler? linkHandler { get; set; } - - protected override void LoadComplete() - { - base.LoadComplete(); - - beatmap.BindValueChanged(b => - { - DifficultyName.Text = b.NewValue.BeatmapInfo.DifficultyName; - - // TODO: should be the mapper of the guest difficulty, but that isn't stored correctly yet (see https://github.com/ppy/osu/issues/12965) - MapperName.Text = b.NewValue.Metadata.Author.Username; - MapperLink.Action = () => linkHandler?.HandleLink(new LinkDetails(LinkAction.OpenUserProfile, b.NewValue.Metadata.Author)); - }, true); - } - } -} From ba5932c1dd4eaff2d6e61a9b4dabc3e52dc5e36b Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 11 Apr 2025 02:11:45 +0900 Subject: [PATCH 038/164] Revert "Use median for statistic display" This reverts commit fa06643bb6c0aacde659640ae0a65c68ab9b0c61. Revert "Remove mean hit error calculation" This reverts commit b3c578e5455c572e34e2def301ba657182747149. --- osu.Game/Rulesets/Scoring/HitEventExtensions.cs | 17 +++++++++++++++++ .../Ranking/Statistics/AverageHitError.cs | 6 +++--- 2 files changed, 20 insertions(+), 3 deletions(-) diff --git a/osu.Game/Rulesets/Scoring/HitEventExtensions.cs b/osu.Game/Rulesets/Scoring/HitEventExtensions.cs index 39fc8b357b..01d800a351 100644 --- a/osu.Game/Rulesets/Scoring/HitEventExtensions.cs +++ b/osu.Game/Rulesets/Scoring/HitEventExtensions.cs @@ -54,6 +54,23 @@ namespace osu.Game.Rulesets.Scoring return result; } + /// + /// Calculates the average hit offset/error for a sequence of s, where negative numbers mean the user hit too early on average. + /// + /// + /// A non-null value if unstable rate could be calculated, + /// and if unstable rate cannot be calculated due to being empty. + /// + public static double? CalculateAverageHitError(this IEnumerable hitEvents) + { + double[] timeOffsets = hitEvents.Where(AffectsUnstableRate).Select(ev => ev.TimeOffset).ToArray(); + + if (timeOffsets.Length == 0) + return null; + + return timeOffsets.Average(); + } + /// /// Calculates the median hit offset/error for a sequence of s, where negative numbers mean the user hit too early on average. /// diff --git a/osu.Game/Screens/Ranking/Statistics/AverageHitError.cs b/osu.Game/Screens/Ranking/Statistics/AverageHitError.cs index 29df085c62..fb7107cc88 100644 --- a/osu.Game/Screens/Ranking/Statistics/AverageHitError.cs +++ b/osu.Game/Screens/Ranking/Statistics/AverageHitError.cs @@ -8,18 +8,18 @@ using osu.Game.Rulesets.Scoring; namespace osu.Game.Screens.Ranking.Statistics { /// - /// Displays the average hit error statistic for a given play. + /// Displays the unstable rate statistic for a given play. /// public partial class AverageHitError : SimpleStatisticItem { /// /// Creates and computes an statistic. /// - /// Sequence of s to calculate the average hit error based on. + /// Sequence of s to calculate the unstable rate based on. public AverageHitError(IEnumerable hitEvents) : base("Average Hit Error") { - Value = hitEvents.CalculateMedianHitError(); + Value = hitEvents.CalculateAverageHitError(); } protected override string DisplayValue(double? value) => value == null ? "(not available)" : $"{Math.Abs(value.Value):N2} ms {(value.Value < 0 ? "early" : "late")}"; From 6bb84e4364256df75949a56cc4d67023a773f00c Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Thu, 20 Mar 2025 11:46:42 -0400 Subject: [PATCH 039/164] Update API beatmap model to include user play count --- osu.Game/Online/API/Requests/Responses/APIBeatmap.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/osu.Game/Online/API/Requests/Responses/APIBeatmap.cs b/osu.Game/Online/API/Requests/Responses/APIBeatmap.cs index 055d2dd8e2..20494a1cbf 100644 --- a/osu.Game/Online/API/Requests/Responses/APIBeatmap.cs +++ b/osu.Game/Online/API/Requests/Responses/APIBeatmap.cs @@ -32,6 +32,9 @@ namespace osu.Game.Online.API.Requests.Responses [JsonProperty(@"playcount")] public int PlayCount { get; set; } + [JsonProperty(@"current_user_playcount")] + public int UserPlayCount { get; set; } + [JsonProperty(@"passcount")] public int PassCount { get; set; } From 55dc64e5b6bafcc8b45f4214f432591f5ef888a0 Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Thu, 20 Mar 2025 11:51:23 -0400 Subject: [PATCH 040/164] Add `DarkOrange` colour set --- osu.Game/Graphics/OsuColour.cs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/osu.Game/Graphics/OsuColour.cs b/osu.Game/Graphics/OsuColour.cs index 2c43876fb2..260ff9f797 100644 --- a/osu.Game/Graphics/OsuColour.cs +++ b/osu.Game/Graphics/OsuColour.cs @@ -23,6 +23,7 @@ namespace osu.Game.Graphics /// /// Retrieves the colour for a given point in the star range. /// + // todo: fix stupid array public Color4 ForStarDifficulty(double starDifficulty) => ColourUtils.SampleFromLinearGradient(new[] { (0.1f, Color4Extensions.FromHex("aaaaaa")), @@ -403,6 +404,12 @@ namespace osu.Game.Graphics public readonly Color4 Orange3 = Color4Extensions.FromHex(@"cca633"); public readonly Color4 Orange4 = Color4Extensions.FromHex(@"6b5c2e"); + public readonly Color4 DarkOrange0 = Color4Extensions.FromHex(@"ffbb99"); + public readonly Color4 DarkOrange1 = Color4Extensions.FromHex(@"ff9966"); + public readonly Color4 DarkOrange2 = Color4Extensions.FromHex(@"eb7e47"); + public readonly Color4 DarkOrange3 = Color4Extensions.FromHex(@"cc6633"); + public readonly Color4 DarkOrange4 = Color4Extensions.FromHex(@"6b422e"); + public readonly Color4 Red0 = Color4Extensions.FromHex(@"ff9b9b"); public readonly Color4 Red1 = Color4Extensions.FromHex(@"ff6666"); public readonly Color4 Red2 = Color4Extensions.FromHex(@"eb4747"); From 36a11d4bf7afa3ca238a561aa2e67ed498aed440 Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Thu, 20 Mar 2025 11:52:13 -0400 Subject: [PATCH 041/164] Add specifications for new song select icons --- osu.Game/Graphics/OsuIcon.cs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/osu.Game/Graphics/OsuIcon.cs b/osu.Game/Graphics/OsuIcon.cs index 9879ef5d14..84ff86a5e5 100644 --- a/osu.Game/Graphics/OsuIcon.cs +++ b/osu.Game/Graphics/OsuIcon.cs @@ -115,6 +115,7 @@ namespace osu.Game.Graphics public static IconUsage ChangelogB => get(OsuIconMapping.ChangelogB); public static IconUsage Chat => get(OsuIconMapping.Chat); public static IconUsage CheckCircle => get(OsuIconMapping.CheckCircle); + public static IconUsage Clock => get(OsuIconMapping.Clock); public static IconUsage CollapseA => get(OsuIconMapping.CollapseA); public static IconUsage Collections => get(OsuIconMapping.Collections); public static IconUsage Cross => get(OsuIconMapping.Cross); @@ -141,6 +142,7 @@ namespace osu.Game.Graphics public static IconUsage Input => get(OsuIconMapping.Input); public static IconUsage Maintenance => get(OsuIconMapping.Maintenance); public static IconUsage Megaphone => get(OsuIconMapping.Megaphone); + public static IconUsage Metronome => get(OsuIconMapping.Metronome); public static IconUsage Music => get(OsuIconMapping.Music); public static IconUsage News => get(OsuIconMapping.News); public static IconUsage Next => get(OsuIconMapping.Next); @@ -204,6 +206,9 @@ namespace osu.Game.Graphics [Description(@"check-circle")] CheckCircle, + [Description(@"clock")] + Clock, + [Description(@"collapse-a")] CollapseA, @@ -282,6 +287,9 @@ namespace osu.Game.Graphics [Description(@"megaphone")] Megaphone, + [Description(@"metronome")] + Metronome, + [Description(@"music")] Music, From 1dbcbbde1538876bbd04e93e66348c1e547b44e4 Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Sun, 23 Mar 2025 23:02:38 -0400 Subject: [PATCH 042/164] Shorten beatmap hit statistics names --- osu.Game.Rulesets.Catch/Beatmaps/CatchBeatmap.cs | 6 +++--- osu.Game.Rulesets.Mania/Beatmaps/ManiaBeatmap.cs | 4 ++-- osu.Game.Rulesets.Osu/Beatmaps/OsuBeatmap.cs | 7 +++---- osu.Game.Rulesets.Taiko/Beatmaps/TaikoBeatmap.cs | 6 +++--- 4 files changed, 11 insertions(+), 12 deletions(-) diff --git a/osu.Game.Rulesets.Catch/Beatmaps/CatchBeatmap.cs b/osu.Game.Rulesets.Catch/Beatmaps/CatchBeatmap.cs index 1f05d66b86..01cec1d815 100644 --- a/osu.Game.Rulesets.Catch/Beatmaps/CatchBeatmap.cs +++ b/osu.Game.Rulesets.Catch/Beatmaps/CatchBeatmap.cs @@ -21,19 +21,19 @@ namespace osu.Game.Rulesets.Catch.Beatmaps { new BeatmapStatistic { - Name = @"Fruit Count", + Name = @"Fruits", Content = fruits.ToString(), CreateIcon = () => new BeatmapStatisticIcon(BeatmapStatisticsIconType.Circles), }, new BeatmapStatistic { - Name = @"Juice Stream Count", + Name = @"Juice Streams", Content = juiceStreams.ToString(), CreateIcon = () => new BeatmapStatisticIcon(BeatmapStatisticsIconType.Sliders), }, new BeatmapStatistic { - Name = @"Banana Shower Count", + Name = @"Banana Showers", Content = bananaShowers.ToString(), CreateIcon = () => new BeatmapStatisticIcon(BeatmapStatisticsIconType.Spinners), } diff --git a/osu.Game.Rulesets.Mania/Beatmaps/ManiaBeatmap.cs b/osu.Game.Rulesets.Mania/Beatmaps/ManiaBeatmap.cs index 8222e5477d..8ddcfa128a 100644 --- a/osu.Game.Rulesets.Mania/Beatmaps/ManiaBeatmap.cs +++ b/osu.Game.Rulesets.Mania/Beatmaps/ManiaBeatmap.cs @@ -41,13 +41,13 @@ namespace osu.Game.Rulesets.Mania.Beatmaps { new BeatmapStatistic { - Name = @"Note Count", + Name = @"Notes", CreateIcon = () => new BeatmapStatisticIcon(BeatmapStatisticsIconType.Circles), Content = notes.ToString(), }, new BeatmapStatistic { - Name = @"Hold Note Count", + Name = @"Hold Notes", CreateIcon = () => new BeatmapStatisticIcon(BeatmapStatisticsIconType.Sliders), Content = holdNotes.ToString(), }, diff --git a/osu.Game.Rulesets.Osu/Beatmaps/OsuBeatmap.cs b/osu.Game.Rulesets.Osu/Beatmaps/OsuBeatmap.cs index a5282877ee..730a194751 100644 --- a/osu.Game.Rulesets.Osu/Beatmaps/OsuBeatmap.cs +++ b/osu.Game.Rulesets.Osu/Beatmaps/OsuBeatmap.cs @@ -4,7 +4,6 @@ using System.Collections.Generic; using System.Linq; using osu.Game.Beatmaps; -using osu.Game.Resources.Localisation.Web; using osu.Game.Rulesets.Osu.Objects; namespace osu.Game.Rulesets.Osu.Beatmaps @@ -21,19 +20,19 @@ namespace osu.Game.Rulesets.Osu.Beatmaps { new BeatmapStatistic { - Name = BeatmapsetsStrings.ShowStatsCountCircles, + Name = "Circles", Content = circles.ToString(), CreateIcon = () => new BeatmapStatisticIcon(BeatmapStatisticsIconType.Circles), }, new BeatmapStatistic { - Name = BeatmapsetsStrings.ShowStatsCountSliders, + Name = "Sliders", Content = sliders.ToString(), CreateIcon = () => new BeatmapStatisticIcon(BeatmapStatisticsIconType.Sliders), }, new BeatmapStatistic { - Name = @"Spinner Count", + Name = @"Spinners", Content = spinners.ToString(), CreateIcon = () => new BeatmapStatisticIcon(BeatmapStatisticsIconType.Spinners), } diff --git a/osu.Game.Rulesets.Taiko/Beatmaps/TaikoBeatmap.cs b/osu.Game.Rulesets.Taiko/Beatmaps/TaikoBeatmap.cs index 41fe63a553..0781485ab8 100644 --- a/osu.Game.Rulesets.Taiko/Beatmaps/TaikoBeatmap.cs +++ b/osu.Game.Rulesets.Taiko/Beatmaps/TaikoBeatmap.cs @@ -20,19 +20,19 @@ namespace osu.Game.Rulesets.Taiko.Beatmaps { new BeatmapStatistic { - Name = @"Hit Count", + Name = @"Hits", CreateIcon = () => new BeatmapStatisticIcon(BeatmapStatisticsIconType.Circles), Content = hits.ToString(), }, new BeatmapStatistic { - Name = @"Drumroll Count", + Name = @"Drumrolls", CreateIcon = () => new BeatmapStatisticIcon(BeatmapStatisticsIconType.Sliders), Content = drumRolls.ToString(), }, new BeatmapStatistic { - Name = @"Swell Count", + Name = @"Swells", CreateIcon = () => new BeatmapStatisticIcon(BeatmapStatisticsIconType.Spinners), Content = swells.ToString(), } From 5c54e57d6d8aa803d3794be3581c81c73af77a52 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 11 Apr 2025 08:01:34 +0200 Subject: [PATCH 043/164] Remove redundant initialisers --- osu.Game/Screens/SelectV2/Carousel.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/SelectV2/Carousel.cs b/osu.Game/Screens/SelectV2/Carousel.cs index 21310b76a1..7b1fd6e999 100644 --- a/osu.Game/Screens/SelectV2/Carousel.cs +++ b/osu.Game/Screens/SelectV2/Carousel.cs @@ -38,12 +38,12 @@ namespace osu.Game.Screens.SelectV2 /// /// Height of the area above the carousel that should be treated as visible due to transparency of elements in front of it. /// - public float BleedTop { get; set; } = 0; + public float BleedTop { get; set; } /// /// Height of the area below the carousel that should be treated as visible due to transparency of elements in front of it. /// - public float BleedBottom { get; set; } = 0; + public float BleedBottom { get; set; } /// /// The number of pixels outside the carousel's vertical bounds to manifest drawables. From 9911e0819eabe986eabe310cfac97ff6330e36c5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 11 Apr 2025 10:13:49 +0200 Subject: [PATCH 044/164] Reduce bleed in tests to allow them to pass --- .../Visual/SongSelect/BeatmapCarouselV2TestScene.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game.Tests/Visual/SongSelect/BeatmapCarouselV2TestScene.cs b/osu.Game.Tests/Visual/SongSelect/BeatmapCarouselV2TestScene.cs index ad8004304a..f2faeab1c4 100644 --- a/osu.Game.Tests/Visual/SongSelect/BeatmapCarouselV2TestScene.cs +++ b/osu.Game.Tests/Visual/SongSelect/BeatmapCarouselV2TestScene.cs @@ -96,8 +96,8 @@ namespace osu.Game.Tests.Visual.SongSelect { Carousel = new BeatmapCarousel { - BleedTop = 200, - BleedBottom = 200, + BleedTop = 50, + BleedBottom = 50, Anchor = Anchor.Centre, Origin = Anchor.Centre, Width = 800, From c52dc9ffe86086e9728e06dd7a0ab0eec816f32c Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 11 Apr 2025 02:33:27 +0900 Subject: [PATCH 045/164] Update difficulty spectrum retrieval function --- osu.Game/Graphics/OsuColour.cs | 22 +++++++++++++++------- 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/osu.Game/Graphics/OsuColour.cs b/osu.Game/Graphics/OsuColour.cs index 260ff9f797..dec16d65bd 100644 --- a/osu.Game/Graphics/OsuColour.cs +++ b/osu.Game/Graphics/OsuColour.cs @@ -20,13 +20,9 @@ namespace osu.Game.Graphics public static Color4 Gray(float amt) => new Color4(amt, amt, amt, 1f); public static Color4 Gray(byte amt) => new Color4(amt, amt, amt, 255); - /// - /// Retrieves the colour for a given point in the star range. - /// - // todo: fix stupid array - public Color4 ForStarDifficulty(double starDifficulty) => ColourUtils.SampleFromLinearGradient(new[] + public static readonly (float, Color4)[] STAR_DIFFICULTY_SPECTRUM = { - (0.1f, Color4Extensions.FromHex("aaaaaa")), + (0.0f, Color4Extensions.FromHex("4290fb")), (0.1f, Color4Extensions.FromHex("4290fb")), (1.25f, Color4Extensions.FromHex("4fc0ff")), (2.0f, Color4Extensions.FromHex("4fffd5")), @@ -38,7 +34,19 @@ namespace osu.Game.Graphics (6.7f, Color4Extensions.FromHex("6563de")), (7.7f, Color4Extensions.FromHex("18158e")), (9.0f, Color4.Black), - }, (float)Math.Round(starDifficulty, 2, MidpointRounding.AwayFromZero)); + (10.0f, Color4.Black), + }; + + /// + /// Retrieves the colour for a given point in the star range. + /// + public Color4 ForStarDifficulty(double starDifficulty, bool showGrayOnZero = true) + { + if (showGrayOnZero && starDifficulty < 0.1f) + return Color4Extensions.FromHex("aaaaaa"); + + return ColourUtils.SampleFromLinearGradient(STAR_DIFFICULTY_SPECTRUM, (float)Math.Round(starDifficulty, 2, MidpointRounding.AwayFromZero)); + } /// /// Retrieves the colour for a . From ac36e228b822cb5ce5f585b1a64aded5a62ebe6a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 10 Apr 2025 14:47:47 +0200 Subject: [PATCH 046/164] Add test exercising osu! replay playback stability after encode --- .../TestSceneReplayStability.cs | 187 ++++++++++++++++++ osu.Game/Screens/Play/GameplayState.cs | 2 + 2 files changed, 189 insertions(+) create mode 100644 osu.Game.Rulesets.Osu.Tests/TestSceneReplayStability.cs diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneReplayStability.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneReplayStability.cs new file mode 100644 index 0000000000..1bd18a59dc --- /dev/null +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneReplayStability.cs @@ -0,0 +1,187 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Collections.Generic; +using System.IO; +using System.Linq; +using NUnit.Framework; +using osu.Framework.Screens; +using osu.Game.Beatmaps; +using osu.Game.Replays; +using osu.Game.Rulesets.Judgements; +using osu.Game.Rulesets.Osu.Beatmaps; +using osu.Game.Rulesets.Osu.Objects; +using osu.Game.Rulesets.Osu.Replays; +using osu.Game.Rulesets.Osu.UI; +using osu.Game.Rulesets.Scoring; +using osu.Game.Scoring; +using osu.Game.Scoring.Legacy; +using osu.Game.Screens.Play; +using osu.Game.Tests.Visual; + +namespace osu.Game.Rulesets.Osu.Tests +{ + [TestFixture] + [Ignore("These tests are expected to fail until an acceptable solution for various replay playback issues concerning rounding of replay frame times & hit windows is found.")] + public partial class TestSceneReplayStability : RateAdjustedBeatmapTestScene + { + private ReplayPlayer currentPlayer = null!; + private readonly List results = new List(); + + private static readonly object[][] test_cases = new[] + { + // OD = 5 test cases. + // GREAT hit window is [ -50ms, 50ms] + // OK hit window is [-100ms, 100ms] + // MEH hit window is [-150ms, 150ms] + // MISS hit window is [-400ms, 400ms] + new object[] { 5f, 49d, HitResult.Great }, + new object[] { 5f, 49.2d, HitResult.Great }, + new object[] { 5f, 49.7d, HitResult.Great }, + new object[] { 5f, 50d, HitResult.Great }, + new object[] { 5f, 50.4d, HitResult.Ok }, + new object[] { 5f, 50.9d, HitResult.Ok }, + new object[] { 5f, 51d, HitResult.Ok }, + new object[] { 5f, 99d, HitResult.Ok }, + new object[] { 5f, 99.2d, HitResult.Ok }, + new object[] { 5f, 99.7d, HitResult.Ok }, + new object[] { 5f, 100d, HitResult.Ok }, + new object[] { 5f, 100.4d, HitResult.Meh }, + new object[] { 5f, 100.9d, HitResult.Meh }, + new object[] { 5f, 101d, HitResult.Meh }, + new object[] { 5f, 149d, HitResult.Meh }, + new object[] { 5f, 149.2d, HitResult.Meh }, + new object[] { 5f, 149.7d, HitResult.Meh }, + new object[] { 5f, 150d, HitResult.Meh }, + new object[] { 5f, 150.4d, HitResult.Miss }, + new object[] { 5f, 150.9d, HitResult.Miss }, + new object[] { 5f, 151d, HitResult.Miss }, + + // OD = 5.7 test cases. + // GREAT hit window is [ -45.8ms, 45.8ms] + // OK hit window is [ -94.4ms, 94.4ms] + // MEH hit window is [-143.0ms, 143.0ms] + // MISS hit window is [-400.0ms, 400.0ms] + new object[] { 5.7f, 45d, HitResult.Great }, + new object[] { 5.7f, 45.2d, HitResult.Great }, + new object[] { 5.7f, 45.8d, HitResult.Great }, + new object[] { 5.7f, 45.9d, HitResult.Ok }, + new object[] { 5.7f, 46d, HitResult.Ok }, + new object[] { 5.7f, 46.4d, HitResult.Ok }, + new object[] { 5.7f, 94d, HitResult.Ok }, + new object[] { 5.7f, 94.2d, HitResult.Ok }, + new object[] { 5.7f, 94.4d, HitResult.Ok }, + new object[] { 5.7f, 94.48d, HitResult.Ok }, + new object[] { 5.7f, 94.9d, HitResult.Meh }, + new object[] { 5.7f, 95d, HitResult.Meh }, + new object[] { 5.7f, 95.4d, HitResult.Meh }, + new object[] { 5.7f, 142d, HitResult.Meh }, + new object[] { 5.7f, 142.7d, HitResult.Meh }, + new object[] { 5.7f, 143d, HitResult.Meh }, + new object[] { 5.7f, 143.4d, HitResult.Miss }, + new object[] { 5.7f, 143.9d, HitResult.Miss }, + new object[] { 5.7f, 144d, HitResult.Miss }, + }; + + [TestCaseSource(nameof(test_cases))] + public void TestHitWindowStability(float overallDifficulty, double hitOffset, HitResult expectedResult) + { + const double hit_circle_time = 100; + + Score originalScore = null!; + Score decodedScore = null!; + IBeatmap beatmap = null!; + + AddStep("create beatmap", () => + { + Beatmap.Value = CreateWorkingBeatmap(beatmap = new OsuBeatmap + { + HitObjects = + { + new HitCircle + { + StartTime = hit_circle_time, + Position = OsuPlayfield.BASE_SIZE / 2 + } + }, + Difficulty = new BeatmapDifficulty { OverallDifficulty = overallDifficulty }, + BeatmapInfo = + { + Ruleset = new OsuRuleset().RulesetInfo, + }, + }); + }); + AddStep("create replay", () => + { + originalScore = new Score + { + Replay = new Replay + { + Frames = + { + new OsuReplayFrame(0, OsuPlayfield.BASE_SIZE / 2), + new OsuReplayFrame(hit_circle_time + hitOffset, OsuPlayfield.BASE_SIZE / 2, OsuAction.LeftButton), + new OsuReplayFrame(hit_circle_time + hitOffset + 20, OsuPlayfield.BASE_SIZE / 2), + } + } + }; + }); + + AddStep("push player", () => pushNewPlayer(originalScore)); + + AddUntilStep("Wait until player is loaded", () => currentPlayer.IsCurrentScreen()); + AddUntilStep("Wait for completion", () => currentPlayer.GameplayState.HasCompleted); + AddAssert("Collected one judgement result", () => results, () => Has.Count.EqualTo(1)); + AddAssert("Judgement result is correct", () => results.Single().Type, () => Is.EqualTo(expectedResult)); + + AddStep("exit player", () => currentPlayer.Exit()); + + AddStep("encode and decode score", () => + { + var encoder = new LegacyScoreEncoder(originalScore, beatmap); + + using (var stream = new MemoryStream()) + { + encoder.Encode(stream, leaveOpen: true); + stream.Position = 0; + decodedScore = new TestScoreDecoder(Beatmap.Value).Parse(stream); + } + }); + + AddStep("push player", () => pushNewPlayer(decodedScore)); + + AddUntilStep("Wait until player is loaded", () => currentPlayer.IsCurrentScreen()); + AddUntilStep("Wait for completion", () => currentPlayer.GameplayState.HasCompleted); + AddAssert("Collected one judgement result", () => results, () => Has.Count.EqualTo(1)); + AddAssert("Judgement result is correct", () => results.Single().Type, () => Is.EqualTo(expectedResult)); + } + + private void pushNewPlayer(Score score) + { + var player = new ReplayPlayer(score); + player.OnLoadComplete += _ => + { + player.GameplayState.ScoreProcessor.NewJudgement += result => + { + if (currentPlayer == player) + results.Add(result); + }; + }; + LoadScreen(currentPlayer = player); + results.Clear(); + } + + private class TestScoreDecoder : LegacyScoreDecoder + { + private readonly WorkingBeatmap beatmap; + + public TestScoreDecoder(WorkingBeatmap beatmap) + { + this.beatmap = beatmap; + } + + protected override Ruleset GetRuleset(int rulesetId) => new OsuRuleset(); + protected override WorkingBeatmap GetBeatmap(string md5Hash) => beatmap; + } + } +} diff --git a/osu.Game/Screens/Play/GameplayState.cs b/osu.Game/Screens/Play/GameplayState.cs index 851e95495f..80546ef6da 100644 --- a/osu.Game/Screens/Play/GameplayState.cs +++ b/osu.Game/Screens/Play/GameplayState.cs @@ -62,6 +62,8 @@ namespace osu.Game.Screens.Play /// public bool HasQuit { get; set; } + public bool HasCompleted => HasPassed || HasFailed || HasQuit; + /// /// A bindable tracking the last judgement result applied to any hit object. /// From 30bb8e335c399945db240856f9598ad2a181929c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 11 Apr 2025 10:42:34 +0200 Subject: [PATCH 047/164] Abstractify test scene I think in this case it's genuinely reasonable to use abstracts to reduce boilerplate. --- .../TestSceneReplayStability.cs | 118 +++--------------- .../Tests/Visual/ReplayStabilityTestScene.cs | 106 ++++++++++++++++ 2 files changed, 126 insertions(+), 98 deletions(-) create mode 100644 osu.Game/Tests/Visual/ReplayStabilityTestScene.cs diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneReplayStability.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneReplayStability.cs index 1bd18a59dc..8af12fbe2f 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneReplayStability.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneReplayStability.cs @@ -1,33 +1,21 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using System.Collections.Generic; -using System.IO; -using System.Linq; using NUnit.Framework; -using osu.Framework.Screens; using osu.Game.Beatmaps; using osu.Game.Replays; -using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Osu.Beatmaps; using osu.Game.Rulesets.Osu.Objects; using osu.Game.Rulesets.Osu.Replays; using osu.Game.Rulesets.Osu.UI; using osu.Game.Rulesets.Scoring; -using osu.Game.Scoring; -using osu.Game.Scoring.Legacy; -using osu.Game.Screens.Play; using osu.Game.Tests.Visual; namespace osu.Game.Rulesets.Osu.Tests { - [TestFixture] [Ignore("These tests are expected to fail until an acceptable solution for various replay playback issues concerning rounding of replay frame times & hit windows is found.")] - public partial class TestSceneReplayStability : RateAdjustedBeatmapTestScene + public partial class TestSceneReplayStability : ReplayStabilityTestScene { - private ReplayPlayer currentPlayer = null!; - private readonly List results = new List(); - private static readonly object[][] test_cases = new[] { // OD = 5 test cases. @@ -88,100 +76,34 @@ namespace osu.Game.Rulesets.Osu.Tests { const double hit_circle_time = 100; - Score originalScore = null!; - Score decodedScore = null!; - IBeatmap beatmap = null!; - - AddStep("create beatmap", () => + var beatmap = new OsuBeatmap { - Beatmap.Value = CreateWorkingBeatmap(beatmap = new OsuBeatmap + HitObjects = { - HitObjects = + new HitCircle { - new HitCircle - { - StartTime = hit_circle_time, - Position = OsuPlayfield.BASE_SIZE / 2 - } - }, - Difficulty = new BeatmapDifficulty { OverallDifficulty = overallDifficulty }, - BeatmapInfo = - { - Ruleset = new OsuRuleset().RulesetInfo, - }, - }); - }); - AddStep("create replay", () => - { - originalScore = new Score - { - Replay = new Replay - { - Frames = - { - new OsuReplayFrame(0, OsuPlayfield.BASE_SIZE / 2), - new OsuReplayFrame(hit_circle_time + hitOffset, OsuPlayfield.BASE_SIZE / 2, OsuAction.LeftButton), - new OsuReplayFrame(hit_circle_time + hitOffset + 20, OsuPlayfield.BASE_SIZE / 2), - } + StartTime = hit_circle_time, + Position = OsuPlayfield.BASE_SIZE / 2 } - }; - }); - - AddStep("push player", () => pushNewPlayer(originalScore)); - - AddUntilStep("Wait until player is loaded", () => currentPlayer.IsCurrentScreen()); - AddUntilStep("Wait for completion", () => currentPlayer.GameplayState.HasCompleted); - AddAssert("Collected one judgement result", () => results, () => Has.Count.EqualTo(1)); - AddAssert("Judgement result is correct", () => results.Single().Type, () => Is.EqualTo(expectedResult)); - - AddStep("exit player", () => currentPlayer.Exit()); - - AddStep("encode and decode score", () => - { - var encoder = new LegacyScoreEncoder(originalScore, beatmap); - - using (var stream = new MemoryStream()) + }, + Difficulty = new BeatmapDifficulty { OverallDifficulty = overallDifficulty }, + BeatmapInfo = { - encoder.Encode(stream, leaveOpen: true); - stream.Position = 0; - decodedScore = new TestScoreDecoder(Beatmap.Value).Parse(stream); - } - }); - - AddStep("push player", () => pushNewPlayer(decodedScore)); - - AddUntilStep("Wait until player is loaded", () => currentPlayer.IsCurrentScreen()); - AddUntilStep("Wait for completion", () => currentPlayer.GameplayState.HasCompleted); - AddAssert("Collected one judgement result", () => results, () => Has.Count.EqualTo(1)); - AddAssert("Judgement result is correct", () => results.Single().Type, () => Is.EqualTo(expectedResult)); - } - - private void pushNewPlayer(Score score) - { - var player = new ReplayPlayer(score); - player.OnLoadComplete += _ => - { - player.GameplayState.ScoreProcessor.NewJudgement += result => - { - if (currentPlayer == player) - results.Add(result); - }; + Ruleset = new OsuRuleset().RulesetInfo, + }, }; - LoadScreen(currentPlayer = player); - results.Clear(); - } - private class TestScoreDecoder : LegacyScoreDecoder - { - private readonly WorkingBeatmap beatmap; - - public TestScoreDecoder(WorkingBeatmap beatmap) + var replay = new Replay { - this.beatmap = beatmap; - } + Frames = + { + new OsuReplayFrame(0, OsuPlayfield.BASE_SIZE / 2), + new OsuReplayFrame(hit_circle_time + hitOffset, OsuPlayfield.BASE_SIZE / 2, OsuAction.LeftButton), + new OsuReplayFrame(hit_circle_time + hitOffset + 20, OsuPlayfield.BASE_SIZE / 2), + } + }; - protected override Ruleset GetRuleset(int rulesetId) => new OsuRuleset(); - protected override WorkingBeatmap GetBeatmap(string md5Hash) => beatmap; + RunTest(beatmap, replay, [expectedResult]); } } } diff --git a/osu.Game/Tests/Visual/ReplayStabilityTestScene.cs b/osu.Game/Tests/Visual/ReplayStabilityTestScene.cs new file mode 100644 index 0000000000..13abedf611 --- /dev/null +++ b/osu.Game/Tests/Visual/ReplayStabilityTestScene.cs @@ -0,0 +1,106 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Collections.Generic; +using System.IO; +using System.Linq; +using NUnit.Framework; +using osu.Framework.Screens; +using osu.Framework.Testing; +using osu.Game.Beatmaps; +using osu.Game.Replays; +using osu.Game.Rulesets; +using osu.Game.Rulesets.Judgements; +using osu.Game.Rulesets.Scoring; +using osu.Game.Scoring; +using osu.Game.Scoring.Legacy; +using osu.Game.Screens.Play; + +namespace osu.Game.Tests.Visual +{ + /// + /// The goal of this abstract test class is to ensure that the process of exporting of a replay does not affect its playback. + /// Use to exercise that property. + /// + [HeadlessTest] + [TestFixture] + public abstract partial class ReplayStabilityTestScene : RateAdjustedBeatmapTestScene + { + private ReplayPlayer currentPlayer = null!; + private readonly List results = new List(); + + /// + /// Runs against the supplied + /// and checks that the judgement results recorded match . + /// Then, encodes the , decodes the result of encoding, runs the result of decoding against the supplied , + /// and checks that the judgement results recorded still match . + /// + protected void RunTest(IBeatmap beatmap, Replay replay, IEnumerable expectedResults) + { + Score originalScore = null!; + Score decodedScore = null!; + + AddStep(@"create replay", () => originalScore = new Score + { + Replay = replay, + ScoreInfo = new ScoreInfo() + }); + + AddStep(@"set beatmap", () => Beatmap.Value = CreateWorkingBeatmap(beatmap)); + AddStep(@"set ruleset", () => Ruleset.Value = beatmap.BeatmapInfo.Ruleset); + AddStep(@"push player", () => pushNewPlayer(originalScore)); + + AddUntilStep(@"wait until player is loaded", () => currentPlayer.IsCurrentScreen()); + AddUntilStep(@"wait for completion", () => currentPlayer.GameplayState.HasCompleted); + AddAssert(@"judgement results before encode are correct", () => results.Select(r => r.Type), () => Is.EquivalentTo(expectedResults)); + + AddStep(@"exit player", () => currentPlayer.Exit()); + + AddStep(@"encode and decode score", () => + { + var encoder = new LegacyScoreEncoder(originalScore, beatmap); + + using (var stream = new MemoryStream()) + { + encoder.Encode(stream, leaveOpen: true); + stream.Position = 0; + decodedScore = new TestScoreDecoder(Beatmap.Value).Parse(stream); + } + }); + + AddStep(@"push player", () => pushNewPlayer(decodedScore)); + + AddUntilStep(@"Wait until player is loaded", () => currentPlayer.IsCurrentScreen()); + AddUntilStep(@"Wait for completion", () => currentPlayer.GameplayState.HasCompleted); + AddAssert(@"judgement results after encode are correct", () => results.Select(r => r.Type), () => Is.EquivalentTo(expectedResults)); + } + + private void pushNewPlayer(Score score) + { + var player = new ReplayPlayer(score); + player.OnLoadComplete += _ => + { + player.GameplayState.ScoreProcessor.NewJudgement += result => + { + if (currentPlayer == player) + results.Add(result); + }; + }; + LoadScreen(currentPlayer = player); + results.Clear(); + } + + private class TestScoreDecoder : LegacyScoreDecoder + { + private readonly WorkingBeatmap beatmap; + + public TestScoreDecoder(WorkingBeatmap beatmap) + { + this.beatmap = beatmap; + } + + protected override Ruleset GetRuleset(int rulesetId) => beatmap.BeatmapInfo.Ruleset.CreateInstance(); + protected override WorkingBeatmap GetBeatmap(string md5Hash) => beatmap; + } + } +} From f458aad4108dea82a91c72f18cb5ffe79d83cfe8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 11 Apr 2025 11:24:31 +0200 Subject: [PATCH 048/164] Add test exercising taiko replay playback stability after encode --- .../TestSceneReplayStability.cs | 92 +++++++++++++++++++ 1 file changed, 92 insertions(+) create mode 100644 osu.Game.Rulesets.Taiko.Tests/TestSceneReplayStability.cs diff --git a/osu.Game.Rulesets.Taiko.Tests/TestSceneReplayStability.cs b/osu.Game.Rulesets.Taiko.Tests/TestSceneReplayStability.cs new file mode 100644 index 0000000000..d245fbd28f --- /dev/null +++ b/osu.Game.Rulesets.Taiko.Tests/TestSceneReplayStability.cs @@ -0,0 +1,92 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using NUnit.Framework; +using osu.Game.Beatmaps; +using osu.Game.Replays; +using osu.Game.Rulesets.Scoring; +using osu.Game.Rulesets.Taiko.Beatmaps; +using osu.Game.Rulesets.Taiko.Objects; +using osu.Game.Rulesets.Taiko.Replays; +using osu.Game.Tests.Visual; + +namespace osu.Game.Rulesets.Taiko.Tests +{ + [Ignore("These tests are expected to fail until an acceptable solution for various replay playback issues concerning rounding of replay frame times & hit windows is found.")] + public partial class TestSceneReplayStability : ReplayStabilityTestScene + { + private static readonly object[][] test_cases = new[] + { + // OD = 5 test cases. + // GREAT hit window is [-35ms, 35ms] + // OK hit window is [-80ms, 80ms] + // MISS hit window is [-95ms, 95ms] + new object[] { 5f, -34d, HitResult.Great }, + new object[] { 5f, -34.2d, HitResult.Great }, + new object[] { 5f, -34.7d, HitResult.Great }, + new object[] { 5f, -35d, HitResult.Great }, + new object[] { 5f, -35.2d, HitResult.Ok }, + new object[] { 5f, -35.8d, HitResult.Ok }, + new object[] { 5f, -36d, HitResult.Ok }, + new object[] { 5f, -79d, HitResult.Ok }, + new object[] { 5f, -79.3d, HitResult.Ok }, + new object[] { 5f, -79.7d, HitResult.Ok }, + new object[] { 5f, -80d, HitResult.Ok }, + new object[] { 5f, -80.2d, HitResult.Miss }, + new object[] { 5f, -80.8d, HitResult.Miss }, + new object[] { 5f, -81d, HitResult.Miss }, + + // OD = 7.8 test cases. + // GREAT hit window is [-26.6ms, 26.6ms] + // OK hit window is [-63.2ms, 63.2ms] + // MISS hit window is [-81.0ms, 81.0ms] + new object[] { 7.8f, -26d, HitResult.Great }, + new object[] { 7.8f, -26.4d, HitResult.Great }, + new object[] { 7.8f, -26.59d, HitResult.Great }, + new object[] { 7.8f, -26.8d, HitResult.Ok }, + new object[] { 7.8f, -27d, HitResult.Ok }, + new object[] { 7.8f, -27.1d, HitResult.Ok }, + new object[] { 7.8f, -63d, HitResult.Ok }, + new object[] { 7.8f, -63.18d, HitResult.Ok }, + new object[] { 7.8f, -63.4d, HitResult.Ok }, + new object[] { 7.8f, -63.7d, HitResult.Miss }, + new object[] { 7.8f, -64d, HitResult.Miss }, + new object[] { 7.8f, -64.2d, HitResult.Miss }, + }; + + [TestCaseSource(nameof(test_cases))] + public void TestHitWindowStability(float overallDifficulty, double hitOffset, HitResult expectedResult) + { + const double hit_time = 100; + + var beatmap = new TaikoBeatmap() + { + HitObjects = + { + new Hit + { + StartTime = hit_time, + Type = HitType.Centre, + } + }, + Difficulty = new BeatmapDifficulty { OverallDifficulty = overallDifficulty }, + BeatmapInfo = + { + Ruleset = new TaikoRuleset().RulesetInfo, + }, + }; + + var replay = new Replay + { + Frames = + { + new TaikoReplayFrame(0), + new TaikoReplayFrame(hit_time + hitOffset, TaikoAction.LeftCentre), + new TaikoReplayFrame(hit_time + hitOffset + 20), + } + }; + + RunTest(beatmap, replay, [expectedResult]); + } + } +} From 674af698b613a0af53632de91d7e8004f0ff4a2e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 11 Apr 2025 12:04:31 +0200 Subject: [PATCH 049/164] Add test exercising mania replay playback stability after encode --- .../TestSceneReplayStability.cs | 143 ++++++++++++++++++ 1 file changed, 143 insertions(+) create mode 100644 osu.Game.Rulesets.Mania.Tests/TestSceneReplayStability.cs diff --git a/osu.Game.Rulesets.Mania.Tests/TestSceneReplayStability.cs b/osu.Game.Rulesets.Mania.Tests/TestSceneReplayStability.cs new file mode 100644 index 0000000000..1f51a1494d --- /dev/null +++ b/osu.Game.Rulesets.Mania.Tests/TestSceneReplayStability.cs @@ -0,0 +1,143 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using NUnit.Framework; +using osu.Game.Beatmaps; +using osu.Game.Replays; +using osu.Game.Rulesets.Mania.Beatmaps; +using osu.Game.Rulesets.Mania.Objects; +using osu.Game.Rulesets.Mania.Replays; +using osu.Game.Rulesets.Scoring; +using osu.Game.Tests.Visual; + +namespace osu.Game.Rulesets.Mania.Tests +{ + [Ignore("These tests are expected to fail until an acceptable solution for various replay playback issues concerning rounding of replay frame times & hit windows is found.")] + public partial class TestSceneReplayStability : ReplayStabilityTestScene + { + private static readonly object[][] test_cases = new[] + { + // OD = 5 test cases. + // PERFECT hit window is [ -19.4ms, 19.4ms] + // GREAT hit window is [ -49.0ms, 49.0ms] + // GOOD hit window is [ -82.0ms, 82.0ms] + // OK hit window is [-112.0ms, 112.0ms] + // MEH hit window is [-136.0ms, 136.0ms] + // MISS hit window is [-173.0ms, 173.0ms] + new object[] { 5f, -19d, HitResult.Perfect }, + new object[] { 5f, -19.2d, HitResult.Perfect }, + new object[] { 5f, -19.38d, HitResult.Perfect }, + new object[] { 5f, -19.44d, HitResult.Great }, + new object[] { 5f, -19.7d, HitResult.Great }, + new object[] { 5f, -20d, HitResult.Great }, + new object[] { 5f, -48d, HitResult.Great }, + new object[] { 5f, -48.4d, HitResult.Great }, + new object[] { 5f, -48.7d, HitResult.Great }, + new object[] { 5f, -49d, HitResult.Great }, + new object[] { 5f, -49.2d, HitResult.Good }, + new object[] { 5f, -49.7d, HitResult.Good }, + new object[] { 5f, -50d, HitResult.Good }, + new object[] { 5f, -81d, HitResult.Good }, + new object[] { 5f, -81.2d, HitResult.Good }, + new object[] { 5f, -81.7d, HitResult.Good }, + new object[] { 5f, -82d, HitResult.Good }, + new object[] { 5f, -82.2d, HitResult.Ok }, + new object[] { 5f, -82.7d, HitResult.Ok }, + new object[] { 5f, -83d, HitResult.Ok }, + new object[] { 5f, -111d, HitResult.Ok }, + new object[] { 5f, -111.2d, HitResult.Ok }, + new object[] { 5f, -111.7d, HitResult.Ok }, + new object[] { 5f, -112d, HitResult.Ok }, + new object[] { 5f, -112.2d, HitResult.Meh }, + new object[] { 5f, -112.7d, HitResult.Meh }, + new object[] { 5f, -113d, HitResult.Meh }, + new object[] { 5f, -135d, HitResult.Meh }, + new object[] { 5f, -135.2d, HitResult.Meh }, + new object[] { 5f, -135.8d, HitResult.Meh }, + new object[] { 5f, -136d, HitResult.Meh }, + new object[] { 5f, -136.2d, HitResult.Miss }, + new object[] { 5f, -136.7d, HitResult.Miss }, + new object[] { 5f, -137d, HitResult.Miss }, + + // OD = 9.3 test cases. + // PERFECT hit window is [ -14.67ms, 14.67ms] + // GREAT hit window is [ -36.10ms, 36.10ms] + // GOOD hit window is [ -69.10ms, 69.10ms] + // OK hit window is [ -99.10ms, 99.10ms] + // MEH hit window is [-123.10ms, 123.10ms] + // MISS hit window is [-160.10ms, 160.10ms] + new object[] { 9.3f, 14d, HitResult.Perfect }, + new object[] { 9.3f, 14.2d, HitResult.Perfect }, + new object[] { 9.3f, 14.6d, HitResult.Perfect }, + new object[] { 9.3f, 14.7d, HitResult.Great }, + new object[] { 9.3f, 15d, HitResult.Great }, + new object[] { 9.3f, 35d, HitResult.Great }, + new object[] { 9.3f, 35.3d, HitResult.Great }, + new object[] { 9.3f, 35.8d, HitResult.Great }, + new object[] { 9.3f, 36.05d, HitResult.Great }, + new object[] { 9.3f, 36.3d, HitResult.Good }, + new object[] { 9.3f, 36.7d, HitResult.Good }, + new object[] { 9.3f, 37d, HitResult.Good }, + new object[] { 9.3f, 68d, HitResult.Good }, + new object[] { 9.3f, 68.4d, HitResult.Good }, + new object[] { 9.3f, 68.9d, HitResult.Good }, + new object[] { 9.3f, 69.07d, HitResult.Good }, + new object[] { 9.3f, 69.25d, HitResult.Ok }, + new object[] { 9.3f, 69.85d, HitResult.Ok }, + new object[] { 9.3f, 70d, HitResult.Ok }, + new object[] { 9.3f, 98d, HitResult.Ok }, + new object[] { 9.3f, 98.3d, HitResult.Ok }, + new object[] { 9.3f, 98.6d, HitResult.Ok }, + new object[] { 9.3f, 99d, HitResult.Ok }, + new object[] { 9.3f, 99.3d, HitResult.Meh }, + new object[] { 9.3f, 99.7d, HitResult.Meh }, + new object[] { 9.3f, 100d, HitResult.Meh }, + new object[] { 9.3f, 122d, HitResult.Meh }, + new object[] { 9.3f, 122.34d, HitResult.Meh }, + new object[] { 9.3f, 122.57d, HitResult.Meh }, + new object[] { 9.3f, 123.04d, HitResult.Meh }, + new object[] { 9.3f, 123.45d, HitResult.Miss }, + new object[] { 9.3f, 123.95d, HitResult.Miss }, + new object[] { 9.3f, 124d, HitResult.Miss }, + }; + + [TestCaseSource(nameof(test_cases))] + public void TestHitWindowStability(float overallDifficulty, double hitOffset, HitResult expectedResult) + { + const double note_time = 100; + + var beatmap = new ManiaBeatmap(new StageDefinition(1)) + { + HitObjects = + { + new Note() + { + StartTime = note_time, + Column = 0, + } + }, + Difficulty = new BeatmapDifficulty + { + OverallDifficulty = overallDifficulty, + CircleSize = 1, + }, + BeatmapInfo = + { + Ruleset = new ManiaRuleset().RulesetInfo, + }, + }; + + var replay = new Replay + { + Frames = + { + new ManiaReplayFrame(0), + new ManiaReplayFrame(note_time + hitOffset, ManiaAction.Key1), + new ManiaReplayFrame(note_time + hitOffset + 20), + } + }; + + RunTest(beatmap, replay, [expectedResult]); + } + } +} From 74227e7b79afb165a49963f187b603863587fe6b Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Thu, 3 Apr 2025 04:55:34 -0400 Subject: [PATCH 050/164] Define standard font sizes --- osu.Game/Graphics/OsuFont.cs | 52 +++++++++++++++++++++++++++++++++++- 1 file changed, 51 insertions(+), 1 deletion(-) diff --git a/osu.Game/Graphics/OsuFont.cs b/osu.Game/Graphics/OsuFont.cs index 7aa98ece95..b314c602f5 100644 --- a/osu.Game/Graphics/OsuFont.cs +++ b/osu.Game/Graphics/OsuFont.cs @@ -15,15 +15,65 @@ namespace osu.Game.Graphics /// public const float DEFAULT_FONT_SIZE = 16; + /// + /// Template font styles which should be preferred whenever possible for UI elements. + /// + public static class Style + { + /// + /// Equivalent to Torus with 32px size and semi-bold weight. + /// + public static FontUsage Title => GetFont(Typeface.TorusAlternate, size: 32, weight: FontWeight.Regular); + + /// + /// Torus with 28px size and semi-bold weight. + /// + public static FontUsage Subtitle => GetFont(size: 28, weight: FontWeight.Regular); + + /// + /// Torus with 22px size and bold weight. + /// + public static FontUsage Heading1 => GetFont(size: 22, weight: FontWeight.Bold); + + /// + /// Torus with 18px size and semi-bold weight. + /// + public static FontUsage Heading2 => GetFont(size: 18, weight: FontWeight.SemiBold); + + /// + /// Torus with 16px size and regular weight. + /// + public static FontUsage Body => GetFont(size: DEFAULT_FONT_SIZE, weight: FontWeight.Regular); + + /// + /// Torus with 14px size and regular weight. + /// + public static FontUsage Caption1 => GetFont(size: 14, weight: FontWeight.Regular); + + /// + /// Torus with 12px size and regular weight. + /// + public static FontUsage Caption2 => GetFont(size: 12, weight: FontWeight.Regular); + } + /// /// The default font. /// - public static FontUsage Default => GetFont(); + public static FontUsage Default => GetFont(weight: FontWeight.Medium); + /// + /// Font face for numeric display. + /// public static FontUsage Numeric => GetFont(Typeface.Venera, weight: FontWeight.Bold); + /// + /// Default font face for UI and game elements. + /// public static FontUsage Torus => GetFont(Typeface.Torus, weight: FontWeight.Regular); + /// + /// Default font face with alternate character set for headings and flair text. + /// public static FontUsage TorusAlternate => GetFont(Typeface.TorusAlternate, weight: FontWeight.Regular); public static FontUsage Inter => GetFont(Typeface.Inter, weight: FontWeight.Regular); From 08c17bdf9ec27f9ee8ab35f9c9c2a3ae93b517f3 Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Fri, 11 Apr 2025 07:45:10 -0400 Subject: [PATCH 051/164] Remove conditional in difficulty spectrum retrieval function Wiil be handled locally instead using the diff in https://github.com/ppy/osu/pull/32764#discussion_r2039384833 --- osu.Game/Graphics/OsuColour.cs | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/osu.Game/Graphics/OsuColour.cs b/osu.Game/Graphics/OsuColour.cs index dec16d65bd..5adecc7182 100644 --- a/osu.Game/Graphics/OsuColour.cs +++ b/osu.Game/Graphics/OsuColour.cs @@ -22,7 +22,7 @@ namespace osu.Game.Graphics public static readonly (float, Color4)[] STAR_DIFFICULTY_SPECTRUM = { - (0.0f, Color4Extensions.FromHex("4290fb")), + (0.1f, Color4Extensions.FromHex("aaaaaa")), (0.1f, Color4Extensions.FromHex("4290fb")), (1.25f, Color4Extensions.FromHex("4fc0ff")), (2.0f, Color4Extensions.FromHex("4fffd5")), @@ -40,13 +40,7 @@ namespace osu.Game.Graphics /// /// Retrieves the colour for a given point in the star range. /// - public Color4 ForStarDifficulty(double starDifficulty, bool showGrayOnZero = true) - { - if (showGrayOnZero && starDifficulty < 0.1f) - return Color4Extensions.FromHex("aaaaaa"); - - return ColourUtils.SampleFromLinearGradient(STAR_DIFFICULTY_SPECTRUM, (float)Math.Round(starDifficulty, 2, MidpointRounding.AwayFromZero)); - } + public Color4 ForStarDifficulty(double starDifficulty) => ColourUtils.SampleFromLinearGradient(STAR_DIFFICULTY_SPECTRUM, (float)Math.Round(starDifficulty, 2, MidpointRounding.AwayFromZero)); /// /// Retrieves the colour for a . From c4cfd3a148344665654ca651738d0f96a716b2af Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 11 Apr 2025 21:14:56 +0900 Subject: [PATCH 052/164] Fix some incorrect/lacking comments --- .../Beatmaps/Drawables/BeatmapSetOnlineStatusPill.cs | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/osu.Game/Beatmaps/Drawables/BeatmapSetOnlineStatusPill.cs b/osu.Game/Beatmaps/Drawables/BeatmapSetOnlineStatusPill.cs index 83b385bb8e..3ee0be61b1 100644 --- a/osu.Game/Beatmaps/Drawables/BeatmapSetOnlineStatusPill.cs +++ b/osu.Game/Beatmaps/Drawables/BeatmapSetOnlineStatusPill.cs @@ -13,6 +13,7 @@ using osu.Game.Graphics; using osu.Game.Graphics.Sprites; using osu.Game.Localisation; using osu.Game.Overlays; +using osuTK; using osuTK.Graphics; namespace osu.Game.Beatmaps.Drawables @@ -20,7 +21,7 @@ namespace osu.Game.Beatmaps.Drawables public partial class BeatmapSetOnlineStatusPill : CircularContainer, IHasTooltip { /// - /// Whether to show as "unknownn" instead of fading out. + /// Whether to show as "unknown" instead of fading out. /// public bool ShowUnknownStatus { get; init; } @@ -104,9 +105,10 @@ namespace osu.Game.Beatmaps.Drawables return; } - // Only animate resizing if we already have a size. - // This avoids animating height from zero. - if (Width > 0) + // The autosize animation on this component is intended to animate horizontal sizing only. + // To avoid vertical autosize animating from zero to non-zero, only apply the duration + // after we have a valid size. + if (Height > 0) { AutoSizeDuration = (float)animation_duration; AutoSizeEasing = Easing.OutQuint; From d4cae3232bde877a5586fc50d70da95aa8cc5a52 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 11 Apr 2025 14:16:24 +0200 Subject: [PATCH 053/164] Fix code quality --- osu.Game.Rulesets.Mania.Tests/TestSceneReplayStability.cs | 4 ++-- osu.Game.Rulesets.Osu.Tests/TestSceneReplayStability.cs | 2 +- osu.Game.Rulesets.Taiko.Tests/TestSceneReplayStability.cs | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/osu.Game.Rulesets.Mania.Tests/TestSceneReplayStability.cs b/osu.Game.Rulesets.Mania.Tests/TestSceneReplayStability.cs index 1f51a1494d..497d8a18b8 100644 --- a/osu.Game.Rulesets.Mania.Tests/TestSceneReplayStability.cs +++ b/osu.Game.Rulesets.Mania.Tests/TestSceneReplayStability.cs @@ -15,7 +15,7 @@ namespace osu.Game.Rulesets.Mania.Tests [Ignore("These tests are expected to fail until an acceptable solution for various replay playback issues concerning rounding of replay frame times & hit windows is found.")] public partial class TestSceneReplayStability : ReplayStabilityTestScene { - private static readonly object[][] test_cases = new[] + private static readonly object[][] test_cases = { // OD = 5 test cases. // PERFECT hit window is [ -19.4ms, 19.4ms] @@ -110,7 +110,7 @@ namespace osu.Game.Rulesets.Mania.Tests { HitObjects = { - new Note() + new Note { StartTime = note_time, Column = 0, diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneReplayStability.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneReplayStability.cs index 8af12fbe2f..aca8f757f2 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneReplayStability.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneReplayStability.cs @@ -16,7 +16,7 @@ namespace osu.Game.Rulesets.Osu.Tests [Ignore("These tests are expected to fail until an acceptable solution for various replay playback issues concerning rounding of replay frame times & hit windows is found.")] public partial class TestSceneReplayStability : ReplayStabilityTestScene { - private static readonly object[][] test_cases = new[] + private static readonly object[][] test_cases = { // OD = 5 test cases. // GREAT hit window is [ -50ms, 50ms] diff --git a/osu.Game.Rulesets.Taiko.Tests/TestSceneReplayStability.cs b/osu.Game.Rulesets.Taiko.Tests/TestSceneReplayStability.cs index d245fbd28f..4a2cd024b0 100644 --- a/osu.Game.Rulesets.Taiko.Tests/TestSceneReplayStability.cs +++ b/osu.Game.Rulesets.Taiko.Tests/TestSceneReplayStability.cs @@ -15,7 +15,7 @@ namespace osu.Game.Rulesets.Taiko.Tests [Ignore("These tests are expected to fail until an acceptable solution for various replay playback issues concerning rounding of replay frame times & hit windows is found.")] public partial class TestSceneReplayStability : ReplayStabilityTestScene { - private static readonly object[][] test_cases = new[] + private static readonly object[][] test_cases = { // OD = 5 test cases. // GREAT hit window is [-35ms, 35ms] @@ -59,7 +59,7 @@ namespace osu.Game.Rulesets.Taiko.Tests { const double hit_time = 100; - var beatmap = new TaikoBeatmap() + var beatmap = new TaikoBeatmap { HitObjects = { From 4b9873f03e656e03ca539d5850ca2e6c97fd80ae Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 11 Apr 2025 21:19:01 +0900 Subject: [PATCH 054/164] Avoid performing colour fades when pill is not visible in the first place --- .../Beatmaps/Drawables/BeatmapSetOnlineStatusPill.cs | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/osu.Game/Beatmaps/Drawables/BeatmapSetOnlineStatusPill.cs b/osu.Game/Beatmaps/Drawables/BeatmapSetOnlineStatusPill.cs index 3ee0be61b1..7b3067e8d6 100644 --- a/osu.Game/Beatmaps/Drawables/BeatmapSetOnlineStatusPill.cs +++ b/osu.Game/Beatmaps/Drawables/BeatmapSetOnlineStatusPill.cs @@ -13,7 +13,6 @@ using osu.Game.Graphics; using osu.Game.Graphics.Sprites; using osu.Game.Localisation; using osu.Game.Overlays; -using osuTK; using osuTK.Graphics; namespace osu.Game.Beatmaps.Drawables @@ -116,6 +115,10 @@ namespace osu.Game.Beatmaps.Drawables this.FadeIn(animation_duration, Easing.OutQuint); + // Handle the case where transition from hidden to non-hidden may cause + // a fade from a colour that doesn't make sense (due to not being able to see the previous colour). + double duration = Alpha > 0 ? animation_duration : 0; + Color4 statusTextColour; if (colourProvider != null) @@ -123,8 +126,8 @@ namespace osu.Game.Beatmaps.Drawables else statusTextColour = status == BeatmapOnlineStatus.Graveyard ? colours.GreySeaFoamLight : Color4.Black; - statusText.FadeColour(statusTextColour, animation_duration, Easing.OutQuint); - background.FadeColour(OsuColour.ForBeatmapSetOnlineStatus(Status) ?? colourProvider?.Light1 ?? colours.GreySeaFoamLighter, animation_duration, Easing.OutQuint); + statusText.FadeColour(statusTextColour, duration, Easing.OutQuint); + background.FadeColour(OsuColour.ForBeatmapSetOnlineStatus(Status) ?? colourProvider?.Light1 ?? colours.GreySeaFoamLighter, duration, Easing.OutQuint); statusText.Text = Status.GetLocalisableDescription().ToUpper(); } From 85556e0c3e29e3ccb12f7324db82d18ff949a7fa Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Fri, 11 Apr 2025 08:31:03 -0400 Subject: [PATCH 055/164] Introduce numeric value in beatmap hit count statistics --- .../Beatmaps/CatchBeatmap.cs | 5 +++++ .../Beatmaps/ManiaBeatmap.cs | 3 +++ osu.Game.Rulesets.Osu/Beatmaps/OsuBeatmap.cs | 5 +++++ .../Beatmaps/TaikoBeatmap.cs | 5 +++++ osu.Game/Beatmaps/BeatmapStatistic.cs | 17 ++++++++++++++++- 5 files changed, 34 insertions(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Catch/Beatmaps/CatchBeatmap.cs b/osu.Game.Rulesets.Catch/Beatmaps/CatchBeatmap.cs index 01cec1d815..e9d087929f 100644 --- a/osu.Game.Rulesets.Catch/Beatmaps/CatchBeatmap.cs +++ b/osu.Game.Rulesets.Catch/Beatmaps/CatchBeatmap.cs @@ -1,6 +1,7 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System; using System.Collections.Generic; using System.Linq; using osu.Game.Beatmaps; @@ -16,6 +17,7 @@ namespace osu.Game.Rulesets.Catch.Beatmaps int fruits = HitObjects.Count(s => s is Fruit); int juiceStreams = HitObjects.Count(s => s is JuiceStream); int bananaShowers = HitObjects.Count(s => s is BananaShower); + int sum = Math.Max(1, fruits + juiceStreams + bananaShowers); return new[] { @@ -24,18 +26,21 @@ namespace osu.Game.Rulesets.Catch.Beatmaps Name = @"Fruits", Content = fruits.ToString(), CreateIcon = () => new BeatmapStatisticIcon(BeatmapStatisticsIconType.Circles), + Ratio = fruits / (float)sum, }, new BeatmapStatistic { Name = @"Juice Streams", Content = juiceStreams.ToString(), CreateIcon = () => new BeatmapStatisticIcon(BeatmapStatisticsIconType.Sliders), + Ratio = juiceStreams / (float)sum, }, new BeatmapStatistic { Name = @"Banana Showers", Content = bananaShowers.ToString(), CreateIcon = () => new BeatmapStatisticIcon(BeatmapStatisticsIconType.Spinners), + Ratio = Math.Min(bananaShowers / 10f, 1), } }; } diff --git a/osu.Game.Rulesets.Mania/Beatmaps/ManiaBeatmap.cs b/osu.Game.Rulesets.Mania/Beatmaps/ManiaBeatmap.cs index 8ddcfa128a..16e1751e95 100644 --- a/osu.Game.Rulesets.Mania/Beatmaps/ManiaBeatmap.cs +++ b/osu.Game.Rulesets.Mania/Beatmaps/ManiaBeatmap.cs @@ -36,6 +36,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps { int notes = HitObjects.Count(s => s is Note); int holdNotes = HitObjects.Count(s => s is HoldNote); + int sum = Math.Max(1, notes + holdNotes); return new[] { @@ -44,12 +45,14 @@ namespace osu.Game.Rulesets.Mania.Beatmaps Name = @"Notes", CreateIcon = () => new BeatmapStatisticIcon(BeatmapStatisticsIconType.Circles), Content = notes.ToString(), + Ratio = notes / (float)sum, }, new BeatmapStatistic { Name = @"Hold Notes", CreateIcon = () => new BeatmapStatisticIcon(BeatmapStatisticsIconType.Sliders), Content = holdNotes.ToString(), + Ratio = holdNotes / (float)sum, }, }; } diff --git a/osu.Game.Rulesets.Osu/Beatmaps/OsuBeatmap.cs b/osu.Game.Rulesets.Osu/Beatmaps/OsuBeatmap.cs index 730a194751..cc73f2860a 100644 --- a/osu.Game.Rulesets.Osu/Beatmaps/OsuBeatmap.cs +++ b/osu.Game.Rulesets.Osu/Beatmaps/OsuBeatmap.cs @@ -1,6 +1,7 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System; using System.Collections.Generic; using System.Linq; using osu.Game.Beatmaps; @@ -15,6 +16,7 @@ namespace osu.Game.Rulesets.Osu.Beatmaps int circles = HitObjects.Count(c => c is HitCircle); int sliders = HitObjects.Count(s => s is Slider); int spinners = HitObjects.Count(s => s is Spinner); + int sum = Math.Max(1, circles + sliders + spinners); return new[] { @@ -23,18 +25,21 @@ namespace osu.Game.Rulesets.Osu.Beatmaps Name = "Circles", Content = circles.ToString(), CreateIcon = () => new BeatmapStatisticIcon(BeatmapStatisticsIconType.Circles), + Ratio = circles / (float)sum, }, new BeatmapStatistic { Name = "Sliders", Content = sliders.ToString(), CreateIcon = () => new BeatmapStatisticIcon(BeatmapStatisticsIconType.Sliders), + Ratio = sliders / (float)sum, }, new BeatmapStatistic { Name = @"Spinners", Content = spinners.ToString(), CreateIcon = () => new BeatmapStatisticIcon(BeatmapStatisticsIconType.Spinners), + Ratio = Math.Min(spinners / 10f, 1), } }; } diff --git a/osu.Game.Rulesets.Taiko/Beatmaps/TaikoBeatmap.cs b/osu.Game.Rulesets.Taiko/Beatmaps/TaikoBeatmap.cs index 0781485ab8..ad4413d84a 100644 --- a/osu.Game.Rulesets.Taiko/Beatmaps/TaikoBeatmap.cs +++ b/osu.Game.Rulesets.Taiko/Beatmaps/TaikoBeatmap.cs @@ -1,6 +1,7 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System; using System.Collections.Generic; using System.Linq; using osu.Game.Beatmaps; @@ -15,6 +16,7 @@ namespace osu.Game.Rulesets.Taiko.Beatmaps int hits = HitObjects.Count(s => s is Hit); int drumRolls = HitObjects.Count(s => s is DrumRoll); int swells = HitObjects.Count(s => s is Swell); + int sum = Math.Max(1, hits + drumRolls + swells); return new[] { @@ -23,18 +25,21 @@ namespace osu.Game.Rulesets.Taiko.Beatmaps Name = @"Hits", CreateIcon = () => new BeatmapStatisticIcon(BeatmapStatisticsIconType.Circles), Content = hits.ToString(), + Ratio = hits / (float)sum, }, new BeatmapStatistic { Name = @"Drumrolls", CreateIcon = () => new BeatmapStatisticIcon(BeatmapStatisticsIconType.Sliders), Content = drumRolls.ToString(), + Ratio = drumRolls / (float)sum, }, new BeatmapStatistic { Name = @"Swells", CreateIcon = () => new BeatmapStatisticIcon(BeatmapStatisticsIconType.Spinners), Content = swells.ToString(), + Ratio = Math.Min(swells / 10f, 1), } }; } diff --git a/osu.Game/Beatmaps/BeatmapStatistic.cs b/osu.Game/Beatmaps/BeatmapStatistic.cs index 13e0e4ad5e..6faf74d9c6 100644 --- a/osu.Game/Beatmaps/BeatmapStatistic.cs +++ b/osu.Game/Beatmaps/BeatmapStatistic.cs @@ -16,7 +16,22 @@ namespace osu.Game.Beatmaps /// public Func CreateIcon; - public string Content; + /// + /// The name of this statistic. + /// public LocalisableString Name; + + /// + /// The text representing the value of this statistic. + /// + public string Content; + + /// + /// The ratio of this statistic compared to other relevant statistics, or null if not applicable. + /// + /// + /// This is used to display a bar on top of the statistic with the given ratio. + /// + public float? Ratio; } } From a2a1ddaa79fe63bd0b97757b59009b6873182107 Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Fri, 11 Apr 2025 06:55:39 -0400 Subject: [PATCH 056/164] Increase group panel height Matches design and also because of the next commit which increases group label size to coexist visually with other panel types. --- osu.Game/Screens/SelectV2/PanelGroup.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/SelectV2/PanelGroup.cs b/osu.Game/Screens/SelectV2/PanelGroup.cs index ecb64f4797..800d7a2d07 100644 --- a/osu.Game/Screens/SelectV2/PanelGroup.cs +++ b/osu.Game/Screens/SelectV2/PanelGroup.cs @@ -18,7 +18,7 @@ namespace osu.Game.Screens.SelectV2 { public partial class PanelGroup : PanelBase { - public const float HEIGHT = CarouselItem.DEFAULT_HEIGHT; + public const float HEIGHT = CarouselItem.DEFAULT_HEIGHT * 1.2f; private Drawable chevronIcon = null!; private OsuSpriteText titleText = null!; From d9d3c93a9696e17d006684659ebaa39bd1e929bc Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Fri, 11 Apr 2025 06:55:26 -0400 Subject: [PATCH 057/164] Use font specification in group panels --- osu.Game/Screens/SelectV2/PanelGroup.cs | 4 +++- osu.Game/Screens/SelectV2/PanelGroupStarDifficulty.cs | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/SelectV2/PanelGroup.cs b/osu.Game/Screens/SelectV2/PanelGroup.cs index 800d7a2d07..410b6c9e86 100644 --- a/osu.Game/Screens/SelectV2/PanelGroup.cs +++ b/osu.Game/Screens/SelectV2/PanelGroup.cs @@ -49,6 +49,8 @@ namespace osu.Game.Screens.SelectV2 { Anchor = Anchor.CentreLeft, Origin = Anchor.CentreLeft, + Font = OsuFont.Style.Heading2, + UseFullGlyphHeight = false, X = 10f, }, new CircularContainer @@ -69,7 +71,7 @@ namespace osu.Game.Screens.SelectV2 { Anchor = Anchor.Centre, Origin = Anchor.Centre, - Font = OsuFont.Torus.With(size: 14.4f, weight: FontWeight.Bold), + Font = OsuFont.Style.Caption1.With(weight: FontWeight.Bold), // TODO: requires Carousel/CarouselItem-side implementation Text = "43", UseFullGlyphHeight = false, diff --git a/osu.Game/Screens/SelectV2/PanelGroupStarDifficulty.cs b/osu.Game/Screens/SelectV2/PanelGroupStarDifficulty.cs index 0dc5a2f365..a238539102 100644 --- a/osu.Game/Screens/SelectV2/PanelGroupStarDifficulty.cs +++ b/osu.Game/Screens/SelectV2/PanelGroupStarDifficulty.cs @@ -93,7 +93,7 @@ namespace osu.Game.Screens.SelectV2 { Anchor = Anchor.Centre, Origin = Anchor.Centre, - Font = OsuFont.Torus.With(size: 14.4f, weight: FontWeight.Bold), + Font = OsuFont.Style.Caption1.With(weight: FontWeight.Bold), // TODO: requires Carousel/CarouselItem-side implementation Text = "43", UseFullGlyphHeight = false, From 12e35557a545d8a50fce35268fafed901cb918b0 Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Fri, 11 Apr 2025 06:57:20 -0400 Subject: [PATCH 058/164] Update group panel design to match latest iteration --- osu.Game/Screens/SelectV2/PanelGroup.cs | 64 ++++++++++++++++++++----- 1 file changed, 51 insertions(+), 13 deletions(-) diff --git a/osu.Game/Screens/SelectV2/PanelGroup.cs b/osu.Game/Screens/SelectV2/PanelGroup.cs index 410b6c9e86..a5786b53c9 100644 --- a/osu.Game/Screens/SelectV2/PanelGroup.cs +++ b/osu.Game/Screens/SelectV2/PanelGroup.cs @@ -5,10 +5,12 @@ using System.Diagnostics; using osu.Framework.Allocation; using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; +using osu.Framework.Graphics.Colour; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Sprites; using osu.Game.Graphics; +using osu.Game.Graphics.Backgrounds; using osu.Game.Graphics.Sprites; using osu.Game.Overlays; using osuTK; @@ -20,27 +22,56 @@ namespace osu.Game.Screens.SelectV2 { public const float HEIGHT = CarouselItem.DEFAULT_HEIGHT * 1.2f; - private Drawable chevronIcon = null!; + private Drawable iconContainer = null!; private OsuSpriteText titleText = null!; + private TrianglesV2 triangles = null!; + private Box glow = null!; + + [Resolved] + private OverlayColourProvider colourProvider { get; set; } = null!; [BackgroundDependencyLoader] - private void load(OverlayColourProvider colourProvider) + private void load() { Height = HEIGHT; - Icon = chevronIcon = new SpriteIcon + Icon = iconContainer = new Container { AlwaysPresent = true, - Icon = FontAwesome.Solid.ChevronDown, - Size = new Vector2(12), - Margin = new MarginPadding { Horizontal = 5f }, - X = 2f, - Colour = colourProvider.Background3, + RelativeSizeAxes = Axes.Y, + Child = new SpriteIcon + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Icon = FontAwesome.Solid.ChevronDown, + Size = new Vector2(12), + Colour = colourProvider.Background3, + }, }; - Background = new Box + Background = new Container { RelativeSizeAxes = Axes.Both, - Colour = colourProvider.Dark1, + Children = new Drawable[] + { + new Box + { + RelativeSizeAxes = Axes.Both, + Colour = colourProvider.Background5, + }, + triangles = new TrianglesV2 + { + RelativeSizeAxes = Axes.Both, + Thickness = 0.02f, + SpawnRatio = 0.6f, + Colour = ColourInfo.GradientHorizontal(colourProvider.Background6, colourProvider.Background5) + }, + glow = new Box + { + RelativeSizeAxes = Axes.Both, + Width = 0.5f, + Colour = ColourInfo.GradientHorizontal(colourProvider.Highlight1, colourProvider.Highlight1.Opacity(0f)), + }, + }, }; AccentColour = colourProvider.Highlight1; Content.Children = new Drawable[] @@ -77,7 +108,7 @@ namespace osu.Game.Screens.SelectV2 UseFullGlyphHeight = false, } }, - } + }, }; } @@ -92,8 +123,15 @@ namespace osu.Game.Screens.SelectV2 { const float duration = 500; - chevronIcon.ResizeWidthTo(Expanded.Value ? 12f : 0f, duration, Easing.OutQuint); - chevronIcon.FadeTo(Expanded.Value ? 1f : 0f, duration, Easing.OutQuint); + iconContainer.ResizeWidthTo(Expanded.Value ? 20f : 5f, duration, Easing.OutQuint); + iconContainer.FadeTo(Expanded.Value ? 1f : 0f, duration, Easing.OutQuint); + + ColourInfo colour = Expanded.Value + ? ColourInfo.GradientHorizontal(colourProvider.Highlight1.Opacity(0.25f), colourProvider.Highlight1.Opacity(0f)) + : ColourInfo.GradientHorizontal(colourProvider.Background6, colourProvider.Background5); + + triangles.FadeColour(colour, duration, Easing.OutQuint); + glow.FadeTo(Expanded.Value ? 0.4f : 0, duration, Easing.OutQuint); } protected override void PrepareForUse() From d3f3c4f6d08303a30ffcefc5c9e182945b4f6fff Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Fri, 11 Apr 2025 06:58:04 -0400 Subject: [PATCH 059/164] Update star rating group panel to look better --- osu.Game/Localisation/SongSelectStrings.cs | 15 +++ .../SelectV2/PanelGroupStarDifficulty.cs | 109 +++++++++++++----- 2 files changed, 93 insertions(+), 31 deletions(-) diff --git a/osu.Game/Localisation/SongSelectStrings.cs b/osu.Game/Localisation/SongSelectStrings.cs index e715ba8880..ecf68e33a8 100644 --- a/osu.Game/Localisation/SongSelectStrings.cs +++ b/osu.Game/Localisation/SongSelectStrings.cs @@ -54,6 +54,21 @@ namespace osu.Game.Localisation /// public static LocalisableString EditBeatmap => new TranslatableString(getKey(@"edit_beatmap"), @"Edit beatmap"); + /// + /// "Below 1 Star" + /// + public static LocalisableString BelowStar => new TranslatableString(getKey(@"below_star"), @"Below 1 Star"); + + /// + /// "1 Star" + /// + public static LocalisableString Star => new TranslatableString(getKey(@"star"), @"1 Star"); + + /// + /// "{0} Stars" + /// + public static LocalisableString Stars(int starNumber) => new TranslatableString(getKey(@"stars"), @"{0} Stars", starNumber); + private static string getKey(string key) => $@"{prefix}:{key}"; } } diff --git a/osu.Game/Screens/SelectV2/PanelGroupStarDifficulty.cs b/osu.Game/Screens/SelectV2/PanelGroupStarDifficulty.cs index a238539102..2fba25b3f0 100644 --- a/osu.Game/Screens/SelectV2/PanelGroupStarDifficulty.cs +++ b/osu.Game/Screens/SelectV2/PanelGroupStarDifficulty.cs @@ -5,17 +5,17 @@ using System.Diagnostics; using osu.Framework.Allocation; using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; +using osu.Framework.Graphics.Colour; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Sprites; -using osu.Game.Beatmaps; -using osu.Game.Beatmaps.Drawables; using osu.Game.Graphics; +using osu.Game.Graphics.Backgrounds; using osu.Game.Graphics.Sprites; -using osu.Game.Graphics.UserInterface; using osu.Game.Overlays; using osuTK; using osuTK.Graphics; +using osu.Game.Localisation; namespace osu.Game.Screens.SelectV2 { @@ -27,28 +27,50 @@ namespace osu.Game.Screens.SelectV2 [Resolved] private OverlayColourProvider colourProvider { get; set; } = null!; - private Drawable chevronIcon = null!; + private Drawable iconContainer = null!; private Box contentBackground = null!; - private StarRatingDisplay starRatingDisplay = null!; - private StarCounter starCounter = null!; + private OsuSpriteText starRatingText = null!; + private TrianglesV2 triangles = null!; + private Box glow = null!; [BackgroundDependencyLoader] private void load() { Height = PanelGroup.HEIGHT; - Icon = chevronIcon = new SpriteIcon + Icon = iconContainer = new Container { AlwaysPresent = true, - Icon = FontAwesome.Solid.ChevronDown, - Size = new Vector2(12), - Margin = new MarginPadding { Horizontal = 5f }, - X = 2f, + RelativeSizeAxes = Axes.Y, + Child = new SpriteIcon + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Icon = FontAwesome.Solid.ChevronDown, + Size = new Vector2(12), + }, }; - Background = contentBackground = new Box + Background = new Container { RelativeSizeAxes = Axes.Both, - Colour = colourProvider.Dark1, + Children = new Drawable[] + { + contentBackground = new Box + { + RelativeSizeAxes = Axes.Both, + }, + triangles = new TrianglesV2 + { + RelativeSizeAxes = Axes.Both, + Thickness = 0.02f, + SpawnRatio = 0.6f, + }, + glow = new Box + { + RelativeSizeAxes = Axes.Both, + Width = 0.5f, + }, + }, }; AccentColour = colourProvider.Highlight1; Content.Children = new Drawable[] @@ -62,17 +84,13 @@ namespace osu.Game.Screens.SelectV2 Margin = new MarginPadding { Left = 10f }, Children = new Drawable[] { - starRatingDisplay = new StarRatingDisplay(default, StarRatingDisplaySize.Small) + starRatingText = new OsuSpriteText { Anchor = Anchor.CentreLeft, Origin = Anchor.CentreLeft, - }, - starCounter = new StarCounter - { - Anchor = Anchor.CentreLeft, - Origin = Anchor.CentreLeft, - Scale = new Vector2(8f / 20f), - }, + UseFullGlyphHeight = false, + Font = OsuFont.Style.Heading2, + } } }, new CircularContainer @@ -110,6 +128,8 @@ namespace osu.Game.Screens.SelectV2 Expanded.BindValueChanged(_ => onExpanded(), true); } + private Color4 ratingColour; + protected override void PrepareForUse() { base.PrepareForUse(); @@ -118,25 +138,52 @@ namespace osu.Game.Screens.SelectV2 int starNumber = (int)((GroupDefinition)Item.Model).Data; - Color4 colour = starNumber >= 9 ? OsuColour.Gray(0.2f) : colours.ForStarDifficulty(starNumber); - Color4 contentColour = starNumber >= 7 ? colours.Orange1 : colourProvider.Background5; + ratingColour = starNumber >= 9 ? OsuColour.Gray(0.2f) : colours.ForStarDifficulty(starNumber); - AccentColour = colour; - contentBackground.Colour = colour.Darken(0.3f); + AccentColour = ratingColour; + contentBackground.Colour = ratingColour.Darken(1f); + glow.Colour = ColourInfo.GradientHorizontal(ratingColour, ratingColour.Opacity(0f)); - starRatingDisplay.Current.Value = new StarDifficulty(starNumber, 0); - starCounter.Current = starNumber; + switch (starNumber) + { + case 0: + starRatingText.Text = SongSelectStrings.BelowStar; + break; - chevronIcon.Colour = contentColour; - starCounter.Colour = contentColour; + case 1: + starRatingText.Text = SongSelectStrings.Star; + break; + + default: + starRatingText.Text = SongSelectStrings.Stars(starNumber); + break; + } + + iconContainer.Colour = starNumber >= 7 ? colourProvider.Content1 : colourProvider.Background5; + starRatingText.Colour = colourProvider.Content1; + + ColourInfo colour; + + if (starNumber >= 8) + colour = ColourInfo.GradientHorizontal(ratingColour, ratingColour.Darken(0.2f)); + else + colour = ColourInfo.GradientHorizontal(ratingColour.Darken(0.6f), ratingColour.Darken(0.8f)); + + triangles.Colour = colour; + + onExpanded(); } private void onExpanded() { const float duration = 500; - chevronIcon.ResizeWidthTo(Expanded.Value ? 12f : 0f, duration, Easing.OutQuint); - chevronIcon.FadeTo(Expanded.Value ? 1f : 0f, duration, Easing.OutQuint); + Debug.Assert(Item != null); + + iconContainer.ResizeWidthTo(Expanded.Value ? 20f : 5f, duration, Easing.OutQuint); + iconContainer.FadeTo(Expanded.Value ? 1f : 0f, duration, Easing.OutQuint); + + glow.FadeTo(Expanded.Value ? 0.4f : 0, duration, Easing.OutQuint); } } } From 6db73b8a13146d05d57254d3a9029eb6a1cd8806 Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Fri, 11 Apr 2025 07:10:54 -0400 Subject: [PATCH 060/164] Change keyboard selection highlight colour to give betetr visuals --- osu.Game/Screens/SelectV2/PanelBase.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/SelectV2/PanelBase.cs b/osu.Game/Screens/SelectV2/PanelBase.cs index 05a1a55c03..32da02a189 100644 --- a/osu.Game/Screens/SelectV2/PanelBase.cs +++ b/osu.Game/Screens/SelectV2/PanelBase.cs @@ -159,7 +159,7 @@ namespace osu.Game.Screens.SelectV2 keyboardSelectionLayer = new Box { Alpha = 0, - Colour = colours.Yellow.Opacity(0.1f), + Colour = colourProvider.Highlight1.Opacity(0.1f), Blending = BlendingParameters.Additive, RelativeSizeAxes = Axes.Both, }, From b95b9b36430fd5213bdf717c68308c45ac5f079a Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Fri, 11 Apr 2025 06:58:10 -0400 Subject: [PATCH 061/164] Improve group panel test scene --- .../TestSceneBeatmapCarouselV2GroupPanel.cs | 90 +++++++++++++------ 1 file changed, 63 insertions(+), 27 deletions(-) diff --git a/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselV2GroupPanel.cs b/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselV2GroupPanel.cs index 9b07f01e52..d62aee77f3 100644 --- a/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselV2GroupPanel.cs +++ b/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselV2GroupPanel.cs @@ -1,8 +1,11 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System; +using NUnit.Framework; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; +using osu.Game.Overlays; using osu.Game.Screens.SelectV2; using osu.Game.Tests.Visual.UserInterface; using osuTK; @@ -16,6 +19,66 @@ namespace osu.Game.Tests.Visual.SongSelectV2 { } + [Test] + public void TestGeneral() + { + AddStep("general", () => CreateThemedContent(OverlayColourScheme.Aquamarine)); + } + + [Test] + public void TestStars() + { + for (int i = 0; i <= 10; i++) + { + int star = i; + + AddStep($"display {i} star(s)", () => + { + ContentContainer.Child = new DependencyProvidingContainer + { + RelativeSizeAxes = Axes.Both, + CachedDependencies = new (Type, object)[] + { + (typeof(OverlayColourProvider), new OverlayColourProvider(OverlayColourScheme.Aquamarine)) + }, + Child = new FillFlowContainer + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Width = 0.5f, + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Direction = FillDirection.Vertical, + Spacing = new Vector2(0f, 5f), + Children = new[] + { + new PanelGroupStarDifficulty + { + Item = new CarouselItem(new GroupDefinition(star, star.ToString())) + }, + new PanelGroupStarDifficulty + { + Item = new CarouselItem(new GroupDefinition(star, star.ToString())), + KeyboardSelected = { Value = true }, + }, + new PanelGroupStarDifficulty + { + Item = new CarouselItem(new GroupDefinition(star, star.ToString())), + Expanded = { Value = true }, + }, + new PanelGroupStarDifficulty + { + Item = new CarouselItem(new GroupDefinition(star, star.ToString())), + Expanded = { Value = true }, + KeyboardSelected = { Value = true }, + }, + }, + } + }; + }); + } + } + protected override Drawable CreateContent() { return new FillFlowContainer @@ -49,33 +112,6 @@ namespace osu.Game.Tests.Visual.SongSelectV2 KeyboardSelected = { Value = true }, Expanded = { Value = true } }, - new PanelGroupStarDifficulty - { - Item = new CarouselItem(new GroupDefinition(1, "1")) - }, - new PanelGroupStarDifficulty - { - Item = new CarouselItem(new GroupDefinition(3, "3")), - Expanded = { Value = true } - }, - new PanelGroupStarDifficulty - { - Item = new CarouselItem(new GroupDefinition(5, "5")), - }, - new PanelGroupStarDifficulty - { - Item = new CarouselItem(new GroupDefinition(7, "7")), - Expanded = { Value = true } - }, - new PanelGroupStarDifficulty - { - Item = new CarouselItem(new GroupDefinition(8, "8")), - }, - new PanelGroupStarDifficulty - { - Item = new CarouselItem(new GroupDefinition(9, "9")), - Expanded = { Value = true } - }, } }; } From 76b94884b824a7665adfdc2b483c9ab67c3f507b Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sun, 13 Apr 2025 15:31:40 +0900 Subject: [PATCH 062/164] Remove localisation support for now The plural handling doesn't cover other languages so it's a bit pointless to localise in this manner. --- osu.Game/Localisation/SongSelectStrings.cs | 15 --------------- .../Screens/SelectV2/PanelGroupStarDifficulty.cs | 7 +++---- 2 files changed, 3 insertions(+), 19 deletions(-) diff --git a/osu.Game/Localisation/SongSelectStrings.cs b/osu.Game/Localisation/SongSelectStrings.cs index ecf68e33a8..e715ba8880 100644 --- a/osu.Game/Localisation/SongSelectStrings.cs +++ b/osu.Game/Localisation/SongSelectStrings.cs @@ -54,21 +54,6 @@ namespace osu.Game.Localisation /// public static LocalisableString EditBeatmap => new TranslatableString(getKey(@"edit_beatmap"), @"Edit beatmap"); - /// - /// "Below 1 Star" - /// - public static LocalisableString BelowStar => new TranslatableString(getKey(@"below_star"), @"Below 1 Star"); - - /// - /// "1 Star" - /// - public static LocalisableString Star => new TranslatableString(getKey(@"star"), @"1 Star"); - - /// - /// "{0} Stars" - /// - public static LocalisableString Stars(int starNumber) => new TranslatableString(getKey(@"stars"), @"{0} Stars", starNumber); - private static string getKey(string key) => $@"{prefix}:{key}"; } } diff --git a/osu.Game/Screens/SelectV2/PanelGroupStarDifficulty.cs b/osu.Game/Screens/SelectV2/PanelGroupStarDifficulty.cs index 2fba25b3f0..7353fd4095 100644 --- a/osu.Game/Screens/SelectV2/PanelGroupStarDifficulty.cs +++ b/osu.Game/Screens/SelectV2/PanelGroupStarDifficulty.cs @@ -15,7 +15,6 @@ using osu.Game.Graphics.Sprites; using osu.Game.Overlays; using osuTK; using osuTK.Graphics; -using osu.Game.Localisation; namespace osu.Game.Screens.SelectV2 { @@ -147,15 +146,15 @@ namespace osu.Game.Screens.SelectV2 switch (starNumber) { case 0: - starRatingText.Text = SongSelectStrings.BelowStar; + starRatingText.Text = @"Below 1 Star"; break; case 1: - starRatingText.Text = SongSelectStrings.Star; + starRatingText.Text = @"1 Star"; break; default: - starRatingText.Text = SongSelectStrings.Stars(starNumber); + starRatingText.Text = $"{starNumber} Stars"; break; } From f79f427547de4273154e37108685e449f66be71d Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sun, 13 Apr 2025 15:34:59 +0900 Subject: [PATCH 063/164] Remove unnecessary assert --- osu.Game/Screens/SelectV2/PanelGroupStarDifficulty.cs | 2 -- 1 file changed, 2 deletions(-) diff --git a/osu.Game/Screens/SelectV2/PanelGroupStarDifficulty.cs b/osu.Game/Screens/SelectV2/PanelGroupStarDifficulty.cs index 7353fd4095..ce46362133 100644 --- a/osu.Game/Screens/SelectV2/PanelGroupStarDifficulty.cs +++ b/osu.Game/Screens/SelectV2/PanelGroupStarDifficulty.cs @@ -177,8 +177,6 @@ namespace osu.Game.Screens.SelectV2 { const float duration = 500; - Debug.Assert(Item != null); - iconContainer.ResizeWidthTo(Expanded.Value ? 20f : 5f, duration, Easing.OutQuint); iconContainer.FadeTo(Expanded.Value ? 1f : 0f, duration, Easing.OutQuint); From 7cdbc2c20add6e8a41db071352812f8ee442a4fd Mon Sep 17 00:00:00 2001 From: Rudi Herouard Date: Sun, 13 Apr 2025 23:35:44 +0200 Subject: [PATCH 064/164] Fix "spins per minute" shows up early #31173 Make isSpinnableTime public in SpinnerRotationTracker and use it to set Tracking in OsuModSpunOut. Tracking was previously set to true, causing the "spins per minute" to appear immediately when the spinner appeared. --- osu.Game.Rulesets.Osu/Mods/OsuModSpunOut.cs | 2 +- .../Skinning/Default/SpinnerRotationTracker.cs | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModSpunOut.cs b/osu.Game.Rulesets.Osu/Mods/OsuModSpunOut.cs index 992f4d5f03..222cf4242a 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModSpunOut.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModSpunOut.cs @@ -37,7 +37,7 @@ namespace osu.Game.Rulesets.Osu.Mods { var spinner = (DrawableSpinner)drawable; - spinner.RotationTracker.Tracking = true; + spinner.RotationTracker.Tracking = spinner.RotationTracker.IsSpinnableTime; // early-return if we were paused to avoid division-by-zero in the subsequent calculations. if (Precision.AlmostEquals(spinner.Clock.Rate, 0)) diff --git a/osu.Game.Rulesets.Osu/Skinning/Default/SpinnerRotationTracker.cs b/osu.Game.Rulesets.Osu/Skinning/Default/SpinnerRotationTracker.cs index 7e97f826f9..7cd1f39871 100644 --- a/osu.Game.Rulesets.Osu/Skinning/Default/SpinnerRotationTracker.cs +++ b/osu.Game.Rulesets.Osu/Skinning/Default/SpinnerRotationTracker.cs @@ -52,7 +52,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Default /// /// Whether currently in the correct time range to allow spinning. /// - private bool isSpinnableTime => drawableSpinner.HitObject.StartTime <= Time.Current && drawableSpinner.HitObject.EndTime > Time.Current; + public bool IsSpinnableTime => drawableSpinner.HitObject.StartTime <= Time.Current && drawableSpinner.HitObject.EndTime > Time.Current; protected override bool OnMouseMove(MouseMoveEvent e) { @@ -79,7 +79,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Default lastAngle = thisAngle; } - IsSpinning.Value = isSpinnableTime && Math.Abs(currentRotation - Rotation) > 10f; + IsSpinning.Value = IsSpinnableTime && Math.Abs(currentRotation - Rotation) > 10f; Rotation = (float)Interpolation.Damp(Rotation, currentRotation, 0.99, Math.Abs(Time.Elapsed)); } @@ -92,7 +92,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Default /// The delta angle. public void AddRotation(float delta) { - if (!isSpinnableTime) + if (!IsSpinnableTime) return; if (!rotationTransferred) From f4cb3a7fb3e4217379af0d4ab7ec44269beed718 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 14 Apr 2025 13:54:11 +0900 Subject: [PATCH 065/164] Add support for closing chat channels with middle click Closes https://github.com/ppy/osu/issues/32797. --- .../Visual/Online/TestSceneChatOverlay.cs | 26 +++++++++++++++++++ .../Chat/ChannelList/ChannelListItem.cs | 12 +++++++++ 2 files changed, 38 insertions(+) diff --git a/osu.Game.Tests/Visual/Online/TestSceneChatOverlay.cs b/osu.Game.Tests/Visual/Online/TestSceneChatOverlay.cs index ab9ee1d8cc..d0fc66252e 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneChatOverlay.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneChatOverlay.cs @@ -215,6 +215,32 @@ namespace osu.Game.Tests.Visual.Online }); } + [Test] + public void TestChannelCloseViaMiddleClick() + { + var testPMChannel = new Channel(testUser); + + AddStep("Show overlay", () => chatOverlay.Show()); + joinTestChannel(0); + joinChannel(testPMChannel); + AddStep("Select PM channel", () => clickDrawable(getChannelListItem(testPMChannel))); + AddStep("Middle click", () => + { + var item = getChannelListItem(testPMChannel); + InputManager.MoveMouseTo(item); + InputManager.Click(MouseButton.Middle); + }); + AddAssert("PM channel closed", () => !channelManager.JoinedChannels.Contains(testPMChannel)); + AddStep("Select normal channel", () => clickDrawable(getChannelListItem(testChannel1))); + AddStep("Click close button", () => + { + var item = getChannelListItem(testChannel1); + InputManager.MoveMouseTo(item); + InputManager.Click(MouseButton.Middle); + }); + AddAssert("Normal channel closed", () => !channelManager.JoinedChannels.Contains(testChannel1)); + } + [Test] public void TestChannelCloseButton() { diff --git a/osu.Game/Overlays/Chat/ChannelList/ChannelListItem.cs b/osu.Game/Overlays/Chat/ChannelList/ChannelListItem.cs index 6107f130ec..3741852993 100644 --- a/osu.Game/Overlays/Chat/ChannelList/ChannelListItem.cs +++ b/osu.Game/Overlays/Chat/ChannelList/ChannelListItem.cs @@ -18,6 +18,7 @@ using osu.Game.Online.Chat; using osu.Game.Overlays.Chat.Listing; using osu.Game.Users.Drawables; using osuTK; +using osuTK.Input; namespace osu.Game.Overlays.Chat.ChannelList { @@ -160,6 +161,17 @@ namespace osu.Game.Overlays.Chat.ChannelList }; } + protected override bool OnMouseDown(MouseDownEvent e) + { + if (e.Button == MouseButton.Middle) + { + close?.TriggerClick(); + return true; + } + + return base.OnMouseDown(e); + } + private ChannelListItemCloseButton? createCloseButton() { if (isSelector || !CanLeave) From 80d9f742da7b9efa0f98ca7715351bcc427e8a70 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Mon, 14 Apr 2025 17:45:15 +0900 Subject: [PATCH 066/164] Combine "spinnable time" conditions --- .../Objects/Drawables/DrawableSpinner.cs | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs index 8c21e6a6bc..64cedd216b 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs @@ -277,13 +277,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables base.Update(); if (HandleUserInput) - { - bool isValidSpinningTime = Time.Current >= HitObject.StartTime && Time.Current <= HitObject.EndTime; - - RotationTracker.Tracking = !Result.HasResult - && correctButtonPressed() - && isValidSpinningTime; - } + RotationTracker.Tracking = RotationTracker.IsSpinnableTime && !Result.HasResult && correctButtonPressed(); if (spinningSample != null && spinnerFrequencyModulate) spinningSample.Frequency.Value = spinning_sample_modulated_base_frequency + Progress; From f05a50f4e19fa215dfd761b9de328945fbe1bd25 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 14 Apr 2025 18:38:04 +0900 Subject: [PATCH 067/164] Rename new property to better explain visual-only usage --- osu.Game.Rulesets.Catch/Beatmaps/CatchBeatmap.cs | 6 +++--- osu.Game.Rulesets.Mania/Beatmaps/ManiaBeatmap.cs | 4 ++-- osu.Game.Rulesets.Osu/Beatmaps/OsuBeatmap.cs | 6 +++--- osu.Game.Rulesets.Taiko/Beatmaps/TaikoBeatmap.cs | 6 +++--- osu.Game/Beatmaps/BeatmapStatistic.cs | 7 ++----- 5 files changed, 13 insertions(+), 16 deletions(-) diff --git a/osu.Game.Rulesets.Catch/Beatmaps/CatchBeatmap.cs b/osu.Game.Rulesets.Catch/Beatmaps/CatchBeatmap.cs index e9d087929f..eadf7f42bc 100644 --- a/osu.Game.Rulesets.Catch/Beatmaps/CatchBeatmap.cs +++ b/osu.Game.Rulesets.Catch/Beatmaps/CatchBeatmap.cs @@ -26,21 +26,21 @@ namespace osu.Game.Rulesets.Catch.Beatmaps Name = @"Fruits", Content = fruits.ToString(), CreateIcon = () => new BeatmapStatisticIcon(BeatmapStatisticsIconType.Circles), - Ratio = fruits / (float)sum, + BarDisplayLength = fruits / (float)sum, }, new BeatmapStatistic { Name = @"Juice Streams", Content = juiceStreams.ToString(), CreateIcon = () => new BeatmapStatisticIcon(BeatmapStatisticsIconType.Sliders), - Ratio = juiceStreams / (float)sum, + BarDisplayLength = juiceStreams / (float)sum, }, new BeatmapStatistic { Name = @"Banana Showers", Content = bananaShowers.ToString(), CreateIcon = () => new BeatmapStatisticIcon(BeatmapStatisticsIconType.Spinners), - Ratio = Math.Min(bananaShowers / 10f, 1), + BarDisplayLength = Math.Min(bananaShowers / 10f, 1), } }; } diff --git a/osu.Game.Rulesets.Mania/Beatmaps/ManiaBeatmap.cs b/osu.Game.Rulesets.Mania/Beatmaps/ManiaBeatmap.cs index 16e1751e95..3ee1b63800 100644 --- a/osu.Game.Rulesets.Mania/Beatmaps/ManiaBeatmap.cs +++ b/osu.Game.Rulesets.Mania/Beatmaps/ManiaBeatmap.cs @@ -45,14 +45,14 @@ namespace osu.Game.Rulesets.Mania.Beatmaps Name = @"Notes", CreateIcon = () => new BeatmapStatisticIcon(BeatmapStatisticsIconType.Circles), Content = notes.ToString(), - Ratio = notes / (float)sum, + BarDisplayLength = notes / (float)sum, }, new BeatmapStatistic { Name = @"Hold Notes", CreateIcon = () => new BeatmapStatisticIcon(BeatmapStatisticsIconType.Sliders), Content = holdNotes.ToString(), - Ratio = holdNotes / (float)sum, + BarDisplayLength = holdNotes / (float)sum, }, }; } diff --git a/osu.Game.Rulesets.Osu/Beatmaps/OsuBeatmap.cs b/osu.Game.Rulesets.Osu/Beatmaps/OsuBeatmap.cs index cc73f2860a..2600f63ab9 100644 --- a/osu.Game.Rulesets.Osu/Beatmaps/OsuBeatmap.cs +++ b/osu.Game.Rulesets.Osu/Beatmaps/OsuBeatmap.cs @@ -25,21 +25,21 @@ namespace osu.Game.Rulesets.Osu.Beatmaps Name = "Circles", Content = circles.ToString(), CreateIcon = () => new BeatmapStatisticIcon(BeatmapStatisticsIconType.Circles), - Ratio = circles / (float)sum, + BarDisplayLength = circles / (float)sum, }, new BeatmapStatistic { Name = "Sliders", Content = sliders.ToString(), CreateIcon = () => new BeatmapStatisticIcon(BeatmapStatisticsIconType.Sliders), - Ratio = sliders / (float)sum, + BarDisplayLength = sliders / (float)sum, }, new BeatmapStatistic { Name = @"Spinners", Content = spinners.ToString(), CreateIcon = () => new BeatmapStatisticIcon(BeatmapStatisticsIconType.Spinners), - Ratio = Math.Min(spinners / 10f, 1), + BarDisplayLength = Math.Min(spinners / 10f, 1), } }; } diff --git a/osu.Game.Rulesets.Taiko/Beatmaps/TaikoBeatmap.cs b/osu.Game.Rulesets.Taiko/Beatmaps/TaikoBeatmap.cs index ad4413d84a..e8cd05ee73 100644 --- a/osu.Game.Rulesets.Taiko/Beatmaps/TaikoBeatmap.cs +++ b/osu.Game.Rulesets.Taiko/Beatmaps/TaikoBeatmap.cs @@ -25,21 +25,21 @@ namespace osu.Game.Rulesets.Taiko.Beatmaps Name = @"Hits", CreateIcon = () => new BeatmapStatisticIcon(BeatmapStatisticsIconType.Circles), Content = hits.ToString(), - Ratio = hits / (float)sum, + BarDisplayLength = hits / (float)sum, }, new BeatmapStatistic { Name = @"Drumrolls", CreateIcon = () => new BeatmapStatisticIcon(BeatmapStatisticsIconType.Sliders), Content = drumRolls.ToString(), - Ratio = drumRolls / (float)sum, + BarDisplayLength = drumRolls / (float)sum, }, new BeatmapStatistic { Name = @"Swells", CreateIcon = () => new BeatmapStatisticIcon(BeatmapStatisticsIconType.Spinners), Content = swells.ToString(), - Ratio = Math.Min(swells / 10f, 1), + BarDisplayLength = Math.Min(swells / 10f, 1), } }; } diff --git a/osu.Game/Beatmaps/BeatmapStatistic.cs b/osu.Game/Beatmaps/BeatmapStatistic.cs index 6faf74d9c6..64e42f3f02 100644 --- a/osu.Game/Beatmaps/BeatmapStatistic.cs +++ b/osu.Game/Beatmaps/BeatmapStatistic.cs @@ -27,11 +27,8 @@ namespace osu.Game.Beatmaps public string Content; /// - /// The ratio of this statistic compared to other relevant statistics, or null if not applicable. + /// The length of a bar which visually represents this statistic's relevance in the beatmap. /// - /// - /// This is used to display a bar on top of the statistic with the given ratio. - /// - public float? Ratio; + public float? BarDisplayLength; } } From 3d47a2b5b2b94a92cbd757b705561519ad3c93d4 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 14 Apr 2025 19:21:36 +0900 Subject: [PATCH 068/164] Don't include spinner types in note sums --- osu.Game.Rulesets.Catch/Beatmaps/CatchBeatmap.cs | 2 +- osu.Game.Rulesets.Osu/Beatmaps/OsuBeatmap.cs | 2 +- osu.Game.Rulesets.Taiko/Beatmaps/TaikoBeatmap.cs | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Game.Rulesets.Catch/Beatmaps/CatchBeatmap.cs b/osu.Game.Rulesets.Catch/Beatmaps/CatchBeatmap.cs index eadf7f42bc..d43290e661 100644 --- a/osu.Game.Rulesets.Catch/Beatmaps/CatchBeatmap.cs +++ b/osu.Game.Rulesets.Catch/Beatmaps/CatchBeatmap.cs @@ -17,7 +17,7 @@ namespace osu.Game.Rulesets.Catch.Beatmaps int fruits = HitObjects.Count(s => s is Fruit); int juiceStreams = HitObjects.Count(s => s is JuiceStream); int bananaShowers = HitObjects.Count(s => s is BananaShower); - int sum = Math.Max(1, fruits + juiceStreams + bananaShowers); + int sum = Math.Max(1, fruits + juiceStreams); return new[] { diff --git a/osu.Game.Rulesets.Osu/Beatmaps/OsuBeatmap.cs b/osu.Game.Rulesets.Osu/Beatmaps/OsuBeatmap.cs index 2600f63ab9..d11b4aac3b 100644 --- a/osu.Game.Rulesets.Osu/Beatmaps/OsuBeatmap.cs +++ b/osu.Game.Rulesets.Osu/Beatmaps/OsuBeatmap.cs @@ -16,7 +16,7 @@ namespace osu.Game.Rulesets.Osu.Beatmaps int circles = HitObjects.Count(c => c is HitCircle); int sliders = HitObjects.Count(s => s is Slider); int spinners = HitObjects.Count(s => s is Spinner); - int sum = Math.Max(1, circles + sliders + spinners); + int sum = Math.Max(1, circles + sliders); return new[] { diff --git a/osu.Game.Rulesets.Taiko/Beatmaps/TaikoBeatmap.cs b/osu.Game.Rulesets.Taiko/Beatmaps/TaikoBeatmap.cs index e8cd05ee73..5b0582ab59 100644 --- a/osu.Game.Rulesets.Taiko/Beatmaps/TaikoBeatmap.cs +++ b/osu.Game.Rulesets.Taiko/Beatmaps/TaikoBeatmap.cs @@ -16,7 +16,7 @@ namespace osu.Game.Rulesets.Taiko.Beatmaps int hits = HitObjects.Count(s => s is Hit); int drumRolls = HitObjects.Count(s => s is DrumRoll); int swells = HitObjects.Count(s => s is Swell); - int sum = Math.Max(1, hits + drumRolls + swells); + int sum = Math.Max(1, hits + drumRolls); return new[] { From 9cfba9008fc726bb785b39f29608ab3492d130a7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 14 Apr 2025 13:13:21 +0200 Subject: [PATCH 069/164] Add extra comments regarding notation --- osu.Game.Rulesets.Mania.Tests/TestSceneReplayStability.cs | 4 ++++ osu.Game.Rulesets.Osu.Tests/TestSceneReplayStability.cs | 4 ++++ osu.Game.Rulesets.Taiko.Tests/TestSceneReplayStability.cs | 4 ++++ 3 files changed, 12 insertions(+) diff --git a/osu.Game.Rulesets.Mania.Tests/TestSceneReplayStability.cs b/osu.Game.Rulesets.Mania.Tests/TestSceneReplayStability.cs index 497d8a18b8..a83b61360b 100644 --- a/osu.Game.Rulesets.Mania.Tests/TestSceneReplayStability.cs +++ b/osu.Game.Rulesets.Mania.Tests/TestSceneReplayStability.cs @@ -17,6 +17,10 @@ namespace osu.Game.Rulesets.Mania.Tests { private static readonly object[][] test_cases = { + // With respect to notation, + // square brackets `[]` represent *closed* or *inclusive* bounds, + // while round brackets `()` represent *open* or *exclusive* bounds. + // OD = 5 test cases. // PERFECT hit window is [ -19.4ms, 19.4ms] // GREAT hit window is [ -49.0ms, 49.0ms] diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneReplayStability.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneReplayStability.cs index aca8f757f2..2303b17d96 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneReplayStability.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneReplayStability.cs @@ -18,6 +18,10 @@ namespace osu.Game.Rulesets.Osu.Tests { private static readonly object[][] test_cases = { + // With respect to notation, + // square brackets `[]` represent *closed* or *inclusive* bounds, + // while round brackets `()` represent *open* or *exclusive* bounds. + // OD = 5 test cases. // GREAT hit window is [ -50ms, 50ms] // OK hit window is [-100ms, 100ms] diff --git a/osu.Game.Rulesets.Taiko.Tests/TestSceneReplayStability.cs b/osu.Game.Rulesets.Taiko.Tests/TestSceneReplayStability.cs index 4a2cd024b0..62bbebcf0b 100644 --- a/osu.Game.Rulesets.Taiko.Tests/TestSceneReplayStability.cs +++ b/osu.Game.Rulesets.Taiko.Tests/TestSceneReplayStability.cs @@ -17,6 +17,10 @@ namespace osu.Game.Rulesets.Taiko.Tests { private static readonly object[][] test_cases = { + // With respect to notation, + // square brackets `[]` represent *closed* or *inclusive* bounds, + // while round brackets `()` represent *open* or *exclusive* bounds. + // OD = 5 test cases. // GREAT hit window is [-35ms, 35ms] // OK hit window is [-80ms, 80ms] From 82b2a92894a796b365404dd42bb78ca38b4bf356 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 14 Apr 2025 13:47:42 +0200 Subject: [PATCH 070/164] Add test cases covering correct legacy replay playback with respect to hitwindow treatment This continues on https://github.com/ppy/osu/pull/32770 via adding test cases which cover treatment of hit windows in stable in osu!, taiko, and mania. The test cases are exportable to beatmap `.osu` files and replay `.osr` files for stable crosscheck by setting `ExportLocation` on the test scene classes to a non-null path. For the most part, osu! and taiko ground truth matches previous findings - hit windows in those rulesets are floored to the nearest integer. The real "star" of this diff is mania, because: - The hit windows in mania depend on: - overall difficulty (as expected) - whether Score V2 is active - if Score V2 is not active, the hit windows also depend on whether the map was converted from another ruleset or not - Regardless of all aforementioned factors, mania hitwindows are *not symmetrical*. Due to what *appears* to be a straight-up bug, it is *not possible to achieve a MEH / 50 hit result when hitting late*. There is specific code that coerces late hits beyond 100 hit window range to full misses: https://github.com/peppy/osu-stable-reference/blob/996648fba06baf4e7d2e0b248959399444017895/osu!/GameplayElements/HitObjectManagerMania.cs#L737-L751 Note that despite the fact that I'm PRing these test cases, none of this is a promise that all of stable behaviours will be returning unchanged when I PR something to actually do something about this and the other issue of replay instability. This is just coverage, to be used for awareness of what's still broken. The extent of how much stable is going to be humored here going forward will be subject to negotiation. --- .../TestSceneLegacyReplayPlayback.cs | 470 ++++++++++++++++++ .../TestSceneLegacyReplayPlayback.cs | 118 +++++ .../TestSceneLegacyReplayPlayback.cs | 102 ++++ .../Visual/LegacyReplayPlaybackTestScene.cs | 157 ++++++ 4 files changed, 847 insertions(+) create mode 100644 osu.Game.Rulesets.Mania.Tests/TestSceneLegacyReplayPlayback.cs create mode 100644 osu.Game.Rulesets.Osu.Tests/TestSceneLegacyReplayPlayback.cs create mode 100644 osu.Game.Rulesets.Taiko.Tests/TestSceneLegacyReplayPlayback.cs create mode 100644 osu.Game/Tests/Visual/LegacyReplayPlaybackTestScene.cs diff --git a/osu.Game.Rulesets.Mania.Tests/TestSceneLegacyReplayPlayback.cs b/osu.Game.Rulesets.Mania.Tests/TestSceneLegacyReplayPlayback.cs new file mode 100644 index 0000000000..acd97b92a9 --- /dev/null +++ b/osu.Game.Rulesets.Mania.Tests/TestSceneLegacyReplayPlayback.cs @@ -0,0 +1,470 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using NUnit.Framework; +using osu.Game.Beatmaps; +using osu.Game.Beatmaps.ControlPoints; +using osu.Game.Replays; +using osu.Game.Rulesets.Mania.Beatmaps; +using osu.Game.Rulesets.Mania.Mods; +using osu.Game.Rulesets.Mania.Objects; +using osu.Game.Rulesets.Mania.Replays; +using osu.Game.Rulesets.Mods; +using osu.Game.Rulesets.Objects; +using osu.Game.Rulesets.Objects.Types; +using osu.Game.Rulesets.Scoring; +using osu.Game.Scoring; +using osu.Game.Tests.Visual; +using osuTK; + +namespace osu.Game.Rulesets.Mania.Tests +{ + [Ignore("These tests are expected to fail until an acceptable solution for various replay playback issues concerning rounding of replay frame times & hit windows is found.")] + public partial class TestSceneLegacyReplayPlayback : LegacyReplayPlaybackTestScene + { + protected override Ruleset CreateRuleset() => new ManiaRuleset(); + + protected override string? ExportLocation => null; + + private static readonly object[][] score_v2_test_cases = + { + // With respect to notation, + // square brackets `[]` represent *closed* or *inclusive* bounds, + // while round brackets `()` represent *open* or *exclusive* bounds. + + // Note that mania hitwindows are heavily idiosyncratic, + // and if you *think* a number here is wrong, probably double check. + + // Known issues / complexities: + // - There is a disparate set of hitwindow ranges for: score V1 non-converts, score V1 converts, and score V2 (regardless of convert) + // - It is NEVER POSSIBLE to get a MEH result when late; exceeding the OK hit windows will result in a MISS. + // Additionally, the OK hit window when late is EXCLUSIVE / OPEN rather than INCLUSIVE / CLOSED. + // Relevant stable source: https://github.com/peppy/osu-stable-reference/blob/996648fba06baf4e7d2e0b248959399444017895/osu!/GameplayElements/HitObjectManagerMania.cs#L737-L751 + // - There is also a seemingly mania-specific issue wherein key inputs registered before time instant 0 get truncated to time 0, + // which is why the beatmaps used below make sure not to cross that boundary (the note starts at t=300ms). + // This is not an issue in osu! or taiko. + // The source of this behaviour has not been investigated in detail. + + // OD = 5 test cases. + // PERFECT hit window is [ -19ms, 19ms] + // GREAT hit window is [ -49ms, 49ms] + // GOOD hit window is [ -82ms, 82ms] + // OK hit window is [-112ms, 112ms) <- not a typo, this side of the interval is OPEN! + // MEH hit window is [-136ms, -----) <- it is NOT POSSIBLE to get a MEH result on a late hit! + new object[] { 5f, -18d, HitResult.Perfect }, + new object[] { 5f, -19d, HitResult.Perfect }, + new object[] { 5f, -20d, HitResult.Great }, + new object[] { 5f, -21d, HitResult.Great }, + new object[] { 5f, -48d, HitResult.Great }, + new object[] { 5f, -49d, HitResult.Great }, + new object[] { 5f, -50d, HitResult.Good }, + new object[] { 5f, -51d, HitResult.Good }, + new object[] { 5f, -81d, HitResult.Good }, + new object[] { 5f, -82d, HitResult.Good }, + new object[] { 5f, -83d, HitResult.Ok }, + new object[] { 5f, -84d, HitResult.Ok }, + new object[] { 5f, -111d, HitResult.Ok }, + new object[] { 5f, -112d, HitResult.Ok }, + new object[] { 5f, -113d, HitResult.Meh }, + new object[] { 5f, -114d, HitResult.Meh }, + new object[] { 5f, -135d, HitResult.Meh }, + new object[] { 5f, -136d, HitResult.Meh }, + new object[] { 5f, -137d, HitResult.Miss }, + new object[] { 5f, -138d, HitResult.Miss }, + new object[] { 5f, 111d, HitResult.Ok }, + new object[] { 5f, 112d, HitResult.Miss }, + new object[] { 5f, 113d, HitResult.Miss }, + new object[] { 5f, 114d, HitResult.Miss }, + new object[] { 5f, 135d, HitResult.Miss }, + new object[] { 5f, 136d, HitResult.Miss }, + new object[] { 5f, 137d, HitResult.Miss }, + new object[] { 5f, 138d, HitResult.Miss }, + + // OD = 9.3 test cases. + // PERFECT hit window is [ -14ms, 14ms] + // GREAT hit window is [ -36ms, 36ms] + // GOOD hit window is [ -69ms, 69ms] + // OK hit window is [ -99ms, 99ms) <- not a typo, this side of the interval is OPEN! + // MEH hit window is [-123ms, -----) <- it is NOT POSSIBLE to get a MEH result on a late hit! + new object[] { 9.3f, 13d, HitResult.Perfect }, + new object[] { 9.3f, 14d, HitResult.Perfect }, + new object[] { 9.3f, 15d, HitResult.Great }, + new object[] { 9.3f, 16d, HitResult.Great }, + new object[] { 9.3f, 35d, HitResult.Great }, + new object[] { 9.3f, 36d, HitResult.Great }, + new object[] { 9.3f, 37d, HitResult.Good }, + new object[] { 9.3f, 38d, HitResult.Good }, + new object[] { 9.3f, 68d, HitResult.Good }, + new object[] { 9.3f, 69d, HitResult.Good }, + new object[] { 9.3f, 70d, HitResult.Ok }, + new object[] { 9.3f, 71d, HitResult.Ok }, + new object[] { 9.3f, 98d, HitResult.Ok }, + new object[] { 9.3f, 99d, HitResult.Miss }, + new object[] { 9.3f, 100d, HitResult.Miss }, + new object[] { 9.3f, 101d, HitResult.Miss }, + new object[] { 9.3f, 122d, HitResult.Miss }, + new object[] { 9.3f, 123d, HitResult.Miss }, + new object[] { 9.3f, 124d, HitResult.Miss }, + new object[] { 9.3f, 125d, HitResult.Miss }, + new object[] { 9.3f, -98d, HitResult.Ok }, + new object[] { 9.3f, -99d, HitResult.Ok }, + new object[] { 9.3f, -100d, HitResult.Meh }, + new object[] { 9.3f, -101d, HitResult.Meh }, + new object[] { 9.3f, -122d, HitResult.Meh }, + new object[] { 9.3f, -123d, HitResult.Meh }, + new object[] { 9.3f, -124d, HitResult.Miss }, + new object[] { 9.3f, -125d, HitResult.Miss }, + }; + + private static readonly object[][] score_v1_non_convert_test_cases = + { + // OD = 5 test cases. + // PERFECT hit window is [ -16ms, 16ms] + // GREAT hit window is [ -49ms, 49ms] + // GOOD hit window is [ -82ms, 82ms] + // OK hit window is [-112ms, 112ms) <- not a typo, this side of the interval is OPEN! + // MEH hit window is [-136ms, -----) <- it is NOT POSSIBLE to get a MEH result on a late hit! + new object[] { 5f, -15d, HitResult.Perfect }, + new object[] { 5f, -16d, HitResult.Perfect }, + new object[] { 5f, -17d, HitResult.Great }, + new object[] { 5f, -18d, HitResult.Great }, + new object[] { 5f, -48d, HitResult.Great }, + new object[] { 5f, -49d, HitResult.Great }, + new object[] { 5f, -50d, HitResult.Good }, + new object[] { 5f, -51d, HitResult.Good }, + new object[] { 5f, -81d, HitResult.Good }, + new object[] { 5f, -82d, HitResult.Good }, + new object[] { 5f, -83d, HitResult.Ok }, + new object[] { 5f, -84d, HitResult.Ok }, + new object[] { 5f, -111d, HitResult.Ok }, + new object[] { 5f, -112d, HitResult.Ok }, + new object[] { 5f, -113d, HitResult.Meh }, + new object[] { 5f, -114d, HitResult.Meh }, + new object[] { 5f, -135d, HitResult.Meh }, + new object[] { 5f, -136d, HitResult.Meh }, + new object[] { 5f, -137d, HitResult.Miss }, + new object[] { 5f, -138d, HitResult.Miss }, + new object[] { 5f, 111d, HitResult.Ok }, + new object[] { 5f, 112d, HitResult.Miss }, + new object[] { 5f, 113d, HitResult.Miss }, + new object[] { 5f, 114d, HitResult.Miss }, + new object[] { 5f, 135d, HitResult.Miss }, + new object[] { 5f, 136d, HitResult.Miss }, + new object[] { 5f, 137d, HitResult.Miss }, + new object[] { 5f, 138d, HitResult.Miss }, + + // OD = 9.3 test cases. + // PERFECT hit window is [ -16ms, 16ms] + // GREAT hit window is [ -36ms, 36ms] + // GOOD hit window is [ -69ms, 69ms] + // OK hit window is [ -99ms, 99ms) <- not a typo, this side of the interval is OPEN! + // MEH hit window is [-123ms, -----) <- it is NOT POSSIBLE to get a MEH result on a late hit! + new object[] { 9.3f, 15d, HitResult.Perfect }, + new object[] { 9.3f, 16d, HitResult.Perfect }, + new object[] { 9.3f, 17d, HitResult.Great }, + new object[] { 9.3f, 18d, HitResult.Great }, + new object[] { 9.3f, 35d, HitResult.Great }, + new object[] { 9.3f, 36d, HitResult.Great }, + new object[] { 9.3f, 37d, HitResult.Good }, + new object[] { 9.3f, 38d, HitResult.Good }, + new object[] { 9.3f, 68d, HitResult.Good }, + new object[] { 9.3f, 69d, HitResult.Good }, + new object[] { 9.3f, 70d, HitResult.Ok }, + new object[] { 9.3f, 71d, HitResult.Ok }, + new object[] { 9.3f, 98d, HitResult.Ok }, + new object[] { 9.3f, 99d, HitResult.Miss }, + new object[] { 9.3f, 100d, HitResult.Miss }, + new object[] { 9.3f, 101d, HitResult.Miss }, + new object[] { 9.3f, 122d, HitResult.Miss }, + new object[] { 9.3f, 123d, HitResult.Miss }, + new object[] { 9.3f, 124d, HitResult.Miss }, + new object[] { 9.3f, 125d, HitResult.Miss }, + new object[] { 9.3f, -98d, HitResult.Ok }, + new object[] { 9.3f, -99d, HitResult.Ok }, + new object[] { 9.3f, -100d, HitResult.Meh }, + new object[] { 9.3f, -101d, HitResult.Meh }, + new object[] { 9.3f, -122d, HitResult.Meh }, + new object[] { 9.3f, -123d, HitResult.Meh }, + new object[] { 9.3f, -124d, HitResult.Miss }, + new object[] { 9.3f, -125d, HitResult.Miss }, + + // OD = 3.1 test cases. + // PERFECT hit window is [ -16ms, 16ms] + // GREAT hit window is [ -54ms, 54ms] + // GOOD hit window is [ -87ms, 87ms] + // OK hit window is [-117ms, 117ms) <- not a typo, this side of the interval is OPEN! + // MEH hit window is [-141ms, -----) <- it is NOT POSSIBLE to get a MEH result on a late hit! + new object[] { 3.1f, 15d, HitResult.Perfect }, + new object[] { 3.1f, 16d, HitResult.Perfect }, + new object[] { 3.1f, 17d, HitResult.Great }, + new object[] { 3.1f, 18d, HitResult.Great }, + new object[] { 3.1f, 53d, HitResult.Great }, + new object[] { 3.1f, 54d, HitResult.Great }, + new object[] { 3.1f, 55d, HitResult.Good }, + new object[] { 3.1f, 56d, HitResult.Good }, + new object[] { 3.1f, 86d, HitResult.Good }, + new object[] { 3.1f, 87d, HitResult.Good }, + new object[] { 3.1f, 88d, HitResult.Ok }, + new object[] { 3.1f, 89d, HitResult.Ok }, + new object[] { 3.1f, 116d, HitResult.Ok }, + new object[] { 3.1f, 117d, HitResult.Miss }, + new object[] { 3.1f, 118d, HitResult.Miss }, + new object[] { 3.1f, 119d, HitResult.Miss }, + new object[] { 3.1f, 140d, HitResult.Miss }, + new object[] { 3.1f, 141d, HitResult.Miss }, + new object[] { 3.1f, 142d, HitResult.Miss }, + new object[] { 3.1f, 143d, HitResult.Miss }, + new object[] { 3.1f, -116d, HitResult.Ok }, + new object[] { 3.1f, -117d, HitResult.Ok }, + new object[] { 3.1f, -118d, HitResult.Meh }, + new object[] { 3.1f, -119d, HitResult.Meh }, + new object[] { 3.1f, -140d, HitResult.Meh }, + new object[] { 3.1f, -141d, HitResult.Meh }, + new object[] { 3.1f, -142d, HitResult.Miss }, + new object[] { 3.1f, -143d, HitResult.Miss }, + }; + + private static readonly object[][] score_v1_convert_test_cases = + { + // OD = 5 test cases. + // PERFECT hit window is [ -16ms, 16ms] + // GREAT hit window is [ -34ms, 34ms] + // GOOD hit window is [ -67ms, 67ms] + // OK hit window is [ -97ms, 97ms) <- not a typo, this side of the interval is OPEN! + // MEH hit window is [-121ms, -----) <- it is NOT POSSIBLE to get a MEH result on a late hit! + new object[] { 5f, -15d, HitResult.Perfect }, + new object[] { 5f, -16d, HitResult.Perfect }, + new object[] { 5f, -17d, HitResult.Great }, + new object[] { 5f, -18d, HitResult.Great }, + new object[] { 5f, -33d, HitResult.Great }, + new object[] { 5f, -34d, HitResult.Great }, + new object[] { 5f, -35d, HitResult.Good }, + new object[] { 5f, -36d, HitResult.Good }, + new object[] { 5f, -66d, HitResult.Good }, + new object[] { 5f, -67d, HitResult.Good }, + new object[] { 5f, -68d, HitResult.Ok }, + new object[] { 5f, -69d, HitResult.Ok }, + new object[] { 5f, -96d, HitResult.Ok }, + new object[] { 5f, -97d, HitResult.Ok }, + new object[] { 5f, -98d, HitResult.Meh }, + new object[] { 5f, -99d, HitResult.Meh }, + new object[] { 5f, -120d, HitResult.Meh }, + new object[] { 5f, -121d, HitResult.Meh }, + new object[] { 5f, -122d, HitResult.Miss }, + new object[] { 5f, -123d, HitResult.Miss }, + new object[] { 5f, 96d, HitResult.Ok }, + new object[] { 5f, 97d, HitResult.Miss }, + new object[] { 5f, 98d, HitResult.Miss }, + new object[] { 5f, 99d, HitResult.Miss }, + new object[] { 5f, 120d, HitResult.Miss }, + new object[] { 5f, 121d, HitResult.Miss }, + new object[] { 5f, 122d, HitResult.Miss }, + new object[] { 5f, 123d, HitResult.Miss }, + + // OD = 3.1 test cases. + // PERFECT hit window is [ -16ms, 16ms] + // GREAT hit window is [ -47ms, 47ms] + // GOOD hit window is [ -77ms, 77ms] + // OK hit window is [ -97ms, 97ms) <- not a typo, this side of the interval is OPEN! + // MEH hit window is [-121ms, -----) <- it is NOT POSSIBLE to get a MEH result on a late hit! + new object[] { 3.1f, 15d, HitResult.Perfect }, + new object[] { 3.1f, 16d, HitResult.Perfect }, + new object[] { 3.1f, 17d, HitResult.Great }, + new object[] { 3.1f, 18d, HitResult.Great }, + new object[] { 3.1f, 46d, HitResult.Great }, + new object[] { 3.1f, 47d, HitResult.Great }, + new object[] { 3.1f, 48d, HitResult.Good }, + new object[] { 3.1f, 49d, HitResult.Good }, + new object[] { 3.1f, 76d, HitResult.Good }, + new object[] { 3.1f, 77d, HitResult.Good }, + new object[] { 3.1f, 78d, HitResult.Ok }, + new object[] { 3.1f, 79d, HitResult.Ok }, + new object[] { 3.1f, 96d, HitResult.Ok }, + new object[] { 3.1f, 97d, HitResult.Miss }, + new object[] { 3.1f, 98d, HitResult.Miss }, + new object[] { 3.1f, 99d, HitResult.Miss }, + new object[] { 3.1f, 120d, HitResult.Miss }, + new object[] { 3.1f, 121d, HitResult.Miss }, + new object[] { 3.1f, 122d, HitResult.Miss }, + new object[] { 3.1f, 123d, HitResult.Miss }, + new object[] { 3.1f, -96d, HitResult.Ok }, + new object[] { 3.1f, -97d, HitResult.Ok }, + new object[] { 3.1f, -98d, HitResult.Meh }, + new object[] { 3.1f, -99d, HitResult.Meh }, + new object[] { 3.1f, -120d, HitResult.Meh }, + new object[] { 3.1f, -121d, HitResult.Meh }, + new object[] { 3.1f, -122d, HitResult.Miss }, + new object[] { 3.1f, -123d, HitResult.Miss }, + }; + + [TestCaseSource(nameof(score_v2_test_cases))] + public void TestHitWindowTreatmentWithScoreV2(float overallDifficulty, double hitOffset, HitResult expectedResult) + { + const double note_time = 300; + + var cpi = new ControlPointInfo(); + cpi.Add(0, new TimingControlPoint { BeatLength = 1000 }); + var beatmap = new ManiaBeatmap(new StageDefinition(1)) + { + HitObjects = + { + new Note + { + StartTime = note_time, + Column = 0, + } + }, + Difficulty = new BeatmapDifficulty + { + OverallDifficulty = overallDifficulty, + CircleSize = 1, + }, + BeatmapInfo = + { + Ruleset = new ManiaRuleset().RulesetInfo, + }, + ControlPointInfo = cpi, + }; + + var replay = new Replay + { + Frames = + { + new ManiaReplayFrame(0), + new ManiaReplayFrame(note_time + hitOffset, ManiaAction.Key1), + new ManiaReplayFrame(note_time + hitOffset + 20), + } + }; + + var score = new Score + { + Replay = replay, + ScoreInfo = new ScoreInfo + { + Ruleset = CreateRuleset()!.RulesetInfo, + Mods = [new ModScoreV2()] + } + }; + + RunTest($@"SV2 single note @ OD{overallDifficulty}", beatmap, $@"SV2 {hitOffset}ms @ OD{overallDifficulty} = {expectedResult}", score, [expectedResult]); + } + + [TestCaseSource(nameof(score_v1_non_convert_test_cases))] + public void TestHitWindowTreatmentWithScoreV1NonConvert(float overallDifficulty, double hitOffset, HitResult expectedResult) + { + const double note_time = 300; + + var cpi = new ControlPointInfo(); + cpi.Add(0, new TimingControlPoint { BeatLength = 1000 }); + var beatmap = new ManiaBeatmap(new StageDefinition(1)) + { + HitObjects = + { + new Note + { + StartTime = note_time, + Column = 0, + } + }, + Difficulty = new BeatmapDifficulty + { + OverallDifficulty = overallDifficulty, + CircleSize = 1, + }, + BeatmapInfo = + { + Ruleset = new ManiaRuleset().RulesetInfo, + }, + ControlPointInfo = cpi, + }; + + var replay = new Replay + { + Frames = + { + new ManiaReplayFrame(0), + new ManiaReplayFrame(note_time + hitOffset, ManiaAction.Key1), + new ManiaReplayFrame(note_time + hitOffset + 20), + } + }; + + var score = new Score + { + Replay = replay, + ScoreInfo = new ScoreInfo + { + Ruleset = CreateRuleset()!.RulesetInfo, + } + }; + + RunTest($@"SV1 single note @ OD{overallDifficulty}", beatmap, $@"SV1 {hitOffset}ms @ OD{overallDifficulty} = {expectedResult}", score, [expectedResult]); + } + + [TestCaseSource(nameof(score_v1_convert_test_cases))] + public void TestHitWindowTreatmentWithScoreV1Convert(float overallDifficulty, double hitOffset, HitResult expectedResult) + { + const double note_time = 300; + + var cpi = new ControlPointInfo(); + cpi.Add(0, new TimingControlPoint { BeatLength = 1000 }); + var beatmap = new Beatmap + { + HitObjects = + { + new FakeCircle + { + StartTime = note_time, + } + }, + Difficulty = new BeatmapDifficulty + { + OverallDifficulty = overallDifficulty, + }, + BeatmapInfo = + { + Ruleset = new RulesetInfo { OnlineID = 0 } + }, + ControlPointInfo = cpi, + }; + + var replay = new Replay + { + Frames = + { + new ManiaReplayFrame(0), + new ManiaReplayFrame(note_time + hitOffset, ManiaAction.Key1), + new ManiaReplayFrame(note_time + hitOffset + 20), + } + }; + + var score = new Score + { + Replay = replay, + ScoreInfo = new ScoreInfo + { + Ruleset = CreateRuleset()!.RulesetInfo, + Mods = [new ManiaModKey1()], + } + }; + + RunTest($@"SV1 convert single note @ OD{overallDifficulty}", beatmap, $@"SV1 convert {hitOffset}ms @ OD{overallDifficulty} = {expectedResult}", score, [expectedResult]); + } + + private class FakeCircle : HitObject, IHasPosition + { + public float X + { + get => Position.X; + set => Position = new Vector2(value, Position.Y); + } + + public float Y + { + get => Position.Y; + set => Position = new Vector2(Position.X, value); + } + + public Vector2 Position { get; set; } + } + } +} diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneLegacyReplayPlayback.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneLegacyReplayPlayback.cs new file mode 100644 index 0000000000..5a085fe17c --- /dev/null +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneLegacyReplayPlayback.cs @@ -0,0 +1,118 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using NUnit.Framework; +using osu.Game.Beatmaps; +using osu.Game.Beatmaps.ControlPoints; +using osu.Game.Replays; +using osu.Game.Rulesets.Osu.Beatmaps; +using osu.Game.Rulesets.Osu.Objects; +using osu.Game.Rulesets.Osu.Replays; +using osu.Game.Rulesets.Osu.UI; +using osu.Game.Rulesets.Scoring; +using osu.Game.Scoring; +using osu.Game.Tests.Visual; +using osuTK; + +namespace osu.Game.Rulesets.Osu.Tests +{ + [Ignore("These tests are expected to fail until an acceptable solution for various replay playback issues concerning rounding of replay frame times & hit windows is found.")] + public partial class TestSceneLegacyReplayPlayback : LegacyReplayPlaybackTestScene + { + protected override Ruleset CreateRuleset() => new OsuRuleset(); + + protected override string? ExportLocation => null; + + private static readonly object[][] test_cases = + { + // With respect to notation, + // square brackets `[]` represent *closed* or *inclusive* bounds, + // while round brackets `()` represent *open* or *exclusive* bounds. + // Additionally, note that offsets provided in double will be rounded to the nearest integer. + + // OD = 5 test cases. + // GREAT hit window is ( -50ms, 50ms) + // OK hit window is (-100ms, 100ms) + // MEH hit window is (-150ms, 150ms) + new object[] { 5f, 48d, HitResult.Great }, + new object[] { 5f, 49d, HitResult.Great }, + new object[] { 5f, 50d, HitResult.Ok }, + new object[] { 5f, 51d, HitResult.Ok }, + new object[] { 5f, 98d, HitResult.Ok }, + new object[] { 5f, 99d, HitResult.Ok }, + new object[] { 5f, 100d, HitResult.Meh }, + new object[] { 5f, 101d, HitResult.Meh }, + new object[] { 5f, 148d, HitResult.Meh }, + new object[] { 5f, 149d, HitResult.Meh }, + new object[] { 5f, 150d, HitResult.Miss }, + new object[] { 5f, 151d, HitResult.Miss }, + + // OD = 5.7 test cases. + // GREAT hit window is ( -45ms, 45ms) + // OK hit window is ( -94ms, 94ms) + // MEH hit window is (-143ms, 143ms) + new object[] { 5.7f, 43d, HitResult.Great }, + new object[] { 5.7f, 44d, HitResult.Great }, + new object[] { 5.7f, 45d, HitResult.Ok }, + new object[] { 5.7f, 46d, HitResult.Ok }, + new object[] { 5.7f, 92d, HitResult.Ok }, + new object[] { 5.7f, 93d, HitResult.Ok }, + new object[] { 5.7f, 94d, HitResult.Meh }, + new object[] { 5.7f, 95d, HitResult.Meh }, + new object[] { 5.7f, 141d, HitResult.Meh }, + new object[] { 5.7f, 142d, HitResult.Meh }, + new object[] { 5.7f, 143d, HitResult.Miss }, + new object[] { 5.7f, 144d, HitResult.Miss }, + }; + + [TestCaseSource(nameof(test_cases))] + public void TestHitWindowTreatment(float overallDifficulty, double hitOffset, HitResult expectedResult) + { + const double hit_circle_time = 100; + + var cpi = new ControlPointInfo(); + cpi.Add(0, new TimingControlPoint { BeatLength = 1000 }); + var beatmap = new OsuBeatmap + { + HitObjects = + { + new HitCircle + { + StartTime = hit_circle_time, + Position = OsuPlayfield.BASE_SIZE / 2 + } + }, + Difficulty = new BeatmapDifficulty { OverallDifficulty = overallDifficulty }, + BeatmapInfo = + { + Ruleset = new OsuRuleset().RulesetInfo, + }, + ControlPointInfo = cpi, + }; + + var replay = new Replay + { + Frames = + { + // required for correct playback in stable + new OsuReplayFrame(0, new Vector2(256, -500)), + new OsuReplayFrame(0, new Vector2(256, -500)), + new OsuReplayFrame(0, OsuPlayfield.BASE_SIZE / 2), + new OsuReplayFrame(hit_circle_time + hitOffset, OsuPlayfield.BASE_SIZE / 2, OsuAction.LeftButton), + new OsuReplayFrame(hit_circle_time + hitOffset + 20, OsuPlayfield.BASE_SIZE / 2), + } + }; + + var score = new Score + { + Replay = replay, + ScoreInfo = new ScoreInfo + { + Ruleset = CreateRuleset()!.RulesetInfo, + } + }; + + RunTest($@"single circle @ OD{overallDifficulty}", beatmap, $@"{hitOffset}ms @ OD{overallDifficulty} = {expectedResult}", score, [expectedResult]); + } + } +} diff --git a/osu.Game.Rulesets.Taiko.Tests/TestSceneLegacyReplayPlayback.cs b/osu.Game.Rulesets.Taiko.Tests/TestSceneLegacyReplayPlayback.cs new file mode 100644 index 0000000000..4703b38e57 --- /dev/null +++ b/osu.Game.Rulesets.Taiko.Tests/TestSceneLegacyReplayPlayback.cs @@ -0,0 +1,102 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using NUnit.Framework; +using osu.Game.Beatmaps; +using osu.Game.Beatmaps.ControlPoints; +using osu.Game.Replays; +using osu.Game.Rulesets.Scoring; +using osu.Game.Rulesets.Taiko.Beatmaps; +using osu.Game.Rulesets.Taiko.Objects; +using osu.Game.Rulesets.Taiko.Replays; +using osu.Game.Scoring; +using osu.Game.Tests.Visual; + +namespace osu.Game.Rulesets.Taiko.Tests +{ + [Ignore("These tests are expected to fail until an acceptable solution for various replay playback issues concerning rounding of replay frame times & hit windows is found.")] + public partial class TestSceneLegacyReplayPlayback : LegacyReplayPlaybackTestScene + { + protected override string? ExportLocation => null; + + protected override Ruleset? CreateRuleset() => new TaikoRuleset(); + + private static readonly object[][] test_cases = + { + // With respect to notation, + // square brackets `[]` represent *closed* or *inclusive* bounds, + // while round brackets `()` represent *open* or *exclusive* bounds. + + // OD = 5 test cases. + // GREAT hit window is (-35ms, 35ms) + // OK hit window is (-80ms, 80ms) + new object[] { 5f, -33d, HitResult.Great }, + new object[] { 5f, -34d, HitResult.Great }, + new object[] { 5f, -35d, HitResult.Ok }, + new object[] { 5f, -36d, HitResult.Ok }, + new object[] { 5f, -78d, HitResult.Ok }, + new object[] { 5f, -79d, HitResult.Ok }, + new object[] { 5f, -80d, HitResult.Miss }, + new object[] { 5f, -81d, HitResult.Miss }, + + // OD = 7.8 test cases. + // GREAT hit window is (-26ms, 26ms) + // OK hit window is (-63ms, 63ms) + new object[] { 7.8f, -24d, HitResult.Great }, + new object[] { 7.8f, -25d, HitResult.Great }, + new object[] { 7.8f, -26d, HitResult.Ok }, + new object[] { 7.8f, -27d, HitResult.Ok }, + new object[] { 7.8f, -61d, HitResult.Ok }, + new object[] { 7.8f, -62d, HitResult.Ok }, + new object[] { 7.8f, -63d, HitResult.Miss }, + new object[] { 7.8f, -64d, HitResult.Miss }, + }; + + [TestCaseSource(nameof(test_cases))] + public void TestHitWindowTreatment(float overallDifficulty, double hitOffset, HitResult expectedResult) + { + const double hit_time = 100; + + var cpi = new ControlPointInfo(); + cpi.Add(0, new TimingControlPoint { BeatLength = 1000 }); + var beatmap = new TaikoBeatmap + { + HitObjects = + { + new Hit + { + StartTime = hit_time, + Type = HitType.Centre, + } + }, + Difficulty = new BeatmapDifficulty { OverallDifficulty = overallDifficulty }, + BeatmapInfo = + { + Ruleset = new TaikoRuleset().RulesetInfo, + }, + ControlPointInfo = cpi, + }; + + var replay = new Replay + { + Frames = + { + new TaikoReplayFrame(0), + new TaikoReplayFrame(hit_time + hitOffset, TaikoAction.LeftCentre), + new TaikoReplayFrame(hit_time + hitOffset + 20), + } + }; + + var score = new Score + { + Replay = replay, + ScoreInfo = new ScoreInfo + { + Ruleset = CreateRuleset()!.RulesetInfo, + } + }; + + RunTest($@"single hit @ OD{overallDifficulty}", beatmap, $@"{hitOffset}ms @ OD{overallDifficulty} = {expectedResult}", score, [expectedResult]); + } + } +} diff --git a/osu.Game/Tests/Visual/LegacyReplayPlaybackTestScene.cs b/osu.Game/Tests/Visual/LegacyReplayPlaybackTestScene.cs new file mode 100644 index 0000000000..5f973d1e4e --- /dev/null +++ b/osu.Game/Tests/Visual/LegacyReplayPlaybackTestScene.cs @@ -0,0 +1,157 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using NUnit.Framework; +using osu.Framework.Extensions; +using osu.Framework.Screens; +using osu.Framework.Testing; +using osu.Game.Beatmaps; +using osu.Game.Beatmaps.Formats; +using osu.Game.IO.Legacy; +using osu.Game.Rulesets; +using osu.Game.Rulesets.Judgements; +using osu.Game.Rulesets.Mods; +using osu.Game.Rulesets.Scoring; +using osu.Game.Scoring; +using osu.Game.Scoring.Legacy; +using osu.Game.Screens.Play; + +namespace osu.Game.Tests.Visual +{ + /// + /// The goal of this abstract test class is to exercise correct playback of replays sourced from previous osu! versions. + /// Use to exercise that property. + /// + [HeadlessTest] + [TestFixture] + public abstract partial class LegacyReplayPlaybackTestScene : RateAdjustedBeatmapTestScene + { + private ReplayPlayer currentPlayer = null!; + private readonly List results = new List(); + + /// + /// This is provided as a convenience for testing behaviour against osu!stable. + /// Setting this field to a non-null path will cause beatmap files and replays used in all test cases + /// to be exported to disk so that they can be cross-checked against stable. + /// + protected abstract string? ExportLocation { get; } + + /// + /// Encodes the supplied , decodes the result of encoding, runs the result of decoding against the supplied , + /// and checks that the judgement results recorded still match . + /// If is set, exports both the beatmap and the replay to said location. + /// + protected void RunTest(string beatmapName, IBeatmap beatmap, string replayName, Score originalScore, IEnumerable expectedResults) + { + IBeatmap playableBeatmap = null!; + MemoryStream beatmapStream = new MemoryStream(); + MemoryStream scoreStream = new MemoryStream(); + Score decodedScore = null!; + + AddStep(@"set up beatmap", () => + { + beatmap.Metadata.Title = beatmapName; + Beatmap.Value = CreateWorkingBeatmap(beatmap); + Ruleset.Value = CreateRuleset()!.RulesetInfo; + playableBeatmap = Beatmap.Value.GetPlayableBeatmap(Ruleset.Value); + + var beatmapEncoder = new LegacyBeatmapEncoder(beatmap, null); + + using (var writer = new StreamWriter(beatmapStream, Encoding.UTF8, leaveOpen: true)) + beatmapEncoder.Encode(writer); + + beatmapStream.Seek(0, SeekOrigin.Begin); + playableBeatmap.BeatmapInfo.MD5Hash = beatmapStream.ComputeMD5Hash(); + }); + + AddStep(@"encode score", () => + { + originalScore.ScoreInfo.BeatmapInfo = playableBeatmap.BeatmapInfo; + var encoder = new LegacyScoreEncoder(originalScore, playableBeatmap); + encoder.Encode(scoreStream, leaveOpen: true); + + // `LegacyScoreEncoder` hardcodes a replay version that belongs to lazer. + // here we want to simulate a stable replay, which should have the classic mod attached etc. + // to that end, we do a post-encode step to specify a stable-like replay version. + scoreStream.Position = 1; + + using (var sw = new SerializationWriter(scoreStream, leaveOpen: true)) + { + const int version = 20250414; + sw.Write(version); + } + + scoreStream.Position = 0; + }); + + if (ExportLocation != null) + { + AddStep("export beatmap", () => + { + using var stream = File.Open(Path.Combine(ExportLocation, $"{beatmapName}.osu"), FileMode.Create); + beatmapStream.CopyTo(stream); + beatmapStream.Position = 0; + }); + + AddStep("export score", () => + { + using var stream = File.Open(Path.Combine(ExportLocation, $@"{replayName}.osr"), FileMode.Create); + scoreStream.CopyTo(stream); + scoreStream.Position = 0; + }); + } + + AddStep(@"decode score", () => + { + using (scoreStream) + { + scoreStream.Position = 0; + decodedScore = new TestScoreDecoder(Beatmap.Value, Ruleset.Value).Parse(scoreStream); + } + }); + + AddAssert(@"classic mod present", () => decodedScore.ScoreInfo.Mods.Any(mod => mod is ModClassic)); + AddStep(@"push player", () => pushNewPlayer(decodedScore)); + + AddUntilStep(@"Wait until player is loaded", () => currentPlayer.IsCurrentScreen()); + AddAssert(@"classic mod present", () => currentPlayer.GameplayState.Mods.Any(mod => mod is ModClassic)); + AddUntilStep(@"Wait for completion", () => currentPlayer.GameplayState.HasCompleted); + AddAssert(@"judgement results after encode are correct", () => results.Select(r => r.Type), () => Is.EquivalentTo(expectedResults)); + } + + private void pushNewPlayer(Score score) + { + var player = new ReplayPlayer(score); + SelectedMods.Value = score.ScoreInfo.Mods; + player.OnLoadComplete += _ => + { + player.GameplayState.ScoreProcessor.NewJudgement += result => + { + if (currentPlayer == player) + results.Add(result); + }; + }; + LoadScreen(currentPlayer = player); + results.Clear(); + } + + private class TestScoreDecoder : LegacyScoreDecoder + { + private readonly WorkingBeatmap beatmap; + private readonly Ruleset ruleset; + + public TestScoreDecoder(WorkingBeatmap beatmap, RulesetInfo ruleset) + { + this.beatmap = beatmap; + this.ruleset = ruleset.CreateInstance(); + } + + protected override Ruleset GetRuleset(int rulesetId) => ruleset; + protected override WorkingBeatmap GetBeatmap(string md5Hash) => beatmap; + } + } +} From 8273583fd07e42319d8febeeffbc61ff6faaed3b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 16 Apr 2025 08:35:35 +0200 Subject: [PATCH 071/164] Fix code quality once more --- .../TestSceneLegacyReplayPlayback.cs | 6 +++--- .../TestSceneLegacyReplayPlayback.cs | 2 +- .../TestSceneLegacyReplayPlayback.cs | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/osu.Game.Rulesets.Mania.Tests/TestSceneLegacyReplayPlayback.cs b/osu.Game.Rulesets.Mania.Tests/TestSceneLegacyReplayPlayback.cs index acd97b92a9..ea66386c9a 100644 --- a/osu.Game.Rulesets.Mania.Tests/TestSceneLegacyReplayPlayback.cs +++ b/osu.Game.Rulesets.Mania.Tests/TestSceneLegacyReplayPlayback.cs @@ -341,7 +341,7 @@ namespace osu.Game.Rulesets.Mania.Tests Replay = replay, ScoreInfo = new ScoreInfo { - Ruleset = CreateRuleset()!.RulesetInfo, + Ruleset = CreateRuleset().RulesetInfo, Mods = [new ModScoreV2()] } }; @@ -393,7 +393,7 @@ namespace osu.Game.Rulesets.Mania.Tests Replay = replay, ScoreInfo = new ScoreInfo { - Ruleset = CreateRuleset()!.RulesetInfo, + Ruleset = CreateRuleset().RulesetInfo, } }; @@ -442,7 +442,7 @@ namespace osu.Game.Rulesets.Mania.Tests Replay = replay, ScoreInfo = new ScoreInfo { - Ruleset = CreateRuleset()!.RulesetInfo, + Ruleset = CreateRuleset().RulesetInfo, Mods = [new ManiaModKey1()], } }; diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneLegacyReplayPlayback.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneLegacyReplayPlayback.cs index 5a085fe17c..c22255bbdf 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneLegacyReplayPlayback.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneLegacyReplayPlayback.cs @@ -108,7 +108,7 @@ namespace osu.Game.Rulesets.Osu.Tests Replay = replay, ScoreInfo = new ScoreInfo { - Ruleset = CreateRuleset()!.RulesetInfo, + Ruleset = CreateRuleset().RulesetInfo, } }; diff --git a/osu.Game.Rulesets.Taiko.Tests/TestSceneLegacyReplayPlayback.cs b/osu.Game.Rulesets.Taiko.Tests/TestSceneLegacyReplayPlayback.cs index 4703b38e57..459312f2b4 100644 --- a/osu.Game.Rulesets.Taiko.Tests/TestSceneLegacyReplayPlayback.cs +++ b/osu.Game.Rulesets.Taiko.Tests/TestSceneLegacyReplayPlayback.cs @@ -19,7 +19,7 @@ namespace osu.Game.Rulesets.Taiko.Tests { protected override string? ExportLocation => null; - protected override Ruleset? CreateRuleset() => new TaikoRuleset(); + protected override Ruleset CreateRuleset() => new TaikoRuleset(); private static readonly object[][] test_cases = { @@ -92,7 +92,7 @@ namespace osu.Game.Rulesets.Taiko.Tests Replay = replay, ScoreInfo = new ScoreInfo { - Ruleset = CreateRuleset()!.RulesetInfo, + Ruleset = CreateRuleset().RulesetInfo, } }; From e8af0dabea0aab5e171f66308080b3f6ae0ff9d6 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 16 Apr 2025 15:53:20 +0900 Subject: [PATCH 072/164] Fix thread safety when calling `BeatmapStore.GetBeatmapSets` While usually we'd handle this locally by moving bind operations to `LoadComponent`, this component was explicitly made to be used in asynchronous scenarios (to allow cases like song select to coexist with realm without adding huge compliexities to the classes locally). So I think it makes sense to hide this as an implementation detail. The locked segments should all be quite fast to run so I do not see a performance issue with lock contention here. --- .../Database/RealmDetachedBeatmapStore.cs | 40 ++++++++++++------- 1 file changed, 25 insertions(+), 15 deletions(-) diff --git a/osu.Game/Database/RealmDetachedBeatmapStore.cs b/osu.Game/Database/RealmDetachedBeatmapStore.cs index b05e07ef31..6954bb320a 100644 --- a/osu.Game/Database/RealmDetachedBeatmapStore.cs +++ b/osu.Game/Database/RealmDetachedBeatmapStore.cs @@ -30,7 +30,8 @@ namespace osu.Game.Database public override IBindableList GetBeatmapSets(CancellationToken? cancellationToken) { loaded.Wait(cancellationToken ?? CancellationToken.None); - return detachedBeatmapSets.GetBoundCopy(); + lock (detachedBeatmapSets) + return detachedBeatmapSets.GetBoundCopy(); } [BackgroundDependencyLoader] @@ -65,8 +66,11 @@ namespace osu.Game.Database { var detached = frozenSets.Detach(); - detachedBeatmapSets.Clear(); - detachedBeatmapSets.AddRange(detached); + lock (detachedBeatmapSets) + { + detachedBeatmapSets.Clear(); + detachedBeatmapSets.AddRange(detached); + } }); } finally @@ -116,22 +120,28 @@ namespace osu.Game.Database if (!loaded.IsSet) return; - // If this ever leads to performance issues, we could dequeue a limited number of operations per update frame. - while (pendingOperations.TryDequeue(out var op)) + if (pendingOperations.Count == 0) + return; + + lock (detachedBeatmapSets) { - switch (op.Type) + // If this ever leads to performance issues, we could dequeue a limited number of operations per update frame. + while (pendingOperations.TryDequeue(out var op)) { - case OperationType.Insert: - detachedBeatmapSets.Insert(op.Index, op.BeatmapSet!); - break; + switch (op.Type) + { + case OperationType.Insert: + detachedBeatmapSets.Insert(op.Index, op.BeatmapSet!); + break; - case OperationType.Update: - detachedBeatmapSets.ReplaceRange(op.Index, 1, new[] { op.BeatmapSet! }); - break; + case OperationType.Update: + detachedBeatmapSets.ReplaceRange(op.Index, 1, new[] { op.BeatmapSet! }); + break; - case OperationType.Remove: - detachedBeatmapSets.RemoveAt(op.Index); - break; + case OperationType.Remove: + detachedBeatmapSets.RemoveAt(op.Index); + break; + } } } } From 64b9d4642adb42db7b619f76ecf5c028d9d1c3e3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 16 Apr 2025 10:27:05 +0200 Subject: [PATCH 073/164] Add failing test --- .../SongSelect/TestSceneBeatmapLeaderboard.cs | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapLeaderboard.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapLeaderboard.cs index ebeba23123..45381b3e02 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapLeaderboard.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapLeaderboard.cs @@ -115,6 +115,27 @@ namespace osu.Game.Tests.Visual.SongSelect checkDisplayedCount(0); } + [Test] + public void TestLocalScoresDisplayWorksWhenStartingOffline() + { + BeatmapInfo beatmapInfo = null!; + + AddStep("Log out", () => API.Logout()); + AddStep(@"Set scope", () => leaderboard.Scope = BeatmapLeaderboardScope.Local); + + AddStep(@"Set beatmap", () => + { + beatmapManager.Import(TestResources.GetQuickTestBeatmapForImport()).WaitSafely(); + beatmapInfo = beatmapManager.GetAllUsableBeatmapSets().First().Beatmaps.First(); + + leaderboard.BeatmapInfo = beatmapInfo; + }); + + clearScores(); + importMoreScores(() => beatmapInfo); + checkDisplayedCount(10); + } + [Test] public void TestLocalScoresDisplayOnBeatmapEdit() { From cf2f6d7233a1a12f44024656db5c482068e8dc15 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 16 Apr 2025 10:27:33 +0200 Subject: [PATCH 074/164] Fix local leaderboards not showing when starting game offline Broke in https://github.com/ppy/osu/pull/32494. Oops. --- osu.Game/Screens/Select/Leaderboards/BeatmapLeaderboard.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/Select/Leaderboards/BeatmapLeaderboard.cs b/osu.Game/Screens/Select/Leaderboards/BeatmapLeaderboard.cs index c52cd61c42..2896e7eab4 100644 --- a/osu.Game/Screens/Select/Leaderboards/BeatmapLeaderboard.cs +++ b/osu.Game/Screens/Select/Leaderboards/BeatmapLeaderboard.cs @@ -103,7 +103,7 @@ namespace osu.Game.Screens.Select.Leaderboards var fetchRuleset = ruleset.Value ?? fetchBeatmapInfo.Ruleset; - if (!api.IsLoggedIn) + if (!api.IsLoggedIn && IsOnlineScope) { SetErrorState(LeaderboardState.NotLoggedIn); return null; From 7a8e96f3223618315669120de68163e21bbf384b Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 16 Apr 2025 16:54:01 +0900 Subject: [PATCH 075/164] Change global shear definition to be a `Vector2` Saves having this defined in 20+ places. If we ever make any changes to shear, it's 100% going to need to be applied to every usage (there will never be a case of multiple different shears in the game). Also fixes a mismatching definition in `ShearedNub`. --- .../SongSelectV2/TestSceneLeaderboardScore.cs | 4 ++-- .../Graphics/UserInterface/DialogButton.cs | 4 ++-- .../Graphics/UserInterface/ShearedButton.cs | 9 +++------ osu.Game/Graphics/UserInterface/ShearedNub.cs | 4 +--- .../UserInterface/ShearedSearchTextBox.cs | 4 ++-- .../UserInterface/ShearedSliderBar.cs | 6 +++--- .../UserInterfaceV2/ShearedDropdown.cs | 20 ++++++------------- osu.Game/OsuGame.cs | 2 +- .../Overlays/Mods/BeatmapAttributesDisplay.cs | 15 ++++++-------- osu.Game/Overlays/Mods/ModColumn.cs | 2 +- .../Mods/ModFooterInformationDisplay.cs | 2 +- osu.Game/Overlays/Mods/ModPanel.cs | 2 +- osu.Game/Overlays/Mods/ModSelectColumn.cs | 6 +++--- osu.Game/Overlays/Mods/ModSelectOverlay.cs | 4 ++-- osu.Game/Overlays/Mods/ModSelectPanel.cs | 9 ++++----- .../Mods/RankingInformationDisplay.cs | 7 +++---- osu.Game/Screens/Footer/ScreenFooterButton.cs | 10 +++------- .../DailyChallenge/DailyChallengeIntro.cs | 18 ++++++++--------- .../Select/Options/BeatmapOptionsButton.cs | 2 +- .../SelectV2/Footer/ScreenFooterButtonMods.cs | 12 +++++------ .../Leaderboards/LeaderboardScoreV2.cs | 20 +++++++++---------- .../SelectV2/UpdateBeatmapSetButton.cs | 4 ++-- 22 files changed, 72 insertions(+), 94 deletions(-) diff --git a/osu.Game.Tests/Visual/SongSelectV2/TestSceneLeaderboardScore.cs b/osu.Game.Tests/Visual/SongSelectV2/TestSceneLeaderboardScore.cs index 26d39c9203..08c0c92285 100644 --- a/osu.Game.Tests/Visual/SongSelectV2/TestSceneLeaderboardScore.cs +++ b/osu.Game.Tests/Visual/SongSelectV2/TestSceneLeaderboardScore.cs @@ -53,7 +53,7 @@ namespace osu.Game.Tests.Visual.SongSelectV2 RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y, Spacing = new Vector2(0f, 2f), - Shear = new Vector2(OsuGame.SHEAR, 0) + Shear = OsuGame.SHEAR, }, drawWidthText = new OsuSpriteText(), }; @@ -121,7 +121,7 @@ namespace osu.Game.Tests.Visual.SongSelectV2 RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y, Spacing = new Vector2(0f, 2f), - Shear = new Vector2(OsuGame.SHEAR, 0) + Shear = OsuGame.SHEAR, }, drawWidthText = new OsuSpriteText(), }; diff --git a/osu.Game/Graphics/UserInterface/DialogButton.cs b/osu.Game/Graphics/UserInterface/DialogButton.cs index c39f41bf72..423d9637b8 100644 --- a/osu.Game/Graphics/UserInterface/DialogButton.cs +++ b/osu.Game/Graphics/UserInterface/DialogButton.cs @@ -129,7 +129,7 @@ namespace osu.Game.Graphics.UserInterface Radius = 5, }, Colour = ButtonColour, - Shear = new Vector2(0.2f, 0), + Shear = OsuGame.SHEAR, Children = new Drawable[] { new Box @@ -149,7 +149,7 @@ namespace osu.Game.Graphics.UserInterface RelativeSizeAxes = Axes.Both, TriangleScale = 4, ColourDark = OsuColour.Gray(0.88f), - Shear = new Vector2(-0.2f, 0), + Shear = -OsuGame.SHEAR, ClampAxes = Axes.Y }, }, diff --git a/osu.Game/Graphics/UserInterface/ShearedButton.cs b/osu.Game/Graphics/UserInterface/ShearedButton.cs index 87d269ccd4..a059490aa8 100644 --- a/osu.Game/Graphics/UserInterface/ShearedButton.cs +++ b/osu.Game/Graphics/UserInterface/ShearedButton.cs @@ -11,7 +11,6 @@ using osu.Framework.Localisation; using osu.Game.Graphics.Containers; using osu.Game.Graphics.Sprites; using osu.Game.Overlays; -using osuTK; namespace osu.Game.Graphics.UserInterface { @@ -66,8 +65,6 @@ namespace osu.Game.Graphics.UserInterface private readonly Box background; private readonly OsuSpriteText text; - private const float shear = OsuGame.SHEAR; - private Colour4? darkerColour; private Colour4? lighterColour; private Colour4? textColour; @@ -91,10 +88,10 @@ namespace osu.Game.Graphics.UserInterface public ShearedButton(float? width = null, float height = DEFAULT_HEIGHT) { Height = height; - Padding = new MarginPadding { Horizontal = shear * height }; + Padding = new MarginPadding { Horizontal = OsuGame.SHEAR.X * height }; Content.CornerRadius = CORNER_RADIUS; - Content.Shear = new Vector2(shear, 0); + Content.Shear = OsuGame.SHEAR; Content.Masking = true; Content.Anchor = Content.Origin = Anchor.Centre; @@ -117,7 +114,7 @@ namespace osu.Game.Graphics.UserInterface Anchor = Anchor.Centre, Origin = Anchor.Centre, AutoSizeAxes = Axes.Both, - Shear = new Vector2(-shear, 0), + Shear = -OsuGame.SHEAR, Child = text = new OsuSpriteText { Font = OsuFont.TorusAlternate.With(size: 17), diff --git a/osu.Game/Graphics/UserInterface/ShearedNub.cs b/osu.Game/Graphics/UserInterface/ShearedNub.cs index 7485f68525..17b50b5d58 100644 --- a/osu.Game/Graphics/UserInterface/ShearedNub.cs +++ b/osu.Game/Graphics/UserInterface/ShearedNub.cs @@ -26,8 +26,6 @@ namespace osu.Game.Graphics.UserInterface public const int HEIGHT = 30; public const float EXPANDED_SIZE = 50; - public static readonly Vector2 SHEAR = new Vector2(0.15f, 0); - private readonly Box fill; private readonly Container main; @@ -40,7 +38,7 @@ namespace osu.Game.Graphics.UserInterface Size = new Vector2(EXPANDED_SIZE, HEIGHT); InternalChild = main = new Container { - Shear = SHEAR, + Shear = OsuGame.SHEAR, BorderColour = Colour4.White, BorderThickness = BORDER_WIDTH, Masking = true, diff --git a/osu.Game/Graphics/UserInterface/ShearedSearchTextBox.cs b/osu.Game/Graphics/UserInterface/ShearedSearchTextBox.cs index c6565726b5..f5fbb3411f 100644 --- a/osu.Game/Graphics/UserInterface/ShearedSearchTextBox.cs +++ b/osu.Game/Graphics/UserInterface/ShearedSearchTextBox.cs @@ -52,7 +52,7 @@ namespace osu.Game.Graphics.UserInterface public ShearedSearchTextBox() { Height = 42; - Shear = new Vector2(OsuGame.SHEAR, 0); + Shear = OsuGame.SHEAR; Masking = true; CornerRadius = corner_radius; @@ -115,7 +115,7 @@ namespace osu.Game.Graphics.UserInterface PlaceholderText = CommonStrings.InputSearch; CornerRadius = corner_radius; - TextContainer.Shear = new Vector2(-OsuGame.SHEAR, 0); + TextContainer.Shear = -OsuGame.SHEAR; } protected override SpriteText CreatePlaceholder() => new SearchPlaceholder(); diff --git a/osu.Game/Graphics/UserInterface/ShearedSliderBar.cs b/osu.Game/Graphics/UserInterface/ShearedSliderBar.cs index a36b9c7a4c..e7b57f5c9e 100644 --- a/osu.Game/Graphics/UserInterface/ShearedSliderBar.cs +++ b/osu.Game/Graphics/UserInterface/ShearedSliderBar.cs @@ -58,7 +58,7 @@ namespace osu.Game.Graphics.UserInterface public ShearedSliderBar() { - Shear = SHEAR; + Shear = OsuGame.SHEAR; Height = HEIGHT; RangePadding = EXPANDED_SIZE / 2; Children = new Drawable[] @@ -98,11 +98,11 @@ namespace osu.Game.Graphics.UserInterface }, nubContainer = new Container { - Shear = -SHEAR, + Shear = -OsuGame.SHEAR, RelativeSizeAxes = Axes.Both, Child = Nub = new ShearedNub { - X = -SHEAR.X * HEIGHT / 2f, + X = -OsuGame.SHEAR.X * HEIGHT / 2f, Origin = Anchor.TopCentre, RelativePositionAxes = Axes.X, Current = { Value = true }, diff --git a/osu.Game/Graphics/UserInterfaceV2/ShearedDropdown.cs b/osu.Game/Graphics/UserInterfaceV2/ShearedDropdown.cs index 0b9c5f294c..609f77dd7e 100644 --- a/osu.Game/Graphics/UserInterfaceV2/ShearedDropdown.cs +++ b/osu.Game/Graphics/UserInterfaceV2/ShearedDropdown.cs @@ -62,8 +62,6 @@ namespace osu.Game.Graphics.UserInterfaceV2 protected partial class ShearedDropdownMenu : OsuDropdown.OsuDropdownMenu { - private readonly Vector2 shear = new Vector2(OsuGame.SHEAR, 0); - public new MarginPadding Padding { get => base.Padding; @@ -72,7 +70,7 @@ namespace osu.Game.Graphics.UserInterfaceV2 public ShearedDropdownMenu() { - Shear = shear; + Shear = OsuGame.SHEAR; Margin = new MarginPadding { Top = 5f }; } @@ -84,12 +82,10 @@ namespace osu.Game.Graphics.UserInterfaceV2 public partial class ShearedMenuItem : DrawableOsuDropdownMenuItem { - private readonly Vector2 shear = new Vector2(OsuGame.SHEAR, 0); - public ShearedMenuItem(MenuItem item) : base(item) { - Foreground.Shear = -shear; + Foreground.Shear = -OsuGame.SHEAR; } } } @@ -125,14 +121,12 @@ namespace osu.Game.Graphics.UserInterfaceV2 public ShearedDropdown Dropdown = null!; private ShearedDropdownSearchBar searchBar = null!; - private readonly Vector2 shear = new Vector2(OsuGame.SHEAR, 0); - [Resolved] private OverlayColourProvider colourProvider { get; set; } = null!; public ShearedDropdownHeader() { - Shear = shear; + Shear = OsuGame.SHEAR; CornerRadius = corner_radius; Masking = true; @@ -167,7 +161,7 @@ namespace osu.Game.Graphics.UserInterfaceV2 { Margin = new MarginPadding { Horizontal = 10f, Vertical = 8f }, Font = OsuFont.Torus.With(size: 16.8f, weight: FontWeight.SemiBold), - Shear = -shear, + Shear = -OsuGame.SHEAR, }, }, }, @@ -178,7 +172,7 @@ namespace osu.Game.Graphics.UserInterfaceV2 RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y, Padding = new MarginPadding { Horizontal = 10f }, - Shear = -shear, + Shear = -OsuGame.SHEAR, Children = new Drawable[] { valueText = new TruncatingSpriteText @@ -286,12 +280,10 @@ namespace osu.Game.Graphics.UserInterfaceV2 private partial class DropdownSearchTextBox : OsuTextBox { - private readonly Vector2 shear = new Vector2(OsuGame.SHEAR, 0); - [BackgroundDependencyLoader] private void load(OverlayColourProvider? colourProvider) { - TextContainer.Shear = -shear; + TextContainer.Shear = -OsuGame.SHEAR; BackgroundUnfocused = colourProvider?.Background5 ?? new Color4(10, 10, 10, 255); BackgroundFocused = colourProvider?.Background5 ?? new Color4(10, 10, 10, 255); } diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs index 0c75a4106a..70a324cd8e 100644 --- a/osu.Game/OsuGame.cs +++ b/osu.Game/OsuGame.cs @@ -102,7 +102,7 @@ namespace osu.Game /// /// A common shear factor applied to most components of the game. /// - public const float SHEAR = 0.2f; + public static readonly Vector2 SHEAR = new Vector2(0.2f, 0); public Toolbar Toolbar { get; private set; } diff --git a/osu.Game/Overlays/Mods/BeatmapAttributesDisplay.cs b/osu.Game/Overlays/Mods/BeatmapAttributesDisplay.cs index dedd1e336e..3cefa07cfa 100644 --- a/osu.Game/Overlays/Mods/BeatmapAttributesDisplay.cs +++ b/osu.Game/Overlays/Mods/BeatmapAttributesDisplay.cs @@ -20,7 +20,6 @@ using osu.Game.Graphics.UserInterface; using osu.Game.Rulesets; using osu.Game.Rulesets.Mods; using osu.Game.Utils; -using osuTK; namespace osu.Game.Overlays.Mods { @@ -66,21 +65,19 @@ namespace osu.Game.Overlays.Mods [BackgroundDependencyLoader] private void load() { - const float shear = OsuGame.SHEAR; - LeftContent.AddRange(new Drawable[] { starRatingDisplay = new StarRatingDisplay(default, animated: true) { Origin = Anchor.CentreLeft, Anchor = Anchor.CentreLeft, - Shear = new Vector2(-shear, 0), + Shear = -OsuGame.SHEAR, }, bpmDisplay = new BPMDisplay { Origin = Anchor.CentreLeft, Anchor = Anchor.CentreLeft, - Shear = new Vector2(-shear, 0), + Shear = -OsuGame.SHEAR, AutoSizeAxes = Axes.Y, Width = 75, } @@ -89,10 +86,10 @@ namespace osu.Game.Overlays.Mods RightContent.Alpha = 0; RightContent.AddRange(new Drawable[] { - circleSizeDisplay = new VerticalAttributeDisplay("CS") { Shear = new Vector2(-shear, 0), }, - drainRateDisplay = new VerticalAttributeDisplay("HP") { Shear = new Vector2(-shear, 0), }, - overallDifficultyDisplay = new VerticalAttributeDisplay("OD") { Shear = new Vector2(-shear, 0), }, - approachRateDisplay = new VerticalAttributeDisplay("AR") { Shear = new Vector2(-shear, 0), }, + circleSizeDisplay = new VerticalAttributeDisplay("CS") { Shear = -OsuGame.SHEAR, }, + drainRateDisplay = new VerticalAttributeDisplay("HP") { Shear = -OsuGame.SHEAR, }, + overallDifficultyDisplay = new VerticalAttributeDisplay("OD") { Shear = -OsuGame.SHEAR, }, + approachRateDisplay = new VerticalAttributeDisplay("AR") { Shear = -OsuGame.SHEAR, }, }); } diff --git a/osu.Game/Overlays/Mods/ModColumn.cs b/osu.Game/Overlays/Mods/ModColumn.cs index 326394a207..7d2ce54074 100644 --- a/osu.Game/Overlays/Mods/ModColumn.cs +++ b/osu.Game/Overlays/Mods/ModColumn.cs @@ -106,7 +106,7 @@ namespace osu.Game.Overlays.Mods Origin = Anchor.CentreLeft, Scale = new Vector2(0.8f), RelativeSizeAxes = Axes.X, - Shear = new Vector2(-OsuGame.SHEAR, 0) + Shear = -OsuGame.SHEAR }); ItemsFlow.Padding = new MarginPadding { diff --git a/osu.Game/Overlays/Mods/ModFooterInformationDisplay.cs b/osu.Game/Overlays/Mods/ModFooterInformationDisplay.cs index 6665a3b8dc..db42200775 100644 --- a/osu.Game/Overlays/Mods/ModFooterInformationDisplay.cs +++ b/osu.Game/Overlays/Mods/ModFooterInformationDisplay.cs @@ -37,7 +37,7 @@ namespace osu.Game.Overlays.Mods Anchor = Anchor.BottomRight, AutoSizeAxes = Axes.X, Height = ShearedButton.DEFAULT_HEIGHT, - Shear = new Vector2(OsuGame.SHEAR, 0), + Shear = OsuGame.SHEAR, CornerRadius = ShearedButton.CORNER_RADIUS, BorderThickness = ShearedButton.BORDER_THICKNESS, Masking = true, diff --git a/osu.Game/Overlays/Mods/ModPanel.cs b/osu.Game/Overlays/Mods/ModPanel.cs index b85904f22b..df72692f48 100644 --- a/osu.Game/Overlays/Mods/ModPanel.cs +++ b/osu.Game/Overlays/Mods/ModPanel.cs @@ -37,7 +37,7 @@ namespace osu.Game.Overlays.Mods Anchor = Anchor.Centre, Origin = Anchor.Centre, Active = { BindTarget = Active }, - Shear = new Vector2(-OsuGame.SHEAR, 0), + Shear = -OsuGame.SHEAR, Scale = new Vector2(HEIGHT / ModSwitchSmall.DEFAULT_SIZE) }; } diff --git a/osu.Game/Overlays/Mods/ModSelectColumn.cs b/osu.Game/Overlays/Mods/ModSelectColumn.cs index 8a499a391c..92c75e3494 100644 --- a/osu.Game/Overlays/Mods/ModSelectColumn.cs +++ b/osu.Game/Overlays/Mods/ModSelectColumn.cs @@ -70,7 +70,7 @@ namespace osu.Game.Overlays.Mods { Width = WIDTH; RelativeSizeAxes = Axes.Y; - Shear = new Vector2(OsuGame.SHEAR, 0); + Shear = OsuGame.SHEAR; InternalChildren = new Drawable[] { @@ -96,7 +96,7 @@ namespace osu.Game.Overlays.Mods { RelativeSizeAxes = Axes.X, Height = header_height, - Shear = new Vector2(-OsuGame.SHEAR, 0), + Shear = -OsuGame.SHEAR, Velocity = 0.7f, ClampAxes = Axes.Y }, @@ -111,7 +111,7 @@ namespace osu.Game.Overlays.Mods AutoSizeAxes = Axes.Y, Anchor = Anchor.CentreLeft, Origin = Anchor.CentreLeft, - Shear = new Vector2(-OsuGame.SHEAR, 0), + Shear = -OsuGame.SHEAR, Padding = new MarginPadding { Horizontal = 17, diff --git a/osu.Game/Overlays/Mods/ModSelectOverlay.cs b/osu.Game/Overlays/Mods/ModSelectOverlay.cs index d36092ebed..9ba3b3774f 100644 --- a/osu.Game/Overlays/Mods/ModSelectOverlay.cs +++ b/osu.Game/Overlays/Mods/ModSelectOverlay.cs @@ -168,7 +168,7 @@ namespace osu.Game.Overlays.Mods Anchor = Anchor.BottomLeft, Origin = Anchor.BottomLeft, Direction = FillDirection.Horizontal, - Shear = new Vector2(OsuGame.SHEAR, 0), + Shear = OsuGame.SHEAR, RelativeSizeAxes = Axes.Y, AutoSizeAxes = Axes.X, Margin = new MarginPadding { Horizontal = 70 }, @@ -726,7 +726,7 @@ namespace osu.Game.Overlays.Mods // DrawWidth/DrawPosition do not include shear effects, and we want to know the full extents of the columns post-shear, // so we have to manually compensate. var topLeft = column.ToSpaceOfOtherDrawable(Vector2.Zero, ScrollContent); - var bottomRight = column.ToSpaceOfOtherDrawable(new Vector2(column.DrawWidth - column.DrawHeight * OsuGame.SHEAR, 0), ScrollContent); + var bottomRight = column.ToSpaceOfOtherDrawable(new Vector2(column.DrawWidth - column.DrawHeight * OsuGame.SHEAR.X, 0), ScrollContent); bool isCurrentlyVisible = Precision.AlmostBigger(topLeft.X, leftVisibleBound) && Precision.DefinitelyBigger(rightVisibleBound, bottomRight.X); diff --git a/osu.Game/Overlays/Mods/ModSelectPanel.cs b/osu.Game/Overlays/Mods/ModSelectPanel.cs index 284356f37e..6d48576742 100644 --- a/osu.Game/Overlays/Mods/ModSelectPanel.cs +++ b/osu.Game/Overlays/Mods/ModSelectPanel.cs @@ -20,7 +20,6 @@ using osu.Game.Graphics; using osu.Game.Graphics.Containers; using osu.Game.Graphics.Sprites; using osu.Game.Graphics.UserInterface; -using osuTK; using osuTK.Graphics; using osuTK.Input; @@ -87,7 +86,7 @@ namespace osu.Game.Overlays.Mods Content.CornerRadius = CORNER_RADIUS; Content.BorderThickness = 2; - Shear = new Vector2(OsuGame.SHEAR, 0); + Shear = OsuGame.SHEAR; Children = new Drawable[] { @@ -128,10 +127,10 @@ namespace osu.Game.Overlays.Mods { Font = OsuFont.TorusAlternate.With(size: 18, weight: FontWeight.SemiBold), RelativeSizeAxes = Axes.X, - Shear = new Vector2(-OsuGame.SHEAR, 0), + Shear = -OsuGame.SHEAR, Margin = new MarginPadding { - Left = -18 * OsuGame.SHEAR + Left = -18 * OsuGame.SHEAR.X }, ShowTooltip = false, // Tooltip is handled by `IncompatibilityDisplayingModPanel`. }, @@ -139,7 +138,7 @@ namespace osu.Game.Overlays.Mods { Font = OsuFont.Default.With(size: 12), RelativeSizeAxes = Axes.X, - Shear = new Vector2(-OsuGame.SHEAR, 0), + Shear = -OsuGame.SHEAR, ShowTooltip = false, // Tooltip is handled by `IncompatibilityDisplayingModPanel`. } } diff --git a/osu.Game/Overlays/Mods/RankingInformationDisplay.cs b/osu.Game/Overlays/Mods/RankingInformationDisplay.cs index 75a8f289d8..11c963f616 100644 --- a/osu.Game/Overlays/Mods/RankingInformationDisplay.cs +++ b/osu.Game/Overlays/Mods/RankingInformationDisplay.cs @@ -15,7 +15,6 @@ using osu.Game.Graphics.UserInterface; using osu.Game.Localisation; using osu.Game.Rulesets.Mods; using osu.Game.Utils; -using osuTK; namespace osu.Game.Overlays.Mods { @@ -52,7 +51,7 @@ namespace osu.Game.Overlays.Mods Anchor = Anchor.BottomRight, Origin = Anchor.BottomRight, RelativeSizeAxes = Axes.Both, - Shear = new Vector2(OsuGame.SHEAR, 0), + Shear = OsuGame.SHEAR, CornerRadius = ShearedButton.CORNER_RADIUS, Masking = true, Children = new Drawable[] @@ -79,7 +78,7 @@ namespace osu.Game.Overlays.Mods { Anchor = Anchor.Centre, Origin = Anchor.Centre, - Shear = new Vector2(-OsuGame.SHEAR, 0), + Shear = -OsuGame.SHEAR, Font = OsuFont.Default.With(size: 17, weight: FontWeight.SemiBold) } } @@ -94,7 +93,7 @@ namespace osu.Game.Overlays.Mods Origin = Anchor.Centre, Child = counter = new EffectCounter { - Shear = new Vector2(-OsuGame.SHEAR, 0), + Shear = -OsuGame.SHEAR, Anchor = Anchor.Centre, Origin = Anchor.Centre, Current = { BindTarget = ModMultiplier } diff --git a/osu.Game/Screens/Footer/ScreenFooterButton.cs b/osu.Game/Screens/Footer/ScreenFooterButton.cs index 6515203ca0..5e96eadfea 100644 --- a/osu.Game/Screens/Footer/ScreenFooterButton.cs +++ b/osu.Game/Screens/Footer/ScreenFooterButton.cs @@ -25,16 +25,12 @@ namespace osu.Game.Screens.Footer { public partial class ScreenFooterButton : OsuClickableContainer, IKeyBindingHandler { - private const float shear = OsuGame.SHEAR; - protected const int CORNER_RADIUS = 10; protected const int BUTTON_HEIGHT = 75; protected const int BUTTON_WIDTH = 116; public Bindable OverlayState = new Bindable(); - protected static readonly Vector2 BUTTON_SHEAR = new Vector2(shear, 0); - [Resolved] private OverlayColourProvider colourProvider { get; set; } = null!; @@ -89,7 +85,7 @@ namespace osu.Game.Screens.Footer Colour = Colour4.Black.Opacity(0.25f), Offset = new Vector2(0, 2), }, - Shear = BUTTON_SHEAR, + Shear = OsuGame.SHEAR, Masking = true, CornerRadius = CORNER_RADIUS, RelativeSizeAxes = Axes.Both, @@ -108,7 +104,7 @@ namespace osu.Game.Screens.Footer { Anchor = Anchor.CentreLeft, Origin = Anchor.CentreLeft, - Shear = -BUTTON_SHEAR, + Shear = -OsuGame.SHEAR, RelativeSizeAxes = Axes.Both, Children = new Drawable[] { @@ -135,7 +131,7 @@ namespace osu.Game.Screens.Footer }, new Container { - Shear = -BUTTON_SHEAR, + Shear = -OsuGame.SHEAR, Anchor = Anchor.BottomCentre, Origin = Anchor.Centre, Y = -CORNER_RADIUS, diff --git a/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallengeIntro.cs b/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallengeIntro.cs index 5b423fbc6d..3ec9217aa4 100644 --- a/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallengeIntro.cs +++ b/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallengeIntro.cs @@ -116,7 +116,7 @@ namespace osu.Game.Screens.OnlinePlay.DailyChallenge RelativeSizeAxes = Axes.Both, Anchor = Anchor.Centre, Origin = Anchor.Centre, - Shear = new Vector2(OsuGame.SHEAR, 0f), + Shear = OsuGame.SHEAR, Children = new Drawable[] { titleContainer = new Container @@ -147,7 +147,7 @@ namespace osu.Game.Screens.OnlinePlay.DailyChallenge Origin = Anchor.Centre, Text = "Today's Challenge", Margin = new MarginPadding { Horizontal = 10f, Vertical = 5f }, - Shear = new Vector2(-OsuGame.SHEAR, 0f), + Shear = -OsuGame.SHEAR, Font = OsuFont.GetFont(size: 32, weight: FontWeight.Light, typeface: Typeface.TorusAlternate), }, } @@ -173,7 +173,7 @@ namespace osu.Game.Screens.OnlinePlay.DailyChallenge Origin = Anchor.Centre, Text = room.Name.Split(':', StringSplitOptions.TrimEntries).Last(), Margin = new MarginPadding { Horizontal = 10f, Vertical = 5f }, - Shear = new Vector2(-OsuGame.SHEAR, 0f), + Shear = -OsuGame.SHEAR, Font = OsuFont.GetFont(size: 32, weight: FontWeight.Light, typeface: Typeface.TorusAlternate), }, } @@ -246,7 +246,7 @@ namespace osu.Game.Screens.OnlinePlay.DailyChallenge { Anchor = Anchor.TopCentre, Origin = Anchor.TopCentre, - Shear = new Vector2(-OsuGame.SHEAR, 0f), + Shear = -OsuGame.SHEAR, MaxWidth = horizontal_info_size, Text = beatmap.BeatmapSet!.Metadata.GetDisplayTitleRomanisable(false), Padding = new MarginPadding { Horizontal = 5f }, @@ -257,7 +257,7 @@ namespace osu.Game.Screens.OnlinePlay.DailyChallenge Text = $"Difficulty: {beatmap.DifficultyName}", Font = OsuFont.GetFont(size: 20, italics: true), MaxWidth = horizontal_info_size, - Shear = new Vector2(-OsuGame.SHEAR, 0f), + Shear = -OsuGame.SHEAR, Anchor = Anchor.TopCentre, Origin = Anchor.TopCentre, }, @@ -266,13 +266,13 @@ namespace osu.Game.Screens.OnlinePlay.DailyChallenge Text = $"by {beatmap.Metadata.Author.Username}", Font = OsuFont.GetFont(size: 16, italics: true), MaxWidth = horizontal_info_size, - Shear = new Vector2(-OsuGame.SHEAR, 0f), + Shear = -OsuGame.SHEAR, Anchor = Anchor.TopCentre, Origin = Anchor.TopCentre, }, starRatingDisplay = new StarRatingDisplay(default) { - Shear = new Vector2(-OsuGame.SHEAR, 0f), + Shear = -OsuGame.SHEAR, Margin = new MarginPadding(5), Anchor = Anchor.TopCentre, Origin = Anchor.TopCentre, @@ -301,7 +301,7 @@ namespace osu.Game.Screens.OnlinePlay.DailyChallenge Anchor = Anchor.TopCentre, Origin = Anchor.TopCentre, AutoSizeAxes = Axes.Both, - Shear = new Vector2(-OsuGame.SHEAR, 0f), + Shear = -OsuGame.SHEAR, Current = { Value = item.RequiredMods.Select(m => m.ToMod(ruleset)).ToArray() @@ -329,7 +329,7 @@ namespace osu.Game.Screens.OnlinePlay.DailyChallenge Origin = Anchor.Centre, FillMode = FillMode.Fit, Scale = new Vector2(1.2f), - Shear = new Vector2(-OsuGame.SHEAR, 0f), + Shear = -OsuGame.SHEAR, }, c => { beatmapBackground.Add(c); diff --git a/osu.Game/Screens/Select/Options/BeatmapOptionsButton.cs b/osu.Game/Screens/Select/Options/BeatmapOptionsButton.cs index 045a518525..572b2427b1 100644 --- a/osu.Game/Screens/Select/Options/BeatmapOptionsButton.cs +++ b/osu.Game/Screens/Select/Options/BeatmapOptionsButton.cs @@ -89,7 +89,7 @@ namespace osu.Game.Screens.Select.Options Anchor = Anchor.Centre, Origin = Anchor.Centre, RelativeSizeAxes = Axes.Both, - Shear = new Vector2(0.2f, 0f), + Shear = OsuGame.SHEAR, Masking = true, EdgeEffect = new EdgeEffectParameters { diff --git a/osu.Game/Screens/SelectV2/Footer/ScreenFooterButtonMods.cs b/osu.Game/Screens/SelectV2/Footer/ScreenFooterButtonMods.cs index 869aef1470..61d69ae197 100644 --- a/osu.Game/Screens/SelectV2/Footer/ScreenFooterButtonMods.cs +++ b/osu.Game/Screens/SelectV2/Footer/ScreenFooterButtonMods.cs @@ -78,7 +78,7 @@ namespace osu.Game.Screens.SelectV2.Footer Y = -5f, Depth = float.MaxValue, Origin = Anchor.BottomLeft, - Shear = BUTTON_SHEAR, + Shear = OsuGame.SHEAR, CornerRadius = CORNER_RADIUS, Size = new Vector2(BUTTON_WIDTH, bar_height), Masking = true, @@ -108,7 +108,7 @@ namespace osu.Game.Screens.SelectV2.Footer { Anchor = Anchor.Centre, Origin = Anchor.Centre, - Shear = -BUTTON_SHEAR, + Shear = -OsuGame.SHEAR, UseFullGlyphHeight = false, Font = OsuFont.Torus.With(size: 14f, weight: FontWeight.Bold) } @@ -130,7 +130,7 @@ namespace osu.Game.Screens.SelectV2.Footer { Anchor = Anchor.Centre, Origin = Anchor.Centre, - Shear = -BUTTON_SHEAR, + Shear = -OsuGame.SHEAR, Scale = new Vector2(0.5f), Current = { BindTarget = Current }, ExpansionMode = ExpansionMode.AlwaysContracted, @@ -139,7 +139,7 @@ namespace osu.Game.Screens.SelectV2.Footer { Anchor = Anchor.Centre, Origin = Anchor.Centre, - Shear = -BUTTON_SHEAR, + Shear = -OsuGame.SHEAR, Font = OsuFont.Torus.With(size: 14f, weight: FontWeight.Bold), Mods = { BindTarget = Current }, } @@ -305,7 +305,7 @@ namespace osu.Game.Screens.SelectV2.Footer Y = -5f; Depth = float.MaxValue; Origin = Anchor.BottomLeft; - Shear = BUTTON_SHEAR; + Shear = OsuGame.SHEAR; CornerRadius = CORNER_RADIUS; AutoSizeAxes = Axes.X; Height = bar_height; @@ -329,7 +329,7 @@ namespace osu.Game.Screens.SelectV2.Footer { Anchor = Anchor.Centre, Origin = Anchor.Centre, - Shear = -BUTTON_SHEAR, + Shear = -OsuGame.SHEAR, Text = ModSelectOverlayStrings.Unranked.ToUpper(), Margin = new MarginPadding { Horizontal = 15 }, UseFullGlyphHeight = false, diff --git a/osu.Game/Screens/SelectV2/Leaderboards/LeaderboardScoreV2.cs b/osu.Game/Screens/SelectV2/Leaderboards/LeaderboardScoreV2.cs index 16599a2080..0b7b2ebbc1 100644 --- a/osu.Game/Screens/SelectV2/Leaderboards/LeaderboardScoreV2.cs +++ b/osu.Game/Screens/SelectV2/Leaderboards/LeaderboardScoreV2.cs @@ -122,7 +122,7 @@ namespace osu.Game.Screens.SelectV2.Leaderboards this.score = score; this.sheared = sheared; - Shear = new Vector2(sheared ? OsuGame.SHEAR : 0, 0); + Shear = sheared ? OsuGame.SHEAR : Vector2.Zero; RelativeSizeAxes = Axes.X; Height = height; } @@ -255,7 +255,7 @@ namespace osu.Game.Screens.SelectV2.Leaderboards { RelativeSizeAxes = Axes.Both, User = score.User, - Shear = new Vector2(sheared ? -OsuGame.SHEAR : 0, 0), + Shear = sheared ? -OsuGame.SHEAR : Vector2.Zero, Anchor = Anchor.BottomLeft, Origin = Anchor.BottomLeft, Colour = ColourInfo.GradientHorizontal(Colour4.White.Opacity(0.5f), Colour4.FromHex(@"222A27").Opacity(1)), @@ -286,7 +286,7 @@ namespace osu.Game.Screens.SelectV2.Leaderboards Anchor = Anchor.Centre, Origin = Anchor.Centre, Scale = new Vector2(1.1f), - Shear = new Vector2(sheared ? -OsuGame.SHEAR : 0, 0), + Shear = sheared ? -OsuGame.SHEAR : Vector2.Zero, RelativeSizeAxes = Axes.Both, }) { @@ -326,7 +326,7 @@ namespace osu.Game.Screens.SelectV2.Leaderboards { flagBadgeAndDateContainer = new FillFlowContainer { - Shear = new Vector2(sheared ? -OsuGame.SHEAR : 0, 0), + Shear = sheared ? -OsuGame.SHEAR : Vector2.Zero, Direction = FillDirection.Horizontal, Spacing = new Vector2(5), AutoSizeAxes = Axes.Both, @@ -356,7 +356,7 @@ namespace osu.Game.Screens.SelectV2.Leaderboards nameLabel = new TruncatingSpriteText { RelativeSizeAxes = Axes.X, - Shear = new Vector2(sheared ? -OsuGame.SHEAR : 0, 0), + Shear = sheared ? -OsuGame.SHEAR : Vector2.Zero, Text = user.Username, Font = OsuFont.GetFont(size: 20, weight: FontWeight.SemiBold) } @@ -372,7 +372,7 @@ namespace osu.Game.Screens.SelectV2.Leaderboards Name = @"Statistics container", Padding = new MarginPadding { Right = 40 }, Spacing = new Vector2(25, 0), - Shear = new Vector2(sheared ? -OsuGame.SHEAR : 0, 0), + Shear = sheared ? -OsuGame.SHEAR : Vector2.Zero, Anchor = Anchor.CentreRight, Origin = Anchor.CentreRight, AutoSizeAxes = Axes.Both, @@ -430,7 +430,7 @@ namespace osu.Game.Screens.SelectV2.Leaderboards }, RankContainer = new Container { - Shear = new Vector2(sheared ? -OsuGame.SHEAR : 0, 0), + Shear = sheared ? -OsuGame.SHEAR : Vector2.Zero, Anchor = Anchor.CentreRight, Origin = Anchor.CentreRight, RelativeSizeAxes = Axes.Y, @@ -488,7 +488,7 @@ namespace osu.Game.Screens.SelectV2.Leaderboards Anchor = Anchor.TopRight, Origin = Anchor.TopRight, UseFullGlyphHeight = false, - Shear = new Vector2(sheared ? -OsuGame.SHEAR : 0, 0), + Shear = sheared ? -OsuGame.SHEAR : Vector2.Zero, Current = scoreManager.GetBindableTotalScoreString(score), Font = OsuFont.GetFont(size: 30, weight: FontWeight.Light), }, @@ -496,7 +496,7 @@ namespace osu.Game.Screens.SelectV2.Leaderboards { Anchor = Anchor.TopRight, Origin = Anchor.TopRight, - Shear = new Vector2(sheared ? -OsuGame.SHEAR : 0, 0), + Shear = sheared ? -OsuGame.SHEAR : Vector2.Zero, AutoSizeAxes = Axes.Both, Direction = FillDirection.Horizontal, Spacing = new Vector2(2f, 0f), @@ -704,7 +704,7 @@ namespace osu.Game.Screens.SelectV2.Leaderboards Child = new OsuSpriteText { - Shear = new Vector2(sheared ? -OsuGame.SHEAR : 0, 0), + Shear = sheared ? -OsuGame.SHEAR : Vector2.Zero, Anchor = Anchor.Centre, Origin = Anchor.Centre, Font = OsuFont.GetFont(size: 20, weight: FontWeight.SemiBold, italics: true), diff --git a/osu.Game/Screens/SelectV2/UpdateBeatmapSetButton.cs b/osu.Game/Screens/SelectV2/UpdateBeatmapSetButton.cs index e2c841f88a..ac7e3856ac 100644 --- a/osu.Game/Screens/SelectV2/UpdateBeatmapSetButton.cs +++ b/osu.Game/Screens/SelectV2/UpdateBeatmapSetButton.cs @@ -69,7 +69,7 @@ namespace osu.Game.Screens.SelectV2 Content.Anchor = Anchor.Centre; Content.Origin = Anchor.Centre; - Content.Shear = new Vector2(OsuGame.SHEAR, 0); + Content.Shear = OsuGame.SHEAR; Content.AddRange(new Drawable[] { @@ -87,7 +87,7 @@ namespace osu.Game.Screens.SelectV2 AutoSizeAxes = Axes.Both, Direction = FillDirection.Horizontal, Spacing = new Vector2(4), - Shear = new Vector2(-OsuGame.SHEAR, 0), + Shear = -OsuGame.SHEAR, Children = new Drawable[] { new Container From 0aff50fbf5eb4284823f3dfcdc92b34601a9ba11 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 16 Apr 2025 18:23:14 +0900 Subject: [PATCH 076/164] Rename song select v2 classes and namespaces This aims to bring some conformity to naming to make it easier to understand component structure for new components. Renames are pulled out of the song select v2 changes and are more relevant there due to many new classes being added. - `V2` suffix is dropped, with v2 components being moved to a relevant V2 namespace. - Related classes have a prefix of the area they are used. - Experimenting with using partial/nested classes in the song select v2 implementation. Not committing to this yet but want to see how it plays out. - Moved base carousel components to a generic namespace to avoid confusion with actual beatmap carousel implementation. --- .../DailyChallenge/TestSceneDailyChallenge.cs | 4 +- .../BeatmapCarouselTestScene.cs} | 9 +- .../TestSceneBeatmapCarousel.cs} | 4 +- ...TestSceneBeatmapCarouselArtistGrouping.cs} | 4 +- ...SceneBeatmapCarouselDifficultyGrouping.cs} | 5 +- .../TestSceneBeatmapCarouselNoGrouping.cs} | 5 +- .../TestSceneBeatmapCarouselScrolling.cs} | 4 +- .../TestSceneFooterButtonMods.cs} | 14 +- .../SongSelectV2/TestSceneLeaderboardScore.cs | 10 +- ...cultyPanel.cs => TestScenePanelBeatmap.cs} | 5 +- ....cs => TestScenePanelBeatmapStandalone.cs} | 5 +- ...V2GroupPanel.cs => TestScenePanelGroup.cs} | 5 +- ...uselV2SetPanel.cs => TestScenePanelSet.cs} | 5 +- ...s => TestScenePanelUpdateBeatmapButton.cs} | 6 +- .../TestSceneScreenFooter.cs | 10 +- .../SongSelectV2/TestSceneSongSelect.cs | 6 +- .../Carousel}/Carousel.cs | 2 +- .../Carousel}/CarouselItem.cs | 2 +- .../Carousel}/ICarouselFilter.cs | 2 +- .../Carousel}/ICarouselPanel.cs | 2 +- .../DailyChallengeLeaderboard.cs | 10 +- osu.Game/Screens/SelectV2/BeatmapCarousel.cs | 1 + .../SelectV2/BeatmapCarouselFilterGrouping.cs | 1 + .../SelectV2/BeatmapCarouselFilterSorting.cs | 1 + ...dScoreV2.cs => BeatmapLeaderboardScore.cs} | 6 +- .../SelectV2/Footer/BeatmapOptionsPopover.cs | 195 ----------------- ...ooterButtonMods.cs => FooterButtonMods.cs} | 6 +- ...uttonOptions.cs => FooterButtonOptions.cs} | 7 +- .../SelectV2/FooterButtonOptions_Popover.cs | 198 ++++++++++++++++++ ...rButtonRandom.cs => FooterButtonRandom.cs} | 4 +- .../SelectV2/{PanelBase.cs => Panel.cs} | 3 +- osu.Game/Screens/SelectV2/PanelBeatmap.cs | 11 +- osu.Game/Screens/SelectV2/PanelBeatmapSet.cs | 11 +- .../SelectV2/PanelBeatmapStandalone.cs | 15 +- osu.Game/Screens/SelectV2/PanelGroup.cs | 3 +- .../SelectV2/PanelGroupStarDifficulty.cs | 2 +- ...pLocalRank.cs => PanelLocalRankDisplay.cs} | 4 +- ...nelBackground.cs => PanelSetBackground.cs} | 2 +- ...tButton.cs => PanelUpdateBeatmapButton.cs} | 4 +- osu.Game/Screens/SelectV2/SongSelect.cs | 7 +- 40 files changed, 308 insertions(+), 292 deletions(-) rename osu.Game.Tests/Visual/{SongSelect/BeatmapCarouselV2TestScene.cs => SongSelectV2/BeatmapCarouselTestScene.cs} (97%) rename osu.Game.Tests/Visual/{SongSelect/TestSceneBeatmapCarouselV2.cs => SongSelectV2/TestSceneBeatmapCarousel.cs} (95%) rename osu.Game.Tests/Visual/{SongSelect/TestSceneBeatmapCarouselV2ArtistGrouping.cs => SongSelectV2/TestSceneBeatmapCarouselArtistGrouping.cs} (98%) rename osu.Game.Tests/Visual/{SongSelect/TestSceneBeatmapCarouselV2DifficultyGrouping.cs => SongSelectV2/TestSceneBeatmapCarouselDifficultyGrouping.cs} (97%) rename osu.Game.Tests/Visual/{SongSelect/TestSceneBeatmapCarouselV2NoGrouping.cs => SongSelectV2/TestSceneBeatmapCarouselNoGrouping.cs} (97%) rename osu.Game.Tests/Visual/{SongSelect/TestSceneBeatmapCarouselV2Scrolling.cs => SongSelectV2/TestSceneBeatmapCarouselScrolling.cs} (94%) rename osu.Game.Tests/Visual/{UserInterface/TestSceneScreenFooterButtonMods.cs => SongSelectV2/TestSceneFooterButtonMods.cs} (92%) rename osu.Game.Tests/Visual/SongSelectV2/{TestSceneBeatmapCarouselV2DifficultyPanel.cs => TestScenePanelBeatmap.cs} (95%) rename osu.Game.Tests/Visual/SongSelectV2/{TestSceneBeatmapCarouselV2StandalonePanel.cs => TestScenePanelBeatmapStandalone.cs} (95%) rename osu.Game.Tests/Visual/SongSelectV2/{TestSceneBeatmapCarouselV2GroupPanel.cs => TestScenePanelGroup.cs} (96%) rename osu.Game.Tests/Visual/SongSelectV2/{TestSceneBeatmapCarouselV2SetPanel.cs => TestScenePanelSet.cs} (95%) rename osu.Game.Tests/Visual/SongSelectV2/{TestSceneUpdateBeatmapSetButtonV2.cs => TestScenePanelUpdateBeatmapButton.cs} (90%) rename osu.Game.Tests/Visual/{UserInterface => SongSelectV2}/TestSceneScreenFooter.cs (98%) rename osu.Game/{Screens/SelectV2 => Graphics/Carousel}/Carousel.cs (99%) rename osu.Game/{Screens/SelectV2 => Graphics/Carousel}/CarouselItem.cs (98%) rename osu.Game/{Screens/SelectV2 => Graphics/Carousel}/ICarouselFilter.cs (95%) rename osu.Game/{Screens/SelectV2 => Graphics/Carousel}/ICarouselPanel.cs (97%) rename osu.Game/Screens/SelectV2/{Leaderboards/LeaderboardScoreV2.cs => BeatmapLeaderboardScore.cs} (99%) delete mode 100644 osu.Game/Screens/SelectV2/Footer/BeatmapOptionsPopover.cs rename osu.Game/Screens/SelectV2/{Footer/ScreenFooterButtonMods.cs => FooterButtonMods.cs} (98%) rename osu.Game/Screens/SelectV2/{Footer/ScreenFooterButtonOptions.cs => FooterButtonOptions.cs} (76%) create mode 100644 osu.Game/Screens/SelectV2/FooterButtonOptions_Popover.cs rename osu.Game/Screens/SelectV2/{Footer/ScreenFooterButtonRandom.cs => FooterButtonRandom.cs} (97%) rename osu.Game/Screens/SelectV2/{PanelBase.cs => Panel.cs} (98%) rename osu.Game/Screens/SelectV2/{TopLocalRank.cs => PanelLocalRankDisplay.cs} (96%) rename osu.Game/Screens/SelectV2/{BeatmapSetPanelBackground.cs => PanelSetBackground.cs} (97%) rename osu.Game/Screens/SelectV2/{UpdateBeatmapSetButton.cs => PanelUpdateBeatmapButton.cs} (98%) diff --git a/osu.Game.Tests/Visual/DailyChallenge/TestSceneDailyChallenge.cs b/osu.Game.Tests/Visual/DailyChallenge/TestSceneDailyChallenge.cs index 185ebc1d39..f1422b4654 100644 --- a/osu.Game.Tests/Visual/DailyChallenge/TestSceneDailyChallenge.cs +++ b/osu.Game.Tests/Visual/DailyChallenge/TestSceneDailyChallenge.cs @@ -15,7 +15,7 @@ using osu.Game.Online.Rooms; using osu.Game.Overlays; using osu.Game.Overlays.Notifications; using osu.Game.Rulesets.Osu.Mods; -using osu.Game.Screens.SelectV2.Leaderboards; +using osu.Game.Screens.SelectV2; using osu.Game.Tests.Resources; using osu.Game.Tests.Visual.Metadata; using osu.Game.Tests.Visual.OnlinePlay; @@ -85,7 +85,7 @@ namespace osu.Game.Tests.Visual.DailyChallenge AddStep("force transforms to finish", () => FinishTransforms(true)); AddStep("right click second score", () => { - InputManager.MoveMouseTo(this.ChildrenOfType().ElementAt(1)); + InputManager.MoveMouseTo(this.ChildrenOfType().ElementAt(1)); InputManager.Click(MouseButton.Right); }); AddAssert("use these mods not present", diff --git a/osu.Game.Tests/Visual/SongSelect/BeatmapCarouselV2TestScene.cs b/osu.Game.Tests/Visual/SongSelectV2/BeatmapCarouselTestScene.cs similarity index 97% rename from osu.Game.Tests/Visual/SongSelect/BeatmapCarouselV2TestScene.cs rename to osu.Game.Tests/Visual/SongSelectV2/BeatmapCarouselTestScene.cs index f2faeab1c4..28a0948696 100644 --- a/osu.Game.Tests/Visual/SongSelect/BeatmapCarouselV2TestScene.cs +++ b/osu.Game.Tests/Visual/SongSelectV2/BeatmapCarouselTestScene.cs @@ -16,6 +16,7 @@ using osu.Framework.Utils; using osu.Game.Beatmaps; using osu.Game.Database; using osu.Game.Graphics; +using osu.Game.Graphics.Carousel; using osu.Game.Graphics.Containers; using osu.Game.Overlays; using osu.Game.Screens.Select; @@ -27,9 +28,9 @@ using osuTK.Graphics; using osuTK.Input; using BeatmapCarousel = osu.Game.Screens.SelectV2.BeatmapCarousel; -namespace osu.Game.Tests.Visual.SongSelect +namespace osu.Game.Tests.Visual.SongSelectV2 { - public abstract partial class BeatmapCarouselV2TestScene : OsuManualInputManagerTestScene + public abstract partial class BeatmapCarouselTestScene : OsuManualInputManagerTestScene { protected readonly BindableList BeatmapSets = new BindableList(); @@ -47,7 +48,7 @@ namespace osu.Game.Tests.Visual.SongSelect private int beatmapCount; - protected BeatmapCarouselV2TestScene() + protected BeatmapCarouselTestScene() { store = new TestBeatmapStore { @@ -191,7 +192,7 @@ namespace osu.Game.Tests.Visual.SongSelect .Where(p => ((ICarouselPanel)p).Item?.IsVisible == true) .OrderBy(p => p.Y) .ElementAt(index) - .ChildrenOfType().Single() + .ChildrenOfType().Single() .TriggerClick(); }); } diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2.cs b/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarousel.cs similarity index 95% rename from osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2.cs rename to osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarousel.cs index 30ca26ce68..5fd921645b 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2.cs +++ b/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarousel.cs @@ -10,13 +10,13 @@ using osu.Game.Screens.Select; using osu.Game.Screens.Select.Filter; using osu.Game.Tests.Resources; -namespace osu.Game.Tests.Visual.SongSelect +namespace osu.Game.Tests.Visual.SongSelectV2 { /// /// Covers common steps which can be used for manual testing. /// [TestFixture] - public partial class TestSceneBeatmapCarouselV2 : BeatmapCarouselV2TestScene + public partial class TestSceneBeatmapCarousel : BeatmapCarouselTestScene { [Test] [Explicit] diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2ArtistGrouping.cs b/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselArtistGrouping.cs similarity index 98% rename from osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2ArtistGrouping.cs rename to osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselArtistGrouping.cs index c378871eac..f0caa796b6 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2ArtistGrouping.cs +++ b/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselArtistGrouping.cs @@ -9,10 +9,10 @@ using osu.Game.Screens.Select; using osu.Game.Screens.Select.Filter; using osu.Game.Screens.SelectV2; -namespace osu.Game.Tests.Visual.SongSelect +namespace osu.Game.Tests.Visual.SongSelectV2 { [TestFixture] - public partial class TestSceneBeatmapCarouselV2ArtistGrouping : BeatmapCarouselV2TestScene + public partial class TestSceneBeatmapCarouselArtistGrouping : BeatmapCarouselTestScene { [SetUpSteps] public void SetUpSteps() diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2DifficultyGrouping.cs b/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselDifficultyGrouping.cs similarity index 97% rename from osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2DifficultyGrouping.cs rename to osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselDifficultyGrouping.cs index 239c693ee1..a4cdf8abcb 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2DifficultyGrouping.cs +++ b/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselDifficultyGrouping.cs @@ -5,15 +5,16 @@ using System.Linq; using NUnit.Framework; using osu.Framework.Testing; using osu.Game.Beatmaps; +using osu.Game.Graphics.Carousel; using osu.Game.Screens.Select; using osu.Game.Screens.Select.Filter; using osu.Game.Screens.SelectV2; using osuTK; -namespace osu.Game.Tests.Visual.SongSelect +namespace osu.Game.Tests.Visual.SongSelectV2 { [TestFixture] - public partial class TestSceneBeatmapCarouselV2DifficultyGrouping : BeatmapCarouselV2TestScene + public partial class TestSceneBeatmapCarouselDifficultyGrouping : BeatmapCarouselTestScene { [SetUpSteps] public void SetUpSteps() diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2NoGrouping.cs b/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselNoGrouping.cs similarity index 97% rename from osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2NoGrouping.cs rename to osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselNoGrouping.cs index b4048a5355..ac02d7a3a9 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2NoGrouping.cs +++ b/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselNoGrouping.cs @@ -5,16 +5,17 @@ using System.Linq; using NUnit.Framework; using osu.Framework.Testing; using osu.Game.Beatmaps; +using osu.Game.Graphics.Carousel; using osu.Game.Screens.Select; using osu.Game.Screens.Select.Filter; using osu.Game.Screens.SelectV2; using osuTK; using osuTK.Input; -namespace osu.Game.Tests.Visual.SongSelect +namespace osu.Game.Tests.Visual.SongSelectV2 { [TestFixture] - public partial class TestSceneBeatmapCarouselV2NoGrouping : BeatmapCarouselV2TestScene + public partial class TestSceneBeatmapCarouselNoGrouping : BeatmapCarouselTestScene { [SetUpSteps] public void SetUpSteps() diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2Scrolling.cs b/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselScrolling.cs similarity index 94% rename from osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2Scrolling.cs rename to osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselScrolling.cs index 890e1dd6e3..da3fc98c19 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2Scrolling.cs +++ b/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselScrolling.cs @@ -8,10 +8,10 @@ using osu.Framework.Testing; using osu.Game.Screens.Select; using osu.Game.Screens.SelectV2; -namespace osu.Game.Tests.Visual.SongSelect +namespace osu.Game.Tests.Visual.SongSelectV2 { [TestFixture] - public partial class TestSceneBeatmapCarouselV2Scrolling : BeatmapCarouselV2TestScene + public partial class TestSceneBeatmapCarouselScrolling : BeatmapCarouselTestScene { [SetUpSteps] public void SetUpSteps() diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneScreenFooterButtonMods.cs b/osu.Game.Tests/Visual/SongSelectV2/TestSceneFooterButtonMods.cs similarity index 92% rename from osu.Game.Tests/Visual/UserInterface/TestSceneScreenFooterButtonMods.cs rename to osu.Game.Tests/Visual/SongSelectV2/TestSceneFooterButtonMods.cs index e86f83ee15..5c2c6eaf1d 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneScreenFooterButtonMods.cs +++ b/osu.Game.Tests/Visual/SongSelectV2/TestSceneFooterButtonMods.cs @@ -13,19 +13,19 @@ using osu.Game.Overlays; using osu.Game.Overlays.Mods; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Osu.Mods; -using osu.Game.Screens.SelectV2.Footer; +using osu.Game.Screens.SelectV2; using osu.Game.Utils; -namespace osu.Game.Tests.Visual.UserInterface +namespace osu.Game.Tests.Visual.SongSelectV2 { - public partial class TestSceneScreenFooterButtonMods : OsuTestScene + public partial class TestSceneFooterButtonMods : OsuTestScene { private readonly TestScreenFooterButtonMods footerButtonMods; [Cached] private OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Aquamarine); - public TestSceneScreenFooterButtonMods() + public TestSceneFooterButtonMods() { Add(footerButtonMods = new TestScreenFooterButtonMods(new TestModSelectOverlay()) { @@ -98,9 +98,9 @@ namespace osu.Game.Tests.Visual.UserInterface public void TestUnrankedBadge() { AddStep(@"Add unranked mod", () => changeMods(new[] { new OsuModDeflate() })); - AddUntilStep("Unranked badge shown", () => footerButtonMods.ChildrenOfType().Single().Alpha == 1); + AddUntilStep("Unranked badge shown", () => footerButtonMods.ChildrenOfType().Single().Alpha == 1); AddStep(@"Clear selected mod", () => changeMods(Array.Empty())); - AddUntilStep("Unranked badge not shown", () => footerButtonMods.ChildrenOfType().Single().Alpha == 0); + AddUntilStep("Unranked badge not shown", () => footerButtonMods.ChildrenOfType().Single().Alpha == 0); } private void changeMods(IReadOnlyList mods) => footerButtonMods.Current.Value = mods; @@ -122,7 +122,7 @@ namespace osu.Game.Tests.Visual.UserInterface } } - private partial class TestScreenFooterButtonMods : ScreenFooterButtonMods + private partial class TestScreenFooterButtonMods : FooterButtonMods { public new OsuSpriteText MultiplierText => base.MultiplierText; diff --git a/osu.Game.Tests/Visual/SongSelectV2/TestSceneLeaderboardScore.cs b/osu.Game.Tests/Visual/SongSelectV2/TestSceneLeaderboardScore.cs index 08c0c92285..b59a31c173 100644 --- a/osu.Game.Tests/Visual/SongSelectV2/TestSceneLeaderboardScore.cs +++ b/osu.Game.Tests/Visual/SongSelectV2/TestSceneLeaderboardScore.cs @@ -20,7 +20,7 @@ using osu.Game.Rulesets.Osu; using osu.Game.Rulesets.Osu.Mods; using osu.Game.Rulesets.Scoring; using osu.Game.Scoring; -using osu.Game.Screens.SelectV2.Leaderboards; +using osu.Game.Screens.SelectV2; using osu.Game.Tests.Resources; using osu.Game.Users; using osuTK; @@ -60,7 +60,7 @@ namespace osu.Game.Tests.Visual.SongSelectV2 foreach (var scoreInfo in getTestScores()) { - fillFlow.Add(new LeaderboardScoreV2(scoreInfo) + fillFlow.Add(new BeatmapLeaderboardScore(scoreInfo) { Rank = scoreInfo.Position, IsPersonalBest = scoreInfo.User.Id == 2, @@ -93,7 +93,7 @@ namespace osu.Game.Tests.Visual.SongSelectV2 foreach (var scoreInfo in getTestScores()) { - fillFlow.Add(new LeaderboardScoreV2(scoreInfo) + fillFlow.Add(new BeatmapLeaderboardScore(scoreInfo) { Rank = scoreInfo.Position, IsPersonalBest = scoreInfo.User.Id == 2, @@ -108,7 +108,7 @@ namespace osu.Game.Tests.Visual.SongSelectV2 [Test] public void TestUseTheseModsDoesNotCopySystemMods() { - LeaderboardScoreV2 score = null!; + BeatmapLeaderboardScore score = null!; AddStep("create content", () => { @@ -146,7 +146,7 @@ namespace osu.Game.Tests.Visual.SongSelectV2 Date = DateTimeOffset.Now.AddYears(-2), }; - fillFlow.Add(score = new LeaderboardScoreV2(scoreInfo) + fillFlow.Add(score = new BeatmapLeaderboardScore(scoreInfo) { Rank = scoreInfo.Position, Shear = Vector2.Zero, diff --git a/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselV2DifficultyPanel.cs b/osu.Game.Tests/Visual/SongSelectV2/TestScenePanelBeatmap.cs similarity index 95% rename from osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselV2DifficultyPanel.cs rename to osu.Game.Tests/Visual/SongSelectV2/TestScenePanelBeatmap.cs index 1947721d5d..53a1355fc2 100644 --- a/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselV2DifficultyPanel.cs +++ b/osu.Game.Tests/Visual/SongSelectV2/TestScenePanelBeatmap.cs @@ -8,6 +8,7 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Utils; using osu.Game.Beatmaps; +using osu.Game.Graphics.Carousel; using osu.Game.Overlays; using osu.Game.Rulesets.Mania; using osu.Game.Rulesets.Osu; @@ -18,14 +19,14 @@ using osuTK; namespace osu.Game.Tests.Visual.SongSelectV2 { - public partial class TestSceneBeatmapCarouselV2DifficultyPanel : ThemeComparisonTestScene + public partial class TestScenePanelBeatmap : ThemeComparisonTestScene { [Resolved] private BeatmapManager beatmaps { get; set; } = null!; private BeatmapInfo beatmap = null!; - public TestSceneBeatmapCarouselV2DifficultyPanel() + public TestScenePanelBeatmap() : base(false) { } diff --git a/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselV2StandalonePanel.cs b/osu.Game.Tests/Visual/SongSelectV2/TestScenePanelBeatmapStandalone.cs similarity index 95% rename from osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselV2StandalonePanel.cs rename to osu.Game.Tests/Visual/SongSelectV2/TestScenePanelBeatmapStandalone.cs index 2dbe9e6cd1..4adee17868 100644 --- a/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselV2StandalonePanel.cs +++ b/osu.Game.Tests/Visual/SongSelectV2/TestScenePanelBeatmapStandalone.cs @@ -8,6 +8,7 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Utils; using osu.Game.Beatmaps; +using osu.Game.Graphics.Carousel; using osu.Game.Overlays; using osu.Game.Rulesets.Mania; using osu.Game.Rulesets.Osu; @@ -18,14 +19,14 @@ using osuTK; namespace osu.Game.Tests.Visual.SongSelectV2 { - public partial class TestSceneBeatmapCarouselV2StandalonePanel : ThemeComparisonTestScene + public partial class TestScenePanelBeatmapStandalone : ThemeComparisonTestScene { [Resolved] private BeatmapManager beatmaps { get; set; } = null!; private BeatmapInfo beatmap = null!; - public TestSceneBeatmapCarouselV2StandalonePanel() + public TestScenePanelBeatmapStandalone() : base(false) { } diff --git a/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselV2GroupPanel.cs b/osu.Game.Tests/Visual/SongSelectV2/TestScenePanelGroup.cs similarity index 96% rename from osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselV2GroupPanel.cs rename to osu.Game.Tests/Visual/SongSelectV2/TestScenePanelGroup.cs index d62aee77f3..54c6cb1c0e 100644 --- a/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselV2GroupPanel.cs +++ b/osu.Game.Tests/Visual/SongSelectV2/TestScenePanelGroup.cs @@ -6,15 +6,16 @@ using NUnit.Framework; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Game.Overlays; +using osu.Game.Graphics.Carousel; using osu.Game.Screens.SelectV2; using osu.Game.Tests.Visual.UserInterface; using osuTK; namespace osu.Game.Tests.Visual.SongSelectV2 { - public partial class TestSceneBeatmapCarouselV2GroupPanel : ThemeComparisonTestScene + public partial class TestScenePanelGroup : ThemeComparisonTestScene { - public TestSceneBeatmapCarouselV2GroupPanel() + public TestScenePanelGroup() : base(false) { } diff --git a/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselV2SetPanel.cs b/osu.Game.Tests/Visual/SongSelectV2/TestScenePanelSet.cs similarity index 95% rename from osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselV2SetPanel.cs rename to osu.Game.Tests/Visual/SongSelectV2/TestScenePanelSet.cs index ef34394e12..16f6b2cc9c 100644 --- a/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselV2SetPanel.cs +++ b/osu.Game.Tests/Visual/SongSelectV2/TestScenePanelSet.cs @@ -8,6 +8,7 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Utils; using osu.Game.Beatmaps; +using osu.Game.Graphics.Carousel; using osu.Game.Overlays; using osu.Game.Screens.SelectV2; using osu.Game.Tests.Resources; @@ -16,14 +17,14 @@ using osuTK; namespace osu.Game.Tests.Visual.SongSelectV2 { - public partial class TestSceneBeatmapCarouselV2SetPanel : ThemeComparisonTestScene + public partial class TestScenePanelSet : ThemeComparisonTestScene { [Resolved] private BeatmapManager beatmaps { get; set; } = null!; private BeatmapSetInfo beatmapSet = null!; - public TestSceneBeatmapCarouselV2SetPanel() + public TestScenePanelSet() : base(false) { } diff --git a/osu.Game.Tests/Visual/SongSelectV2/TestSceneUpdateBeatmapSetButtonV2.cs b/osu.Game.Tests/Visual/SongSelectV2/TestScenePanelUpdateBeatmapButton.cs similarity index 90% rename from osu.Game.Tests/Visual/SongSelectV2/TestSceneUpdateBeatmapSetButtonV2.cs rename to osu.Game.Tests/Visual/SongSelectV2/TestScenePanelUpdateBeatmapButton.cs index ba3f2635b0..781691d3db 100644 --- a/osu.Game.Tests/Visual/SongSelectV2/TestSceneUpdateBeatmapSetButtonV2.cs +++ b/osu.Game.Tests/Visual/SongSelectV2/TestScenePanelUpdateBeatmapButton.cs @@ -9,14 +9,14 @@ using osu.Game.Screens.SelectV2; namespace osu.Game.Tests.Visual.SongSelectV2 { - public partial class TestSceneUpdateBeatmapSetButtonV2 : OsuTestScene + public partial class TestScenePanelUpdateBeatmapButton : OsuTestScene { - private UpdateBeatmapSetButton button = null!; + private PanelUpdateBeatmapButton button = null!; [SetUp] public void SetUp() => Schedule(() => { - Child = button = new UpdateBeatmapSetButton + Child = button = new PanelUpdateBeatmapButton { Anchor = Anchor.Centre, Origin = Anchor.Centre, diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneScreenFooter.cs b/osu.Game.Tests/Visual/SongSelectV2/TestSceneScreenFooter.cs similarity index 98% rename from osu.Game.Tests/Visual/UserInterface/TestSceneScreenFooter.cs rename to osu.Game.Tests/Visual/SongSelectV2/TestSceneScreenFooter.cs index 054bbb39d1..bdecebd64f 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneScreenFooter.cs +++ b/osu.Game.Tests/Visual/SongSelectV2/TestSceneScreenFooter.cs @@ -15,9 +15,9 @@ using osu.Game.Graphics.UserInterface; using osu.Game.Overlays; using osu.Game.Overlays.Mods; using osu.Game.Screens.Footer; -using osu.Game.Screens.SelectV2.Footer; +using osu.Game.Screens.SelectV2; -namespace osu.Game.Tests.Visual.UserInterface +namespace osu.Game.Tests.Visual.SongSelectV2 { public partial class TestSceneScreenFooter : OsuManualInputManagerTestScene { @@ -51,9 +51,9 @@ namespace osu.Game.Tests.Visual.UserInterface screenFooter.SetButtons(new ScreenFooterButton[] { - new ScreenFooterButtonMods(modOverlay) { Current = SelectedMods }, - new ScreenFooterButtonRandom(), - new ScreenFooterButtonOptions(), + new FooterButtonMods(modOverlay) { Current = SelectedMods }, + new FooterButtonRandom(), + new FooterButtonOptions(), }); }); diff --git a/osu.Game.Tests/Visual/SongSelectV2/TestSceneSongSelect.cs b/osu.Game.Tests/Visual/SongSelectV2/TestSceneSongSelect.cs index 630f3c95ee..986ad6fc46 100644 --- a/osu.Game.Tests/Visual/SongSelectV2/TestSceneSongSelect.cs +++ b/osu.Game.Tests/Visual/SongSelectV2/TestSceneSongSelect.cs @@ -22,7 +22,7 @@ using osu.Game.Rulesets.Taiko; using osu.Game.Screens; using osu.Game.Screens.Footer; using osu.Game.Screens.Menu; -using osu.Game.Screens.SelectV2.Footer; +using osu.Game.Screens.SelectV2; using osuTK.Input; namespace osu.Game.Tests.Visual.SongSelectV2 @@ -78,7 +78,7 @@ namespace osu.Game.Tests.Visual.SongSelectV2 { base.SetUpSteps(); - AddStep("load screen", () => Stack.Push(new Screens.SelectV2.SoloSongSelect())); + AddStep("load screen", () => Stack.Push(new SoloSongSelect())); AddUntilStep("wait for load", () => Stack.CurrentScreen is Screens.SelectV2.SongSelect songSelect && songSelect.IsLoaded); } @@ -199,7 +199,7 @@ namespace osu.Game.Tests.Visual.SongSelectV2 { AddStep("Press F1", () => { - InputManager.MoveMouseTo(this.ChildrenOfType().Single()); + InputManager.MoveMouseTo(this.ChildrenOfType().Single()); InputManager.Click(MouseButton.Left); }); AddAssert("Overlay visible", () => this.ChildrenOfType().Single().State.Value == Visibility.Visible); diff --git a/osu.Game/Screens/SelectV2/Carousel.cs b/osu.Game/Graphics/Carousel/Carousel.cs similarity index 99% rename from osu.Game/Screens/SelectV2/Carousel.cs rename to osu.Game/Graphics/Carousel/Carousel.cs index 7b1fd6e999..a9c8aecd6c 100644 --- a/osu.Game/Screens/SelectV2/Carousel.cs +++ b/osu.Game/Graphics/Carousel/Carousel.cs @@ -24,7 +24,7 @@ using osu.Game.Input.Bindings; using osuTK; using osuTK.Input; -namespace osu.Game.Screens.SelectV2 +namespace osu.Game.Graphics.Carousel { /// /// A highly efficient vertical list display that is used primarily for the song select screen, diff --git a/osu.Game/Screens/SelectV2/CarouselItem.cs b/osu.Game/Graphics/Carousel/CarouselItem.cs similarity index 98% rename from osu.Game/Screens/SelectV2/CarouselItem.cs rename to osu.Game/Graphics/Carousel/CarouselItem.cs index 36dc48a497..223c8d9869 100644 --- a/osu.Game/Screens/SelectV2/CarouselItem.cs +++ b/osu.Game/Graphics/Carousel/CarouselItem.cs @@ -3,7 +3,7 @@ using System; -namespace osu.Game.Screens.SelectV2 +namespace osu.Game.Graphics.Carousel { /// /// Represents a single display item for display in a . diff --git a/osu.Game/Screens/SelectV2/ICarouselFilter.cs b/osu.Game/Graphics/Carousel/ICarouselFilter.cs similarity index 95% rename from osu.Game/Screens/SelectV2/ICarouselFilter.cs rename to osu.Game/Graphics/Carousel/ICarouselFilter.cs index f510a7cd4b..570f480aab 100644 --- a/osu.Game/Screens/SelectV2/ICarouselFilter.cs +++ b/osu.Game/Graphics/Carousel/ICarouselFilter.cs @@ -5,7 +5,7 @@ using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; -namespace osu.Game.Screens.SelectV2 +namespace osu.Game.Graphics.Carousel { /// /// An interface representing a filter operation which can be run on a . diff --git a/osu.Game/Screens/SelectV2/ICarouselPanel.cs b/osu.Game/Graphics/Carousel/ICarouselPanel.cs similarity index 97% rename from osu.Game/Screens/SelectV2/ICarouselPanel.cs rename to osu.Game/Graphics/Carousel/ICarouselPanel.cs index 4fba0d2827..5f0ebc263c 100644 --- a/osu.Game/Screens/SelectV2/ICarouselPanel.cs +++ b/osu.Game/Graphics/Carousel/ICarouselPanel.cs @@ -5,7 +5,7 @@ using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Pooling; -namespace osu.Game.Screens.SelectV2 +namespace osu.Game.Graphics.Carousel { /// /// An interface to be attached to any s which are used for display inside a . diff --git a/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallengeLeaderboard.cs b/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallengeLeaderboard.cs index 4736ba28db..401053599e 100644 --- a/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallengeLeaderboard.cs +++ b/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallengeLeaderboard.cs @@ -17,7 +17,7 @@ using osu.Game.Online.Rooms; using osu.Game.Rulesets; using osu.Game.Rulesets.Mods; using osu.Game.Scoring; -using osu.Game.Screens.SelectV2.Leaderboards; +using osu.Game.Screens.SelectV2; using osuTK; namespace osu.Game.Screens.OnlinePlay.DailyChallenge @@ -40,7 +40,7 @@ namespace osu.Game.Screens.OnlinePlay.DailyChallenge private readonly Room room; private readonly PlaylistItem playlistItem; - private FillFlowContainer scoreFlow = null!; + private FillFlowContainer scoreFlow = null!; private Container userBestContainer = null!; private SectionHeader userBestHeader = null!; private LoadingLayer loadingLayer = null!; @@ -91,7 +91,7 @@ namespace osu.Game.Screens.OnlinePlay.DailyChallenge new OsuScrollContainer { RelativeSizeAxes = Axes.Both, - Child = scoreFlow = new FillFlowContainer + Child = scoreFlow = new FillFlowContainer { RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y, @@ -158,7 +158,7 @@ namespace osu.Game.Screens.OnlinePlay.DailyChallenge } else { - LoadComponentsAsync(best.Select((s, index) => new LeaderboardScoreV2(s, sheared: false) + LoadComponentsAsync(best.Select((s, index) => new BeatmapLeaderboardScore(s, sheared: false) { Rank = index + 1, IsPersonalBest = s.UserID == api.LocalUser.Value.Id, @@ -178,7 +178,7 @@ namespace osu.Game.Screens.OnlinePlay.DailyChallenge if (userBest != null) { - userBestContainer.Add(new LeaderboardScoreV2(userBest, sheared: false) + userBestContainer.Add(new BeatmapLeaderboardScore(userBest, sheared: false) { Rank = userBest.Position, IsPersonalBest = true, diff --git a/osu.Game/Screens/SelectV2/BeatmapCarousel.cs b/osu.Game/Screens/SelectV2/BeatmapCarousel.cs index 994b0fb6c0..9cb7d152de 100644 --- a/osu.Game/Screens/SelectV2/BeatmapCarousel.cs +++ b/osu.Game/Screens/SelectV2/BeatmapCarousel.cs @@ -13,6 +13,7 @@ using osu.Framework.Graphics.Pooling; using osu.Framework.Utils; using osu.Game.Beatmaps; using osu.Game.Database; +using osu.Game.Graphics.Carousel; using osu.Game.Graphics.UserInterface; using osu.Game.Screens.Select; diff --git a/osu.Game/Screens/SelectV2/BeatmapCarouselFilterGrouping.cs b/osu.Game/Screens/SelectV2/BeatmapCarouselFilterGrouping.cs index 8f9d5cc31b..3360437544 100644 --- a/osu.Game/Screens/SelectV2/BeatmapCarouselFilterGrouping.cs +++ b/osu.Game/Screens/SelectV2/BeatmapCarouselFilterGrouping.cs @@ -6,6 +6,7 @@ using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; using osu.Game.Beatmaps; +using osu.Game.Graphics.Carousel; using osu.Game.Screens.Select; using osu.Game.Screens.Select.Filter; diff --git a/osu.Game/Screens/SelectV2/BeatmapCarouselFilterSorting.cs b/osu.Game/Screens/SelectV2/BeatmapCarouselFilterSorting.cs index 3cdbbb4fed..22a67321db 100644 --- a/osu.Game/Screens/SelectV2/BeatmapCarouselFilterSorting.cs +++ b/osu.Game/Screens/SelectV2/BeatmapCarouselFilterSorting.cs @@ -7,6 +7,7 @@ using System.Linq; using System.Threading; using System.Threading.Tasks; using osu.Game.Beatmaps; +using osu.Game.Graphics.Carousel; using osu.Game.Screens.Select; using osu.Game.Screens.Select.Filter; using osu.Game.Utils; diff --git a/osu.Game/Screens/SelectV2/Leaderboards/LeaderboardScoreV2.cs b/osu.Game/Screens/SelectV2/BeatmapLeaderboardScore.cs similarity index 99% rename from osu.Game/Screens/SelectV2/Leaderboards/LeaderboardScoreV2.cs rename to osu.Game/Screens/SelectV2/BeatmapLeaderboardScore.cs index 0b7b2ebbc1..c9413a9414 100644 --- a/osu.Game/Screens/SelectV2/Leaderboards/LeaderboardScoreV2.cs +++ b/osu.Game/Screens/SelectV2/BeatmapLeaderboardScore.cs @@ -41,9 +41,9 @@ using osuTK; using osuTK.Graphics; using CommonStrings = osu.Game.Localisation.CommonStrings; -namespace osu.Game.Screens.SelectV2.Leaderboards +namespace osu.Game.Screens.SelectV2 { - public partial class LeaderboardScoreV2 : OsuClickableContainer, IHasContextMenu, IHasCustomTooltip + public partial class BeatmapLeaderboardScore : OsuClickableContainer, IHasContextMenu, IHasCustomTooltip { public Bindable> SelectedMods = new Bindable>(); @@ -117,7 +117,7 @@ namespace osu.Game.Screens.SelectV2.Leaderboards public ITooltip GetCustomTooltip() => new LeaderboardScoreTooltip(); public virtual ScoreInfo TooltipContent => score; - public LeaderboardScoreV2(ScoreInfo score, bool sheared = true) + public BeatmapLeaderboardScore(ScoreInfo score, bool sheared = true) { this.score = score; this.sheared = sheared; diff --git a/osu.Game/Screens/SelectV2/Footer/BeatmapOptionsPopover.cs b/osu.Game/Screens/SelectV2/Footer/BeatmapOptionsPopover.cs deleted file mode 100644 index fb2e32dfdc..0000000000 --- a/osu.Game/Screens/SelectV2/Footer/BeatmapOptionsPopover.cs +++ /dev/null @@ -1,195 +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.Linq; -using osu.Framework.Allocation; -using osu.Framework.Bindables; -using osu.Framework.Extensions.LocalisationExtensions; -using osu.Framework.Graphics; -using osu.Framework.Graphics.Containers; -using osu.Framework.Graphics.Sprites; -using osu.Framework.Input.Events; -using osu.Framework.Localisation; -using osu.Game.Beatmaps; -using osu.Game.Collections; -using osu.Game.Graphics; -using osu.Game.Graphics.Containers; -using osu.Game.Graphics.Sprites; -using osu.Game.Graphics.UserInterface; -using osu.Game.Graphics.UserInterfaceV2; -using osu.Game.Localisation; -using osu.Game.Overlays; -using osuTK; -using osuTK.Graphics; -using osuTK.Input; -using WebCommonStrings = osu.Game.Resources.Localisation.Web.CommonStrings; - -namespace osu.Game.Screens.SelectV2.Footer -{ - public partial class BeatmapOptionsPopover : OsuPopover - { - private FillFlowContainer buttonFlow = null!; - private readonly ScreenFooterButtonOptions footerButton; - - [Cached] - private readonly OverlayColourProvider colourProvider; - - private WorkingBeatmap beatmapWhenOpening = null!; - - [Resolved] - private IBindable beatmap { get; set; } = null!; - - public BeatmapOptionsPopover(ScreenFooterButtonOptions footerButton, OverlayColourProvider colourProvider) - { - this.footerButton = footerButton; - this.colourProvider = colourProvider; - } - - [BackgroundDependencyLoader] - private void load(ManageCollectionsDialog? manageCollectionsDialog, OsuColour colours, BeatmapManager? beatmapManager) - { - Content.Padding = new MarginPadding(5); - - Child = buttonFlow = new FillFlowContainer - { - AutoSizeAxes = Axes.Both, - Direction = FillDirection.Vertical, - Spacing = new Vector2(3), - }; - - beatmapWhenOpening = beatmap.Value; - - addHeader(CommonStrings.General); - addButton(SongSelectStrings.ManageCollections, FontAwesome.Solid.Book, () => manageCollectionsDialog?.Show()); - - addHeader(SongSelectStrings.ForAllDifficulties, beatmapWhenOpening.BeatmapSetInfo.ToString()); - addButton(SongSelectStrings.DeleteBeatmap, FontAwesome.Solid.Trash, () => { }, colours.Red1); // songSelect?.DeleteBeatmap(beatmapWhenOpening.BeatmapSetInfo); - - addHeader(SongSelectStrings.ForSelectedDifficulty, beatmapWhenOpening.BeatmapInfo.DifficultyName); - // TODO: make work, and make show "unplayed" or "played" based on status. - addButton(SongSelectStrings.MarkAsPlayed, FontAwesome.Regular.TimesCircle, null); - addButton(SongSelectStrings.ClearAllLocalScores, FontAwesome.Solid.Eraser, () => { }, colours.Red1); // songSelect?.ClearScores(beatmapWhenOpening.BeatmapInfo); - - // if (songSelect != null && songSelect.AllowEditing) - addButton(SongSelectStrings.EditBeatmap, FontAwesome.Solid.PencilAlt, () => { }); // songSelect.Edit(beatmapWhenOpening.BeatmapInfo); - - addButton(WebCommonStrings.ButtonsHide.ToSentence(), FontAwesome.Solid.Magic, () => beatmapManager?.Hide(beatmapWhenOpening.BeatmapInfo)); - } - - protected override void LoadComplete() - { - base.LoadComplete(); - - ScheduleAfterChildren(() => GetContainingFocusManager()!.ChangeFocus(this)); - - beatmap.BindValueChanged(_ => Hide()); - } - - private void addHeader(LocalisableString text, string? context = null) - { - var textFlow = new OsuTextFlowContainer - { - AutoSizeAxes = Axes.Y, - RelativeSizeAxes = Axes.X, - Padding = new MarginPadding(10), - }; - - textFlow.AddText(text, t => t.Font = OsuFont.Default.With(weight: FontWeight.SemiBold)); - - if (context != null) - { - textFlow.NewLine(); - textFlow.AddText(context, t => - { - t.Colour = colourProvider.Content2; - t.Font = t.Font.With(size: 13); - }); - } - - buttonFlow.Add(textFlow); - } - - private void addButton(LocalisableString text, IconUsage icon, Action? action, Color4? colour = null) - { - var button = new OptionButton - { - Text = text, - Icon = icon, - TextColour = colour, - Action = () => - { - Scheduler.AddDelayed(Hide, 50); - action?.Invoke(); - }, - }; - - buttonFlow.Add(button); - } - - private partial class OptionButton : OsuButton - { - public IconUsage Icon { get; init; } - public Color4? TextColour { get; init; } - - public OptionButton() - { - Size = new Vector2(265, 50); - } - - [BackgroundDependencyLoader] - private void load(OverlayColourProvider colourProvider) - { - BackgroundColour = colourProvider.Background3; - - SpriteText.Colour = TextColour ?? Color4.White; - Content.CornerRadius = 10; - - Add(new SpriteIcon - { - Anchor = Anchor.CentreLeft, - Origin = Anchor.CentreLeft, - Size = new Vector2(17), - X = 15, - Icon = Icon, - Colour = TextColour ?? Color4.White, - }); - } - - protected override SpriteText CreateText() => new OsuSpriteText - { - Depth = -1, - Origin = Anchor.CentreLeft, - Anchor = Anchor.CentreLeft, - X = 40 - }; - } - - protected override bool OnKeyDown(KeyDownEvent e) - { - // don't absorb control as ToolbarRulesetSelector uses control + number to navigate - if (e.ControlPressed) return false; - - if (!e.Repeat && e.Key >= Key.Number1 && e.Key <= Key.Number9) - { - int requested = e.Key - Key.Number1; - - OptionButton? found = buttonFlow.Children.OfType().ElementAtOrDefault(requested); - - if (found != null) - { - found.TriggerClick(); - return true; - } - } - - return base.OnKeyDown(e); - } - - protected override void UpdateState(ValueChangedEvent state) - { - base.UpdateState(state); - footerButton.OverlayState.Value = state.NewValue; - } - } -} diff --git a/osu.Game/Screens/SelectV2/Footer/ScreenFooterButtonMods.cs b/osu.Game/Screens/SelectV2/FooterButtonMods.cs similarity index 98% rename from osu.Game/Screens/SelectV2/Footer/ScreenFooterButtonMods.cs rename to osu.Game/Screens/SelectV2/FooterButtonMods.cs index 61d69ae197..833ea96139 100644 --- a/osu.Game/Screens/SelectV2/Footer/ScreenFooterButtonMods.cs +++ b/osu.Game/Screens/SelectV2/FooterButtonMods.cs @@ -28,9 +28,9 @@ using osu.Game.Utils; using osuTK; using osuTK.Graphics; -namespace osu.Game.Screens.SelectV2.Footer +namespace osu.Game.Screens.SelectV2 { - public partial class ScreenFooterButtonMods : ScreenFooterButton, IHasCurrentValue> + public partial class FooterButtonMods : ScreenFooterButton, IHasCurrentValue> { private const float bar_height = 30f; private const float mod_display_portion = 0.65f; @@ -58,7 +58,7 @@ namespace osu.Game.Screens.SelectV2.Footer [Resolved] private OverlayColourProvider colourProvider { get; set; } = null!; - public ScreenFooterButtonMods(ModSelectOverlay overlay) + public FooterButtonMods(ModSelectOverlay overlay) : base(overlay) { } diff --git a/osu.Game/Screens/SelectV2/Footer/ScreenFooterButtonOptions.cs b/osu.Game/Screens/SelectV2/FooterButtonOptions.cs similarity index 76% rename from osu.Game/Screens/SelectV2/Footer/ScreenFooterButtonOptions.cs rename to osu.Game/Screens/SelectV2/FooterButtonOptions.cs index 72409b5566..41edaf2a02 100644 --- a/osu.Game/Screens/SelectV2/Footer/ScreenFooterButtonOptions.cs +++ b/osu.Game/Screens/SelectV2/FooterButtonOptions.cs @@ -5,15 +5,14 @@ using osu.Framework.Allocation; using osu.Framework.Extensions; using osu.Framework.Graphics.Cursor; using osu.Framework.Graphics.Sprites; -using osu.Framework.Graphics.UserInterface; using osu.Game.Graphics; using osu.Game.Input.Bindings; using osu.Game.Overlays; using osu.Game.Screens.Footer; -namespace osu.Game.Screens.SelectV2.Footer +namespace osu.Game.Screens.SelectV2 { - public partial class ScreenFooterButtonOptions : ScreenFooterButton, IHasPopover + public partial class FooterButtonOptions : ScreenFooterButton, IHasPopover { [Resolved] private OverlayColourProvider colourProvider { get; set; } = null!; @@ -29,6 +28,6 @@ namespace osu.Game.Screens.SelectV2.Footer Action = this.ShowPopover; } - public Popover GetPopover() => new BeatmapOptionsPopover(this, colourProvider); + public Framework.Graphics.UserInterface.Popover GetPopover() => new Popover(this, colourProvider); } } diff --git a/osu.Game/Screens/SelectV2/FooterButtonOptions_Popover.cs b/osu.Game/Screens/SelectV2/FooterButtonOptions_Popover.cs new file mode 100644 index 0000000000..76b841ee99 --- /dev/null +++ b/osu.Game/Screens/SelectV2/FooterButtonOptions_Popover.cs @@ -0,0 +1,198 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using System.Linq; +using osu.Framework.Allocation; +using osu.Framework.Bindables; +using osu.Framework.Extensions.LocalisationExtensions; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Sprites; +using osu.Framework.Input.Events; +using osu.Framework.Localisation; +using osu.Game.Beatmaps; +using osu.Game.Collections; +using osu.Game.Graphics; +using osu.Game.Graphics.Containers; +using osu.Game.Graphics.Sprites; +using osu.Game.Graphics.UserInterface; +using osu.Game.Graphics.UserInterfaceV2; +using osu.Game.Localisation; +using osu.Game.Overlays; +using osuTK; +using osuTK.Graphics; +using osuTK.Input; +using WebCommonStrings = osu.Game.Resources.Localisation.Web.CommonStrings; + +namespace osu.Game.Screens.SelectV2 +{ + public partial class FooterButtonOptions + { + public partial class Popover : OsuPopover + { + private FillFlowContainer buttonFlow = null!; + private readonly FooterButtonOptions footerButton; + + [Cached] + private readonly OverlayColourProvider colourProvider; + + private WorkingBeatmap beatmapWhenOpening = null!; + + [Resolved] + private IBindable beatmap { get; set; } = null!; + + public Popover(FooterButtonOptions footerButton, OverlayColourProvider colourProvider) + { + this.footerButton = footerButton; + this.colourProvider = colourProvider; + } + + [BackgroundDependencyLoader] + private void load(ManageCollectionsDialog? manageCollectionsDialog, OsuColour colours, BeatmapManager? beatmapManager) + { + Content.Padding = new MarginPadding(5); + + Child = buttonFlow = new FillFlowContainer + { + AutoSizeAxes = Axes.Both, + Direction = FillDirection.Vertical, + Spacing = new Vector2(3), + }; + + beatmapWhenOpening = beatmap.Value; + + addHeader(CommonStrings.General); + addButton(SongSelectStrings.ManageCollections, FontAwesome.Solid.Book, () => manageCollectionsDialog?.Show()); + + addHeader(SongSelectStrings.ForAllDifficulties, beatmapWhenOpening.BeatmapSetInfo.ToString()); + addButton(SongSelectStrings.DeleteBeatmap, FontAwesome.Solid.Trash, () => { }, colours.Red1); // songSelect?.DeleteBeatmap(beatmapWhenOpening.BeatmapSetInfo); + + addHeader(SongSelectStrings.ForSelectedDifficulty, beatmapWhenOpening.BeatmapInfo.DifficultyName); + // TODO: make work, and make show "unplayed" or "played" based on status. + addButton(SongSelectStrings.MarkAsPlayed, FontAwesome.Regular.TimesCircle, null); + addButton(SongSelectStrings.ClearAllLocalScores, FontAwesome.Solid.Eraser, () => { }, colours.Red1); // songSelect?.ClearScores(beatmapWhenOpening.BeatmapInfo); + + // if (songSelect != null && songSelect.AllowEditing) + addButton(SongSelectStrings.EditBeatmap, FontAwesome.Solid.PencilAlt, () => { }); // songSelect.Edit(beatmapWhenOpening.BeatmapInfo); + + addButton(WebCommonStrings.ButtonsHide.ToSentence(), FontAwesome.Solid.Magic, () => beatmapManager?.Hide(beatmapWhenOpening.BeatmapInfo)); + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + ScheduleAfterChildren(() => GetContainingFocusManager()!.ChangeFocus(this)); + + beatmap.BindValueChanged(_ => Hide()); + } + + private void addHeader(LocalisableString text, string? context = null) + { + var textFlow = new OsuTextFlowContainer + { + AutoSizeAxes = Axes.Y, + RelativeSizeAxes = Axes.X, + Padding = new MarginPadding(10), + }; + + textFlow.AddText(text, t => t.Font = OsuFont.Default.With(weight: FontWeight.SemiBold)); + + if (context != null) + { + textFlow.NewLine(); + textFlow.AddText(context, t => + { + t.Colour = colourProvider.Content2; + t.Font = t.Font.With(size: 13); + }); + } + + buttonFlow.Add(textFlow); + } + + private void addButton(LocalisableString text, IconUsage icon, Action? action, Color4? colour = null) + { + var button = new OptionButton + { + Text = text, + Icon = icon, + TextColour = colour, + Action = () => + { + Scheduler.AddDelayed(Hide, 50); + action?.Invoke(); + }, + }; + + buttonFlow.Add(button); + } + + private partial class OptionButton : OsuButton + { + public IconUsage Icon { get; init; } + public Color4? TextColour { get; init; } + + public OptionButton() + { + Size = new Vector2(265, 50); + } + + [BackgroundDependencyLoader] + private void load(OverlayColourProvider colourProvider) + { + BackgroundColour = colourProvider.Background3; + + SpriteText.Colour = TextColour ?? Color4.White; + Content.CornerRadius = 10; + + Add(new SpriteIcon + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + Size = new Vector2(17), + X = 15, + Icon = Icon, + Colour = TextColour ?? Color4.White, + }); + } + + protected override SpriteText CreateText() => new OsuSpriteText + { + Depth = -1, + Origin = Anchor.CentreLeft, + Anchor = Anchor.CentreLeft, + X = 40 + }; + } + + protected override bool OnKeyDown(KeyDownEvent e) + { + // don't absorb control as ToolbarRulesetSelector uses control + number to navigate + if (e.ControlPressed) return false; + + if (!e.Repeat && e.Key >= Key.Number1 && e.Key <= Key.Number9) + { + int requested = e.Key - Key.Number1; + + OptionButton? found = buttonFlow.Children.OfType().ElementAtOrDefault(requested); + + if (found != null) + { + found.TriggerClick(); + return true; + } + } + + return base.OnKeyDown(e); + } + + protected override void UpdateState(ValueChangedEvent state) + { + base.UpdateState(state); + footerButton.OverlayState.Value = state.NewValue; + } + } + } +} diff --git a/osu.Game/Screens/SelectV2/Footer/ScreenFooterButtonRandom.cs b/osu.Game/Screens/SelectV2/FooterButtonRandom.cs similarity index 97% rename from osu.Game/Screens/SelectV2/Footer/ScreenFooterButtonRandom.cs rename to osu.Game/Screens/SelectV2/FooterButtonRandom.cs index dbdb6fe79b..88b139da97 100644 --- a/osu.Game/Screens/SelectV2/Footer/ScreenFooterButtonRandom.cs +++ b/osu.Game/Screens/SelectV2/FooterButtonRandom.cs @@ -14,9 +14,9 @@ using osu.Game.Screens.Footer; using osuTK; using osuTK.Input; -namespace osu.Game.Screens.SelectV2.Footer +namespace osu.Game.Screens.SelectV2 { - public partial class ScreenFooterButtonRandom : ScreenFooterButton + public partial class FooterButtonRandom : ScreenFooterButton { public Action? NextRandom { get; set; } public Action? PreviousRandom { get; set; } diff --git a/osu.Game/Screens/SelectV2/PanelBase.cs b/osu.Game/Screens/SelectV2/Panel.cs similarity index 98% rename from osu.Game/Screens/SelectV2/PanelBase.cs rename to osu.Game/Screens/SelectV2/Panel.cs index 32da02a189..c22a88a55f 100644 --- a/osu.Game/Screens/SelectV2/PanelBase.cs +++ b/osu.Game/Screens/SelectV2/Panel.cs @@ -12,6 +12,7 @@ using osu.Framework.Graphics.Pooling; using osu.Framework.Graphics.Shapes; using osu.Framework.Input.Events; using osu.Game.Graphics; +using osu.Game.Graphics.Carousel; using osu.Game.Graphics.UserInterface; using osu.Game.Overlays; using osuTK; @@ -19,7 +20,7 @@ using osuTK.Graphics; namespace osu.Game.Screens.SelectV2 { - public abstract partial class PanelBase : PoolableDrawable, ICarouselPanel + public abstract partial class Panel : PoolableDrawable, ICarouselPanel { private const float corner_radius = 10; diff --git a/osu.Game/Screens/SelectV2/PanelBeatmap.cs b/osu.Game/Screens/SelectV2/PanelBeatmap.cs index d4bf3519fa..6742577389 100644 --- a/osu.Game/Screens/SelectV2/PanelBeatmap.cs +++ b/osu.Game/Screens/SelectV2/PanelBeatmap.cs @@ -11,6 +11,7 @@ using osu.Framework.Graphics.Containers; using osu.Game.Beatmaps; using osu.Game.Beatmaps.Drawables; using osu.Game.Graphics; +using osu.Game.Graphics.Carousel; using osu.Game.Graphics.Containers; using osu.Game.Graphics.Sprites; using osu.Game.Graphics.UserInterface; @@ -22,7 +23,7 @@ using osuTK; namespace osu.Game.Screens.SelectV2 { - public partial class PanelBeatmap : PanelBase + public partial class PanelBeatmap : Panel { public const float HEIGHT = CarouselItem.DEFAULT_HEIGHT; @@ -30,7 +31,7 @@ namespace osu.Game.Screens.SelectV2 private ConstrainedIconContainer difficultyIcon = null!; private OsuSpriteText keyCountText = null!; private StarRatingDisplay starRatingDisplay = null!; - private TopLocalRank difficultyRank = null!; + private PanelLocalRankDisplay localRank = null!; private OsuSpriteText difficultyText = null!; private OsuSpriteText authorText = null!; @@ -100,7 +101,7 @@ namespace osu.Game.Screens.SelectV2 Anchor = Anchor.CentreLeft, Origin = Anchor.CentreLeft, }, - difficultyRank = new TopLocalRank + localRank = new PanelLocalRankDisplay { Anchor = Anchor.CentreLeft, Origin = Anchor.CentreLeft, @@ -174,7 +175,7 @@ namespace osu.Game.Screens.SelectV2 difficultyIcon.Icon = beatmap.Ruleset.CreateInstance().CreateIcon(); - difficultyRank.Beatmap = beatmap; + localRank.Beatmap = beatmap; difficultyText.Text = beatmap.DifficultyName; authorText.Text = BeatmapsetsStrings.ShowDetailsMappedBy(beatmap.Metadata.Author.Username); @@ -186,7 +187,7 @@ namespace osu.Game.Screens.SelectV2 { base.FreeAfterUse(); - difficultyRank.Beatmap = null; + localRank.Beatmap = null; starDifficultyBindable = null; } diff --git a/osu.Game/Screens/SelectV2/PanelBeatmapSet.cs b/osu.Game/Screens/SelectV2/PanelBeatmapSet.cs index 9e9ef612ea..179d4d6444 100644 --- a/osu.Game/Screens/SelectV2/PanelBeatmapSet.cs +++ b/osu.Game/Screens/SelectV2/PanelBeatmapSet.cs @@ -11,22 +11,23 @@ using osu.Framework.Localisation; using osu.Game.Beatmaps; using osu.Game.Beatmaps.Drawables; using osu.Game.Graphics; +using osu.Game.Graphics.Carousel; using osu.Game.Graphics.Sprites; using osu.Game.Overlays; using osuTK; namespace osu.Game.Screens.SelectV2 { - public partial class PanelBeatmapSet : PanelBase + public partial class PanelBeatmapSet : Panel { public const float HEIGHT = CarouselItem.DEFAULT_HEIGHT * 1.6f; - private BeatmapSetPanelBackground background = null!; + private PanelSetBackground background = null!; private OsuSpriteText titleText = null!; private OsuSpriteText artistText = null!; private Drawable chevronIcon = null!; - private UpdateBeatmapSetButton updateButton = null!; + private PanelUpdateBeatmapButton updateButton = null!; private BeatmapSetOnlineStatusPill statusPill = null!; private DifficultySpectrumDisplay difficultiesDisplay = null!; @@ -60,7 +61,7 @@ namespace osu.Game.Screens.SelectV2 }, }; - Background = background = new BeatmapSetPanelBackground + Background = background = new PanelSetBackground { RelativeSizeAxes = Axes.Both, }; @@ -89,7 +90,7 @@ namespace osu.Game.Screens.SelectV2 Margin = new MarginPadding { Top = 5f }, Children = new Drawable[] { - updateButton = new UpdateBeatmapSetButton + updateButton = new PanelUpdateBeatmapButton { Anchor = Anchor.CentreLeft, Origin = Anchor.CentreLeft, diff --git a/osu.Game/Screens/SelectV2/PanelBeatmapStandalone.cs b/osu.Game/Screens/SelectV2/PanelBeatmapStandalone.cs index f893bb0caf..a0d7484587 100644 --- a/osu.Game/Screens/SelectV2/PanelBeatmapStandalone.cs +++ b/osu.Game/Screens/SelectV2/PanelBeatmapStandalone.cs @@ -13,6 +13,7 @@ using osu.Framework.Localisation; using osu.Game.Beatmaps; using osu.Game.Beatmaps.Drawables; using osu.Game.Graphics; +using osu.Game.Graphics.Carousel; using osu.Game.Graphics.Containers; using osu.Game.Graphics.Sprites; using osu.Game.Overlays; @@ -23,7 +24,7 @@ using osuTK; namespace osu.Game.Screens.SelectV2 { - public partial class PanelBeatmapStandalone : PanelBase + public partial class PanelBeatmapStandalone : Panel { public const float HEIGHT = CarouselItem.DEFAULT_HEIGHT * 1.6f; @@ -48,17 +49,17 @@ namespace osu.Game.Screens.SelectV2 private IBindable? starDifficultyBindable; private CancellationTokenSource? starDifficultyCancellationSource; - private BeatmapSetPanelBackground background = null!; + private PanelSetBackground background = null!; private OsuSpriteText titleText = null!; private OsuSpriteText artistText = null!; - private UpdateBeatmapSetButton updateButton = null!; + private PanelUpdateBeatmapButton updateButton = null!; private BeatmapSetOnlineStatusPill statusPill = null!; private ConstrainedIconContainer difficultyIcon = null!; private FillFlowContainer difficultyLine = null!; private StarRatingDisplay difficultyStarRating = null!; - private TopLocalRank difficultyRank = null!; + private PanelLocalRankDisplay difficultyRank = null!; private OsuSpriteText difficultyKeyCountText = null!; private OsuSpriteText difficultyName = null!; private OsuSpriteText difficultyAuthor = null!; @@ -80,7 +81,7 @@ namespace osu.Game.Screens.SelectV2 Colour = colourProvider.Background5, }; - Background = background = new BeatmapSetPanelBackground + Background = background = new PanelSetBackground { RelativeSizeAxes = Axes.Both, }; @@ -109,7 +110,7 @@ namespace osu.Game.Screens.SelectV2 Margin = new MarginPadding { Top = 5f }, Children = new Drawable[] { - updateButton = new UpdateBeatmapSetButton + updateButton = new PanelUpdateBeatmapButton { Anchor = Anchor.CentreLeft, Origin = Anchor.CentreLeft, @@ -136,7 +137,7 @@ namespace osu.Game.Screens.SelectV2 Scale = new Vector2(8f / 9f), Margin = new MarginPadding { Right = 5f }, }, - difficultyRank = new TopLocalRank + difficultyRank = new PanelLocalRankDisplay { Scale = new Vector2(8f / 11), Origin = Anchor.CentreLeft, diff --git a/osu.Game/Screens/SelectV2/PanelGroup.cs b/osu.Game/Screens/SelectV2/PanelGroup.cs index a5786b53c9..ac4857d2f3 100644 --- a/osu.Game/Screens/SelectV2/PanelGroup.cs +++ b/osu.Game/Screens/SelectV2/PanelGroup.cs @@ -11,6 +11,7 @@ using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Sprites; using osu.Game.Graphics; using osu.Game.Graphics.Backgrounds; +using osu.Game.Graphics.Carousel; using osu.Game.Graphics.Sprites; using osu.Game.Overlays; using osuTK; @@ -18,7 +19,7 @@ using osuTK.Graphics; namespace osu.Game.Screens.SelectV2 { - public partial class PanelGroup : PanelBase + public partial class PanelGroup : Panel { public const float HEIGHT = CarouselItem.DEFAULT_HEIGHT * 1.2f; diff --git a/osu.Game/Screens/SelectV2/PanelGroupStarDifficulty.cs b/osu.Game/Screens/SelectV2/PanelGroupStarDifficulty.cs index ce46362133..4ef3bd724c 100644 --- a/osu.Game/Screens/SelectV2/PanelGroupStarDifficulty.cs +++ b/osu.Game/Screens/SelectV2/PanelGroupStarDifficulty.cs @@ -18,7 +18,7 @@ using osuTK.Graphics; namespace osu.Game.Screens.SelectV2 { - public partial class PanelGroupStarDifficulty : PanelBase + public partial class PanelGroupStarDifficulty : Panel { [Resolved] private OsuColour colours { get; set; } = null!; diff --git a/osu.Game/Screens/SelectV2/TopLocalRank.cs b/osu.Game/Screens/SelectV2/PanelLocalRankDisplay.cs similarity index 96% rename from osu.Game/Screens/SelectV2/TopLocalRank.cs rename to osu.Game/Screens/SelectV2/PanelLocalRankDisplay.cs index 2a72a05db7..588e7e650e 100644 --- a/osu.Game/Screens/SelectV2/TopLocalRank.cs +++ b/osu.Game/Screens/SelectV2/PanelLocalRankDisplay.cs @@ -19,7 +19,7 @@ using Realms; namespace osu.Game.Screens.SelectV2 { - public partial class TopLocalRank : CompositeDrawable + public partial class PanelLocalRankDisplay : CompositeDrawable { private BeatmapInfo? beatmap; @@ -48,7 +48,7 @@ namespace osu.Game.Screens.SelectV2 private readonly UpdateableRank updateable; - public TopLocalRank(BeatmapInfo? beatmap = null) + public PanelLocalRankDisplay(BeatmapInfo? beatmap = null) { AutoSizeAxes = Axes.Both; diff --git a/osu.Game/Screens/SelectV2/BeatmapSetPanelBackground.cs b/osu.Game/Screens/SelectV2/PanelSetBackground.cs similarity index 97% rename from osu.Game/Screens/SelectV2/BeatmapSetPanelBackground.cs rename to osu.Game/Screens/SelectV2/PanelSetBackground.cs index 798acf62ee..99dbf90556 100644 --- a/osu.Game/Screens/SelectV2/BeatmapSetPanelBackground.cs +++ b/osu.Game/Screens/SelectV2/PanelSetBackground.cs @@ -14,7 +14,7 @@ using osuTK.Graphics; namespace osu.Game.Screens.SelectV2 { - public partial class BeatmapSetPanelBackground : ModelBackedDrawable + public partial class PanelSetBackground : ModelBackedDrawable { protected override double TransformDuration => 400; diff --git a/osu.Game/Screens/SelectV2/UpdateBeatmapSetButton.cs b/osu.Game/Screens/SelectV2/PanelUpdateBeatmapButton.cs similarity index 98% rename from osu.Game/Screens/SelectV2/UpdateBeatmapSetButton.cs rename to osu.Game/Screens/SelectV2/PanelUpdateBeatmapButton.cs index ac7e3856ac..2a850321a6 100644 --- a/osu.Game/Screens/SelectV2/UpdateBeatmapSetButton.cs +++ b/osu.Game/Screens/SelectV2/PanelUpdateBeatmapButton.cs @@ -22,7 +22,7 @@ using osuTK.Graphics; namespace osu.Game.Screens.SelectV2 { - public partial class UpdateBeatmapSetButton : OsuAnimatedButton + public partial class PanelUpdateBeatmapButton : OsuAnimatedButton { private BeatmapSetInfo? beatmapSet; @@ -53,7 +53,7 @@ namespace osu.Game.Screens.SelectV2 [Resolved] private IDialogOverlay? dialogOverlay { get; set; } - public UpdateBeatmapSetButton() + public PanelUpdateBeatmapButton() { Size = new Vector2(75f, 22f); } diff --git a/osu.Game/Screens/SelectV2/SongSelect.cs b/osu.Game/Screens/SelectV2/SongSelect.cs index e295656a21..67ca110dab 100644 --- a/osu.Game/Screens/SelectV2/SongSelect.cs +++ b/osu.Game/Screens/SelectV2/SongSelect.cs @@ -11,7 +11,6 @@ using osu.Game.Overlays.Mods; using osu.Game.Screens.Footer; using osu.Game.Screens.Menu; using osu.Game.Screens.Select; -using osu.Game.Screens.SelectV2.Footer; namespace osu.Game.Screens.SelectV2 { @@ -77,9 +76,9 @@ namespace osu.Game.Screens.SelectV2 public override IReadOnlyList CreateFooterButtons() => new ScreenFooterButton[] { - new ScreenFooterButtonMods(modSelectOverlay) { Current = Mods }, - new ScreenFooterButtonRandom(), - new ScreenFooterButtonOptions(), + new FooterButtonMods(modSelectOverlay) { Current = Mods }, + new FooterButtonRandom(), + new FooterButtonOptions(), }; protected override void LoadComplete() From 6fe1695d39d4c9f774807ad3bc8623c45587d099 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 16 Apr 2025 19:40:47 +0900 Subject: [PATCH 077/164] Use full namespace isntead of weird using statement --- osu.Game/Screens/OnlinePlay/DrawableRoomPlaylistItem.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/DrawableRoomPlaylistItem.cs b/osu.Game/Screens/OnlinePlay/DrawableRoomPlaylistItem.cs index 0afeaa9532..85a87b0dff 100644 --- a/osu.Game/Screens/OnlinePlay/DrawableRoomPlaylistItem.cs +++ b/osu.Game/Screens/OnlinePlay/DrawableRoomPlaylistItem.cs @@ -38,7 +38,6 @@ using osu.Game.Users.Drawables; using osuTK; using osuTK.Graphics; using osu.Game.Localisation; -using WebLocalisation = osu.Game.Resources.Localisation.Web; namespace osu.Game.Screens.OnlinePlay { @@ -561,7 +560,7 @@ namespace osu.Game.Screens.OnlinePlay Size = new Vector2(30, 30), Alpha = AllowEditing ? 1 : 0, Action = () => RequestEdit?.Invoke(Item), - TooltipText = WebLocalisation.CommonStrings.ButtonsEdit + TooltipText = Resources.Localisation.Web.CommonStrings.ButtonsEdit }, removeButton = new PlaylistRemoveButton { From 6021d85e633915a0092923ecf16f06fb1554ce66 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 16 Apr 2025 18:41:05 +0900 Subject: [PATCH 078/164] Add keywords for converted setting --- .../Settings/Sections/UserInterface/SongSelectSettings.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/osu.Game/Overlays/Settings/Sections/UserInterface/SongSelectSettings.cs b/osu.Game/Overlays/Settings/Sections/UserInterface/SongSelectSettings.cs index cb0d738a2c..d15008f858 100644 --- a/osu.Game/Overlays/Settings/Sections/UserInterface/SongSelectSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/UserInterface/SongSelectSettings.cs @@ -23,6 +23,7 @@ namespace osu.Game.Overlays.Settings.Sections.UserInterface { LabelText = UserInterfaceStrings.ShowConvertedBeatmaps, Current = config.GetBindable(OsuSetting.ShowConvertedBeatmaps), + Keywords = new[] { "converts", "converted" } }, new SettingsEnumDropdown { From 2ac1b8903727a49ababc1c89952895d1c98e7f1a Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 16 Apr 2025 18:42:32 +0900 Subject: [PATCH 079/164] Make some test methods static for future reuse --- .../SongSelect/TestSceneBeatmapInfoWedge.cs | 24 +++++++++---------- .../SongSelect/TestSceneBeatmapLeaderboard.cs | 12 ++++++---- 2 files changed, 19 insertions(+), 17 deletions(-) diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapInfoWedge.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapInfoWedge.cs index d8573b2d03..8132f8a841 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapInfoWedge.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapInfoWedge.cs @@ -69,7 +69,7 @@ namespace osu.Game.Tests.Visual.SongSelect foreach (var rulesetInfo in rulesets.AvailableRulesets) { var instance = rulesetInfo.CreateInstance(); - var testBeatmap = createTestBeatmap(rulesetInfo); + var testBeatmap = CreateTestBeatmap(rulesetInfo); beatmaps.Add(testBeatmap); @@ -124,6 +124,12 @@ namespace osu.Game.Tests.Visual.SongSelect AddStep("reset mods", () => SelectedMods.SetDefault()); } + [Test] + public void TestTruncation() + { + selectBeatmap(CreateLongMetadata()); + } + [Test] public void TestNullBeatmap() { @@ -135,17 +141,11 @@ namespace osu.Game.Tests.Visual.SongSelect AddAssert("check no info labels", () => !infoWedge.Info.ChildrenOfType().Any()); } - [Test] - public void TestTruncation() - { - selectBeatmap(createLongMetadata()); - } - [Test] public void TestBPMUpdates() { const double bpm = 120; - IBeatmap beatmap = createTestBeatmap(new OsuRuleset().RulesetInfo); + IBeatmap beatmap = CreateTestBeatmap(new OsuRuleset().RulesetInfo); beatmap.ControlPointInfo.Add(0, new TimingControlPoint { BeatLength = 60 * 1000 / bpm }); OsuModDoubleTime doubleTime = null!; @@ -167,7 +167,7 @@ namespace osu.Game.Tests.Visual.SongSelect [TestCase(120, 120.4, "DT", "180")] public void TestVaryingBPM(double commonBpm, double otherBpm, string? mod, string expectedDisplay) { - IBeatmap beatmap = createTestBeatmap(new OsuRuleset().RulesetInfo); + IBeatmap beatmap = CreateTestBeatmap(new OsuRuleset().RulesetInfo); beatmap.ControlPointInfo.Add(0, new TimingControlPoint { BeatLength = 60 * 1000 / commonBpm }); beatmap.ControlPointInfo.Add(100, new TimingControlPoint { BeatLength = 60 * 1000 / otherBpm }); beatmap.ControlPointInfo.Add(200, new TimingControlPoint { BeatLength = 60 * 1000 / commonBpm }); @@ -191,7 +191,7 @@ namespace osu.Game.Tests.Visual.SongSelect [TestCase] public void TestLengthUpdates() { - IBeatmap beatmap = createTestBeatmap(new OsuRuleset().RulesetInfo); + IBeatmap beatmap = CreateTestBeatmap(new OsuRuleset().RulesetInfo); double drain = beatmap.CalculateDrainLength(); beatmap.BeatmapInfo.Length = drain; @@ -248,7 +248,7 @@ namespace osu.Game.Tests.Visual.SongSelect AddUntilStep("wait for async load", () => infoWedge.DisplayedContent != containerBefore); } - private IBeatmap createTestBeatmap(RulesetInfo ruleset) + public static IBeatmap CreateTestBeatmap(RulesetInfo ruleset) { List objects = new List(); for (double i = 0; i < 50000; i += 1000) @@ -274,7 +274,7 @@ namespace osu.Game.Tests.Visual.SongSelect }; } - private IBeatmap createLongMetadata() + public static IBeatmap CreateLongMetadata() { return new Beatmap { diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapLeaderboard.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapLeaderboard.cs index 45381b3e02..70f2fb1361 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapLeaderboard.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapLeaderboard.cs @@ -42,6 +42,7 @@ namespace osu.Game.Tests.Visual.SongSelect private RulesetStore rulesetStore = null!; private BeatmapManager beatmapManager = null!; private PlaySongSelect songSelect = null!; + private LeaderboardManager leaderboardManager = null!; protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent) @@ -52,9 +53,10 @@ namespace osu.Game.Tests.Visual.SongSelect dependencies.Cache(beatmapManager = new BeatmapManager(LocalStorage, Realm, null, dependencies.Get(), Resources, dependencies.Get(), Beatmap.Default)); dependencies.Cache(scoreManager = new ScoreManager(rulesetStore, () => beatmapManager, LocalStorage, Realm, API)); dependencies.CacheAs(songSelect = new PlaySongSelect()); - Dependencies.Cache(Realm); dependencies.Cache(leaderboardManager = new LeaderboardManager()); + Dependencies.Cache(Realm); + return dependencies; } @@ -204,8 +206,8 @@ namespace osu.Game.Tests.Visual.SongSelect public void TestGlobalScoresDisplay() { AddStep(@"Set scope", () => leaderboard.Scope = BeatmapLeaderboardScope.Global); - AddStep(@"New Scores", () => leaderboard.SetScores(generateSampleScores(new BeatmapInfo()))); - AddStep(@"New Scores with teams", () => leaderboard.SetScores(generateSampleScores(new BeatmapInfo()).Select(s => + AddStep(@"New Scores", () => leaderboard.SetScores(GenerateSampleScores(new BeatmapInfo()))); + AddStep(@"New Scores with teams", () => leaderboard.SetScores(GenerateSampleScores(new BeatmapInfo()).Select(s => { s.User.Team = new APITeam(); return s; @@ -310,7 +312,7 @@ namespace osu.Game.Tests.Visual.SongSelect { AddStep(@"Import new scores", () => { - foreach (var score in generateSampleScores(beatmapInfo())) + foreach (var score in GenerateSampleScores(beatmapInfo())) scoreManager.Import(score); }); } @@ -326,7 +328,7 @@ namespace osu.Game.Tests.Visual.SongSelect private void checkStoredCount(int expected) => AddUntilStep($"Total scores stored is {expected}", () => Realm.Run(r => r.All().Count(s => !s.DeletePending)), () => Is.EqualTo(expected)); - private static ScoreInfo[] generateSampleScores(BeatmapInfo beatmapInfo) + public static ScoreInfo[] GenerateSampleScores(BeatmapInfo beatmapInfo) { return new[] { From 4c1f4a512cb89f36b4a30343267cc8cdb03c38a8 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 16 Apr 2025 18:42:57 +0900 Subject: [PATCH 080/164] Avoid adding arbitrary background in `SongSelectComponentsTestScene` --- .../SongSelectV2/SongSelectComponentsTestScene.cs | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/osu.Game.Tests/Visual/SongSelectV2/SongSelectComponentsTestScene.cs b/osu.Game.Tests/Visual/SongSelectV2/SongSelectComponentsTestScene.cs index 8694722acc..9e9cd3505a 100644 --- a/osu.Game.Tests/Visual/SongSelectV2/SongSelectComponentsTestScene.cs +++ b/osu.Game.Tests/Visual/SongSelectV2/SongSelectComponentsTestScene.cs @@ -4,7 +4,6 @@ using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; -using osu.Framework.Graphics.Shapes; using osu.Framework.Testing; using osu.Game.Graphics.Cursor; using osu.Game.Overlays; @@ -20,7 +19,6 @@ namespace osu.Game.Tests.Visual.SongSelectV2 { RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y, - Padding = new MarginPadding(10), }; private Container? resizeContainer; @@ -33,15 +31,9 @@ namespace osu.Game.Tests.Visual.SongSelectV2 { RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y, - Padding = new MarginPadding(10), Width = relativeWidth, Children = new Drawable[] { - new Box - { - RelativeSizeAxes = Axes.Both, - Colour = ColourProvider.Background5, - }, Content } }; @@ -55,6 +47,12 @@ namespace osu.Game.Tests.Visual.SongSelectV2 }); } + protected override void LoadComplete() + { + base.LoadComplete(); + ChangeBackgroundColour(ColourProvider.Background6); + } + [SetUpSteps] public virtual void SetUpSteps() { From 1cca936e285b008d136d279a4ea8773416a4f4a1 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 16 Apr 2025 18:43:32 +0900 Subject: [PATCH 081/164] Add global screen margin for new screen designs --- osu.Game/OsuGame.cs | 5 +++++ osu.Game/Screens/Footer/ScreenFooter.cs | 4 ++-- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs index 70a324cd8e..0c6a06a8fc 100644 --- a/osu.Game/OsuGame.cs +++ b/osu.Game/OsuGame.cs @@ -104,6 +104,11 @@ namespace osu.Game /// public static readonly Vector2 SHEAR = new Vector2(0.2f, 0); + /// + /// For elements placed close to the screen edge, this is the margin to leave to the edge. + /// + public const float SCREEN_EDGE_MARGIN = 12f; + public Toolbar Toolbar { get; private set; } private ChatOverlay chatOverlay; diff --git a/osu.Game/Screens/Footer/ScreenFooter.cs b/osu.Game/Screens/Footer/ScreenFooter.cs index f75250a832..94f4ceeb1a 100644 --- a/osu.Game/Screens/Footer/ScreenFooter.cs +++ b/osu.Game/Screens/Footer/ScreenFooter.cs @@ -104,14 +104,14 @@ namespace osu.Game.Screens.Footer }, BackButton = new ScreenBackButton { - Margin = new MarginPadding { Bottom = 15f, Left = 12f }, + Margin = new MarginPadding { Bottom = OsuGame.SCREEN_EDGE_MARGIN, Left = OsuGame.SCREEN_EDGE_MARGIN }, Anchor = Anchor.BottomLeft, Origin = Anchor.BottomLeft, Action = onBackPressed, }, hiddenButtonsContainer = new Container { - Margin = new MarginPadding { Left = 12f + ScreenBackButton.BUTTON_WIDTH + padding }, + Margin = new MarginPadding { Left = OsuGame.SCREEN_EDGE_MARGIN + ScreenBackButton.BUTTON_WIDTH + padding }, Y = 10f, Anchor = Anchor.BottomLeft, Origin = Anchor.BottomLeft, From 376b4e89299f61b05f441ebdee1923a245f4aa66 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 16 Apr 2025 18:45:48 +0900 Subject: [PATCH 082/164] Disable masking of `Carousel` The default for carousels should be unmasked as their usage generally sees them overflowing outside their main usage area (see `bleed` variables). --- osu.Game/Graphics/Carousel/Carousel.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/osu.Game/Graphics/Carousel/Carousel.cs b/osu.Game/Graphics/Carousel/Carousel.cs index a9c8aecd6c..3a02eb7119 100644 --- a/osu.Game/Graphics/Carousel/Carousel.cs +++ b/osu.Game/Graphics/Carousel/Carousel.cs @@ -228,6 +228,7 @@ namespace osu.Game.Graphics.Carousel { InternalChild = Scroll = new CarouselScrollContainer { + Masking = false, RelativeSizeAxes = Axes.Both, }; From 07d0c7443c6f9749277377b882fbf3211d0d282e Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 16 Apr 2025 18:46:37 +0900 Subject: [PATCH 083/164] Add animated fade when online status pill has an unknown status --- osu.Game/Beatmaps/Drawables/BeatmapSetOnlineStatusPill.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Beatmaps/Drawables/BeatmapSetOnlineStatusPill.cs b/osu.Game/Beatmaps/Drawables/BeatmapSetOnlineStatusPill.cs index 7b3067e8d6..c6a3c7db3c 100644 --- a/osu.Game/Beatmaps/Drawables/BeatmapSetOnlineStatusPill.cs +++ b/osu.Game/Beatmaps/Drawables/BeatmapSetOnlineStatusPill.cs @@ -100,7 +100,7 @@ namespace osu.Game.Beatmaps.Drawables { if (Status == BeatmapOnlineStatus.None && !ShowUnknownStatus) { - Hide(); + this.FadeOut(animation_duration, Easing.OutQuint); return; } From 51ad6289ca4b2417c02ae928957f8726551982da Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 16 Apr 2025 20:21:48 +0900 Subject: [PATCH 084/164] Fix global offset adjust control showing adjustment available when it shouldn't MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Audio offset is integer based in configuration, so let's make sure not to show that there's an applicable offset when the value difference is too low. I've also fixed rounding to match expectations (`AudioOffset` is precision limited to integer), and handled the case where a user adjusts the slider but also has a suggested offset – previously it would not enable the button after slider adjustments but now it will work as expected. --- ...estSceneHitEventTimingDistributionGraph.cs | 13 +++++ .../TestSceneAudioOffsetAdjustControl.cs | 51 +++++++++++++++++++ osu.Game/Localisation/AudioSettingsStrings.cs | 5 ++ .../Audio/AudioOffsetAdjustControl.cs | 22 ++++++-- 4 files changed, 86 insertions(+), 5 deletions(-) diff --git a/osu.Game.Tests/Visual/Ranking/TestSceneHitEventTimingDistributionGraph.cs b/osu.Game.Tests/Visual/Ranking/TestSceneHitEventTimingDistributionGraph.cs index 760210c370..bb4b785db0 100644 --- a/osu.Game.Tests/Visual/Ranking/TestSceneHitEventTimingDistributionGraph.cs +++ b/osu.Game.Tests/Visual/Ranking/TestSceneHitEventTimingDistributionGraph.cs @@ -168,6 +168,19 @@ namespace osu.Game.Tests.Visual.Ranking }; }); + public static List CreateHitEvents(double offset = 0, int count = 50) + { + var hitEvents = new List(); + + for (int i = 0; i < count; i++) + { + for (int j = 0; j < count; j++) + hitEvents.Add(new HitEvent(offset, 1.0, HitResult.Perfect, placeholder_object, placeholder_object, null)); + } + + return hitEvents; + } + public static List CreateDistributedHitEvents(double centre = 0, double range = 25) { var hitEvents = new List(); diff --git a/osu.Game.Tests/Visual/Settings/TestSceneAudioOffsetAdjustControl.cs b/osu.Game.Tests/Visual/Settings/TestSceneAudioOffsetAdjustControl.cs index 85cde966b1..2fc5378ba1 100644 --- a/osu.Game.Tests/Visual/Settings/TestSceneAudioOffsetAdjustControl.cs +++ b/osu.Game.Tests/Visual/Settings/TestSceneAudioOffsetAdjustControl.cs @@ -1,11 +1,14 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System.Linq; using NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Extensions.ObjectExtensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.UserInterface; +using osu.Framework.Testing; using osu.Framework.Utils; using osu.Game.Configuration; using osu.Game.Overlays.Settings.Sections.Audio; @@ -70,16 +73,54 @@ namespace osu.Game.Tests.Visual.Settings AddStep("clear history", () => tracker.ClearHistory()); } + [Test] + public void TestRounding() + { + AddStep("set new score", () => statics.SetValue(Static.LastLocalUserScore, new ScoreInfo + { + HitEvents = TestSceneHitEventTimingDistributionGraph.CreateHitEvents(0.6), + BeatmapInfo = Beatmap.Value.BeatmapInfo, + })); + + checkButtonEnabled(); + AddStep("click button", () => adjustControl.ChildrenOfType public static LocalisableString SuggestedOffsetNote => new TranslatableString(getKey(@"suggested_offset_note"), @"Play a few beatmaps to receive a suggested offset!"); + /// + /// "Based on the last {0} play(s), your offset is set correctly!" + /// + public static LocalisableString SuggestedOffsetCorrect(int plays) => new TranslatableString(getKey(@"suggested_offset_correct"), @"Based on the last {0} play(s), your offset is set correctly!", plays); + /// /// "Based on the last {0} play(s), the suggested offset is {1} ms." /// diff --git a/osu.Game/Overlays/Settings/Sections/Audio/AudioOffsetAdjustControl.cs b/osu.Game/Overlays/Settings/Sections/Audio/AudioOffsetAdjustControl.cs index b9f043a233..04496428ee 100644 --- a/osu.Game/Overlays/Settings/Sections/Audio/AudioOffsetAdjustControl.cs +++ b/osu.Game/Overlays/Settings/Sections/Audio/AudioOffsetAdjustControl.cs @@ -109,6 +109,7 @@ namespace osu.Game.Overlays.Settings.Sections.Audio base.LoadComplete(); averageHitErrorHistory.BindCollectionChanged(updateDisplay, true); + current.BindValueChanged(_ => updateHintText()); SuggestedOffset.BindValueChanged(_ => updateHintText(), true); } @@ -148,17 +149,28 @@ namespace osu.Game.Overlays.Settings.Sections.Audio break; } - SuggestedOffset.Value = averageHitErrorHistory.Any() ? averageHitErrorHistory.Average(dataPoint => dataPoint.SuggestedGlobalAudioOffset) : null; + SuggestedOffset.Value = averageHitErrorHistory.Any() ? Math.Round(averageHitErrorHistory.Average(dataPoint => dataPoint.SuggestedGlobalAudioOffset)) : null; } private float getXPositionForOffset(double offset) => (float)(Math.Clamp(offset, current.MinValue, current.MaxValue) / (2 * current.MaxValue)); private void updateHintText() { - hintText.Text = SuggestedOffset.Value == null - ? AudioSettingsStrings.SuggestedOffsetNote - : AudioSettingsStrings.SuggestedOffsetValueReceived(averageHitErrorHistory.Count, SuggestedOffset.Value.ToLocalisableString(@"N0")); - applySuggestion.Enabled.Value = SuggestedOffset.Value != null; + if (SuggestedOffset.Value == null) + { + applySuggestion.Enabled.Value = false; + hintText.Text = AudioSettingsStrings.SuggestedOffsetNote; + } + else if (Math.Abs(SuggestedOffset.Value.Value - current.Value) < 1) + { + applySuggestion.Enabled.Value = false; + hintText.Text = AudioSettingsStrings.SuggestedOffsetCorrect(averageHitErrorHistory.Count); + } + else + { + applySuggestion.Enabled.Value = true; + hintText.Text = AudioSettingsStrings.SuggestedOffsetValueReceived(averageHitErrorHistory.Count, SuggestedOffset.Value.ToLocalisableString(@"N0")); + } } private partial class OffsetSliderBar : RoundedSliderBar From c231571f06167b4445148bf29ac70c4facb3f8e3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 16 Apr 2025 13:46:35 +0200 Subject: [PATCH 085/164] Separate gameplay leaderboard data management from display This is a prerequisite for supporting skinning of leaderboards. - New `IGameplayLeaderboardProvider` and `IGameplayLeaderboardScore` interfaces are introduced. They are strictly concerned with supplying leaderboard data. - Logic of managing display, which was previously jammed into the inheritance hierarchy of `GameplayLeaderboard`, is now moved into `IGameplayLeaderboardProvider` implementations. Solo play, multiplayer, and multiplayer spectator get their own implementation of the interface. - The inheritance hierarchy of `GameplayLeaderboard` and per-player overriding of the implementation of the gameplay leaderboard is gone. Only one drawable class (renamed to `DrawableGameplayLeaderboard`) is allowed to display the leaderboards, across all modes of play. --- .../Gameplay/TestSceneGameplayLeaderboard.cs | 81 ++++++++++-- .../TestSceneSoloGameplayLeaderboard.cs | 124 ------------------ ...MultiplayerGameplayLeaderboardTestScene.cs | 39 +++++- .../TestSceneMultiSpectatorLeaderboard.cs | 31 +++-- .../TestSceneMultiSpectatorScreen.cs | 2 +- ...TestSceneMultiplayerGameplayLeaderboard.cs | 16 +-- ...ceneMultiplayerGameplayLeaderboardTeams.cs | 15 ++- .../Online/Leaderboards/LeaderboardManager.cs | 6 +- .../Multiplayer/MultiplayerPlayer.cs | 51 +++---- .../Spectate/MultiSpectatorScreen.cs | 28 ++-- ...oard.cs => DrawableGameplayLeaderboard.cs} | 59 +++++---- ...cs => DrawableGameplayLeaderboardScore.cs} | 26 ++-- .../Play/HUD/IGameplayLeaderboardScore.cs | 67 ++++++++++ .../Screens/Play/HUD/ILeaderboardScore.cs | 31 ----- .../Play/HUD/SoloGameplayLeaderboard.cs | 108 --------------- osu.Game/Screens/Play/Player.cs | 28 ++-- osu.Game/Screens/Play/ReplayPlayer.cs | 38 ++---- osu.Game/Screens/Play/SoloPlayer.cs | 57 ++------ .../Leaderboards/GameplayLeaderboardScore.cs | 59 +++++++++ .../IGameplayLeaderboardProvider.cs | 25 ++++ .../MultiSpectatorLeaderboardProvider.cs} | 7 +- .../MultiplayerLeaderboardProvider.cs} | 114 +++++++--------- .../SoloGameplayLeaderboardProvider.cs | 41 ++++++ 23 files changed, 508 insertions(+), 545 deletions(-) delete mode 100644 osu.Game.Tests/Visual/Gameplay/TestSceneSoloGameplayLeaderboard.cs rename osu.Game/Screens/Play/HUD/{GameplayLeaderboard.cs => DrawableGameplayLeaderboard.cs} (74%) rename osu.Game/Screens/Play/HUD/{GameplayLeaderboardScore.cs => DrawableGameplayLeaderboardScore.cs} (96%) create mode 100644 osu.Game/Screens/Play/HUD/IGameplayLeaderboardScore.cs delete mode 100644 osu.Game/Screens/Play/HUD/ILeaderboardScore.cs delete mode 100644 osu.Game/Screens/Play/HUD/SoloGameplayLeaderboard.cs create mode 100644 osu.Game/Screens/Select/Leaderboards/GameplayLeaderboardScore.cs create mode 100644 osu.Game/Screens/Select/Leaderboards/IGameplayLeaderboardProvider.cs rename osu.Game/Screens/{OnlinePlay/Multiplayer/Spectate/MultiSpectatorLeaderboard.cs => Select/Leaderboards/MultiSpectatorLeaderboardProvider.cs} (76%) rename osu.Game/Screens/{Play/HUD/MultiplayerGameplayLeaderboard.cs => Select/Leaderboards/MultiplayerLeaderboardProvider.cs} (68%) create mode 100644 osu.Game/Screens/Select/Leaderboards/SoloGameplayLeaderboardProvider.cs diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneGameplayLeaderboard.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneGameplayLeaderboard.cs index 1787230117..23cd262dd0 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneGameplayLeaderboard.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneGameplayLeaderboard.cs @@ -1,11 +1,11 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - +using System; using System.Collections.Generic; using System.Linq; using NUnit.Framework; +using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Extensions.PolygonExtensions; using osu.Framework.Graphics; @@ -15,7 +15,10 @@ using osu.Framework.Testing; using osu.Framework.Utils; using osu.Game.Online.API; using osu.Game.Online.API.Requests.Responses; +using osu.Game.Rulesets.Scoring; using osu.Game.Screens.Play.HUD; +using osu.Game.Screens.Select.Leaderboards; +using osu.Game.Users; using osuTK; namespace osu.Game.Tests.Visual.Gameplay @@ -23,7 +26,10 @@ namespace osu.Game.Tests.Visual.Gameplay [TestFixture] public partial class TestSceneGameplayLeaderboard : OsuTestScene { - private TestGameplayLeaderboard leaderboard; + private TestDrawableGameplayLeaderboard leaderboard = null!; + + [Cached(typeof(IGameplayLeaderboardProvider))] + private TestGameplayLeaderboardProvider leaderboardProvider = new TestGameplayLeaderboardProvider(); private readonly BindableLong playerScore = new BindableLong(); @@ -57,10 +63,10 @@ namespace osu.Game.Tests.Visual.Gameplay // has caused layout to not work in the past. AddUntilStep("wait for fill flow layout", - () => leaderboard.ChildrenOfType>().First().ScreenSpaceDrawQuad.Intersects(leaderboard.ScreenSpaceDrawQuad)); + () => leaderboard.ChildrenOfType>().First().ScreenSpaceDrawQuad.Intersects(leaderboard.ScreenSpaceDrawQuad)); AddUntilStep("wait for some scores not masked away", - () => leaderboard.ChildrenOfType().Any(s => leaderboard.ScreenSpaceDrawQuad.Contains(s.ScreenSpaceDrawQuad.Centre))); + () => leaderboard.ChildrenOfType().Any(s => leaderboard.ScreenSpaceDrawQuad.Contains(s.ScreenSpaceDrawQuad.Centre))); AddUntilStep("wait for tracked score fully visible", () => leaderboard.ScreenSpaceDrawQuad.Intersects(leaderboard.TrackedScore!.ScreenSpaceDrawQuad)); @@ -139,7 +145,7 @@ namespace osu.Game.Tests.Visual.Gameplay checkHeight(8); void checkHeight(int panelCount) - => AddAssert($"leaderboard height is {panelCount} panels high", () => leaderboard.DrawHeight == (GameplayLeaderboardScore.PANEL_HEIGHT + leaderboard.Spacing) * panelCount); + => AddAssert($"leaderboard height is {panelCount} panels high", () => leaderboard.DrawHeight == (DrawableGameplayLeaderboardScore.PANEL_HEIGHT + leaderboard.Spacing) * panelCount); } [Test] @@ -179,6 +185,27 @@ namespace osu.Game.Tests.Visual.Gameplay () => Does.Contain("#FF549A")); } + [Test] + public void TestTrackedScorePosition([Values] bool partial) + { + createLeaderboard(partial); + + AddStep("add many scores in one go", () => + { + for (int i = 0; i < 49; i++) + createRandomScore(new APIUser { Username = $"Player {i + 1}" }); + + // Add player at end to force an animation down the whole list. + playerScore.Value = 0; + createLeaderboardScore(playerScore, new APIUser { Username = "You", Id = 3 }, true); + }); + + if (partial) + AddUntilStep("tracked player has null position", () => leaderboard.TrackedScore?.ScorePosition, () => Is.Null); + else + AddUntilStep("tracked player is #50", () => leaderboard.TrackedScore?.ScorePosition, () => Is.EqualTo(50)); + } + private void addLocalPlayer() { AddStep("add local player", () => @@ -188,11 +215,13 @@ namespace osu.Game.Tests.Visual.Gameplay }); } - private void createLeaderboard() + private void createLeaderboard(bool partial = false) { AddStep("create leaderboard", () => { - Child = leaderboard = new TestGameplayLeaderboard + leaderboardProvider.Scores.Clear(); + leaderboardProvider.IsPartial = partial; + Child = leaderboard = new TestDrawableGameplayLeaderboard { Anchor = Anchor.Centre, Origin = Anchor.Centre, @@ -205,11 +234,11 @@ namespace osu.Game.Tests.Visual.Gameplay private void createLeaderboardScore(BindableLong score, APIUser user, bool isTracked = false) { - var leaderboardScore = leaderboard.Add(user, isTracked); - leaderboardScore.TotalScore.BindTo(score); + var leaderboardScore = new TestDrawableGameplayLeaderboardScore(user, isTracked, score); + leaderboardProvider.Scores.Add(leaderboardScore); } - private partial class TestGameplayLeaderboard : GameplayLeaderboard + private partial class TestDrawableGameplayLeaderboard : DrawableGameplayLeaderboard { public float Spacing => Flow.Spacing.Y; @@ -220,8 +249,36 @@ namespace osu.Game.Tests.Visual.Gameplay return scoreItem != null && scoreItem.ScorePosition == expectedPosition; } - public IEnumerable GetAllScoresForUsername(string username) + public IEnumerable GetAllScoresForUsername(string username) => Flow.Where(i => i.User?.Username == username); } + + private class TestGameplayLeaderboardProvider : IGameplayLeaderboardProvider + { + IBindableList IGameplayLeaderboardProvider.Scores => Scores; + public BindableList Scores { get; } = new BindableList(); + public bool IsPartial { get; set; } + } + + private class TestDrawableGameplayLeaderboardScore : IGameplayLeaderboardScore + { + public IUser User { get; } + public bool Tracked { get; } + public BindableLong TotalScore { get; } = new BindableLong(); + public BindableDouble Accuracy { get; } = new BindableDouble(); + public BindableInt Combo { get; } = new BindableInt(); + public BindableBool HasQuit { get; } = new BindableBool(); + public Bindable DisplayOrder { get; } = new BindableLong(); + public Func GetDisplayScore { get; set; } + public Colour4? TeamColour => null; + + public TestDrawableGameplayLeaderboardScore(IUser user, bool isTracked, Bindable totalScore) + { + User = user; + Tracked = isTracked; + TotalScore.BindTo(totalScore); + GetDisplayScore = _ => TotalScore.Value; + } + } } } diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSoloGameplayLeaderboard.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSoloGameplayLeaderboard.cs deleted file mode 100644 index dbd14db818..0000000000 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneSoloGameplayLeaderboard.cs +++ /dev/null @@ -1,124 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -using System.Collections.Generic; -using System.Linq; -using NUnit.Framework; -using osu.Framework.Allocation; -using osu.Framework.Bindables; -using osu.Framework.Graphics; -using osu.Framework.Testing; -using osu.Framework.Utils; -using osu.Game.Configuration; -using osu.Game.Online.API.Requests.Responses; -using osu.Game.Rulesets.Osu; -using osu.Game.Rulesets.Scoring; -using osu.Game.Scoring; -using osu.Game.Screens.Play.HUD; -using osu.Game.Screens.Select; -using osu.Game.Tests.Gameplay; - -namespace osu.Game.Tests.Visual.Gameplay -{ - public partial class TestSceneSoloGameplayLeaderboard : OsuTestScene - { - [Cached(typeof(ScoreProcessor))] - private readonly ScoreProcessor scoreProcessor = TestGameplayState.Create(new OsuRuleset()).ScoreProcessor; - - private readonly BindableList scores = new BindableList(); - - private readonly Bindable configVisibility = new Bindable(); - private readonly Bindable beatmapTabType = new Bindable(); - - private SoloGameplayLeaderboard leaderboard = null!; - - [BackgroundDependencyLoader] - private void load(OsuConfigManager config) - { - config.BindWith(OsuSetting.GameplayLeaderboard, configVisibility); - config.BindWith(OsuSetting.BeatmapDetailTab, beatmapTabType); - } - - [SetUpSteps] - public void SetUpSteps() - { - AddStep("clear scores", () => scores.Clear()); - - AddStep("create component", () => - { - var trackingUser = new APIUser - { - Username = "local user", - Id = 2, - }; - - Child = leaderboard = new SoloGameplayLeaderboard(trackingUser) - { - Scores = { BindTarget = scores }, - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - AlwaysVisible = { Value = false }, - Expanded = { Value = true }, - }; - }); - - AddStep("add scores", () => scores.AddRange(createSampleScores())); - } - - [Test] - public void TestLocalUser() - { - AddSliderStep("score", 0, 1000000, 500000, v => scoreProcessor.TotalScore.Value = v); - AddSliderStep("accuracy", 0f, 1f, 0.5f, v => scoreProcessor.Accuracy.Value = v); - AddSliderStep("combo", 0, 10000, 0, v => scoreProcessor.HighestCombo.Value = v); - AddStep("toggle expanded", () => leaderboard.Expanded.Value = !leaderboard.Expanded.Value); - } - - [TestCase(PlayBeatmapDetailArea.TabType.Local, 51)] - [TestCase(PlayBeatmapDetailArea.TabType.Global, null)] - [TestCase(PlayBeatmapDetailArea.TabType.Country, null)] - [TestCase(PlayBeatmapDetailArea.TabType.Friends, null)] - public void TestTrackedScorePosition(PlayBeatmapDetailArea.TabType tabType, int? expectedOverflowIndex) - { - AddStep($"change TabType to {tabType}", () => beatmapTabType.Value = tabType); - AddUntilStep("tracked player is #50", () => leaderboard.TrackedScore?.ScorePosition, () => Is.EqualTo(50)); - - AddStep("add one more score", () => scores.Add(new ScoreInfo { User = new APIUser { Username = "New player 1" }, TotalScore = RNG.Next(600000, 1000000) })); - - AddUntilStep("wait for sort", () => leaderboard.ChildrenOfType().First().ScorePosition != null); - - if (expectedOverflowIndex == null) - AddUntilStep("tracked player has null position", () => leaderboard.TrackedScore?.ScorePosition, () => Is.Null); - else - AddUntilStep($"tracked player is #{expectedOverflowIndex}", () => leaderboard.TrackedScore?.ScorePosition, () => Is.EqualTo(expectedOverflowIndex)); - } - - [Test] - public void TestVisibility() - { - AddStep("set config visible true", () => configVisibility.Value = true); - AddUntilStep("leaderboard visible", () => leaderboard.Alpha == 1); - - AddStep("set config visible false", () => configVisibility.Value = false); - AddUntilStep("leaderboard not visible", () => leaderboard.Alpha == 0); - - AddStep("set always visible", () => leaderboard.AlwaysVisible.Value = true); - AddUntilStep("leaderboard visible", () => leaderboard.Alpha == 1); - - AddStep("set config visible true", () => configVisibility.Value = true); - AddAssert("leaderboard still visible", () => leaderboard.Alpha == 1); - } - - private static List createSampleScores() - { - return new[] - { - new ScoreInfo { User = new APIUser { Username = @"peppy" }, TotalScore = RNG.Next(500000, 1000000) }, - new ScoreInfo { User = new APIUser { Username = @"smoogipoo" }, TotalScore = RNG.Next(500000, 1000000) }, - new ScoreInfo { User = new APIUser { Username = @"spaceman_atlas" }, TotalScore = RNG.Next(500000, 1000000) }, - new ScoreInfo { User = new APIUser { Username = @"frenzibyte" }, TotalScore = RNG.Next(500000, 1000000) }, - new ScoreInfo { User = new APIUser { Username = @"Susko3" }, TotalScore = RNG.Next(500000, 1000000) }, - }.Concat(Enumerable.Range(0, 44).Select(i => new ScoreInfo { User = new APIUser { Username = $"User {i + 1}" }, TotalScore = 1000000 - i * 10000 })).ToList(); - } - } -} diff --git a/osu.Game.Tests/Visual/Multiplayer/MultiplayerGameplayLeaderboardTestScene.cs b/osu.Game.Tests/Visual/Multiplayer/MultiplayerGameplayLeaderboardTestScene.cs index 1eb08ad3c8..644b7f522e 100644 --- a/osu.Game.Tests/Visual/Multiplayer/MultiplayerGameplayLeaderboardTestScene.cs +++ b/osu.Game.Tests/Visual/Multiplayer/MultiplayerGameplayLeaderboardTestScene.cs @@ -10,6 +10,7 @@ using Moq; using NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Bindables; +using osu.Framework.Graphics; using osu.Framework.Testing; using osu.Framework.Utils; using osu.Game.Configuration; @@ -20,6 +21,7 @@ using osu.Game.Online.Spectator; using osu.Game.Replays.Legacy; using osu.Game.Rulesets.Scoring; using osu.Game.Screens.Play.HUD; +using osu.Game.Screens.Select.Leaderboards; namespace osu.Game.Tests.Visual.Multiplayer { @@ -29,11 +31,13 @@ namespace osu.Game.Tests.Visual.Multiplayer protected readonly BindableList MultiplayerUsers = new BindableList(); - protected MultiplayerGameplayLeaderboard? Leaderboard { get; private set; } + protected MultiplayerLeaderboardProvider? LeaderboardProvider { get; private set; } + + protected DrawableGameplayLeaderboard? Leaderboard { get; private set; } protected virtual MultiplayerRoomUser CreateUser(int userId) => new MultiplayerRoomUser(userId); - protected abstract MultiplayerGameplayLeaderboard CreateLeaderboard(); + protected abstract MultiplayerLeaderboardProvider CreateLeaderboardProvider(); private readonly BindableList multiplayerUserIds = new BindableList(); private readonly BindableDictionary watchedUserStates = new BindableDictionary(); @@ -124,11 +128,21 @@ namespace osu.Game.Tests.Visual.Multiplayer AddStep("create leaderboard", () => { - Leaderboard?.Expire(); + Clear(true); Beatmap.Value = CreateWorkingBeatmap(Ruleset.Value); - LoadComponentAsync(Leaderboard = CreateLeaderboard(), Add); + LoadComponentAsync(LeaderboardProvider = CreateLeaderboardProvider(), Add); + Add(new DependencyProvidingContainer + { + RelativeSizeAxes = Axes.Both, + CachedDependencies = [(typeof(IGameplayLeaderboardProvider), LeaderboardProvider)], + Child = Leaderboard = new DrawableGameplayLeaderboard + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + } + }); }); AddUntilStep("wait for load", () => Leaderboard!.IsLoaded); @@ -159,10 +173,18 @@ namespace osu.Game.Tests.Visual.Multiplayer return false; }); - AddStep("check stop watching requests were sent", () => + AddUntilStep("check stop watching requests were sent", () => { - foreach (var user in MultiplayerUsers) - spectatorClient.Verify(s => s.StopWatchingUser(user.UserID), Times.Once); + try + { + foreach (var user in MultiplayerUsers) + spectatorClient.Verify(s => s.StopWatchingUser(user.UserID), Times.Once); + return true; + } + catch + { + return false; + } }); } @@ -204,12 +226,14 @@ namespace osu.Game.Tests.Visual.Multiplayer header.Combo++; header.MaxCombo = Math.Max(header.MaxCombo, header.Combo); header.Statistics[HitResult.Meh]++; + header.TotalScore += 50; break; default: header.Combo++; header.MaxCombo = Math.Max(header.MaxCombo, header.Combo); header.Statistics[HitResult.Great]++; + header.TotalScore += 300; break; } @@ -218,3 +242,4 @@ namespace osu.Game.Tests.Visual.Multiplayer } } } + diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiSpectatorLeaderboard.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiSpectatorLeaderboard.cs index 60358dfbc4..806de68f07 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiSpectatorLeaderboard.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiSpectatorLeaderboard.cs @@ -9,15 +9,16 @@ using osu.Framework.Testing; using osu.Framework.Timing; using osu.Game.Online.API.Requests.Responses; using osu.Game.Online.Multiplayer; -using osu.Game.Screens.OnlinePlay.Multiplayer.Spectate; using osu.Game.Screens.Play.HUD; +using osu.Game.Screens.Select.Leaderboards; namespace osu.Game.Tests.Visual.Multiplayer { public partial class TestSceneMultiSpectatorLeaderboard : MultiplayerTestScene { private Dictionary clocks = null!; - private MultiSpectatorLeaderboard? leaderboard; + private MultiSpectatorLeaderboardProvider? leaderboardProvider; + private DrawableGameplayLeaderboard leaderboard = null!; [SetUpSteps] public override void SetUpSteps() @@ -29,7 +30,7 @@ namespace osu.Game.Tests.Visual.Multiplayer AddStep("reset", () => { - leaderboard?.RemoveAndDisposeImmediately(); + Clear(true); clocks = new Dictionary { @@ -48,21 +49,27 @@ namespace osu.Game.Tests.Visual.Multiplayer { Beatmap.Value = CreateWorkingBeatmap(Ruleset.Value); - LoadComponentAsync(leaderboard = new MultiSpectatorLeaderboard(clocks.Keys.Select(id => new MultiplayerRoomUser(id)).ToArray()) + LoadComponentAsync(leaderboardProvider = new MultiSpectatorLeaderboardProvider(clocks.Keys.Select(id => new MultiplayerRoomUser(id)).ToArray()), Add); + Add(new DependencyProvidingContainer { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Expanded = { Value = true } - }, Add); + RelativeSizeAxes = Axes.Both, + CachedDependencies = [(typeof(IGameplayLeaderboardProvider), leaderboardProvider)], + Child = leaderboard = new DrawableGameplayLeaderboard + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Expanded = { Value = true } + } + }); }); - AddUntilStep("wait for load", () => leaderboard!.IsLoaded); - AddUntilStep("wait for user population", () => leaderboard.ChildrenOfType().Count() == 2); + AddUntilStep("wait for load", () => leaderboard.IsLoaded); + AddUntilStep("wait for user population", () => leaderboard.ChildrenOfType().Count() == 2); AddStep("add clock sources", () => { foreach ((int userId, var clock) in clocks) - leaderboard!.AddClock(userId, clock); + leaderboardProvider!.AddClock(userId, clock); }); } @@ -123,6 +130,6 @@ namespace osu.Game.Tests.Visual.Multiplayer => AddStep($"set user {userId} time {time}", () => clocks[userId].CurrentTime = time); private void assertCombo(int userId, int expectedCombo) - => AddUntilStep($"player {userId} has {expectedCombo} combo", () => this.ChildrenOfType().Single(s => s.User?.OnlineID == userId).Combo.Value == expectedCombo); + => AddUntilStep($"player {userId} has {expectedCombo} combo", () => this.ChildrenOfType().Single(s => s.User?.OnlineID == userId).Combo.Value == expectedCombo); } } diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiSpectatorScreen.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiSpectatorScreen.cs index aa98dc59db..6f6d7b31b5 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiSpectatorScreen.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiSpectatorScreen.cs @@ -560,7 +560,7 @@ namespace osu.Game.Tests.Visual.Multiplayer private PlayerArea getInstance(int userId) => spectatorScreen.ChildrenOfType().Single(p => p.UserId == userId); - private GameplayLeaderboardScore getLeaderboardScore(int userId) => spectatorScreen.ChildrenOfType().Single(s => s.User?.OnlineID == userId); + private DrawableGameplayLeaderboardScore getLeaderboardScore(int userId) => spectatorScreen.ChildrenOfType().Single(s => s.User?.OnlineID == userId); private int[] getPlayerIds(int count) => Enumerable.Range(PLAYER_1_ID, count).ToArray(); } diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerGameplayLeaderboard.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerGameplayLeaderboard.cs index 2f232a6164..53e265decb 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerGameplayLeaderboard.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerGameplayLeaderboard.cs @@ -9,7 +9,7 @@ using osu.Game.Online.API; using osu.Game.Online.Multiplayer; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Osu.Mods; -using osu.Game.Screens.Play.HUD; +using osu.Game.Screens.Select.Leaderboards; namespace osu.Game.Tests.Visual.Multiplayer { @@ -25,27 +25,25 @@ namespace osu.Game.Tests.Visual.Multiplayer return user; } - protected override MultiplayerGameplayLeaderboard CreateLeaderboard() - { - return new TestLeaderboard(MultiplayerUsers.ToArray()) + protected override MultiplayerLeaderboardProvider CreateLeaderboardProvider() => + new TestLeaderboard(MultiplayerUsers.ToArray()) { Anchor = Anchor.Centre, Origin = Anchor.Centre, }; - } [Test] public void TestPerUserMods() { - AddStep("first user has no mods", () => Assert.That(((TestLeaderboard)Leaderboard!).UserMods[0], Is.Empty)); + AddStep("first user has no mods", () => Assert.That(((TestLeaderboard)LeaderboardProvider!).UserMods[0], Is.Empty)); AddStep("last user has NF mod", () => { - Assert.That(((TestLeaderboard)Leaderboard!).UserMods[TOTAL_USERS - 1], Has.One.Items); - Assert.That(((TestLeaderboard)Leaderboard).UserMods[TOTAL_USERS - 1].Single(), Is.TypeOf()); + Assert.That(((TestLeaderboard)LeaderboardProvider!).UserMods[TOTAL_USERS - 1], Has.One.Items); + Assert.That(((TestLeaderboard)LeaderboardProvider).UserMods[TOTAL_USERS - 1].Single(), Is.TypeOf()); }); } - private partial class TestLeaderboard : MultiplayerGameplayLeaderboard + private partial class TestLeaderboard : MultiplayerLeaderboardProvider { public Dictionary> UserMods => UserScores.ToDictionary(kvp => kvp.Key, kvp => kvp.Value.ScoreProcessor.Mods); diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerGameplayLeaderboardTeams.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerGameplayLeaderboardTeams.cs index 3f1db308c0..15efde7abe 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerGameplayLeaderboardTeams.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerGameplayLeaderboardTeams.cs @@ -7,6 +7,7 @@ using osu.Game.Online.Multiplayer; using osu.Game.Online.Multiplayer.MatchTypes.TeamVersus; using osu.Game.Screens.OnlinePlay.Multiplayer; using osu.Game.Screens.Play.HUD; +using osu.Game.Screens.Select.Leaderboards; namespace osu.Game.Tests.Visual.Multiplayer { @@ -24,8 +25,8 @@ namespace osu.Game.Tests.Visual.Multiplayer return user; } - protected override MultiplayerGameplayLeaderboard CreateLeaderboard() => - new MultiplayerGameplayLeaderboard(MultiplayerUsers.ToArray()) + protected override MultiplayerLeaderboardProvider CreateLeaderboardProvider() => + new MultiplayerLeaderboardProvider(MultiplayerUsers.ToArray()) { Anchor = Anchor.Centre, Origin = Anchor.Centre, @@ -39,17 +40,17 @@ namespace osu.Game.Tests.Visual.Multiplayer { LoadComponentAsync(new MatchScoreDisplay { - Team1Score = { BindTarget = Leaderboard!.TeamScores[0] }, - Team2Score = { BindTarget = Leaderboard.TeamScores[1] } + Team1Score = { BindTarget = LeaderboardProvider!.TeamScores[0] }, + Team2Score = { BindTarget = LeaderboardProvider.TeamScores[1] } }, Add); LoadComponentAsync(new GameplayMatchScoreDisplay { Anchor = Anchor.BottomCentre, Origin = Anchor.BottomCentre, - Team1Score = { BindTarget = Leaderboard.TeamScores[0] }, - Team2Score = { BindTarget = Leaderboard.TeamScores[1] }, - Expanded = { BindTarget = Leaderboard.Expanded }, + Team1Score = { BindTarget = LeaderboardProvider.TeamScores[0] }, + Team2Score = { BindTarget = LeaderboardProvider.TeamScores[1] }, + Expanded = { BindTarget = Leaderboard!.Expanded }, }, Add); }); } diff --git a/osu.Game/Online/Leaderboards/LeaderboardManager.cs b/osu.Game/Online/Leaderboards/LeaderboardManager.cs index ff3fe39a96..121f68c12b 100644 --- a/osu.Game/Online/Leaderboards/LeaderboardManager.cs +++ b/osu.Game/Online/Leaderboards/LeaderboardManager.cs @@ -94,7 +94,7 @@ namespace osu.Game.Online.Leaderboards var result = new LeaderboardScores ( - response.Scores.Select(s => s.ToScoreInfo(rulesets, newCriteria.Beatmap)).OrderByTotalScore(), + response.Scores.Select(s => s.ToScoreInfo(rulesets, newCriteria.Beatmap)).OrderByTotalScore().ToArray(), response.UserScore?.CreateScoreInfo(rulesets, newCriteria.Beatmap) ); inFlightOnlineRequest = null; @@ -138,7 +138,7 @@ namespace osu.Game.Online.Leaderboards newScores = newScores.Detach().OrderByTotalScore(); - scores.Value = new LeaderboardScores(newScores, null); + scores.Value = new LeaderboardScores(newScores.ToArray(), null); if (localFetchCompletionSource != null && localFetchCompletionSource == lastFetchCompletionSource) { @@ -155,7 +155,7 @@ namespace osu.Game.Online.Leaderboards Mod[]? ExactMods ); - public record LeaderboardScores(IEnumerable TopScores, ScoreInfo? UserScore) + public record LeaderboardScores(ICollection TopScores, ScoreInfo? UserScore) { public IEnumerable AllScores { diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerPlayer.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerPlayer.cs index 3d4b46f49e..d6f5529d4a 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerPlayer.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerPlayer.cs @@ -15,8 +15,8 @@ using osu.Game.Online.Multiplayer; using osu.Game.Online.Rooms; using osu.Game.Scoring; using osu.Game.Screens.Play; -using osu.Game.Screens.Play.HUD; using osu.Game.Screens.Ranking; +using osu.Game.Screens.Select.Leaderboards; using osu.Game.Users; namespace osu.Game.Screens.OnlinePlay.Multiplayer @@ -25,6 +25,8 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer { protected override bool PauseOnFocusLost => false; + protected override bool ShowLeaderboard => true; + protected override UserActivity InitialActivity => new UserActivity.InMultiplayerGame(Beatmap.Value.BeatmapInfo, Ruleset.Value); [Resolved] @@ -33,10 +35,13 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer private IBindable isConnected = null!; private readonly TaskCompletionSource resultsReady = new TaskCompletionSource(); - private readonly MultiplayerRoomUser[] users; private LoadingLayer loadingDisplay = null!; - private MultiplayerGameplayLeaderboard multiplayerLeaderboard = null!; + + [Cached(typeof(IGameplayLeaderboardProvider))] + private readonly MultiplayerLeaderboardProvider leaderboardProvider; + + private GameplayMatchScoreDisplay teamScoreDisplay = null!; /// /// Construct a multiplayer player. @@ -55,7 +60,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer AlwaysShowLeaderboard = true, }) { - this.users = users; + leaderboardProvider = new MultiplayerLeaderboardProvider(users); } [BackgroundDependencyLoader] @@ -71,26 +76,24 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer Expanded = { BindTarget = LeaderboardExpandedState }, }, chat => HUDOverlay.LeaderboardFlow.Insert(2, chat)); - HUDOverlay.Add(loadingDisplay = new LoadingLayer(true) { Depth = float.MaxValue }); - } - - protected override GameplayLeaderboard CreateGameplayLeaderboard() => multiplayerLeaderboard = new MultiplayerGameplayLeaderboard(users); - - protected override void AddLeaderboardToHUD(GameplayLeaderboard leaderboard) - { - Debug.Assert(leaderboard == multiplayerLeaderboard); - - HUDOverlay.LeaderboardFlow.Insert(0, leaderboard); - - if (multiplayerLeaderboard.TeamScores.Count >= 2) + LoadComponentAsync(teamScoreDisplay = new GameplayMatchScoreDisplay { - LoadComponentAsync(new GameplayMatchScoreDisplay + Expanded = { BindTarget = HUDOverlay.ShowHud }, + Alpha = 0, + }, scoreDisplay => HUDOverlay.LeaderboardFlow.Insert(1, scoreDisplay)); + LoadComponentAsync(leaderboardProvider, loaded => + { + AddInternal(loaded); + + if (loaded.HasTeams) { - Team1Score = { BindTarget = multiplayerLeaderboard.TeamScores.First().Value }, - Team2Score = { BindTarget = multiplayerLeaderboard.TeamScores.Last().Value }, - Expanded = { BindTarget = HUDOverlay.ShowHud }, - }, scoreDisplay => HUDOverlay.LeaderboardFlow.Insert(1, scoreDisplay)); - } + teamScoreDisplay.Alpha = 1; + teamScoreDisplay.Team1Score.BindTarget = leaderboardProvider.TeamScores.First().Value; + teamScoreDisplay.Team2Score.BindTarget = leaderboardProvider.TeamScores.Last().Value; + } + }); + + HUDOverlay.Add(loadingDisplay = new LoadingLayer(true) { Depth = float.MaxValue }); } protected override void LoadAsyncComplete() @@ -195,8 +198,8 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer { Debug.Assert(Room.RoomID != null); - return multiplayerLeaderboard.TeamScores.Count == 2 - ? new MultiplayerTeamResultsScreen(score, Room.RoomID.Value, PlaylistItem, multiplayerLeaderboard.TeamScores) + return leaderboardProvider.TeamScores.Count == 2 + ? new MultiplayerTeamResultsScreen(score, Room.RoomID.Value, PlaylistItem, leaderboardProvider.TeamScores) { IsLocalPlay = true, } diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorScreen.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorScreen.cs index 33c3c60ed3..85b6966eaa 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorScreen.cs @@ -15,6 +15,7 @@ using osu.Game.Online.Rooms; using osu.Game.Online.Spectator; using osu.Game.Screens.Play; using osu.Game.Screens.Play.HUD; +using osu.Game.Screens.Select.Leaderboards; using osu.Game.Screens.Spectate; using osu.Game.Users; using osuTK; @@ -47,17 +48,18 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate [Resolved] private MultiplayerClient multiplayerClient { get; set; } = null!; + [Cached(typeof(IGameplayLeaderboardProvider))] + private MultiSpectatorLeaderboardProvider leaderboardProvider { get; set; } + private IAggregateAudioAdjustment? boundAdjustments; private readonly PlayerArea[] instances; private MasterGameplayClockContainer masterClockContainer = null!; private SpectatorSyncManager syncManager = null!; private PlayerGrid grid = null!; - private MultiSpectatorLeaderboard leaderboard = null!; private PlayerArea? currentAudioSource; private readonly Room room; - private readonly MultiplayerRoomUser[] users; /// /// Creates a new . @@ -68,9 +70,9 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate : base(users.Select(u => u.UserID).ToArray()) { this.room = room; - this.users = users; instances = new PlayerArea[Users.Count]; + leaderboardProvider = new MultiSpectatorLeaderboardProvider(users); } [BackgroundDependencyLoader] @@ -133,25 +135,25 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate for (int i = 0; i < Users.Count; i++) grid.Add(instances[i] = new PlayerArea(Users[i], syncManager.CreateManagedClock())); - LoadComponentAsync(leaderboard = new MultiSpectatorLeaderboard(users) - { - Expanded = { Value = true }, - }, _ => + LoadComponentAsync(leaderboardProvider, _ => { + AddInternal(leaderboardProvider); foreach (var instance in instances) - leaderboard.AddClock(instance.UserId, instance.SpectatorPlayerClock); + leaderboardProvider.AddClock(instance.UserId, instance.SpectatorPlayerClock); - leaderboardFlow.Insert(0, leaderboard); - - if (leaderboard.TeamScores.Count == 2) + if (leaderboardProvider.TeamScores.Count == 2) { LoadComponentAsync(new MatchScoreDisplay { - Team1Score = { BindTarget = leaderboard.TeamScores.First().Value }, - Team2Score = { BindTarget = leaderboard.TeamScores.Last().Value }, + Team1Score = { BindTarget = leaderboardProvider.TeamScores.First().Value }, + Team2Score = { BindTarget = leaderboardProvider.TeamScores.Last().Value }, }, scoreDisplayContainer.Add); } }); + leaderboardFlow.Insert(0, new DrawableGameplayLeaderboard + { + Expanded = { Value = true } + }); LoadComponentAsync(new GameplayChatDisplay(room) { diff --git a/osu.Game/Screens/Play/HUD/GameplayLeaderboard.cs b/osu.Game/Screens/Play/HUD/DrawableGameplayLeaderboard.cs similarity index 74% rename from osu.Game/Screens/Play/HUD/GameplayLeaderboard.cs rename to osu.Game/Screens/Play/HUD/DrawableGameplayLeaderboard.cs index f6694505dc..85f5281bef 100644 --- a/osu.Game/Screens/Play/HUD/GameplayLeaderboard.cs +++ b/osu.Game/Screens/Play/HUD/DrawableGameplayLeaderboard.cs @@ -3,6 +3,7 @@ using System; using System.Linq; +using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Caching; using osu.Framework.Extensions.Color4Extensions; @@ -10,33 +11,39 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Colour; using osu.Framework.Graphics.Containers; using osu.Game.Graphics.Containers; -using osu.Game.Users; +using osu.Game.Screens.Select.Leaderboards; +using osu.Game.Skinning; using osuTK; using osuTK.Graphics; namespace osu.Game.Screens.Play.HUD { - public abstract partial class GameplayLeaderboard : CompositeDrawable + public partial class DrawableGameplayLeaderboard : CompositeDrawable { private readonly Cached sorting = new Cached(); public Bindable Expanded = new Bindable(); - protected readonly FillFlowContainer Flow; + protected readonly FillFlowContainer Flow; private bool requiresScroll; private readonly OsuScrollContainer scroll; - public GameplayLeaderboardScore? TrackedScore { get; private set; } + public DrawableGameplayLeaderboardScore? TrackedScore { get; private set; } + + [Resolved] + private IGameplayLeaderboardProvider? leaderboardProvider { get; set; } + + private readonly IBindableList scores = new BindableList(); private const int max_panels = 8; /// /// Create a new leaderboard. /// - protected GameplayLeaderboard() + public DrawableGameplayLeaderboard() { - Width = GameplayLeaderboardScore.EXTENDED_WIDTH + GameplayLeaderboardScore.SHEAR_WIDTH; + Width = DrawableGameplayLeaderboardScore.EXTENDED_WIDTH + DrawableGameplayLeaderboardScore.SHEAR_WIDTH; InternalChildren = new Drawable[] { @@ -44,10 +51,10 @@ namespace osu.Game.Screens.Play.HUD { ClampExtension = 0, RelativeSizeAxes = Axes.Both, - Child = Flow = new FillFlowContainer + Child = Flow = new FillFlowContainer { RelativeSizeAxes = Axes.X, - X = GameplayLeaderboardScore.SHEAR_WIDTH, + X = DrawableGameplayLeaderboardScore.SHEAR_WIDTH, AutoSizeAxes = Axes.Y, Direction = FillDirection.Vertical, Spacing = new Vector2(2.5f), @@ -62,22 +69,28 @@ namespace osu.Game.Screens.Play.HUD { base.LoadComplete(); + if (leaderboardProvider != null) + { + scores.BindTo(leaderboardProvider.Scores); + scores.BindCollectionChanged((_, _) => + { + Clear(); + foreach (var score in scores) + Add(score); + }, true); + } + Scheduler.AddDelayed(sort, 1000, true); } /// /// Adds a player to the leaderboard. /// - /// The player. - /// - /// Whether the player should be tracked on the leaderboard. - /// Set to true for the local player or a player whose replay is currently being played. - /// - public ILeaderboardScore Add(IUser? user, bool isTracked) + public void Add(IGameplayLeaderboardScore score) { - var drawable = CreateLeaderboardScoreDrawable(user, isTracked); + var drawable = CreateLeaderboardScoreDrawable(score); - if (isTracked) + if (score.Tracked) { if (TrackedScore != null) throw new InvalidOperationException("Cannot track more than one score."); @@ -92,10 +105,8 @@ namespace osu.Game.Screens.Play.HUD drawable.DisplayOrder.BindValueChanged(_ => sorting.Invalidate(), true); int displayCount = Math.Min(Flow.Count, max_panels); - Height = displayCount * (GameplayLeaderboardScore.PANEL_HEIGHT + Flow.Spacing.Y); + Height = displayCount * (DrawableGameplayLeaderboardScore.PANEL_HEIGHT + Flow.Spacing.Y); requiresScroll = displayCount != Flow.Count; - - return drawable; } public void Clear() @@ -105,8 +116,8 @@ namespace osu.Game.Screens.Play.HUD scroll.ScrollToStart(false); } - protected virtual GameplayLeaderboardScore CreateLeaderboardScoreDrawable(IUser? user, bool isTracked) => - new GameplayLeaderboardScore(user, isTracked); + protected virtual DrawableGameplayLeaderboardScore CreateLeaderboardScoreDrawable(IGameplayLeaderboardScore score) => + new DrawableGameplayLeaderboardScore(score); protected override void Update() { @@ -119,7 +130,7 @@ namespace osu.Game.Screens.Play.HUD scroll.ScrollTo(scrollTarget); } - const float panel_height = GameplayLeaderboardScore.PANEL_HEIGHT; + const float panel_height = DrawableGameplayLeaderboardScore.PANEL_HEIGHT; float fadeBottom = (float)(scroll.Current + scroll.DrawHeight); float fadeTop = (float)(scroll.Current + panel_height); @@ -171,14 +182,12 @@ namespace osu.Game.Screens.Play.HUD for (int i = 0; i < Flow.Count; i++) { Flow.SetLayoutPosition(orderedByScore[i], i); - orderedByScore[i].ScorePosition = CheckValidScorePosition(orderedByScore[i], i + 1) ? i + 1 : null; + orderedByScore[i].ScorePosition = i + 1 == Flow.Count && leaderboardProvider?.IsPartial == true ? null : i + 1; } sorting.Validate(); } - protected virtual bool CheckValidScorePosition(GameplayLeaderboardScore score, int position) => true; - private partial class InputDisabledScrollContainer : OsuScrollContainer { public InputDisabledScrollContainer() diff --git a/osu.Game/Screens/Play/HUD/GameplayLeaderboardScore.cs b/osu.Game/Screens/Play/HUD/DrawableGameplayLeaderboardScore.cs similarity index 96% rename from osu.Game/Screens/Play/HUD/GameplayLeaderboardScore.cs rename to osu.Game/Screens/Play/HUD/DrawableGameplayLeaderboardScore.cs index 3d46517a68..f04d3ee492 100644 --- a/osu.Game/Screens/Play/HUD/GameplayLeaderboardScore.cs +++ b/osu.Game/Screens/Play/HUD/DrawableGameplayLeaderboardScore.cs @@ -22,7 +22,7 @@ using osuTK.Graphics; namespace osu.Game.Screens.Play.HUD { - public partial class GameplayLeaderboardScore : CompositeDrawable, ILeaderboardScore + public partial class DrawableGameplayLeaderboardScore : CompositeDrawable { public const float EXTENDED_WIDTH = regular_width + top_player_left_width_extension; @@ -112,19 +112,27 @@ namespace osu.Game.Screens.Play.HUD private bool isFriend; /// - /// Creates a new . + /// Creates a new . /// - /// The score's player. - /// Whether the player is the local user or a replay player. - public GameplayLeaderboardScore(IUser? user, bool tracked) + public DrawableGameplayLeaderboardScore(IGameplayLeaderboardScore score) { - User = user; - Tracked = tracked; + User = score.User; + Tracked = score.Tracked; + TotalScore.BindTo(score.TotalScore); + Accuracy.BindTo(score.Accuracy); + Combo.BindTo(score.Combo); + HasQuit.BindTo(score.HasQuit); + DisplayOrder.BindTo(score.DisplayOrder); + GetDisplayScore = score.GetDisplayScore; + + if (score.TeamColour != null) + { + BackgroundColour = score.TeamColour.Value; + TextColour = Color4.White; + } AutoSizeAxes = Axes.X; Height = PANEL_HEIGHT; - - GetDisplayScore = _ => TotalScore.Value; } [BackgroundDependencyLoader] diff --git a/osu.Game/Screens/Play/HUD/IGameplayLeaderboardScore.cs b/osu.Game/Screens/Play/HUD/IGameplayLeaderboardScore.cs new file mode 100644 index 0000000000..20c7b16d79 --- /dev/null +++ b/osu.Game/Screens/Play/HUD/IGameplayLeaderboardScore.cs @@ -0,0 +1,67 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using osu.Framework.Bindables; +using osu.Framework.Graphics; +using osu.Game.Rulesets.Scoring; +using osu.Game.Users; + +namespace osu.Game.Screens.Play.HUD +{ + /// + /// Represents a score shown on a gameplay leaderboard. + /// The score is expected to update itself as gameplay progresses. + /// + public interface IGameplayLeaderboardScore + { + /// + /// The user playing. + /// + IUser User { get; } + + /// + /// Whether the score is being tracked. + /// Generally understood as true when this score is the score of the local user currently playing. + /// + bool Tracked { get; } + + /// + /// The current total of the score. + /// + BindableLong TotalScore { get; } + + /// + /// The current accuracy of the score. + /// + BindableDouble Accuracy { get; } + + /// + /// The current combo of the score. + /// + BindableInt Combo { get; } + + /// + /// Whether the user playing has quit. + /// + BindableBool HasQuit { get; } + + /// + /// An optional value to guarantee stable ordering. + /// Lower numbers will appear higher in cases of ties. + /// + Bindable DisplayOrder { get; } + + /// + /// A custom function which handles converting a score to a display score using a provide . + /// + /// + /// If no function is provided, will be used verbatim. + Func GetDisplayScore { get; set; } + + /// + /// The colour of the team that the user playing is on, if any. + /// + Colour4? TeamColour { get; } + } +} diff --git a/osu.Game/Screens/Play/HUD/ILeaderboardScore.cs b/osu.Game/Screens/Play/HUD/ILeaderboardScore.cs deleted file mode 100644 index 1a5d7fd9a8..0000000000 --- a/osu.Game/Screens/Play/HUD/ILeaderboardScore.cs +++ /dev/null @@ -1,31 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -using System; -using osu.Framework.Bindables; -using osu.Game.Rulesets.Scoring; - -namespace osu.Game.Screens.Play.HUD -{ - public interface ILeaderboardScore - { - BindableLong TotalScore { get; } - BindableDouble Accuracy { get; } - BindableInt Combo { get; } - - BindableBool HasQuit { get; } - - /// - /// An optional value to guarantee stable ordering. - /// Lower numbers will appear higher in cases of ties. - /// - Bindable DisplayOrder { get; } - - /// - /// A custom function which handles converting a score to a display score using a provide . - /// - /// - /// If no function is provided, will be used verbatim. - Func GetDisplayScore { set; } - } -} diff --git a/osu.Game/Screens/Play/HUD/SoloGameplayLeaderboard.cs b/osu.Game/Screens/Play/HUD/SoloGameplayLeaderboard.cs deleted file mode 100644 index e9bb1d2101..0000000000 --- a/osu.Game/Screens/Play/HUD/SoloGameplayLeaderboard.cs +++ /dev/null @@ -1,108 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -using System.Linq; -using osu.Framework.Allocation; -using osu.Framework.Bindables; -using osu.Framework.Graphics; -using osu.Game.Configuration; -using osu.Game.Online.API.Requests; -using osu.Game.Rulesets.Scoring; -using osu.Game.Scoring; -using osu.Game.Scoring.Legacy; -using osu.Game.Screens.Select; -using osu.Game.Users; - -namespace osu.Game.Screens.Play.HUD -{ - public partial class SoloGameplayLeaderboard : GameplayLeaderboard - { - private const int duration = 100; - - private readonly Bindable configVisibility = new Bindable(); - - private readonly Bindable scoreSource = new Bindable(); - - private readonly IUser trackingUser; - - public readonly IBindableList Scores = new BindableList(); - - [Resolved] - private ScoreProcessor scoreProcessor { get; set; } = null!; - - /// - /// Whether the leaderboard should be visible regardless of the configuration value. - /// This is true by default, but can be changed. - /// - public readonly Bindable AlwaysVisible = new Bindable(true); - - public SoloGameplayLeaderboard(IUser trackingUser) - { - this.trackingUser = trackingUser; - } - - [BackgroundDependencyLoader] - private void load(OsuConfigManager config) - { - config.BindWith(OsuSetting.GameplayLeaderboard, configVisibility); - config.BindWith(OsuSetting.BeatmapDetailTab, scoreSource); - } - - protected override void LoadComplete() - { - base.LoadComplete(); - - Scores.BindCollectionChanged((_, _) => Scheduler.AddOnce(showScores), true); - - // Alpha will be updated via `updateVisibility` below. - Alpha = 0; - - AlwaysVisible.BindValueChanged(_ => updateVisibility()); - configVisibility.BindValueChanged(_ => updateVisibility(), true); - } - - private void showScores() - { - Clear(); - - if (!Scores.Any()) - return; - - foreach (var s in Scores) - { - var score = Add(s.User, false); - - score.GetDisplayScore = s.GetDisplayScore; - score.TotalScore.Value = s.TotalScore; - score.Accuracy.Value = s.Accuracy; - score.Combo.Value = s.MaxCombo; - score.DisplayOrder.Value = s.OnlineID > 0 ? s.OnlineID : s.Date.ToUnixTimeSeconds(); - } - - ILeaderboardScore local = Add(trackingUser, true); - - local.GetDisplayScore = scoreProcessor.GetDisplayScore; - local.TotalScore.BindTarget = scoreProcessor.TotalScore; - local.Accuracy.BindTarget = scoreProcessor.Accuracy; - local.Combo.BindTarget = scoreProcessor.HighestCombo; - - // Local score should always show lower than any existing scores in cases of ties. - local.DisplayOrder.Value = long.MaxValue; - } - - protected override bool CheckValidScorePosition(GameplayLeaderboardScore score, int position) - { - // change displayed position to '-' when there are 50 already submitted scores and tracked score is last - if (score.Tracked && scoreSource.Value != PlayBeatmapDetailArea.TabType.Local) - { - if (position == Flow.Count && Flow.Count > GetScoresRequest.MAX_SCORES_PER_REQUEST) - return false; - } - - return base.CheckValidScorePosition(score, position); - } - - private void updateVisibility() => - this.FadeTo(AlwaysVisible.Value || configVisibility.Value ? 1 : 0, duration); - } -} diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs index b2e502406a..14bb1a1794 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -929,34 +929,30 @@ namespace osu.Game.Screens.Play #region Gameplay leaderboard + protected virtual bool ShowLeaderboard => false; + protected readonly Bindable LeaderboardExpandedState = new BindableBool(); private void loadLeaderboard() { + if (!ShowLeaderboard) + return; + HUDOverlay.HoldingForHUD.BindValueChanged(_ => updateLeaderboardExpandedState()); LocalUserPlaying.BindValueChanged(_ => updateLeaderboardExpandedState(), true); - var gameplayLeaderboard = CreateGameplayLeaderboard(); - - if (gameplayLeaderboard != null) + var gameplayLeaderboard = new DrawableGameplayLeaderboard(); + LoadComponentAsync(gameplayLeaderboard, leaderboard => { - LoadComponentAsync(gameplayLeaderboard, leaderboard => - { - if (!LoadedBeatmapSuccessfully) - return; + if (!LoadedBeatmapSuccessfully) + return; - leaderboard.Expanded.BindTo(LeaderboardExpandedState); + leaderboard.Expanded.BindTo(LeaderboardExpandedState); - AddLeaderboardToHUD(leaderboard); - }); - } + HUDOverlay.LeaderboardFlow.Add(leaderboard); + }); } - [CanBeNull] - protected virtual GameplayLeaderboard CreateGameplayLeaderboard() => null; - - protected virtual void AddLeaderboardToHUD(GameplayLeaderboard leaderboard) => HUDOverlay.LeaderboardFlow.Add(leaderboard); - private void updateLeaderboardExpandedState() => LeaderboardExpandedState.Value = !LocalUserPlaying.Value || HUDOverlay.HoldingForHUD.Value; diff --git a/osu.Game/Screens/Play/ReplayPlayer.cs b/osu.Game/Screens/Play/ReplayPlayer.cs index a5952f3ff3..c997a67dea 100644 --- a/osu.Game/Screens/Play/ReplayPlayer.cs +++ b/osu.Game/Screens/Play/ReplayPlayer.cs @@ -8,18 +8,16 @@ using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using osu.Framework.Allocation; -using osu.Framework.Bindables; using osu.Framework.Input.Bindings; using osu.Framework.Input.Events; using osu.Game.Beatmaps; using osu.Game.Configuration; using osu.Game.Input.Bindings; -using osu.Game.Online.Leaderboards; using osu.Game.Rulesets.Mods; using osu.Game.Scoring; -using osu.Game.Screens.Play.HUD; using osu.Game.Screens.Play.PlayerSettings; using osu.Game.Screens.Ranking; +using osu.Game.Screens.Select.Leaderboards; using osu.Game.Users; namespace osu.Game.Screens.Play @@ -35,6 +33,9 @@ namespace osu.Game.Screens.Play private PlaybackSettings playbackSettings; + [Cached(typeof(IGameplayLeaderboardProvider))] + private readonly SoloGameplayLeaderboardProvider leaderboardProvider = new SoloGameplayLeaderboardProvider(); + protected override UserActivity InitialActivity => new UserActivity.WatchingReplay(Score.ScoreInfo); private bool isAutoplayPlayback => GameplayState.Mods.OfType().Any(); @@ -48,6 +49,8 @@ namespace osu.Game.Screens.Play return base.CheckModsAllowFailure(); } + protected override bool ShowLeaderboard => true; + public ReplayPlayer(Score score, PlayerConfiguration configuration = null) : this((_, _) => score, configuration) { @@ -60,12 +63,6 @@ namespace osu.Game.Screens.Play this.createScore = createScore; } - [Resolved] - private LeaderboardManager leaderboardManager { get; set; } = null!; - - private readonly IBindable globalScores = new Bindable(); - private readonly BindableList localScores = new BindableList(); - /// /// Add a settings group to the HUD overlay. Intended to be used by rulesets to add replay-specific settings. /// @@ -82,6 +79,8 @@ namespace osu.Game.Screens.Play if (!LoadedBeatmapSuccessfully) return; + AddInternal(leaderboardProvider); + playbackSettings = new PlaybackSettings { Depth = float.MaxValue, @@ -94,20 +93,6 @@ namespace osu.Game.Screens.Play HUDOverlay.PlayerSettingsOverlay.AddAtStart(playbackSettings); } - protected override void LoadComplete() - { - base.LoadComplete(); - - globalScores.BindTo(leaderboardManager.Scores); - globalScores.BindValueChanged(_ => - { - localScores.Clear(); - - if (globalScores.Value is LeaderboardScores g) - localScores.AddRange(g.AllScores.OrderByTotalScore()); - }, true); - } - protected override void PrepareReplay() { DrawableRuleset?.SetReplayScore(Score); @@ -118,13 +103,6 @@ namespace osu.Game.Screens.Play // Don't re-import replay scores as they're already present in the database. protected override Task ImportScore(Score score) => Task.CompletedTask; - protected override GameplayLeaderboard CreateGameplayLeaderboard() => - new SoloGameplayLeaderboard(Score.ScoreInfo.User) - { - AlwaysVisible = { Value = true }, - Scores = { BindTarget = localScores } - }; - protected override ResultsScreen CreateResults(ScoreInfo score) => new SoloResultsScreen(score) { // Only show the relevant button otherwise things look silly. diff --git a/osu.Game/Screens/Play/SoloPlayer.cs b/osu.Game/Screens/Play/SoloPlayer.cs index ed5dea98cd..e4e42e2f08 100644 --- a/osu.Game/Screens/Play/SoloPlayer.cs +++ b/osu.Game/Screens/Play/SoloPlayer.cs @@ -5,50 +5,34 @@ using System; using System.Diagnostics; -using System.Threading.Tasks; +using JetBrains.Annotations; using osu.Framework.Allocation; -using osu.Framework.Bindables; using osu.Game.Beatmaps; using osu.Game.Extensions; using osu.Game.Online.API; -using osu.Game.Online.Leaderboards; using osu.Game.Online.Rooms; using osu.Game.Online.Solo; using osu.Game.Scoring; -using osu.Game.Screens.Play.HUD; +using osu.Game.Screens.Select.Leaderboards; namespace osu.Game.Screens.Play { public partial class SoloPlayer : SubmittingPlayer { - public SoloPlayer() - : this(null) - { - } + protected override bool ShowLeaderboard => true; - protected SoloPlayer(PlayerConfiguration configuration = null) + [Cached(typeof(IGameplayLeaderboardProvider))] + private readonly SoloGameplayLeaderboardProvider leaderboardProvider = new SoloGameplayLeaderboardProvider(); + + public SoloPlayer([CanBeNull] PlayerConfiguration configuration = null) : base(configuration) { } - [Resolved] - private LeaderboardManager leaderboardManager { get; set; } = null!; - - private readonly IBindable globalScores = new Bindable(); - private readonly BindableList localScores = new BindableList(); - - protected override void LoadComplete() + [BackgroundDependencyLoader] + private void load() { - base.LoadComplete(); - - globalScores.BindTo(leaderboardManager.Scores); - globalScores.BindValueChanged(_ => - { - localScores.Clear(); - - if (globalScores.Value is LeaderboardScores g) - localScores.AddRange(g.AllScores.OrderByTotalScore()); - }, true); + AddInternal(leaderboardProvider); } protected override APIRequest CreateTokenRequest() @@ -65,30 +49,13 @@ namespace osu.Game.Screens.Play return new CreateSoloScoreRequest(Beatmap.Value.BeatmapInfo, rulesetId, Game.VersionHash); } - protected override GameplayLeaderboard CreateGameplayLeaderboard() => - new SoloGameplayLeaderboard(Score.ScoreInfo.User) - { - AlwaysVisible = { Value = false }, - Scores = { BindTarget = localScores } - }; - protected override bool ShouldExitOnTokenRetrievalFailure(Exception exception) => false; - protected override Task ImportScore(Score score) - { - // Before importing a score, stop binding the leaderboard with its score source. - // This avoids a case where the imported score may cause a leaderboard refresh - // (if the leaderboard's source is local). - globalScores.UnbindBindings(); - - return base.ImportScore(score); - } - protected override APIRequest CreateSubmissionRequest(Score score, long token) { - IBeatmapInfo beatmap = score.ScoreInfo.BeatmapInfo; + IBeatmapInfo beatmap = score.ScoreInfo.BeatmapInfo!; - Debug.Assert(beatmap!.OnlineID > 0); + Debug.Assert(beatmap.OnlineID > 0); return new SubmitSoloScoreRequest(score.ScoreInfo, token, beatmap.OnlineID); } diff --git a/osu.Game/Screens/Select/Leaderboards/GameplayLeaderboardScore.cs b/osu.Game/Screens/Select/Leaderboards/GameplayLeaderboardScore.cs new file mode 100644 index 0000000000..ba3e4f728b --- /dev/null +++ b/osu.Game/Screens/Select/Leaderboards/GameplayLeaderboardScore.cs @@ -0,0 +1,59 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using osu.Framework.Bindables; +using osu.Framework.Graphics; +using osu.Game.Online.Spectator; +using osu.Game.Rulesets.Scoring; +using osu.Game.Scoring; +using osu.Game.Scoring.Legacy; +using osu.Game.Screens.Play.HUD; +using osu.Game.Users; + +namespace osu.Game.Screens.Select.Leaderboards +{ + public class GameplayLeaderboardScore : IGameplayLeaderboardScore + { + public IUser User { get; } + public bool Tracked { get; } + public BindableLong TotalScore { get; } = new BindableLong(); + public BindableDouble Accuracy { get; } = new BindableDouble(); + public BindableInt Combo { get; } = new BindableInt(); + public BindableBool HasQuit { get; } = new BindableBool(); + public Bindable DisplayOrder { get; } = new BindableLong(); + public Func GetDisplayScore { get; set; } + public Colour4? TeamColour { get; init; } + + public GameplayLeaderboardScore(IUser user, ScoreProcessor scoreProcessor, bool tracked) + { + User = user; + Tracked = tracked; + TotalScore.BindTarget = scoreProcessor.TotalScore; + Accuracy.BindTarget = scoreProcessor.Accuracy; + Combo.BindTarget = scoreProcessor.Combo; + GetDisplayScore = scoreProcessor.GetDisplayScore; + } + + public GameplayLeaderboardScore(IUser user, SpectatorScoreProcessor scoreProcessor, bool tracked) + { + User = user; + Tracked = tracked; + TotalScore.BindTarget = scoreProcessor.TotalScore; + Accuracy.BindTarget = scoreProcessor.Accuracy; + Combo.BindTarget = scoreProcessor.Combo; + GetDisplayScore = scoreProcessor.GetDisplayScore; + } + + public GameplayLeaderboardScore(ScoreInfo scoreInfo, bool tracked) + { + User = scoreInfo.User; + Tracked = tracked; + TotalScore.Value = scoreInfo.TotalScore; + Accuracy.Value = scoreInfo.Accuracy; + Combo.Value = scoreInfo.Combo; + DisplayOrder.Value = scoreInfo.OnlineID > 0 ? scoreInfo.OnlineID : scoreInfo.Date.ToUnixTimeSeconds(); + GetDisplayScore = scoreInfo.GetDisplayScore; + } + } +} diff --git a/osu.Game/Screens/Select/Leaderboards/IGameplayLeaderboardProvider.cs b/osu.Game/Screens/Select/Leaderboards/IGameplayLeaderboardProvider.cs new file mode 100644 index 0000000000..0138f855e2 --- /dev/null +++ b/osu.Game/Screens/Select/Leaderboards/IGameplayLeaderboardProvider.cs @@ -0,0 +1,25 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Bindables; +using osu.Game.Screens.Play.HUD; + +namespace osu.Game.Screens.Select.Leaderboards +{ + /// + /// Provides a leaderboard to show during gameplay. + /// + public interface IGameplayLeaderboardProvider + { + /// + /// List of all scores to display on the leaderboard. + /// + public IBindableList Scores { get; } + + /// + /// Whether this leaderboard is a partial leaderboard (e.g. contains only the top 50 of all scores), + /// or is a full leaderboard (contains all scores that there will ever be). + /// + bool IsPartial { get; } + } +} diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorLeaderboard.cs b/osu.Game/Screens/Select/Leaderboards/MultiSpectatorLeaderboardProvider.cs similarity index 76% rename from osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorLeaderboard.cs rename to osu.Game/Screens/Select/Leaderboards/MultiSpectatorLeaderboardProvider.cs index ed92b719fc..19ae12a6ca 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorLeaderboard.cs +++ b/osu.Game/Screens/Select/Leaderboards/MultiSpectatorLeaderboardProvider.cs @@ -4,13 +4,12 @@ using System; using osu.Framework.Timing; using osu.Game.Online.Multiplayer; -using osu.Game.Screens.Play.HUD; -namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate +namespace osu.Game.Screens.Select.Leaderboards { - public partial class MultiSpectatorLeaderboard : MultiplayerGameplayLeaderboard + public partial class MultiSpectatorLeaderboardProvider : MultiplayerLeaderboardProvider { - public MultiSpectatorLeaderboard(MultiplayerRoomUser[] users) + public MultiSpectatorLeaderboardProvider(MultiplayerRoomUser[] users) : base(users) { } diff --git a/osu.Game/Screens/Play/HUD/MultiplayerGameplayLeaderboard.cs b/osu.Game/Screens/Select/Leaderboards/MultiplayerLeaderboardProvider.cs similarity index 68% rename from osu.Game/Screens/Play/HUD/MultiplayerGameplayLeaderboard.cs rename to osu.Game/Screens/Select/Leaderboards/MultiplayerLeaderboardProvider.cs index 922def6174..1c2b400164 100644 --- a/osu.Game/Screens/Play/HUD/MultiplayerGameplayLeaderboard.cs +++ b/osu.Game/Screens/Select/Leaderboards/MultiplayerLeaderboardProvider.cs @@ -11,6 +11,7 @@ using osu.Framework.Bindables; using osu.Framework.Extensions; using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Extensions.ObjectExtensions; +using osu.Framework.Graphics.Containers; using osu.Game.Configuration; using osu.Game.Database; using osu.Game.Graphics; @@ -20,20 +21,31 @@ using osu.Game.Online.Multiplayer; using osu.Game.Online.Multiplayer.MatchTypes.TeamVersus; using osu.Game.Online.Spectator; using osu.Game.Rulesets.Scoring; -using osu.Game.Users; +using osu.Game.Screens.Play.HUD; using osuTK.Graphics; -namespace osu.Game.Screens.Play.HUD +namespace osu.Game.Screens.Select.Leaderboards { [LongRunningLoad] - public partial class MultiplayerGameplayLeaderboard : GameplayLeaderboard + public partial class MultiplayerLeaderboardProvider : CompositeComponent, IGameplayLeaderboardProvider { - protected readonly Dictionary UserScores = new Dictionary(); + public IBindableList Scores => scores; + private readonly BindableList scores = new BindableList(); + protected readonly Dictionary UserScores = new Dictionary(); public readonly SortedDictionary TeamScores = new SortedDictionary(); + public bool HasTeams => TeamScores.Count > 0; + + public bool IsPartial => false; + + private readonly MultiplayerRoomUser[] users; + + private readonly Bindable scoringMode = new Bindable(); + private readonly IBindableList playingUserIds = new BindableList(); + [Resolved] - private OsuColour colours { get; set; } = null!; + private UserLookupCache userLookupCache { get; set; } = null!; [Resolved] private SpectatorClient spectatorClient { get; set; } = null!; @@ -42,31 +54,19 @@ namespace osu.Game.Screens.Play.HUD private MultiplayerClient multiplayerClient { get; set; } = null!; [Resolved] - private UserLookupCache userLookupCache { get; set; } = null!; + private OsuColour colours { get; set; } = null!; - private Bindable scoringMode = null!; - - private readonly MultiplayerRoomUser[] playingUsers; - - private readonly IBindableList playingUserIds = new BindableList(); - - private bool hasTeams => TeamScores.Count > 0; - - /// - /// Construct a new leaderboard. - /// - /// IDs of all users in this match. - public MultiplayerGameplayLeaderboard(MultiplayerRoomUser[] users) + public MultiplayerLeaderboardProvider(MultiplayerRoomUser[] users) { - playingUsers = users; + this.users = users; } [BackgroundDependencyLoader] private void load(OsuConfigManager config, IAPIProvider api, CancellationToken cancellationToken) { - scoringMode = config.GetBindable(OsuSetting.ScoreDisplayMode); + config.BindWith(OsuSetting.ScoreDisplayMode, scoringMode); - foreach (var user in playingUsers) + foreach (var user in users) { var scoreProcessor = new SpectatorScoreProcessor(user.UserID); scoreProcessor.Mode.BindTo(scoringMode); @@ -80,29 +80,29 @@ namespace osu.Game.Screens.Play.HUD TeamScores.Add(team, new BindableLong()); } - userLookupCache.GetUsersAsync(playingUsers.Select(u => u.UserID).ToArray(), cancellationToken) + userLookupCache.GetUsersAsync(users.Select(u => u.UserID).ToArray(), cancellationToken) .ContinueWith(task => { Schedule(() => { - var users = task.GetResultSafely(); + var lookedUpUsers = task.GetResultSafely(); - for (int i = 0; i < users.Length; i++) + for (int i = 0; i < lookedUpUsers.Length; i++) { - var user = users[i] ?? new APIUser + var user = lookedUpUsers[i] ?? new APIUser { - Id = playingUsers[i].UserID, + Id = users[i].UserID, Username = "Unknown user", }; var trackedUser = UserScores[user.Id]; - var leaderboardScore = Add(user, user.Id == api.LocalUser.Value.Id); - leaderboardScore.GetDisplayScore = trackedUser.ScoreProcessor.GetDisplayScore; - leaderboardScore.Accuracy.BindTo(trackedUser.ScoreProcessor.Accuracy); - leaderboardScore.TotalScore.BindTo(trackedUser.ScoreProcessor.TotalScore); - leaderboardScore.Combo.BindTo(trackedUser.ScoreProcessor.Combo); - leaderboardScore.HasQuit.BindTo(trackedUser.UserQuit); + var leaderboardScore = new GameplayLeaderboardScore(user, trackedUser.ScoreProcessor, user.Id == api.LocalUser.Value.Id) + { + HasQuit = { BindTarget = trackedUser.UserQuit }, + TeamColour = UserScores[user.OnlineID].Team is int team ? getTeamColour(team) : null, + }; + scores.Add(leaderboardScore); } }); }, cancellationToken); @@ -113,7 +113,7 @@ namespace osu.Game.Screens.Play.HUD base.LoadComplete(); // BindableList handles binding in a really bad way (Clear then AddRange) so we need to do this manually.. - foreach (var user in playingUsers) + foreach (var user in users) { spectatorClient.WatchUser(user.UserID); @@ -127,34 +127,6 @@ namespace osu.Game.Screens.Play.HUD playingUserIds.BindCollectionChanged(playingUsersChanged); } - protected override GameplayLeaderboardScore CreateLeaderboardScoreDrawable(IUser? user, bool isTracked) - { - var leaderboardScore = base.CreateLeaderboardScoreDrawable(user, isTracked); - - if (user != null) - { - if (UserScores[user.OnlineID].Team is int team) - { - leaderboardScore.BackgroundColour = getTeamColour(team).Lighten(1.2f); - leaderboardScore.TextColour = Color4.White; - } - } - - return leaderboardScore; - } - - private Color4 getTeamColour(int team) - { - switch (team) - { - case 0: - return colours.TeamColourRed; - - default: - return colours.TeamColourBlue; - } - } - private void playingUsersChanged(object? sender, NotifyCollectionChangedEventArgs e) { switch (e.Action) @@ -176,10 +148,10 @@ namespace osu.Game.Screens.Play.HUD private void updateTotals() { - if (!hasTeams) + if (!HasTeams) return; - foreach (var scores in TeamScores.Values) scores.Value = 0; + foreach (var teamTotal in TeamScores.Values) teamTotal.Value = 0; foreach (var u in UserScores.Values) { @@ -191,13 +163,25 @@ namespace osu.Game.Screens.Play.HUD } } + private Color4 getTeamColour(int team) + { + switch (team) + { + case 0: + return colours.TeamColourRed.Lighten(1.2f); + + default: + return colours.TeamColourBlue.Lighten(1.2f); + } + } + protected override void Dispose(bool isDisposing) { base.Dispose(isDisposing); if (spectatorClient.IsNotNull()) { - foreach (var user in playingUsers) + foreach (var user in users) spectatorClient.StopWatchingUser(user.UserID); } } diff --git a/osu.Game/Screens/Select/Leaderboards/SoloGameplayLeaderboardProvider.cs b/osu.Game/Screens/Select/Leaderboards/SoloGameplayLeaderboardProvider.cs new file mode 100644 index 0000000000..125e8fdc9d --- /dev/null +++ b/osu.Game/Screens/Select/Leaderboards/SoloGameplayLeaderboardProvider.cs @@ -0,0 +1,41 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Allocation; +using osu.Framework.Bindables; +using osu.Framework.Graphics; +using osu.Game.Online.Leaderboards; +using osu.Game.Scoring; +using osu.Game.Screens.Play; +using osu.Game.Screens.Play.HUD; + +namespace osu.Game.Screens.Select.Leaderboards +{ + public partial class SoloGameplayLeaderboardProvider : Component, IGameplayLeaderboardProvider + { + public bool IsPartial { get; private set; } + + public IBindableList Scores => scores; + private readonly BindableList scores = new BindableList(); + + [BackgroundDependencyLoader] + private void load(LeaderboardManager leaderboardManager, GameplayState gameplayState) + { + var globalScores = leaderboardManager.Scores.Value; + + IsPartial = leaderboardManager.CurrentCriteria?.Scope != BeatmapLeaderboardScope.Local && globalScores?.TopScores.Count >= 50; + + if (globalScores != null) + { + foreach (var topScore in globalScores.AllScores.OrderByTotalScore()) + scores.Add(new GameplayLeaderboardScore(topScore, false)); + } + + scores.Add(new GameplayLeaderboardScore(gameplayState.Score.ScoreInfo.User, gameplayState.ScoreProcessor, true) + { + // Local score should always show lower than any existing scores in cases of ties. + DisplayOrder = { Value = long.MaxValue } + }); + } + } +} From 1a68edfa58e34f1d3fa05f0f5e0b660c77efea10 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Thu, 17 Apr 2025 03:30:53 +0900 Subject: [PATCH 086/164] Add failing test --- .../Multiplayer/TestSceneMultiplayer.cs | 39 +++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs index 8066ea1b94..a8004f2685 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs @@ -1056,6 +1056,45 @@ namespace osu.Game.Tests.Visual.Multiplayer AddAssert("hidden is selected", () => SelectedMods.Value, () => Has.One.TypeOf(typeof(OsuModHidden))); } + [FlakyTest] + [Test] + public void TestGlobalBeatmapDoesNotChangeAtResults() + { + createRoom(() => new Room + { + Name = "Test Room", + QueueMode = QueueMode.AllPlayers, + Playlist = + [ + new PlaylistItem(beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First(b => b.Ruleset.OnlineID == 0)).BeatmapInfo) + { + RulesetID = new OsuRuleset().RulesetInfo.OnlineID, + AllowedMods = new[] { new APIMod { Acronym = "HD" } }, + }, + new PlaylistItem(beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First(b => b.Ruleset.OnlineID == 1)).BeatmapInfo) + { + RulesetID = new TaikoRuleset().RulesetInfo.OnlineID, + AllowedMods = new[] { new APIMod { Acronym = "HD" } }, + }, + ] + }); + + enterGameplay(); + + // Gameplay runs in real-time, so we need to incrementally check if gameplay has finished in order to not time out. + for (double i = 1000; i < TestResources.QUICK_BEATMAP_LENGTH; i += 1000) + { + double time = i; + AddUntilStep($"wait for time > {i}", () => this.ChildrenOfType().SingleOrDefault()?.CurrentTime > time); + } + + AddUntilStep("wait for results", () => multiplayerComponents.CurrentScreen is ResultsScreen); + + AddAssert("global beatmap still matches first playlist item", () => Beatmap.Value.BeatmapInfo.OnlineID, () => Is.EqualTo(multiplayerClient.ClientRoom!.Playlist[0].BeatmapID)); + AddStep("return to match", () => multiplayerComponents.Exit()); + AddAssert("global beatmap still matches first playlist item", () => Beatmap.Value.BeatmapInfo.OnlineID, () => Is.EqualTo(multiplayerClient.ClientRoom!.Playlist[1].BeatmapID)); + } + private void enterGameplay() { pressReadyButton(); From 6052fbb4f927a4562e55a1295da90d9d88618c22 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Thu, 17 Apr 2025 03:03:49 +0900 Subject: [PATCH 087/164] Fix multiplayer background changing in results screen --- .../OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs index ac1c6cf22c..6d271a0077 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs @@ -467,7 +467,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer { if (settings.PlaylistItemId != lastPlaylistItemId) { - updateGameplayState(); + Scheduler.AddOnce(updateGameplayState); lastPlaylistItemId = settings.PlaylistItemId; } @@ -480,7 +480,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer private void onItemChanged(MultiplayerPlaylistItem item) { if (item.ID == client.Room?.Settings.PlaylistItemId) - updateGameplayState(); + Scheduler.AddOnce(updateGameplayState); } /// @@ -489,7 +489,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer private void onUserStyleChanged(MultiplayerRoomUser user) { if (user.Equals(client.LocalUser)) - updateGameplayState(); + Scheduler.AddOnce(updateGameplayState); } /// @@ -498,7 +498,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer private void onUserModsChanged(MultiplayerRoomUser user) { if (user.Equals(client.LocalUser)) - updateGameplayState(); + Scheduler.AddOnce(updateGameplayState); } /// From b71281bec8054d17484041b5ef6117ad5b8ee38f Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 17 Apr 2025 16:11:03 +0900 Subject: [PATCH 088/164] Fix osu!mania beatmap objects getting corrupted when updating beatmap background Closes https://github.com/ppy/osu/issues/32825. Tested manually to fix the issue. Setting up test coverage for this is going to likely take over an hour compared to the 30 second fix, so please advise if required. I couldn't find any existing tests which perform this flow. --- 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 cab6eddaa4..a1c81eedec 100644 --- a/osu.Game/Screens/Edit/Setup/ResourcesSection.cs +++ b/osu.Game/Screens/Edit/Setup/ResourcesSection.cs @@ -192,7 +192,7 @@ namespace osu.Game.Screens.Edit.Setup // note that this triggers a full save flow, including triggering a difficulty calculation. // this is not a cheap operation and should be reconsidered in the future. var beatmapWorking = beatmaps.GetWorkingBeatmap(b); - beatmaps.Save(b, beatmapWorking.Beatmap, beatmapWorking.GetSkin()); + beatmaps.Save(b, beatmapWorking.GetPlayableBeatmap(b.Ruleset), beatmapWorking.GetSkin()); } } From 67c6f8acdd44c221b507a69b51d7b9af6709cebc Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 9 Sep 2024 18:30:15 +0900 Subject: [PATCH 089/164] End high performance session when showing results screen --- osu.Game/Screens/Play/Player.cs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs index b2e502406a..eab964def7 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -58,6 +58,11 @@ namespace osu.Game.Screens.Play public override bool AllowUserExit => false; // handled by HoldForMenuButton + /// + /// Raised after all gameplay has finished. + /// + public event Action OnShowingResults; + protected override bool PlayExitSound => !isRestarting; protected override UserActivity InitialActivity => new UserActivity.InSoloGame(Beatmap.Value.BeatmapInfo, Ruleset.Value); @@ -873,6 +878,7 @@ namespace osu.Game.Screens.Play // This player instance may already be in the process of exiting. return; + OnShowingResults?.Invoke(); this.Push(CreateResults(prepareScoreForDisplayTask.GetResultSafely())); }, Time.Current + delay, 50); From a15230eba189dadecc3434b7b15a52b8ceb82915 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 9 Sep 2024 18:41:43 +0900 Subject: [PATCH 090/164] Centralise calls to end high performance sessions --- osu.Game/Screens/Play/PlayerLoader.cs | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/osu.Game/Screens/Play/PlayerLoader.cs b/osu.Game/Screens/Play/PlayerLoader.cs index bd4b62fd59..24d18a1610 100644 --- a/osu.Game/Screens/Play/PlayerLoader.cs +++ b/osu.Game/Screens/Play/PlayerLoader.cs @@ -3,6 +3,7 @@ using System; using System.Diagnostics; +using System.Threading; using System.Threading.Tasks; using ManagedBass.Fx; using osu.Framework.Allocation; @@ -302,8 +303,7 @@ namespace osu.Game.Screens.Play Debug.Assert(CurrentPlayer != null); - highPerformanceSession?.Dispose(); - highPerformanceSession = null; + endHighPerformance(); // prepare for a retry. CurrentPlayer = null; @@ -349,8 +349,7 @@ namespace osu.Game.Screens.Play BackgroundBrightnessReduction = false; Beatmap.Value.Track.RemoveAdjustment(AdjustableProperty.Volume, volumeAdjustment); - highPerformanceSession?.Dispose(); - highPerformanceSession = null; + endHighPerformance(); return base.OnExiting(e); } @@ -587,7 +586,9 @@ namespace osu.Game.Screens.Play scheduledPushPlayer = Scheduler.AddDelayed(() => { // ensure that once we have reached this "point of no return", readyForPush will be false for all future checks (until a new player instance is prepared). - var consumedPlayer = consumePlayer(); + Player consumedPlayer = consumePlayer(); + + consumedPlayer.OnShowingResults += endHighPerformance; ContentOut(); @@ -623,6 +624,8 @@ namespace osu.Game.Screens.Play scheduledPushPlayer = null; } + private void endHighPerformance() => Interlocked.Exchange(ref highPerformanceSession, null)?.Dispose(); + #region Disposal protected override void Dispose(bool isDisposing) @@ -635,8 +638,7 @@ namespace osu.Game.Screens.Play DisposalTask = LoadTask?.ContinueWith(_ => CurrentPlayer?.Dispose()); } - highPerformanceSession?.Dispose(); - highPerformanceSession = null; + endHighPerformance(); } #endregion From 1fad2a8f2cf632dbd71dfb2e9e5a93e3e41e9095 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 17 Apr 2025 10:05:57 +0200 Subject: [PATCH 091/164] Add failing test --- osu.Game.Tests/Scores/IO/ImportScoreTest.cs | 29 +++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/osu.Game.Tests/Scores/IO/ImportScoreTest.cs b/osu.Game.Tests/Scores/IO/ImportScoreTest.cs index 9c72804a6b..6558834a63 100644 --- a/osu.Game.Tests/Scores/IO/ImportScoreTest.cs +++ b/osu.Game.Tests/Scores/IO/ImportScoreTest.cs @@ -215,6 +215,35 @@ namespace osu.Game.Tests.Scores.IO } } + [Test] + public void TestScoreWithInvalidModCombinationsWillNotImport() + { + using (HeadlessGameHost host = new CleanRunHeadlessGameHost()) + { + try + { + var osu = LoadOsuIntoHost(host, true); + + var beatmap = BeatmapImportHelper.LoadOszIntoOsu(osu, TestResources.GetQuickTestBeatmapForImport()).GetResultSafely(); + + var toImport = new ScoreInfo + { + User = new APIUser { Username = "Test user" }, + BeatmapInfo = beatmap.Beatmaps.First(), + Ruleset = new OsuRuleset().RulesetInfo, + ClientVersion = "12345", + Mods = new Mod[] { new OsuModHalfTime(), new OsuModDoubleTime() }, + }; + + Assert.Throws(() => LoadScoreIntoOsu(osu, toImport)); + } + finally + { + host.Exit(); + } + } + } + [Test] public void TestImportStatistics() { From 485c3e8e5385cd5004a2829cc1d45c25e4ea9d59 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 17 Apr 2025 10:10:07 +0200 Subject: [PATCH 092/164] Refuse to import scores specifying incompatible mods Supersedes https://github.com/ppy/osu/pull/32817. The messaging of the failure to the user is maybe not the cleanest, but I'm not sure it's worth putting time in to improve it? --- osu.Game/Scoring/ScoreImporter.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/osu.Game/Scoring/ScoreImporter.cs b/osu.Game/Scoring/ScoreImporter.cs index 69c53af16f..4b3f4a5e63 100644 --- a/osu.Game/Scoring/ScoreImporter.cs +++ b/osu.Game/Scoring/ScoreImporter.cs @@ -17,6 +17,7 @@ using osu.Game.Scoring.Legacy; using osu.Game.Online.API; using osu.Game.Online.API.Requests; using osu.Game.Online.API.Requests.Responses; +using osu.Game.Utils; using Realms; namespace osu.Game.Scoring @@ -90,6 +91,9 @@ namespace osu.Game.Scoring ArgumentNullException.ThrowIfNull(model.BeatmapInfo); ArgumentNullException.ThrowIfNull(model.Ruleset); + if (!ModUtils.CheckCompatibleSet(model.Mods)) + throw new InvalidOperationException(@"The score specifies an incompatible set of mods!"); + if (string.IsNullOrEmpty(model.StatisticsJson)) model.StatisticsJson = JsonConvert.SerializeObject(model.Statistics); From a82bb5c2f6ae67ce9aa32656188bb286b5b4fef6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 17 Apr 2025 10:36:04 +0200 Subject: [PATCH 093/164] Add theoretically-valid-but-practically-not commented-out test cases --- osu.Game.Rulesets.Mania.Tests/TestSceneReplayStability.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/osu.Game.Rulesets.Mania.Tests/TestSceneReplayStability.cs b/osu.Game.Rulesets.Mania.Tests/TestSceneReplayStability.cs index 497d8a18b8..b70657815c 100644 --- a/osu.Game.Rulesets.Mania.Tests/TestSceneReplayStability.cs +++ b/osu.Game.Rulesets.Mania.Tests/TestSceneReplayStability.cs @@ -27,6 +27,7 @@ namespace osu.Game.Rulesets.Mania.Tests new object[] { 5f, -19d, HitResult.Perfect }, new object[] { 5f, -19.2d, HitResult.Perfect }, new object[] { 5f, -19.38d, HitResult.Perfect }, + // new object[] { 5f, -19.4d, HitResult.Perfect }, <- in theory this should work, in practice it does not (fails even before encode & rounding due to floating point precision issues) new object[] { 5f, -19.44d, HitResult.Great }, new object[] { 5f, -19.7d, HitResult.Great }, new object[] { 5f, -20d, HitResult.Great }, @@ -69,6 +70,7 @@ namespace osu.Game.Rulesets.Mania.Tests new object[] { 9.3f, 14d, HitResult.Perfect }, new object[] { 9.3f, 14.2d, HitResult.Perfect }, new object[] { 9.3f, 14.6d, HitResult.Perfect }, + // new object[] { 9.3f, 14.67d, HitResult.Perfect }, <- in theory this should work, in practice it does not (fails even before encode & rounding due to floating point precision issues) new object[] { 9.3f, 14.7d, HitResult.Great }, new object[] { 9.3f, 15d, HitResult.Great }, new object[] { 9.3f, 35d, HitResult.Great }, From 5f4afe156fe6523f35029dfb1a7af225783db448 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 17 Apr 2025 11:37:28 +0200 Subject: [PATCH 094/164] Fix garbage data in test case --- osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapLeaderboard.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapLeaderboard.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapLeaderboard.cs index 70f2fb1361..44f64365f0 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapLeaderboard.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapLeaderboard.cs @@ -342,7 +342,6 @@ namespace osu.Game.Tests.Visual.SongSelect Mods = new Mod[] { new OsuModHidden(), - new OsuModHardRock(), new OsuModFlashlight { FollowDelay = { Value = 200 }, From 708d9ae1b013562bf5f7e3cfc290b05a57d4ac9e Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 17 Apr 2025 19:21:33 +0900 Subject: [PATCH 095/164] Adjust `PlayerLoader` logic to avoid threading safety requirements --- osu.Game/Screens/Play/PlayerLoader.cs | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/osu.Game/Screens/Play/PlayerLoader.cs b/osu.Game/Screens/Play/PlayerLoader.cs index 24d18a1610..d22717abd4 100644 --- a/osu.Game/Screens/Play/PlayerLoader.cs +++ b/osu.Game/Screens/Play/PlayerLoader.cs @@ -3,12 +3,12 @@ using System; using System.Diagnostics; -using System.Threading; using System.Threading.Tasks; using ManagedBass.Fx; using osu.Framework.Allocation; using osu.Framework.Audio; using osu.Framework.Bindables; +using osu.Framework.Development; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; @@ -564,6 +564,8 @@ namespace osu.Game.Screens.Play private void pushWhenLoaded() { + Debug.Assert(ThreadSafety.IsUpdateThread); + if (!this.IsCurrentScreen()) return; if (!readyForPush) @@ -624,7 +626,13 @@ namespace osu.Game.Screens.Play scheduledPushPlayer = null; } - private void endHighPerformance() => Interlocked.Exchange(ref highPerformanceSession, null)?.Dispose(); + private void endHighPerformance() + { + Debug.Assert(ThreadSafety.IsUpdateThread); + + highPerformanceSession?.Dispose(); + highPerformanceSession = null; + } #region Disposal @@ -638,7 +646,8 @@ namespace osu.Game.Screens.Play DisposalTask = LoadTask?.ContinueWith(_ => CurrentPlayer?.Dispose()); } - endHighPerformance(); + // This is only a failsafe; should be disposed more immediately by `endHighPerformance` call. + highPerformanceSession?.Dispose(); } #endregion From 47d943afd78fa3015685a8bc85969b2e622a9e57 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Thu, 17 Apr 2025 20:05:34 +0900 Subject: [PATCH 096/164] Fix incorrect assertion message MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Bartłomiej Dach --- osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs index a8004f2685..03fe9b8b58 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs @@ -1092,7 +1092,7 @@ namespace osu.Game.Tests.Visual.Multiplayer AddAssert("global beatmap still matches first playlist item", () => Beatmap.Value.BeatmapInfo.OnlineID, () => Is.EqualTo(multiplayerClient.ClientRoom!.Playlist[0].BeatmapID)); AddStep("return to match", () => multiplayerComponents.Exit()); - AddAssert("global beatmap still matches first playlist item", () => Beatmap.Value.BeatmapInfo.OnlineID, () => Is.EqualTo(multiplayerClient.ClientRoom!.Playlist[1].BeatmapID)); + AddAssert("global beatmap matches second playlist item", () => Beatmap.Value.BeatmapInfo.OnlineID, () => Is.EqualTo(multiplayerClient.ClientRoom!.Playlist[1].BeatmapID)); } private void enterGameplay() From 9e2a05a1fb423b9a5a7cb173da48c958f1f46ded Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Thu, 17 Apr 2025 08:21:32 -0400 Subject: [PATCH 097/164] Update song select panel metrics in line with standard specifications and apply minor adjustments --- .../Drawables/DifficultySpectrumDisplay.cs | 2 +- osu.Game/Graphics/Carousel/CarouselItem.cs | 2 +- osu.Game/Screens/SelectV2/PanelBeatmap.cs | 13 +++++----- osu.Game/Screens/SelectV2/PanelBeatmapSet.cs | 13 +++++----- .../SelectV2/PanelBeatmapStandalone.cs | 24 ++++++++----------- .../SelectV2/PanelUpdateBeatmapButton.cs | 6 ++--- 6 files changed, 28 insertions(+), 32 deletions(-) diff --git a/osu.Game/Beatmaps/Drawables/DifficultySpectrumDisplay.cs b/osu.Game/Beatmaps/Drawables/DifficultySpectrumDisplay.cs index fc41c7c6dc..b7f4d4ca61 100644 --- a/osu.Game/Beatmaps/Drawables/DifficultySpectrumDisplay.cs +++ b/osu.Game/Beatmaps/Drawables/DifficultySpectrumDisplay.cs @@ -141,7 +141,7 @@ namespace osu.Game.Beatmaps.Drawables Add(countText = new OsuSpriteText { - Font = OsuFont.Default.With(size: 12), + Font = OsuFont.Style.Caption1, Anchor = Anchor.Centre, Origin = Anchor.Centre, Padding = new MarginPadding { Bottom = 1 } diff --git a/osu.Game/Graphics/Carousel/CarouselItem.cs b/osu.Game/Graphics/Carousel/CarouselItem.cs index 223c8d9869..47e83beca6 100644 --- a/osu.Game/Graphics/Carousel/CarouselItem.cs +++ b/osu.Game/Graphics/Carousel/CarouselItem.cs @@ -11,7 +11,7 @@ namespace osu.Game.Graphics.Carousel /// public sealed class CarouselItem : IComparable { - public const float DEFAULT_HEIGHT = 50; + public const float DEFAULT_HEIGHT = 45; /// /// The model this item is representing. diff --git a/osu.Game/Screens/SelectV2/PanelBeatmap.cs b/osu.Game/Screens/SelectV2/PanelBeatmap.cs index 6742577389..c8ae443364 100644 --- a/osu.Game/Screens/SelectV2/PanelBeatmap.cs +++ b/osu.Game/Screens/SelectV2/PanelBeatmap.cs @@ -73,7 +73,7 @@ namespace osu.Game.Screens.SelectV2 Icon = difficultyIcon = new ConstrainedIconContainer { - Size = new Vector2(20), + Size = new Vector2(16f), Margin = new MarginPadding { Horizontal = 5f }, Colour = colourProvider.Background5, }; @@ -100,12 +100,13 @@ namespace osu.Game.Screens.SelectV2 { Anchor = Anchor.CentreLeft, Origin = Anchor.CentreLeft, + Scale = new Vector2(0.875f), }, localRank = new PanelLocalRankDisplay { Anchor = Anchor.CentreLeft, Origin = Anchor.CentreLeft, - Scale = new Vector2(0.75f) + Scale = new Vector2(0.65f) }, starCounter = new StarCounter { @@ -123,22 +124,22 @@ namespace osu.Game.Screens.SelectV2 { keyCountText = new OsuSpriteText { - Font = OsuFont.GetFont(size: 18, weight: FontWeight.SemiBold), + Font = OsuFont.Style.Body.With(weight: FontWeight.SemiBold), Anchor = Anchor.BottomLeft, Origin = Anchor.BottomLeft, Alpha = 0, }, difficultyText = new OsuSpriteText { - Font = OsuFont.GetFont(size: 18, weight: FontWeight.SemiBold), + Font = OsuFont.Style.Body.With(weight: FontWeight.SemiBold), Anchor = Anchor.BottomLeft, Origin = Anchor.BottomLeft, - Margin = new MarginPadding { Right = 8f }, + Margin = new MarginPadding { Right = 5f }, }, authorText = new OsuSpriteText { Colour = colourProvider.Content2, - Font = OsuFont.GetFont(weight: FontWeight.SemiBold), + Font = OsuFont.Style.Caption1.With(weight: FontWeight.SemiBold), Anchor = Anchor.BottomLeft, Origin = Anchor.BottomLeft } diff --git a/osu.Game/Screens/SelectV2/PanelBeatmapSet.cs b/osu.Game/Screens/SelectV2/PanelBeatmapSet.cs index 179d4d6444..7f5aa6ffe8 100644 --- a/osu.Game/Screens/SelectV2/PanelBeatmapSet.cs +++ b/osu.Game/Screens/SelectV2/PanelBeatmapSet.cs @@ -20,7 +20,7 @@ namespace osu.Game.Screens.SelectV2 { public partial class PanelBeatmapSet : Panel { - public const float HEIGHT = CarouselItem.DEFAULT_HEIGHT * 1.6f; + public const float HEIGHT = CarouselItem.DEFAULT_HEIGHT * 1.7f; private PanelSetBackground background = null!; @@ -55,7 +55,7 @@ namespace osu.Game.Screens.SelectV2 Anchor = Anchor.Centre, Origin = Anchor.Centre, Icon = FontAwesome.Solid.ChevronRight, - Size = new Vector2(12), + Size = new Vector2(8), X = 1f, Colour = colourProvider.Background5, }, @@ -77,17 +77,17 @@ namespace osu.Game.Screens.SelectV2 { titleText = new OsuSpriteText { - Font = OsuFont.GetFont(weight: FontWeight.Bold, size: 22, italics: true), + Font = OsuFont.Style.Heading1.With(typeface: Typeface.TorusAlternate), }, artistText = new OsuSpriteText { - Font = OsuFont.GetFont(weight: FontWeight.SemiBold, size: 17, italics: true), + Font = OsuFont.Style.Body.With(weight: FontWeight.SemiBold), }, new FillFlowContainer { Direction = FillDirection.Horizontal, AutoSizeAxes = Axes.Both, - Margin = new MarginPadding { Top = 5f }, + Margin = new MarginPadding { Top = 4f }, Children = new Drawable[] { updateButton = new PanelUpdateBeatmapButton @@ -100,8 +100,7 @@ namespace osu.Game.Screens.SelectV2 { Origin = Anchor.CentreLeft, Anchor = Anchor.CentreLeft, - TextSize = 11, - TextPadding = new MarginPadding { Horizontal = 8, Vertical = 2 }, + TextSize = OsuFont.Style.Caption2.Size, Margin = new MarginPadding { Right = 5f }, }, difficultiesDisplay = new DifficultySpectrumDisplay diff --git a/osu.Game/Screens/SelectV2/PanelBeatmapStandalone.cs b/osu.Game/Screens/SelectV2/PanelBeatmapStandalone.cs index a0d7484587..a90a84d115 100644 --- a/osu.Game/Screens/SelectV2/PanelBeatmapStandalone.cs +++ b/osu.Game/Screens/SelectV2/PanelBeatmapStandalone.cs @@ -26,7 +26,7 @@ namespace osu.Game.Screens.SelectV2 { public partial class PanelBeatmapStandalone : Panel { - public const float HEIGHT = CarouselItem.DEFAULT_HEIGHT * 1.6f; + public const float HEIGHT = CarouselItem.DEFAULT_HEIGHT * 1.7f; [Resolved] private IBindable ruleset { get; set; } = null!; @@ -76,7 +76,7 @@ namespace osu.Game.Screens.SelectV2 Icon = difficultyIcon = new ConstrainedIconContainer { - Size = new Vector2(20), + Size = new Vector2(16), Margin = new MarginPadding { Horizontal = 5f }, Colour = colourProvider.Background5, }; @@ -95,19 +95,16 @@ namespace osu.Game.Screens.SelectV2 { titleText = new OsuSpriteText { - Font = OsuFont.GetFont(weight: FontWeight.Bold, size: 22, italics: true), - Shadow = true, + Font = OsuFont.Style.Heading1.With(typeface: Typeface.TorusAlternate), }, artistText = new OsuSpriteText { - Font = OsuFont.GetFont(weight: FontWeight.SemiBold, size: 17, italics: true), - Shadow = true, + Font = OsuFont.Style.Body.With(weight: FontWeight.SemiBold), }, new FillFlowContainer { Direction = FillDirection.Horizontal, AutoSizeAxes = Axes.Both, - Margin = new MarginPadding { Top = 5f }, Children = new Drawable[] { updateButton = new PanelUpdateBeatmapButton @@ -120,8 +117,7 @@ namespace osu.Game.Screens.SelectV2 { Origin = Anchor.CentreLeft, Anchor = Anchor.CentreLeft, - TextSize = 11, - TextPadding = new MarginPadding { Horizontal = 8, Vertical = 2 }, + TextSize = OsuFont.Style.Caption2.Size, Margin = new MarginPadding { Right = 5f }, }, difficultyLine = new FillFlowContainer @@ -134,19 +130,19 @@ namespace osu.Game.Screens.SelectV2 { Origin = Anchor.CentreLeft, Anchor = Anchor.CentreLeft, - Scale = new Vector2(8f / 9f), + Scale = new Vector2(0.875f), Margin = new MarginPadding { Right = 5f }, }, difficultyRank = new PanelLocalRankDisplay { - Scale = new Vector2(8f / 11), + Scale = new Vector2(0.65f), Origin = Anchor.CentreLeft, Anchor = Anchor.CentreLeft, Margin = new MarginPadding { Right = 5f }, }, difficultyKeyCountText = new OsuSpriteText { - Font = OsuFont.GetFont(size: 18, weight: FontWeight.SemiBold), + Font = OsuFont.Style.Heading2, Anchor = Anchor.BottomLeft, Origin = Anchor.BottomLeft, Alpha = 0, @@ -154,7 +150,7 @@ namespace osu.Game.Screens.SelectV2 }, difficultyName = new OsuSpriteText { - Font = OsuFont.GetFont(size: 18, weight: FontWeight.SemiBold), + Font = OsuFont.Style.Heading2, Origin = Anchor.BottomLeft, Anchor = Anchor.BottomLeft, Margin = new MarginPadding { Right = 5f, Bottom = 2f }, @@ -162,7 +158,7 @@ namespace osu.Game.Screens.SelectV2 difficultyAuthor = new OsuSpriteText { Colour = colourProvider.Content2, - Font = OsuFont.GetFont(weight: FontWeight.SemiBold), + Font = OsuFont.Style.Caption1.With(weight: FontWeight.SemiBold), Origin = Anchor.BottomLeft, Anchor = Anchor.BottomLeft, Margin = new MarginPadding { Right = 5f, Bottom = 2f }, diff --git a/osu.Game/Screens/SelectV2/PanelUpdateBeatmapButton.cs b/osu.Game/Screens/SelectV2/PanelUpdateBeatmapButton.cs index 2a850321a6..4c767df9d8 100644 --- a/osu.Game/Screens/SelectV2/PanelUpdateBeatmapButton.cs +++ b/osu.Game/Screens/SelectV2/PanelUpdateBeatmapButton.cs @@ -55,7 +55,7 @@ namespace osu.Game.Screens.SelectV2 public PanelUpdateBeatmapButton() { - Size = new Vector2(75f, 22f); + Size = new Vector2(72, 22f); } private Bindable preferNoVideo = null!; @@ -63,7 +63,7 @@ namespace osu.Game.Screens.SelectV2 [BackgroundDependencyLoader] private void load(OsuConfigManager config) { - const float icon_size = 14; + const float icon_size = 12; preferNoVideo = config.GetBindable(OsuSetting.PreferNoVideo); @@ -110,7 +110,7 @@ namespace osu.Game.Screens.SelectV2 { Anchor = Anchor.CentreLeft, Origin = Anchor.CentreLeft, - Font = OsuFont.Default.With(weight: FontWeight.Bold), + Font = OsuFont.Style.Body.With(weight: FontWeight.SemiBold), Text = "Update", } } From 144cec14682baba5b8397c4e9a03df150047ff27 Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Thu, 17 Apr 2025 08:23:33 -0400 Subject: [PATCH 098/164] Add test cases to visualise rank display in panels --- .../SongSelectV2/TestScenePanelBeatmap.cs | 19 +++++++++++++++++++ .../TestScenePanelBeatmapStandalone.cs | 19 +++++++++++++++++++ 2 files changed, 38 insertions(+) diff --git a/osu.Game.Tests/Visual/SongSelectV2/TestScenePanelBeatmap.cs b/osu.Game.Tests/Visual/SongSelectV2/TestScenePanelBeatmap.cs index 53a1355fc2..c0a77553c2 100644 --- a/osu.Game.Tests/Visual/SongSelectV2/TestScenePanelBeatmap.cs +++ b/osu.Game.Tests/Visual/SongSelectV2/TestScenePanelBeatmap.cs @@ -1,17 +1,23 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System; using System.Linq; using NUnit.Framework; using osu.Framework.Allocation; +using osu.Framework.Extensions; +using osu.Framework.Extensions.IEnumerableExtensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; +using osu.Framework.Testing; using osu.Framework.Utils; using osu.Game.Beatmaps; using osu.Game.Graphics.Carousel; +using osu.Game.Online.Leaderboards; using osu.Game.Overlays; using osu.Game.Rulesets.Mania; using osu.Game.Rulesets.Osu; +using osu.Game.Scoring; using osu.Game.Screens.SelectV2; using osu.Game.Tests.Resources; using osu.Game.Tests.Visual.UserInterface; @@ -66,6 +72,19 @@ namespace osu.Game.Tests.Visual.SongSelectV2 AddToggleStep("mania ruleset", v => Ruleset.Value = v ? new ManiaRuleset().RulesetInfo : new OsuRuleset().RulesetInfo); } + [Test] + public void TestLocalRank() + { + foreach (var rank in Enum.GetValues()) + { + AddStep($"set {rank.GetDescription()} rank", () => this.ChildrenOfType().ForEach(p => + { + p.Show(); + p.Rank = rank; + })); + } + } + protected override Drawable CreateContent() { return new FillFlowContainer diff --git a/osu.Game.Tests/Visual/SongSelectV2/TestScenePanelBeatmapStandalone.cs b/osu.Game.Tests/Visual/SongSelectV2/TestScenePanelBeatmapStandalone.cs index 4adee17868..93e495320f 100644 --- a/osu.Game.Tests/Visual/SongSelectV2/TestScenePanelBeatmapStandalone.cs +++ b/osu.Game.Tests/Visual/SongSelectV2/TestScenePanelBeatmapStandalone.cs @@ -1,17 +1,23 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System; using System.Linq; using NUnit.Framework; using osu.Framework.Allocation; +using osu.Framework.Extensions; +using osu.Framework.Extensions.IEnumerableExtensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; +using osu.Framework.Testing; using osu.Framework.Utils; using osu.Game.Beatmaps; using osu.Game.Graphics.Carousel; +using osu.Game.Online.Leaderboards; using osu.Game.Overlays; using osu.Game.Rulesets.Mania; using osu.Game.Rulesets.Osu; +using osu.Game.Scoring; using osu.Game.Screens.SelectV2; using osu.Game.Tests.Resources; using osu.Game.Tests.Visual.UserInterface; @@ -66,6 +72,19 @@ namespace osu.Game.Tests.Visual.SongSelectV2 AddToggleStep("mania ruleset", v => Ruleset.Value = v ? new ManiaRuleset().RulesetInfo : new OsuRuleset().RulesetInfo); } + [Test] + public void TestLocalRank() + { + foreach (var rank in Enum.GetValues()) + { + AddStep($"set {rank.GetDescription()} rank", () => this.ChildrenOfType().ForEach(p => + { + p.Show(); + p.Rank = rank; + })); + } + } + protected override Drawable CreateContent() { return new FillFlowContainer From d546bbaf8f25bbcf3f74221e0a4ec04d5a781acc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 17 Apr 2025 14:26:34 +0200 Subject: [PATCH 099/164] Attempt to fix tests --- .../MultiplayerGameplayLeaderboardTestScene.cs | 17 +++++++++++++---- .../SoloGameplayLeaderboardProvider.cs | 17 ++++++++++------- 2 files changed, 23 insertions(+), 11 deletions(-) diff --git a/osu.Game.Tests/Visual/Multiplayer/MultiplayerGameplayLeaderboardTestScene.cs b/osu.Game.Tests/Visual/Multiplayer/MultiplayerGameplayLeaderboardTestScene.cs index 644b7f522e..1481629ba0 100644 --- a/osu.Game.Tests/Visual/Multiplayer/MultiplayerGameplayLeaderboardTestScene.cs +++ b/osu.Game.Tests/Visual/Multiplayer/MultiplayerGameplayLeaderboardTestScene.cs @@ -147,10 +147,19 @@ namespace osu.Game.Tests.Visual.Multiplayer AddUntilStep("wait for load", () => Leaderboard!.IsLoaded); - AddStep("check watch requests were sent", () => + AddUntilStep("check watch requests were sent", () => { - foreach (var user in MultiplayerUsers) - spectatorClient.Verify(s => s.WatchUser(user.UserID), Times.Once); + try + { + foreach (var user in MultiplayerUsers) + spectatorClient.Verify(s => s.WatchUser(user.UserID), Times.Once); + + return true; + } + catch (MockException) + { + return false; + } }); } @@ -181,7 +190,7 @@ namespace osu.Game.Tests.Visual.Multiplayer spectatorClient.Verify(s => s.StopWatchingUser(user.UserID), Times.Once); return true; } - catch + catch (MockException) { return false; } diff --git a/osu.Game/Screens/Select/Leaderboards/SoloGameplayLeaderboardProvider.cs b/osu.Game/Screens/Select/Leaderboards/SoloGameplayLeaderboardProvider.cs index 125e8fdc9d..216fda8d9f 100644 --- a/osu.Game/Screens/Select/Leaderboards/SoloGameplayLeaderboardProvider.cs +++ b/osu.Game/Screens/Select/Leaderboards/SoloGameplayLeaderboardProvider.cs @@ -19,11 +19,11 @@ namespace osu.Game.Screens.Select.Leaderboards private readonly BindableList scores = new BindableList(); [BackgroundDependencyLoader] - private void load(LeaderboardManager leaderboardManager, GameplayState gameplayState) + private void load(LeaderboardManager? leaderboardManager, GameplayState? gameplayState) { - var globalScores = leaderboardManager.Scores.Value; + var globalScores = leaderboardManager?.Scores.Value; - IsPartial = leaderboardManager.CurrentCriteria?.Scope != BeatmapLeaderboardScope.Local && globalScores?.TopScores.Count >= 50; + IsPartial = leaderboardManager?.CurrentCriteria?.Scope != BeatmapLeaderboardScope.Local && globalScores?.TopScores.Count >= 50; if (globalScores != null) { @@ -31,11 +31,14 @@ namespace osu.Game.Screens.Select.Leaderboards scores.Add(new GameplayLeaderboardScore(topScore, false)); } - scores.Add(new GameplayLeaderboardScore(gameplayState.Score.ScoreInfo.User, gameplayState.ScoreProcessor, true) + if (gameplayState != null) { - // Local score should always show lower than any existing scores in cases of ties. - DisplayOrder = { Value = long.MaxValue } - }); + scores.Add(new GameplayLeaderboardScore(gameplayState.Score.ScoreInfo.User, gameplayState.ScoreProcessor, true) + { + // Local score should always show lower than any existing scores in cases of ties. + DisplayOrder = { Value = long.MaxValue } + }); + } } } } From 8e3bace2721ca9ec66978f1ab20675bbee143608 Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Wed, 16 Apr 2025 06:53:03 -0400 Subject: [PATCH 100/164] Add general constants in `SongSelect` --- osu.Game/Screens/SelectV2/SongSelect.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/osu.Game/Screens/SelectV2/SongSelect.cs b/osu.Game/Screens/SelectV2/SongSelect.cs index 67ca110dab..ca09b2a40a 100644 --- a/osu.Game/Screens/SelectV2/SongSelect.cs +++ b/osu.Game/Screens/SelectV2/SongSelect.cs @@ -22,6 +22,10 @@ namespace osu.Game.Screens.SelectV2 { private const float logo_scale = 0.4f; + public const float WEDGE_CONTENT_MARGIN = CORNER_RADIUS_HIDE_OFFSET + OsuGame.SCREEN_EDGE_MARGIN; + public const float CORNER_RADIUS_HIDE_OFFSET = 20f; + public const float ENTER_DURATION = 600; + private readonly ModSelectOverlay modSelectOverlay = new ModSelectOverlay(OverlayColourScheme.Aquamarine) { ShowPresets = true, From 89a8c50a45afcd6893fb2e06f48e07e66d6b80fc Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Wed, 16 Apr 2025 06:53:11 -0400 Subject: [PATCH 101/164] Add `WedgeBackground` --- osu.Game/Screens/SelectV2/WedgeBackground.cs | 54 ++++++++++++++++++++ 1 file changed, 54 insertions(+) create mode 100644 osu.Game/Screens/SelectV2/WedgeBackground.cs diff --git a/osu.Game/Screens/SelectV2/WedgeBackground.cs b/osu.Game/Screens/SelectV2/WedgeBackground.cs new file mode 100644 index 0000000000..ecfbd51260 --- /dev/null +++ b/osu.Game/Screens/SelectV2/WedgeBackground.cs @@ -0,0 +1,54 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Allocation; +using osu.Framework.Extensions.Color4Extensions; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Colour; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; +using osu.Game.Overlays; + +namespace osu.Game.Screens.SelectV2 +{ + internal partial class WedgeBackground : CompositeDrawable + { + public float StartAlpha { get; init; } = 0.9f; + + public float FinalAlpha { get; init; } = 0.6f; + + public float WidthForGradient { get; init; } = 0.3f; + + [BackgroundDependencyLoader] + private void load(OverlayColourProvider colourProvider) + { + RelativeSizeAxes = Axes.Both; + + InternalChildren = new Drawable[] + { + new Box + { + Blending = BlendingParameters.Additive, + RelativeSizeAxes = Axes.Both, + Width = 0.6f, + Alpha = 0.5f, + Colour = ColourInfo.GradientHorizontal(colourProvider.Background2, colourProvider.Background2.Opacity(0)), + }, + new Box + { + RelativeSizeAxes = Axes.Both, + Width = 1 - WidthForGradient, + Colour = colourProvider.Background5.Opacity(StartAlpha), + }, + new Box + { + Anchor = Anchor.TopRight, + Origin = Anchor.TopRight, + RelativeSizeAxes = Axes.Both, + Width = WidthForGradient, + Colour = ColourInfo.GradientHorizontal(colourProvider.Background5.Opacity(StartAlpha), colourProvider.Background5.Opacity(FinalAlpha)), + }, + }; + } + } +} From 10c421682af3e11f03451c471cd180cb127bc98a Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Wed, 16 Apr 2025 08:15:59 -0400 Subject: [PATCH 102/164] Add popover layer in test scene base class and use half width by default --- .../SongSelectComponentsTestScene.cs | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/osu.Game.Tests/Visual/SongSelectV2/SongSelectComponentsTestScene.cs b/osu.Game.Tests/Visual/SongSelectV2/SongSelectComponentsTestScene.cs index 9e9cd3505a..87c96763d5 100644 --- a/osu.Game.Tests/Visual/SongSelectV2/SongSelectComponentsTestScene.cs +++ b/osu.Game.Tests/Visual/SongSelectV2/SongSelectComponentsTestScene.cs @@ -4,6 +4,7 @@ using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Cursor; using osu.Framework.Testing; using osu.Game.Graphics.Cursor; using osu.Game.Overlays; @@ -27,18 +28,19 @@ namespace osu.Game.Tests.Visual.SongSelectV2 [BackgroundDependencyLoader] private void load() { - base.Content.Child = resizeContainer = new Container + base.Content.Child = new PopoverContainer { - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, - Width = relativeWidth, - Children = new Drawable[] + RelativeSizeAxes = Axes.Both, + Child = resizeContainer = new Container { - Content + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Width = relativeWidth, + Child = Content } }; - AddSliderStep("change relative width", 0, 1f, 1f, v => + AddSliderStep("change relative width", 0, 1f, 0.5f, v => { if (resizeContainer != null) resizeContainer.Width = v; From bfe8cc47ecd6f4758965ddc23eb8ab7690062e86 Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Thu, 17 Apr 2025 07:37:50 -0400 Subject: [PATCH 103/164] Introduce customisation properties to base song select test scene --- .../Visual/SongSelectV2/SongSelectComponentsTestScene.cs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/osu.Game.Tests/Visual/SongSelectV2/SongSelectComponentsTestScene.cs b/osu.Game.Tests/Visual/SongSelectV2/SongSelectComponentsTestScene.cs index 87c96763d5..f86ca869e1 100644 --- a/osu.Game.Tests/Visual/SongSelectV2/SongSelectComponentsTestScene.cs +++ b/osu.Game.Tests/Visual/SongSelectV2/SongSelectComponentsTestScene.cs @@ -25,6 +25,9 @@ namespace osu.Game.Tests.Visual.SongSelectV2 private Container? resizeContainer; private float relativeWidth; + protected virtual Anchor ComponentAnchor => Anchor.TopLeft; + protected virtual float InitialRelativeWidth => 0.5f; + [BackgroundDependencyLoader] private void load() { @@ -33,6 +36,8 @@ namespace osu.Game.Tests.Visual.SongSelectV2 RelativeSizeAxes = Axes.Both, Child = resizeContainer = new Container { + Anchor = ComponentAnchor, + Origin = ComponentAnchor, RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y, Width = relativeWidth, @@ -40,7 +45,7 @@ namespace osu.Game.Tests.Visual.SongSelectV2 } }; - AddSliderStep("change relative width", 0, 1f, 0.5f, v => + AddSliderStep("change relative width", 0, 1f, InitialRelativeWidth, v => { if (resizeContainer != null) resizeContainer.Width = v; From f93e731a5556a541b8c0fb4fb888492a1232720c Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Thu, 17 Apr 2025 08:51:53 -0400 Subject: [PATCH 104/164] Adjust sheared dropdown menu padding --- .../UserInterfaceV2/ShearedDropdown.cs | 43 ++++++++----------- 1 file changed, 17 insertions(+), 26 deletions(-) diff --git a/osu.Game/Graphics/UserInterfaceV2/ShearedDropdown.cs b/osu.Game/Graphics/UserInterfaceV2/ShearedDropdown.cs index 609f77dd7e..d77b9be2da 100644 --- a/osu.Game/Graphics/UserInterfaceV2/ShearedDropdown.cs +++ b/osu.Game/Graphics/UserInterfaceV2/ShearedDropdown.cs @@ -36,16 +36,6 @@ namespace osu.Game.Graphics.UserInterfaceV2 } } - protected override void Update() - { - base.Update(); - - var header = (ShearedDropdownHeader)Header; - var menu = (ShearedDropdownMenu)Menu; - - menu.Padding = new MarginPadding { Left = header.LabelContainer.DrawWidth - 10f, Right = 6f }; - } - public bool OnPressed(KeyBindingPressEvent e) { if (e.Repeat) return false; @@ -62,16 +52,15 @@ namespace osu.Game.Graphics.UserInterfaceV2 protected partial class ShearedDropdownMenu : OsuDropdown.OsuDropdownMenu { - public new MarginPadding Padding - { - get => base.Padding; - set => base.Padding = value; - } - public ShearedDropdownMenu() { Shear = OsuGame.SHEAR; Margin = new MarginPadding { Top = 5f }; + Padding = new MarginPadding + { + Left = -6f, + Right = 6f + }; } protected override DrawableDropdownMenuItem CreateDrawableDropdownMenuItem(MenuItem item) => new ShearedMenuItem(item) @@ -92,8 +81,6 @@ namespace osu.Game.Graphics.UserInterfaceV2 public partial class ShearedDropdownHeader : DropdownHeader { - private const float corner_radius = 5f; - private LocalisableString label; protected override LocalisableString Label @@ -127,7 +114,7 @@ namespace osu.Game.Graphics.UserInterfaceV2 public ShearedDropdownHeader() { Shear = OsuGame.SHEAR; - CornerRadius = corner_radius; + CornerRadius = ShearedButton.CORNER_RADIUS; Masking = true; Foreground.Children = new Drawable[] @@ -148,7 +135,8 @@ namespace osu.Game.Graphics.UserInterfaceV2 { LabelContainer = new Container { - CornerRadius = corner_radius, + Depth = float.MaxValue, + CornerRadius = ShearedButton.CORNER_RADIUS, Masking = true, AutoSizeAxes = Axes.Both, Children = new Drawable[] @@ -159,8 +147,13 @@ namespace osu.Game.Graphics.UserInterfaceV2 }, labelText = new OsuSpriteText { - Margin = new MarginPadding { Horizontal = 10f, Vertical = 8f }, - Font = OsuFont.Torus.With(size: 16.8f, weight: FontWeight.SemiBold), + Margin = new MarginPadding + { + Horizontal = 10f, + // Chosen specifically so the height of these dropdowns matches ShearedToggleButton (30). + Vertical = 7f + }, + Font = OsuFont.Style.Body.With(weight: FontWeight.SemiBold), Shear = -OsuGame.SHEAR, }, }, @@ -180,7 +173,7 @@ namespace osu.Game.Graphics.UserInterfaceV2 Anchor = Anchor.CentreLeft, Origin = Anchor.CentreLeft, Padding = new MarginPadding { Right = 15f }, - Font = OsuFont.Torus.With(size: 16.8f, weight: FontWeight.SemiBold), + Font = OsuFont.Style.Body, RelativeSizeAxes = Axes.X, }, chevron = new SpriteIcon @@ -197,8 +190,6 @@ namespace osu.Game.Graphics.UserInterfaceV2 } }, }; - - AddInternal(LabelContainer.CreateProxy()); } [BackgroundDependencyLoader] @@ -223,7 +214,7 @@ namespace osu.Game.Graphics.UserInterfaceV2 searchBar.Padding = new MarginPadding { Left = LabelContainer.DrawWidth }; // By limiting the width we avoid this box showing up as an outline around the drawables that are on top of it. - Background.Padding = new MarginPadding { Left = LabelContainer.DrawWidth - corner_radius }; + Background.Padding = new MarginPadding { Left = LabelContainer.DrawWidth - ShearedButton.CORNER_RADIUS }; } protected override bool OnHover(HoverEvent e) From a6a8e2a44fb410f3382b0ddb7f4a2b2b777a38b3 Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Thu, 17 Apr 2025 07:36:03 -0400 Subject: [PATCH 105/164] Move collection dropdown test coverage to isolated test scene --- .../TestSceneCollectionDropdown.cs | 271 ++++++++++++++++++ .../SongSelect/TestSceneFilterControl.cs | 252 +--------------- 2 files changed, 272 insertions(+), 251 deletions(-) create mode 100644 osu.Game.Tests/Visual/Collections/TestSceneCollectionDropdown.cs diff --git a/osu.Game.Tests/Visual/Collections/TestSceneCollectionDropdown.cs b/osu.Game.Tests/Visual/Collections/TestSceneCollectionDropdown.cs new file mode 100644 index 0000000000..a47f3c5108 --- /dev/null +++ b/osu.Game.Tests/Visual/Collections/TestSceneCollectionDropdown.cs @@ -0,0 +1,271 @@ +// 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 NUnit.Framework; +using osu.Framework.Allocation; +using osu.Framework.Extensions; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Sprites; +using osu.Framework.Graphics.UserInterface; +using osu.Framework.Platform; +using osu.Framework.Testing; +using osu.Game.Beatmaps; +using osu.Game.Collections; +using osu.Game.Graphics.UserInterface; +using osu.Game.Overlays; +using osu.Game.Rulesets; +using osu.Game.Tests.Resources; +using osuTK.Input; +using Realms; + +namespace osu.Game.Tests.Visual.Collections +{ + public partial class TestSceneCollectionDropdown : OsuManualInputManagerTestScene + { + private BeatmapManager beatmapManager = null!; + private CollectionDropdown dropdown = null!; + + [Cached] + private readonly OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Aquamarine); + + [BackgroundDependencyLoader] + private void load(GameHost host) + { + Dependencies.Cache(new RealmRulesetStore(Realm)); + Dependencies.Cache(beatmapManager = new BeatmapManager(LocalStorage, Realm, null, Audio, Resources, host, Beatmap.Default)); + Dependencies.Cache(Realm); + + beatmapManager.Import(TestResources.GetQuickTestBeatmapForImport()).WaitSafely(); + } + + [SetUp] + public void SetUp() => Schedule(() => + { + writeAndRefresh(r => r.RemoveAll()); + + Child = new Container + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Child = dropdown = new CollectionDropdown + { + Width = 300, + Y = 100, + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre, + }, + }; + }); + + [Test] + public void TestEmptyCollectionFilterContainsAllBeatmaps() + { + assertCollectionDropdownContains("All beatmaps"); + assertCollectionHeaderDisplays("All beatmaps"); + } + + [Test] + public void TestCollectionAddedToDropdown() + { + AddStep("add collection", () => writeAndRefresh(r => r.Add(new BeatmapCollection(name: "1")))); + AddStep("add collection", () => writeAndRefresh(r => r.Add(new BeatmapCollection(name: "2")))); + assertCollectionDropdownContains("1"); + assertCollectionDropdownContains("2"); + } + + [Test] + public void TestCollectionsCleared() + { + AddStep("add collection", () => writeAndRefresh(r => r.Add(new BeatmapCollection(name: "1")))); + AddStep("add collection", () => writeAndRefresh(r => r.Add(new BeatmapCollection(name: "2")))); + AddStep("add collection", () => writeAndRefresh(r => r.Add(new BeatmapCollection(name: "3")))); + + AddAssert("check count 5", () => dropdown.ChildrenOfType().Single().ChildrenOfType().Count(), () => Is.EqualTo(5)); + + AddStep("delete all collections", () => writeAndRefresh(r => r.RemoveAll())); + + AddAssert("check count 2", () => dropdown.ChildrenOfType().Single().ChildrenOfType().Count(), () => Is.EqualTo(2)); + } + + [Test] + public void TestCollectionRemovedFromDropdown() + { + BeatmapCollection first = null!; + + AddStep("add collection", () => writeAndRefresh(r => r.Add(first = new BeatmapCollection(name: "1")))); + AddStep("add collection", () => writeAndRefresh(r => r.Add(new BeatmapCollection(name: "2")))); + AddStep("remove collection", () => writeAndRefresh(r => r.Remove(first))); + + assertCollectionDropdownContains("1", false); + assertCollectionDropdownContains("2"); + } + + [Test] + public void TestCollectionRenamed() + { + AddStep("add collection", () => writeAndRefresh(r => r.Add(new BeatmapCollection(name: "1")))); + assertCollectionDropdownContains("1"); + AddStep("select collection", () => dropdown.Current.Value = dropdown.ItemSource.ElementAt(1)); + + addExpandHeaderStep(); + + AddStep("change name", () => writeAndRefresh(_ => getFirstCollection().Name = "First")); + + assertCollectionDropdownContains("First"); + assertCollectionHeaderDisplays("First"); + } + + [Test] + public void TestAllBeatmapFilterDoesNotHaveAddButton() + { + addExpandHeaderStep(); + AddStep("hover all beatmaps", () => InputManager.MoveMouseTo(getAddOrRemoveButton(0))); + AddAssert("'All beatmaps' filter does not have add button", () => !getAddOrRemoveButton(0).IsPresent); + } + + [Test] + public void TestCollectionFilterHasAddButton() + { + addExpandHeaderStep(); + AddStep("add collection", () => writeAndRefresh(r => r.Add(new BeatmapCollection(name: "1")))); + assertCollectionDropdownContains("1"); + AddStep("hover collection", () => InputManager.MoveMouseTo(getAddOrRemoveButton(1))); + AddAssert("collection has add button", () => getAddOrRemoveButton(1).IsPresent); + } + + [Test] + public void TestButtonDisabledAndEnabledWithBeatmapChanges() + { + addExpandHeaderStep(); + + AddStep("add collection", () => writeAndRefresh(r => r.Add(new BeatmapCollection(name: "1")))); + assertCollectionDropdownContains("1"); + + AddStep("select available beatmap", () => Beatmap.Value = beatmapManager.GetWorkingBeatmap(beatmapManager.GetAllUsableBeatmapSets().First().Beatmaps[0])); + AddAssert("button enabled", () => getAddOrRemoveButton(1).Enabled.Value); + + AddStep("set dummy beatmap", () => Beatmap.SetDefault()); + AddAssert("button disabled", () => !getAddOrRemoveButton(1).Enabled.Value); + } + + [Test] + public void TestButtonChangesWhenAddedAndRemovedFromCollection() + { + addExpandHeaderStep(); + + AddStep("select available beatmap", () => Beatmap.Value = beatmapManager.GetWorkingBeatmap(beatmapManager.GetAllUsableBeatmapSets().First().Beatmaps[0])); + + AddStep("add collection", () => writeAndRefresh(r => r.Add(new BeatmapCollection(name: "1")))); + assertCollectionDropdownContains("1"); + + assertFirstButtonIs(FontAwesome.Solid.PlusSquare); + + AddStep("add beatmap to collection", () => writeAndRefresh(r => getFirstCollection().BeatmapMD5Hashes.Add(Beatmap.Value.BeatmapInfo.MD5Hash))); + assertFirstButtonIs(FontAwesome.Solid.MinusSquare); + + AddStep("remove beatmap from collection", () => writeAndRefresh(r => getFirstCollection().BeatmapMD5Hashes.Clear())); + assertFirstButtonIs(FontAwesome.Solid.PlusSquare); + } + + [Test] + public void TestButtonAddsAndRemovesBeatmap() + { + addExpandHeaderStep(); + + AddStep("select available beatmap", () => Beatmap.Value = beatmapManager.GetWorkingBeatmap(beatmapManager.GetAllUsableBeatmapSets().First().Beatmaps[0])); + + AddStep("add collection", () => writeAndRefresh(r => r.Add(new BeatmapCollection(name: "1")))); + assertCollectionDropdownContains("1"); + assertFirstButtonIs(FontAwesome.Solid.PlusSquare); + + addClickAddOrRemoveButtonStep(1); + AddAssert("collection contains beatmap", () => getFirstCollection().BeatmapMD5Hashes.Contains(Beatmap.Value.BeatmapInfo.MD5Hash)); + assertFirstButtonIs(FontAwesome.Solid.MinusSquare); + + addClickAddOrRemoveButtonStep(1); + AddAssert("collection does not contain beatmap", () => !getFirstCollection().BeatmapMD5Hashes.Contains(Beatmap.Value.BeatmapInfo.MD5Hash)); + assertFirstButtonIs(FontAwesome.Solid.PlusSquare); + } + + [Test] + public void TestManageCollectionsFilterIsNotSelected() + { + bool received = false; + + addExpandHeaderStep(); + + AddStep("add collection", () => writeAndRefresh(r => r.Add(new BeatmapCollection(name: "1", new List { "abc" })))); + assertCollectionDropdownContains("1"); + + AddStep("select collection", () => + { + InputManager.MoveMouseTo(getCollectionDropdownItemAt(1)); + InputManager.Click(MouseButton.Left); + }); + + addExpandHeaderStep(); + + AddStep("watch for filter requests", () => + { + received = false; + dropdown.ChildrenOfType().First().RequestFilter = () => received = true; + }); + + AddStep("click manage collections filter", () => + { + int lastItemIndex = dropdown.ChildrenOfType().Single().Items.Count() - 1; + InputManager.MoveMouseTo(getCollectionDropdownItemAt(lastItemIndex)); + InputManager.Click(MouseButton.Left); + }); + + AddAssert("collection filter still selected", () => dropdown.Current.Value.CollectionName == "1"); + + AddAssert("filter request not fired", () => !received); + } + + private void writeAndRefresh(Action action) => Realm.Write(r => + { + action(r); + r.Refresh(); + }); + + private BeatmapCollection getFirstCollection() => Realm.Run(r => r.All().First()); + + private void assertCollectionHeaderDisplays(string collectionName, bool shouldDisplay = true) + => AddUntilStep($"collection dropdown header displays '{collectionName}'", + () => shouldDisplay == dropdown.ChildrenOfType().Any(h => h.ChildrenOfType().Any(t => t.Text == collectionName))); + + private void assertFirstButtonIs(IconUsage icon) => AddUntilStep($"button is {icon.Icon.ToString()}", () => getAddOrRemoveButton(1).Icon.Equals(icon)); + + private void assertCollectionDropdownContains(string collectionName, bool shouldContain = true) => + AddUntilStep($"collection dropdown {(shouldContain ? "contains" : "does not contain")} '{collectionName}'", + // A bit of a roundabout way of going about this, see: https://github.com/ppy/osu-framework/issues/3871 + https://github.com/ppy/osu-framework/issues/3872 + () => shouldContain == dropdown.ChildrenOfType().Any(i => i.ChildrenOfType().OfType().First().Text == collectionName)); + + private IconButton getAddOrRemoveButton(int index) + => getCollectionDropdownItemAt(index).ChildrenOfType().Single(); + + private void addExpandHeaderStep() => AddStep("expand header", () => + { + InputManager.MoveMouseTo(dropdown.ChildrenOfType().Single()); + InputManager.Click(MouseButton.Left); + }); + + private void addClickAddOrRemoveButtonStep(int index) => AddStep("click add or remove button", () => + { + InputManager.MoveMouseTo(getAddOrRemoveButton(index)); + InputManager.Click(MouseButton.Left); + }); + + private Menu.DrawableMenuItem getCollectionDropdownItemAt(int index) + { + // todo: we should be able to use Items, but apparently that's not guaranteed to be ordered... see: https://github.com/ppy/osu-framework/pull/6079 + CollectionFilterMenuItem item = dropdown.ChildrenOfType().Single().ItemSource.ElementAt(index); + return dropdown.ChildrenOfType().Single(i => i.Item.Text.Value == item.CollectionName); + } + } +} diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneFilterControl.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneFilterControl.cs index a639d50eee..41e44357d7 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneFilterControl.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneFilterControl.cs @@ -1,57 +1,18 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using System; -using System.Collections.Generic; -using System.Linq; using NUnit.Framework; -using osu.Framework.Allocation; -using osu.Framework.Extensions; using osu.Framework.Graphics; -using osu.Framework.Graphics.Containers; -using osu.Framework.Graphics.Sprites; -using osu.Framework.Graphics.UserInterface; -using osu.Framework.Platform; -using osu.Framework.Testing; -using osu.Game.Beatmaps; -using osu.Game.Collections; -using osu.Game.Graphics.UserInterface; -using osu.Game.Rulesets; using osu.Game.Screens.Select; -using osu.Game.Tests.Resources; -using osuTK.Input; -using Realms; namespace osu.Game.Tests.Visual.SongSelect { public partial class TestSceneFilterControl : OsuManualInputManagerTestScene { - protected override Container Content { get; } = new Container { RelativeSizeAxes = Axes.Both }; - - private BeatmapManager beatmapManager = null!; - private FilterControl control = null!; - - [BackgroundDependencyLoader] - private void load(GameHost host) - { - Dependencies.Cache(new RealmRulesetStore(Realm)); - Dependencies.Cache(beatmapManager = new BeatmapManager(LocalStorage, Realm, null, Audio, Resources, host, Beatmap.Default)); - Dependencies.Cache(Realm); - - beatmapManager.Import(TestResources.GetQuickTestBeatmapForImport()).WaitSafely(); - - base.Content.AddRange(new Drawable[] - { - Content - }); - } - [SetUp] public void SetUp() => Schedule(() => { - writeAndRefresh(r => r.RemoveAll()); - - Child = control = new FilterControl + Child = new FilterControl { Anchor = Anchor.Centre, Origin = Anchor.Centre, @@ -59,216 +20,5 @@ namespace osu.Game.Tests.Visual.SongSelect Height = FilterControl.HEIGHT, }; }); - - [Test] - public void TestEmptyCollectionFilterContainsAllBeatmaps() - { - assertCollectionDropdownContains("All beatmaps"); - assertCollectionHeaderDisplays("All beatmaps"); - } - - [Test] - public void TestCollectionAddedToDropdown() - { - AddStep("add collection", () => writeAndRefresh(r => r.Add(new BeatmapCollection(name: "1")))); - AddStep("add collection", () => writeAndRefresh(r => r.Add(new BeatmapCollection(name: "2")))); - assertCollectionDropdownContains("1"); - assertCollectionDropdownContains("2"); - } - - [Test] - public void TestCollectionsCleared() - { - AddStep("add collection", () => writeAndRefresh(r => r.Add(new BeatmapCollection(name: "1")))); - AddStep("add collection", () => writeAndRefresh(r => r.Add(new BeatmapCollection(name: "2")))); - AddStep("add collection", () => writeAndRefresh(r => r.Add(new BeatmapCollection(name: "3")))); - - AddAssert("check count 5", () => control.ChildrenOfType().Single().ChildrenOfType().Count(), () => Is.EqualTo(5)); - - AddStep("delete all collections", () => writeAndRefresh(r => r.RemoveAll())); - - AddAssert("check count 2", () => control.ChildrenOfType().Single().ChildrenOfType().Count(), () => Is.EqualTo(2)); - } - - [Test] - public void TestCollectionRemovedFromDropdown() - { - BeatmapCollection first = null!; - - AddStep("add collection", () => writeAndRefresh(r => r.Add(first = new BeatmapCollection(name: "1")))); - AddStep("add collection", () => writeAndRefresh(r => r.Add(new BeatmapCollection(name: "2")))); - AddStep("remove collection", () => writeAndRefresh(r => r.Remove(first))); - - assertCollectionDropdownContains("1", false); - assertCollectionDropdownContains("2"); - } - - [Test] - public void TestCollectionRenamed() - { - AddStep("add collection", () => writeAndRefresh(r => r.Add(new BeatmapCollection(name: "1")))); - assertCollectionDropdownContains("1"); - AddStep("select collection", () => - { - var dropdown = control.ChildrenOfType().Single(); - dropdown.Current.Value = dropdown.ItemSource.ElementAt(1); - }); - - addExpandHeaderStep(); - - AddStep("change name", () => writeAndRefresh(_ => getFirstCollection().Name = "First")); - - assertCollectionDropdownContains("First"); - assertCollectionHeaderDisplays("First"); - } - - [Test] - public void TestAllBeatmapFilterDoesNotHaveAddButton() - { - addExpandHeaderStep(); - AddStep("hover all beatmaps", () => InputManager.MoveMouseTo(getAddOrRemoveButton(0))); - AddAssert("'All beatmaps' filter does not have add button", () => !getAddOrRemoveButton(0).IsPresent); - } - - [Test] - public void TestCollectionFilterHasAddButton() - { - addExpandHeaderStep(); - AddStep("add collection", () => writeAndRefresh(r => r.Add(new BeatmapCollection(name: "1")))); - assertCollectionDropdownContains("1"); - AddStep("hover collection", () => InputManager.MoveMouseTo(getAddOrRemoveButton(1))); - AddAssert("collection has add button", () => getAddOrRemoveButton(1).IsPresent); - } - - [Test] - public void TestButtonDisabledAndEnabledWithBeatmapChanges() - { - addExpandHeaderStep(); - - AddStep("add collection", () => writeAndRefresh(r => r.Add(new BeatmapCollection(name: "1")))); - assertCollectionDropdownContains("1"); - - AddStep("select available beatmap", () => Beatmap.Value = beatmapManager.GetWorkingBeatmap(beatmapManager.GetAllUsableBeatmapSets().First().Beatmaps[0])); - AddAssert("button enabled", () => getAddOrRemoveButton(1).Enabled.Value); - - AddStep("set dummy beatmap", () => Beatmap.SetDefault()); - AddAssert("button disabled", () => !getAddOrRemoveButton(1).Enabled.Value); - } - - [Test] - public void TestButtonChangesWhenAddedAndRemovedFromCollection() - { - addExpandHeaderStep(); - - AddStep("select available beatmap", () => Beatmap.Value = beatmapManager.GetWorkingBeatmap(beatmapManager.GetAllUsableBeatmapSets().First().Beatmaps[0])); - - AddStep("add collection", () => writeAndRefresh(r => r.Add(new BeatmapCollection(name: "1")))); - assertCollectionDropdownContains("1"); - - assertFirstButtonIs(FontAwesome.Solid.PlusSquare); - - AddStep("add beatmap to collection", () => writeAndRefresh(r => getFirstCollection().BeatmapMD5Hashes.Add(Beatmap.Value.BeatmapInfo.MD5Hash))); - assertFirstButtonIs(FontAwesome.Solid.MinusSquare); - - AddStep("remove beatmap from collection", () => writeAndRefresh(r => getFirstCollection().BeatmapMD5Hashes.Clear())); - assertFirstButtonIs(FontAwesome.Solid.PlusSquare); - } - - [Test] - public void TestButtonAddsAndRemovesBeatmap() - { - addExpandHeaderStep(); - - AddStep("select available beatmap", () => Beatmap.Value = beatmapManager.GetWorkingBeatmap(beatmapManager.GetAllUsableBeatmapSets().First().Beatmaps[0])); - - AddStep("add collection", () => writeAndRefresh(r => r.Add(new BeatmapCollection(name: "1")))); - assertCollectionDropdownContains("1"); - assertFirstButtonIs(FontAwesome.Solid.PlusSquare); - - addClickAddOrRemoveButtonStep(1); - AddAssert("collection contains beatmap", () => getFirstCollection().BeatmapMD5Hashes.Contains(Beatmap.Value.BeatmapInfo.MD5Hash)); - assertFirstButtonIs(FontAwesome.Solid.MinusSquare); - - addClickAddOrRemoveButtonStep(1); - AddAssert("collection does not contain beatmap", () => !getFirstCollection().BeatmapMD5Hashes.Contains(Beatmap.Value.BeatmapInfo.MD5Hash)); - assertFirstButtonIs(FontAwesome.Solid.PlusSquare); - } - - [Test] - public void TestManageCollectionsFilterIsNotSelected() - { - bool received = false; - - addExpandHeaderStep(); - - AddStep("add collection", () => writeAndRefresh(r => r.Add(new BeatmapCollection(name: "1", new List { "abc" })))); - assertCollectionDropdownContains("1"); - - AddStep("select collection", () => - { - InputManager.MoveMouseTo(getCollectionDropdownItemAt(1)); - InputManager.Click(MouseButton.Left); - }); - - addExpandHeaderStep(); - - AddStep("watch for filter requests", () => - { - received = false; - control.ChildrenOfType().First().RequestFilter = () => received = true; - }); - - AddStep("click manage collections filter", () => - { - int lastItemIndex = control.ChildrenOfType().Single().Items.Count() - 1; - InputManager.MoveMouseTo(getCollectionDropdownItemAt(lastItemIndex)); - InputManager.Click(MouseButton.Left); - }); - - AddAssert("collection filter still selected", () => control.CreateCriteria().CollectionBeatmapMD5Hashes?.Any() == true); - - AddAssert("filter request not fired", () => !received); - } - - private void writeAndRefresh(Action action) => Realm.Write(r => - { - action(r); - r.Refresh(); - }); - - private BeatmapCollection getFirstCollection() => Realm.Run(r => r.All().First()); - - private void assertCollectionHeaderDisplays(string collectionName, bool shouldDisplay = true) - => AddUntilStep($"collection dropdown header displays '{collectionName}'", - () => shouldDisplay == (control.ChildrenOfType().Single().ChildrenOfType().First().Text == collectionName)); - - private void assertFirstButtonIs(IconUsage icon) => AddUntilStep($"button is {icon.Icon.ToString()}", () => getAddOrRemoveButton(1).Icon.Equals(icon)); - - private void assertCollectionDropdownContains(string collectionName, bool shouldContain = true) => - AddUntilStep($"collection dropdown {(shouldContain ? "contains" : "does not contain")} '{collectionName}'", - // A bit of a roundabout way of going about this, see: https://github.com/ppy/osu-framework/issues/3871 + https://github.com/ppy/osu-framework/issues/3872 - () => shouldContain == control.ChildrenOfType().Any(i => i.ChildrenOfType().OfType().First().Text == collectionName)); - - private IconButton getAddOrRemoveButton(int index) - => getCollectionDropdownItemAt(index).ChildrenOfType().Single(); - - private void addExpandHeaderStep() => AddStep("expand header", () => - { - InputManager.MoveMouseTo(control.ChildrenOfType().Single()); - InputManager.Click(MouseButton.Left); - }); - - private void addClickAddOrRemoveButtonStep(int index) => AddStep("click add or remove button", () => - { - InputManager.MoveMouseTo(getAddOrRemoveButton(index)); - InputManager.Click(MouseButton.Left); - }); - - private Menu.DrawableMenuItem getCollectionDropdownItemAt(int index) - { - // todo: we should be able to use Items, but apparently that's not guaranteed to be ordered... see: https://github.com/ppy/osu-framework/pull/6079 - CollectionFilterMenuItem item = control.ChildrenOfType().Single().ItemSource.ElementAt(index); - return control.ChildrenOfType().Single(i => i.Item.Text.Value == item.CollectionName); - } } } From 54c13937af6e06cc1bc234f88627be80a52dad9a Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Thu, 17 Apr 2025 07:36:14 -0400 Subject: [PATCH 106/164] Add sheared collection dropdown --- .../TestSceneShearedCollectionDropdown.cs | 271 ++++++++++++++++++ .../Collections/ShearedCollectionDropdown.cs | 270 +++++++++++++++++ 2 files changed, 541 insertions(+) create mode 100644 osu.Game.Tests/Visual/Collections/TestSceneShearedCollectionDropdown.cs create mode 100644 osu.Game/Collections/ShearedCollectionDropdown.cs diff --git a/osu.Game.Tests/Visual/Collections/TestSceneShearedCollectionDropdown.cs b/osu.Game.Tests/Visual/Collections/TestSceneShearedCollectionDropdown.cs new file mode 100644 index 0000000000..f1afdf2019 --- /dev/null +++ b/osu.Game.Tests/Visual/Collections/TestSceneShearedCollectionDropdown.cs @@ -0,0 +1,271 @@ +// 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 NUnit.Framework; +using osu.Framework.Allocation; +using osu.Framework.Extensions; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Sprites; +using osu.Framework.Graphics.UserInterface; +using osu.Framework.Platform; +using osu.Framework.Testing; +using osu.Game.Beatmaps; +using osu.Game.Collections; +using osu.Game.Graphics.UserInterface; +using osu.Game.Overlays; +using osu.Game.Rulesets; +using osu.Game.Tests.Resources; +using osuTK.Input; +using Realms; + +namespace osu.Game.Tests.Visual.Collections +{ + public partial class TestSceneShearedCollectionDropdown : OsuManualInputManagerTestScene + { + private BeatmapManager beatmapManager = null!; + private ShearedCollectionDropdown dropdown = null!; + + [Cached] + private readonly OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Aquamarine); + + [BackgroundDependencyLoader] + private void load(GameHost host) + { + Dependencies.Cache(new RealmRulesetStore(Realm)); + Dependencies.Cache(beatmapManager = new BeatmapManager(LocalStorage, Realm, null, Audio, Resources, host, Beatmap.Default)); + Dependencies.Cache(Realm); + + beatmapManager.Import(TestResources.GetQuickTestBeatmapForImport()).WaitSafely(); + } + + [SetUp] + public void SetUp() => Schedule(() => + { + writeAndRefresh(r => r.RemoveAll()); + + Child = new Container + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Child = dropdown = new ShearedCollectionDropdown + { + Width = 300, + Y = 100, + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre, + }, + }; + }); + + [Test] + public void TestEmptyCollectionFilterContainsAllBeatmaps() + { + assertCollectionDropdownContains("All beatmaps"); + assertCollectionHeaderDisplays("All beatmaps"); + } + + [Test] + public void TestCollectionAddedToDropdown() + { + AddStep("add collection", () => writeAndRefresh(r => r.Add(new BeatmapCollection(name: "1")))); + AddStep("add collection", () => writeAndRefresh(r => r.Add(new BeatmapCollection(name: "2")))); + assertCollectionDropdownContains("1"); + assertCollectionDropdownContains("2"); + } + + [Test] + public void TestCollectionsCleared() + { + AddStep("add collection", () => writeAndRefresh(r => r.Add(new BeatmapCollection(name: "1")))); + AddStep("add collection", () => writeAndRefresh(r => r.Add(new BeatmapCollection(name: "2")))); + AddStep("add collection", () => writeAndRefresh(r => r.Add(new BeatmapCollection(name: "3")))); + + AddAssert("check count 5", () => dropdown.ChildrenOfType().Single().ChildrenOfType().Count(), () => Is.EqualTo(5)); + + AddStep("delete all collections", () => writeAndRefresh(r => r.RemoveAll())); + + AddAssert("check count 2", () => dropdown.ChildrenOfType().Single().ChildrenOfType().Count(), () => Is.EqualTo(2)); + } + + [Test] + public void TestCollectionRemovedFromDropdown() + { + BeatmapCollection first = null!; + + AddStep("add collection", () => writeAndRefresh(r => r.Add(first = new BeatmapCollection(name: "1")))); + AddStep("add collection", () => writeAndRefresh(r => r.Add(new BeatmapCollection(name: "2")))); + AddStep("remove collection", () => writeAndRefresh(r => r.Remove(first))); + + assertCollectionDropdownContains("1", false); + assertCollectionDropdownContains("2"); + } + + [Test] + public void TestCollectionRenamed() + { + AddStep("add collection", () => writeAndRefresh(r => r.Add(new BeatmapCollection(name: "1")))); + assertCollectionDropdownContains("1"); + AddStep("select collection", () => dropdown.Current.Value = dropdown.ItemSource.ElementAt(1)); + + addExpandHeaderStep(); + + AddStep("change name", () => writeAndRefresh(_ => getFirstCollection().Name = "First")); + + assertCollectionDropdownContains("First"); + assertCollectionHeaderDisplays("First"); + } + + [Test] + public void TestAllBeatmapFilterDoesNotHaveAddButton() + { + addExpandHeaderStep(); + AddStep("hover all beatmaps", () => InputManager.MoveMouseTo(getAddOrRemoveButton(0))); + AddAssert("'All beatmaps' filter does not have add button", () => !getAddOrRemoveButton(0).IsPresent); + } + + [Test] + public void TestCollectionFilterHasAddButton() + { + addExpandHeaderStep(); + AddStep("add collection", () => writeAndRefresh(r => r.Add(new BeatmapCollection(name: "1")))); + assertCollectionDropdownContains("1"); + AddStep("hover collection", () => InputManager.MoveMouseTo(getAddOrRemoveButton(1))); + AddAssert("collection has add button", () => getAddOrRemoveButton(1).IsPresent); + } + + [Test] + public void TestButtonDisabledAndEnabledWithBeatmapChanges() + { + addExpandHeaderStep(); + + AddStep("add collection", () => writeAndRefresh(r => r.Add(new BeatmapCollection(name: "1")))); + assertCollectionDropdownContains("1"); + + AddStep("select available beatmap", () => Beatmap.Value = beatmapManager.GetWorkingBeatmap(beatmapManager.GetAllUsableBeatmapSets().First().Beatmaps[0])); + AddAssert("button enabled", () => getAddOrRemoveButton(1).Enabled.Value); + + AddStep("set dummy beatmap", () => Beatmap.SetDefault()); + AddAssert("button disabled", () => !getAddOrRemoveButton(1).Enabled.Value); + } + + [Test] + public void TestButtonChangesWhenAddedAndRemovedFromCollection() + { + addExpandHeaderStep(); + + AddStep("select available beatmap", () => Beatmap.Value = beatmapManager.GetWorkingBeatmap(beatmapManager.GetAllUsableBeatmapSets().First().Beatmaps[0])); + + AddStep("add collection", () => writeAndRefresh(r => r.Add(new BeatmapCollection(name: "1")))); + assertCollectionDropdownContains("1"); + + assertFirstButtonIs(FontAwesome.Solid.PlusSquare); + + AddStep("add beatmap to collection", () => writeAndRefresh(r => getFirstCollection().BeatmapMD5Hashes.Add(Beatmap.Value.BeatmapInfo.MD5Hash))); + assertFirstButtonIs(FontAwesome.Solid.MinusSquare); + + AddStep("remove beatmap from collection", () => writeAndRefresh(r => getFirstCollection().BeatmapMD5Hashes.Clear())); + assertFirstButtonIs(FontAwesome.Solid.PlusSquare); + } + + [Test] + public void TestButtonAddsAndRemovesBeatmap() + { + addExpandHeaderStep(); + + AddStep("select available beatmap", () => Beatmap.Value = beatmapManager.GetWorkingBeatmap(beatmapManager.GetAllUsableBeatmapSets().First().Beatmaps[0])); + + AddStep("add collection", () => writeAndRefresh(r => r.Add(new BeatmapCollection(name: "1")))); + assertCollectionDropdownContains("1"); + assertFirstButtonIs(FontAwesome.Solid.PlusSquare); + + addClickAddOrRemoveButtonStep(1); + AddAssert("collection contains beatmap", () => getFirstCollection().BeatmapMD5Hashes.Contains(Beatmap.Value.BeatmapInfo.MD5Hash)); + assertFirstButtonIs(FontAwesome.Solid.MinusSquare); + + addClickAddOrRemoveButtonStep(1); + AddAssert("collection does not contain beatmap", () => !getFirstCollection().BeatmapMD5Hashes.Contains(Beatmap.Value.BeatmapInfo.MD5Hash)); + assertFirstButtonIs(FontAwesome.Solid.PlusSquare); + } + + [Test] + public void TestManageCollectionsFilterIsNotSelected() + { + bool received = false; + + addExpandHeaderStep(); + + AddStep("add collection", () => writeAndRefresh(r => r.Add(new BeatmapCollection(name: "1", new List { "abc" })))); + assertCollectionDropdownContains("1"); + + AddStep("select collection", () => + { + InputManager.MoveMouseTo(getCollectionDropdownItemAt(1)); + InputManager.Click(MouseButton.Left); + }); + + addExpandHeaderStep(); + + AddStep("watch for filter requests", () => + { + received = false; + dropdown.ChildrenOfType().First().RequestFilter = () => received = true; + }); + + AddStep("click manage collections filter", () => + { + int lastItemIndex = dropdown.ChildrenOfType().Single().Items.Count() - 1; + InputManager.MoveMouseTo(getCollectionDropdownItemAt(lastItemIndex)); + InputManager.Click(MouseButton.Left); + }); + + AddAssert("collection filter still selected", () => dropdown.Current.Value.CollectionName == "1"); + + AddAssert("filter request not fired", () => !received); + } + + private void writeAndRefresh(Action action) => Realm.Write(r => + { + action(r); + r.Refresh(); + }); + + private BeatmapCollection getFirstCollection() => Realm.Run(r => r.All().First()); + + private void assertCollectionHeaderDisplays(string collectionName, bool shouldDisplay = true) + => AddUntilStep($"collection dropdown header displays '{collectionName}'", + () => shouldDisplay == dropdown.ChildrenOfType().Any(h => h.ChildrenOfType().Any(t => t.Text == collectionName))); + + private void assertFirstButtonIs(IconUsage icon) => AddUntilStep($"button is {icon.Icon.ToString()}", () => getAddOrRemoveButton(1).Icon.Equals(icon)); + + private void assertCollectionDropdownContains(string collectionName, bool shouldContain = true) => + AddUntilStep($"collection dropdown {(shouldContain ? "contains" : "does not contain")} '{collectionName}'", + // A bit of a roundabout way of going about this, see: https://github.com/ppy/osu-framework/issues/3871 + https://github.com/ppy/osu-framework/issues/3872 + () => shouldContain == dropdown.ChildrenOfType().Any(i => i.ChildrenOfType().OfType().First().Text == collectionName)); + + private IconButton getAddOrRemoveButton(int index) + => getCollectionDropdownItemAt(index).ChildrenOfType().Single(); + + private void addExpandHeaderStep() => AddStep("expand header", () => + { + InputManager.MoveMouseTo(dropdown.ChildrenOfType().Single()); + InputManager.Click(MouseButton.Left); + }); + + private void addClickAddOrRemoveButtonStep(int index) => AddStep("click add or remove button", () => + { + InputManager.MoveMouseTo(getAddOrRemoveButton(index)); + InputManager.Click(MouseButton.Left); + }); + + private Menu.DrawableMenuItem getCollectionDropdownItemAt(int index) + { + // todo: we should be able to use Items, but apparently that's not guaranteed to be ordered... see: https://github.com/ppy/osu-framework/pull/6079 + CollectionFilterMenuItem item = dropdown.ChildrenOfType().Single().ItemSource.ElementAt(index); + return dropdown.ChildrenOfType().Single(i => i.Item.Text.Value == item.CollectionName); + } + } +} diff --git a/osu.Game/Collections/ShearedCollectionDropdown.cs b/osu.Game/Collections/ShearedCollectionDropdown.cs new file mode 100644 index 0000000000..2bb2f5bfe7 --- /dev/null +++ b/osu.Game/Collections/ShearedCollectionDropdown.cs @@ -0,0 +1,270 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using System.Diagnostics; +using System.Linq; +using osu.Framework.Allocation; +using osu.Framework.Bindables; +using osu.Framework.Extensions.ObjectExtensions; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Sprites; +using osu.Framework.Graphics.UserInterface; +using osu.Framework.Input.Events; +using osu.Framework.Localisation; +using osu.Game.Beatmaps; +using osu.Game.Database; +using osu.Game.Graphics.Containers; +using osu.Game.Graphics.UserInterface; +using osu.Game.Graphics.UserInterfaceV2; +using osuTK; +using Realms; + +namespace osu.Game.Collections +{ + /// + /// A dropdown to select the collection to be used to filter results. + /// + public partial class ShearedCollectionDropdown : ShearedDropdown + { + /// + /// Whether to show the "manage collections..." menu item in the dropdown. + /// + protected virtual bool ShowManageCollectionsItem => true; + + public Action? RequestFilter { private get; set; } + + private readonly BindableList filters = new BindableList(); + + [Resolved] + private ManageCollectionsDialog? manageCollectionsDialog { get; set; } + + [Resolved] + private RealmAccess realm { get; set; } = null!; + + private IDisposable? realmSubscription; + + private readonly CollectionFilterMenuItem allBeatmapsItem = new AllBeatmapsCollectionFilterMenuItem(); + + public ShearedCollectionDropdown() + : base("Collection") + { + ItemSource = filters; + + Current.Value = allBeatmapsItem; + AlwaysShowSearchBar = true; + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + realmSubscription = realm.RegisterForNotifications(r => r.All().OrderBy(c => c.Name), collectionsChanged); + + Current.BindValueChanged(selectionChanged); + } + + private void collectionsChanged(IRealmCollection collections, ChangeSet? changes) + { + if (changes == null) + { + filters.Clear(); + filters.Add(allBeatmapsItem); + filters.AddRange(collections.Select(c => new CollectionFilterMenuItem(c.ToLive(realm)))); + if (ShowManageCollectionsItem) + filters.Add(new ManageCollectionsFilterMenuItem()); + } + else + { + foreach (int i in changes.DeletedIndices.OrderDescending()) + filters.RemoveAt(i + 1); + + foreach (int i in changes.InsertedIndices) + filters.Insert(i + 1, new CollectionFilterMenuItem(collections[i].ToLive(realm))); + + var selectedItem = SelectedItem?.Value; + + foreach (int i in changes.NewModifiedIndices) + { + var updatedItem = collections[i]; + + // This is responsible for updating the state of the +/- button and the collection's name. + // TODO: we can probably make the menu items update with changes to avoid this. + filters.RemoveAt(i + 1); + filters.Insert(i + 1, new CollectionFilterMenuItem(updatedItem.ToLive(realm))); + + if (updatedItem.ID == selectedItem?.Collection?.ID) + { + // This current update and schedule is required to work around dropdown headers not updating text even when the selected item + // changes. It's not great but honestly the whole dropdown menu structure isn't great. This needs to be fixed, but I'll issue + // a warning that it's going to be a frustrating journey. + Current.Value = allBeatmapsItem; + Schedule(() => + { + // current may have changed before the scheduled call is run. + if (Current.Value != allBeatmapsItem) + return; + + Current.Value = filters.SingleOrDefault(f => f.Collection?.ID == selectedItem.Collection?.ID) ?? filters[0]; + }); + + // Trigger an external re-filter if the current item was in the change set. + RequestFilter?.Invoke(); + break; + } + } + } + } + + private Live? lastFiltered; + + private void selectionChanged(ValueChangedEvent filter) + { + // May be null during .Clear(). + if (filter.NewValue.IsNull()) + return; + + // Never select the manage collection filter - rollback to the previous filter. + // This is done after the above since it is important that bindable is unbound from OldValue, which is lost after forcing it back to the old value. + if (filter.NewValue is ManageCollectionsFilterMenuItem) + { + Current.Value = filter.OldValue; + manageCollectionsDialog?.Show(); + return; + } + + var newCollection = filter.NewValue.Collection; + + // This dropdown be weird. + // We only care about filtering if the actual collection has changed. + if (newCollection != lastFiltered) + { + RequestFilter?.Invoke(); + lastFiltered = newCollection; + } + } + + protected override void Dispose(bool isDisposing) + { + base.Dispose(isDisposing); + realmSubscription?.Dispose(); + } + + protected override LocalisableString GenerateItemText(CollectionFilterMenuItem item) => item.CollectionName; + + protected sealed override DropdownMenu CreateMenu() => CreateCollectionMenu(); + + protected virtual ShearedCollectionDropdownMenu CreateCollectionMenu() => new ShearedCollectionDropdownMenu(); + + protected partial class ShearedCollectionDropdownMenu : ShearedDropdownMenu + { + public ShearedCollectionDropdownMenu() + { + MaxHeight = 200; + } + + protected override DrawableDropdownMenuItem CreateDrawableDropdownMenuItem(MenuItem item) => new DrawableCollectionMenuItem(item) + { + BackgroundColourHover = HoverColour, + BackgroundColourSelected = SelectionColour + }; + } + + protected partial class DrawableCollectionMenuItem : ShearedDropdownMenu.ShearedMenuItem + { + private IconButton addOrRemoveButton = null!; + + private bool beatmapInCollection; + + private readonly Live? collection; + + [Resolved] + private IBindable beatmap { get; set; } = null!; + + public DrawableCollectionMenuItem(MenuItem item) + : base(item) + { + collection = ((DropdownMenuItem)item).Value.Collection; + } + + [BackgroundDependencyLoader] + private void load() + { + AddInternal(addOrRemoveButton = new NoFocusChangeIconButton + { + Anchor = Anchor.CentreRight, + Origin = Anchor.CentreRight, + Shear = -OsuGame.SHEAR, + X = -OsuScrollContainer.SCROLL_BAR_WIDTH, + Scale = new Vector2(0.65f), + Action = addOrRemove, + }); + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + if (collection != null) + { + beatmap.BindValueChanged(_ => + { + beatmapInCollection = collection.PerformRead(c => c.BeatmapMD5Hashes.Contains(beatmap.Value.BeatmapInfo.MD5Hash)); + + addOrRemoveButton.Enabled.Value = !beatmap.IsDefault; + addOrRemoveButton.Icon = beatmapInCollection ? FontAwesome.Solid.MinusSquare : FontAwesome.Solid.PlusSquare; + addOrRemoveButton.TooltipText = beatmapInCollection ? "Remove selected beatmap" : "Add selected beatmap"; + + updateButtonVisibility(); + }, true); + } + + updateButtonVisibility(); + } + + protected override bool OnHover(HoverEvent e) + { + updateButtonVisibility(); + return base.OnHover(e); + } + + protected override void OnHoverLost(HoverLostEvent e) + { + updateButtonVisibility(); + base.OnHoverLost(e); + } + + protected override void OnSelectChange() + { + base.OnSelectChange(); + updateButtonVisibility(); + } + + private void updateButtonVisibility() + { + if (collection == null) + addOrRemoveButton.Alpha = 0; + else + addOrRemoveButton.Alpha = IsHovered || IsPreSelected || beatmapInCollection ? 1 : 0; + } + + private void addOrRemove() + { + Debug.Assert(collection != null); + + collection.PerformWrite(c => + { + if (!c.BeatmapMD5Hashes.Remove(beatmap.Value.BeatmapInfo.MD5Hash)) + c.BeatmapMD5Hashes.Add(beatmap.Value.BeatmapInfo.MD5Hash); + }); + } + + protected override Drawable CreateContent() => (Content)base.CreateContent(); + + private partial class NoFocusChangeIconButton : IconButton + { + public override bool ChangeFocusOnClick => false; + } + } + } +} From 2c690ae94c334925d0cfdad1415d00bdcb06452c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 18 Apr 2025 07:38:11 +0200 Subject: [PATCH 107/164] Fix code quality --- osu.Game.Tests/Visual/Gameplay/TestSceneGameplayLeaderboard.cs | 3 ++- osu.Game/Screens/Play/HUD/DrawableGameplayLeaderboard.cs | 1 - 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneGameplayLeaderboard.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneGameplayLeaderboard.cs index 23cd262dd0..5703ee754c 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneGameplayLeaderboard.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneGameplayLeaderboard.cs @@ -7,6 +7,7 @@ using System.Linq; using NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Bindables; +using osu.Framework.Extensions.ObjectExtensions; using osu.Framework.Extensions.PolygonExtensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; @@ -37,7 +38,7 @@ namespace osu.Game.Tests.Visual.Gameplay { AddStep("toggle expanded", () => { - if (leaderboard != null) + if (leaderboard.IsNotNull()) leaderboard.Expanded.Value = !leaderboard.Expanded.Value; }); diff --git a/osu.Game/Screens/Play/HUD/DrawableGameplayLeaderboard.cs b/osu.Game/Screens/Play/HUD/DrawableGameplayLeaderboard.cs index 85f5281bef..92baa46695 100644 --- a/osu.Game/Screens/Play/HUD/DrawableGameplayLeaderboard.cs +++ b/osu.Game/Screens/Play/HUD/DrawableGameplayLeaderboard.cs @@ -12,7 +12,6 @@ using osu.Framework.Graphics.Colour; using osu.Framework.Graphics.Containers; using osu.Game.Graphics.Containers; using osu.Game.Screens.Select.Leaderboards; -using osu.Game.Skinning; using osuTK; using osuTK.Graphics; From b02ff0f95c166d0ee0fcdc4b49c9899ad064f07f Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 18 Apr 2025 16:24:31 +0900 Subject: [PATCH 108/164] Update framework --- osu.Android.props | 2 +- osu.Game/Graphics/Containers/LinkFlowContainer.cs | 2 +- osu.Game/osu.Game.csproj | 2 +- osu.iOS.props | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/osu.Android.props b/osu.Android.props index 8e383a705c..98ad145482 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 2fa83c3ab0..6949aea22e 100644 --- a/osu.iOS.props +++ b/osu.iOS.props @@ -17,6 +17,6 @@ -all - + From e44b134552275c1023c95585f925ee66e3b5fdf9 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 18 Apr 2025 17:30:31 +0900 Subject: [PATCH 109/164] Move number formatting to extension method and reuse for `AudioOffsetAdjustmentControl` Also adds some basic test coverage. --- .../NumberFormattingExtensionsTest.cs | 46 +++++++++++++++++ .../Extensions/NumberFormattingExtensions.cs | 51 +++++++++++++++++++ .../Graphics/UserInterface/OsuSliderBar.cs | 35 +------------ .../Audio/AudioOffsetAdjustControl.cs | 4 +- 4 files changed, 101 insertions(+), 35 deletions(-) create mode 100644 osu.Game.Tests/Extensions/NumberFormattingExtensionsTest.cs create mode 100644 osu.Game/Extensions/NumberFormattingExtensions.cs diff --git a/osu.Game.Tests/Extensions/NumberFormattingExtensionsTest.cs b/osu.Game.Tests/Extensions/NumberFormattingExtensionsTest.cs new file mode 100644 index 0000000000..7dcbc6f24a --- /dev/null +++ b/osu.Game.Tests/Extensions/NumberFormattingExtensionsTest.cs @@ -0,0 +1,46 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using NUnit.Framework; +using osu.Game.Extensions; + +namespace osu.Game.Tests.Extensions +{ + [TestFixture] + public class NumberFormattingExtensionsTest + { + [TestCase(-1, false, 0, ExpectedResult = "-1")] + [TestCase(0, false, 0, ExpectedResult = "0")] + [TestCase(1, false, 0, ExpectedResult = "1")] + [TestCase(500, false, 10, ExpectedResult = "500")] + [TestCase(-1, true, 0, ExpectedResult = "-1%")] + [TestCase(0, true, 0, ExpectedResult = "0%")] + [TestCase(1, true, 0, ExpectedResult = "1%")] + [TestCase(50, true, 0, ExpectedResult = "50%")] + public string TestInteger(int input, bool percent, int decimalDigits) + { + return input.ToStandardFormattedString(decimalDigits, percent); + } + + [TestCase(-1, false, 0, ExpectedResult = "-1")] + [TestCase(-1e-6, false, 0, ExpectedResult = "0")] + [TestCase(-1e-6, false, 6, ExpectedResult = "-0.000001")] + [TestCase(0, false, 10, ExpectedResult = "0")] + [TestCase(0, false, 0, ExpectedResult = "0")] + [TestCase(1e-6, false, 0, ExpectedResult = "0")] + [TestCase(1e-6, false, 6, ExpectedResult = "0.000001")] + [TestCase(1, false, 0, ExpectedResult = "1")] + [TestCase(1.528, false, 2, ExpectedResult = "1.53")] + [TestCase(500, false, 10, ExpectedResult = "500")] + [TestCase(-0.1, true, 0, ExpectedResult = "-10%")] + [TestCase(0, true, 0, ExpectedResult = "0%")] + [TestCase(0.4, true, 0, ExpectedResult = "40%")] + [TestCase(0.48333, true, 2, ExpectedResult = "48%")] + [TestCase(0.48333, true, 4, ExpectedResult = "48.33%")] + [TestCase(1, true, 0, ExpectedResult = "100%")] + public string TestDouble(double input, bool percent, int decimalDigits) + { + return input.ToStandardFormattedString(decimalDigits, percent); + } + } +} diff --git a/osu.Game/Extensions/NumberFormattingExtensions.cs b/osu.Game/Extensions/NumberFormattingExtensions.cs new file mode 100644 index 0000000000..5832e4ba9b --- /dev/null +++ b/osu.Game/Extensions/NumberFormattingExtensions.cs @@ -0,0 +1,51 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using System.Globalization; +using System.Numerics; +using osu.Game.Utils; + +namespace osu.Game.Extensions +{ + public static class NumberFormattingExtensions + { + /// + /// For a given numeric type, return a formatted string in the standard format we use for display everywhere. + /// + /// The numeric value. + /// The maximum number of decimals to be considered in the original value. + /// Whether the output should be a percentage. For integer types, 0-100 is mapped to 0-100%; for other types 0-1 is mapped to 0-100%. + /// The formatted output. + public static string ToStandardFormattedString(this T value, int maxDecimalDigits, bool asPercentage) where T : struct, INumber, IMinMaxValue + { + double floatValue = double.CreateTruncating(value); + + decimal decimalPrecision = normalise(decimal.CreateTruncating(value), maxDecimalDigits); + + // Find the number of significant digits (we could have less than maxDecimalDigits after normalize()) + int significantDigits = FormatUtils.FindPrecision(decimalPrecision); + + if (asPercentage) + { + if (value is int) + floatValue /= 100; + + return floatValue.ToString($@"P{Math.Max(0, significantDigits - 2)}"); + } + + string negativeSign = Math.Round(floatValue, significantDigits) < 0 ? "-" : string.Empty; + + return $"{negativeSign}{Math.Abs(floatValue).ToString($"N{significantDigits}")}"; + } + + /// + /// Removes all non-significant digits, keeping at most a requested number of decimal digits. + /// + /// The decimal to normalize. + /// The maximum number of decimal digits to keep. The final result may have fewer decimal digits than this value. + /// The normalised decimal. + private static decimal normalise(decimal d, int sd) + => decimal.Parse(Math.Round(d, sd).ToString(string.Concat("0.", new string('#', sd)), CultureInfo.InvariantCulture), CultureInfo.InvariantCulture); + } +} diff --git a/osu.Game/Graphics/UserInterface/OsuSliderBar.cs b/osu.Game/Graphics/UserInterface/OsuSliderBar.cs index 4b52ac4a3a..24b0e7b0f5 100644 --- a/osu.Game/Graphics/UserInterface/OsuSliderBar.cs +++ b/osu.Game/Graphics/UserInterface/OsuSliderBar.cs @@ -1,9 +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.Numerics; -using System.Globalization; using osu.Framework.Allocation; using osu.Framework.Audio; using osu.Framework.Audio.Sample; @@ -11,7 +9,7 @@ using osu.Framework.Graphics.Cursor; using osu.Framework.Graphics.UserInterface; using osu.Framework.Localisation; using osu.Framework.Utils; -using osu.Game.Utils; +using osu.Game.Extensions; namespace osu.Game.Graphics.UserInterface { @@ -85,35 +83,6 @@ namespace osu.Game.Graphics.UserInterface channel.Play(); } - public LocalisableString GetDisplayableValue(T value) - { - if (CurrentNumber.IsInteger) - return int.CreateTruncating(value).ToString("N0"); - - double floatValue = double.CreateTruncating(value); - - decimal decimalPrecision = normalise(decimal.CreateTruncating(CurrentNumber.Precision), max_decimal_digits); - - // Find the number of significant digits (we could have less than 5 after normalize()) - int significantDigits = FormatUtils.FindPrecision(decimalPrecision); - - if (DisplayAsPercentage) - { - return floatValue.ToString($@"P{Math.Max(0, significantDigits - 2)}"); - } - - string negativeSign = Math.Round(floatValue, significantDigits) < 0 ? "-" : string.Empty; - - return $"{negativeSign}{Math.Abs(floatValue).ToString($"N{significantDigits}")}"; - } - - /// - /// Removes all non-significant digits, keeping at most a requested number of decimal digits. - /// - /// The decimal to normalize. - /// The maximum number of decimal digits to keep. The final result may have fewer decimal digits than this value. - /// The normalised decimal. - private decimal normalise(decimal d, int sd) - => decimal.Parse(Math.Round(d, sd).ToString(string.Concat("0.", new string('#', sd)), CultureInfo.InvariantCulture), CultureInfo.InvariantCulture); + public LocalisableString GetDisplayableValue(T value) => CurrentNumber.Value.ToStandardFormattedString(max_decimal_digits, DisplayAsPercentage); } } diff --git a/osu.Game/Overlays/Settings/Sections/Audio/AudioOffsetAdjustControl.cs b/osu.Game/Overlays/Settings/Sections/Audio/AudioOffsetAdjustControl.cs index 04496428ee..2629cd2183 100644 --- a/osu.Game/Overlays/Settings/Sections/Audio/AudioOffsetAdjustControl.cs +++ b/osu.Game/Overlays/Settings/Sections/Audio/AudioOffsetAdjustControl.cs @@ -8,13 +8,13 @@ using System.Linq; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Extensions.IEnumerableExtensions; -using osu.Framework.Extensions.LocalisationExtensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.UserInterface; using osu.Framework.Localisation; using osu.Game.Configuration; +using osu.Game.Extensions; using osu.Game.Graphics; using osu.Game.Graphics.Containers; using osu.Game.Graphics.UserInterface; @@ -169,7 +169,7 @@ namespace osu.Game.Overlays.Settings.Sections.Audio else { applySuggestion.Enabled.Value = true; - hintText.Text = AudioSettingsStrings.SuggestedOffsetValueReceived(averageHitErrorHistory.Count, SuggestedOffset.Value.ToLocalisableString(@"N0")); + hintText.Text = AudioSettingsStrings.SuggestedOffsetValueReceived(averageHitErrorHistory.Count, SuggestedOffset.Value.Value.ToStandardFormattedString(0, false)); } } From 62d83cff9a31dcb26b311788c628921e4a0b82b7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 18 Apr 2025 10:55:43 +0200 Subject: [PATCH 110/164] Add a test for negative zero (yes, *negative zero*) --- osu.Game.Tests/Extensions/NumberFormattingExtensionsTest.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/osu.Game.Tests/Extensions/NumberFormattingExtensionsTest.cs b/osu.Game.Tests/Extensions/NumberFormattingExtensionsTest.cs index 7dcbc6f24a..fca39f86ec 100644 --- a/osu.Game.Tests/Extensions/NumberFormattingExtensionsTest.cs +++ b/osu.Game.Tests/Extensions/NumberFormattingExtensionsTest.cs @@ -27,6 +27,7 @@ namespace osu.Game.Tests.Extensions [TestCase(-1e-6, false, 6, ExpectedResult = "-0.000001")] [TestCase(0, false, 10, ExpectedResult = "0")] [TestCase(0, false, 0, ExpectedResult = "0")] + [TestCase(double.NegativeZero, false, 0, ExpectedResult = "0")] [TestCase(1e-6, false, 0, ExpectedResult = "0")] [TestCase(1e-6, false, 6, ExpectedResult = "0.000001")] [TestCase(1, false, 0, ExpectedResult = "1")] From c28f2a932c0cc108b654da9c303a6cbb5b3d9918 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 18 Apr 2025 11:23:20 +0200 Subject: [PATCH 111/164] Add failing test --- .../Extensions/NumberFormattingExtensionsTest.cs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/osu.Game.Tests/Extensions/NumberFormattingExtensionsTest.cs b/osu.Game.Tests/Extensions/NumberFormattingExtensionsTest.cs index fca39f86ec..b02bf01019 100644 --- a/osu.Game.Tests/Extensions/NumberFormattingExtensionsTest.cs +++ b/osu.Game.Tests/Extensions/NumberFormattingExtensionsTest.cs @@ -43,5 +43,12 @@ namespace osu.Game.Tests.Extensions { return input.ToStandardFormattedString(decimalDigits, percent); } + + [Test] + [SetCulture("fr-FR")] + public void TestCultureInsensitivity() + { + Assert.That(0.4.ToStandardFormattedString(maxDecimalDigits: 2, asPercentage: true), Is.EqualTo("40%")); + } } } From e5636a84f1f3cdebb32290c31841bb073f3fdbea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 18 Apr 2025 11:23:26 +0200 Subject: [PATCH 112/164] Fix culture variance in new formatting helper --- osu.Game/Extensions/NumberFormattingExtensions.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Extensions/NumberFormattingExtensions.cs b/osu.Game/Extensions/NumberFormattingExtensions.cs index 5832e4ba9b..618b086a5b 100644 --- a/osu.Game/Extensions/NumberFormattingExtensions.cs +++ b/osu.Game/Extensions/NumberFormattingExtensions.cs @@ -31,12 +31,12 @@ namespace osu.Game.Extensions if (value is int) floatValue /= 100; - return floatValue.ToString($@"P{Math.Max(0, significantDigits - 2)}"); + return floatValue.ToString($@"0.{new string('0', Math.Max(0, significantDigits - 2))}%", CultureInfo.InvariantCulture); } string negativeSign = Math.Round(floatValue, significantDigits) < 0 ? "-" : string.Empty; - return $"{negativeSign}{Math.Abs(floatValue).ToString($"N{significantDigits}")}"; + return FormattableString.Invariant($"{negativeSign}{Math.Abs(floatValue).ToString($"N{significantDigits}")}"); } /// From 39f9eabf40b39126fbf62ad3664cca0357f92c54 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 18 Apr 2025 12:01:04 +0200 Subject: [PATCH 113/164] Add failing test for incorrect score position treatment --- .../Visual/Gameplay/TestSceneGameplayLeaderboard.cs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneGameplayLeaderboard.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneGameplayLeaderboard.cs index 5703ee754c..e8b5326244 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneGameplayLeaderboard.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneGameplayLeaderboard.cs @@ -205,6 +205,9 @@ namespace osu.Game.Tests.Visual.Gameplay AddUntilStep("tracked player has null position", () => leaderboard.TrackedScore?.ScorePosition, () => Is.Null); else AddUntilStep("tracked player is #50", () => leaderboard.TrackedScore?.ScorePosition, () => Is.EqualTo(50)); + + AddStep("move tracked player to top", () => leaderboard.TrackedScore!.TotalScore.Value = 8_000_000); + AddUntilStep("all players have non-null position", () => leaderboard.AllScores.Select(s => s.ScorePosition), () => Does.Not.Contain(null)); } private void addLocalPlayer() @@ -252,6 +255,8 @@ namespace osu.Game.Tests.Visual.Gameplay public IEnumerable GetAllScoresForUsername(string username) => Flow.Where(i => i.User?.Username == username); + + public IEnumerable AllScores => Flow; } private class TestGameplayLeaderboardProvider : IGameplayLeaderboardProvider From 3ecf56b6f60538af9f31b12a8ae6c0212430268d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 18 Apr 2025 12:02:02 +0200 Subject: [PATCH 114/164] Fix incorrect score position treatment if last score on partial leaderboard isn't tracked --- osu.Game/Screens/Play/HUD/DrawableGameplayLeaderboard.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/Play/HUD/DrawableGameplayLeaderboard.cs b/osu.Game/Screens/Play/HUD/DrawableGameplayLeaderboard.cs index 92baa46695..7cfdb9631b 100644 --- a/osu.Game/Screens/Play/HUD/DrawableGameplayLeaderboard.cs +++ b/osu.Game/Screens/Play/HUD/DrawableGameplayLeaderboard.cs @@ -180,8 +180,9 @@ namespace osu.Game.Screens.Play.HUD for (int i = 0; i < Flow.Count; i++) { - Flow.SetLayoutPosition(orderedByScore[i], i); - orderedByScore[i].ScorePosition = i + 1 == Flow.Count && leaderboardProvider?.IsPartial == true ? null : i + 1; + var score = orderedByScore[i]; + Flow.SetLayoutPosition(score, i); + score.ScorePosition = i + 1 == Flow.Count && leaderboardProvider?.IsPartial == true && score.Tracked ? null : i + 1; } sorting.Validate(); From 006670c4423d96b522b5ed0c297f1818047a1c27 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 18 Apr 2025 12:02:21 +0200 Subject: [PATCH 115/164] Add clarification to `IsPartial` xmldoc --- .../Select/Leaderboards/IGameplayLeaderboardProvider.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/osu.Game/Screens/Select/Leaderboards/IGameplayLeaderboardProvider.cs b/osu.Game/Screens/Select/Leaderboards/IGameplayLeaderboardProvider.cs index 0138f855e2..0d88e7bf6c 100644 --- a/osu.Game/Screens/Select/Leaderboards/IGameplayLeaderboardProvider.cs +++ b/osu.Game/Screens/Select/Leaderboards/IGameplayLeaderboardProvider.cs @@ -20,6 +20,9 @@ namespace osu.Game.Screens.Select.Leaderboards /// Whether this leaderboard is a partial leaderboard (e.g. contains only the top 50 of all scores), /// or is a full leaderboard (contains all scores that there will ever be). /// + /// + /// If this is and a tracked score is last on the leaderboard, it will show an "unknown" score position. + /// bool IsPartial { get; } } } From b80ea2647542995fda6d0c1159db42d79757b0e8 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 18 Apr 2025 19:07:44 +0900 Subject: [PATCH 116/164] Add accounting of nested group items for group panel display purposes --- osu.Game/Graphics/Carousel/CarouselItem.cs | 5 +++++ .../SelectV2/BeatmapCarouselFilterGrouping.cs | 12 +++++++++++- osu.Game/Screens/SelectV2/PanelGroup.cs | 6 +++--- 3 files changed, 19 insertions(+), 4 deletions(-) diff --git a/osu.Game/Graphics/Carousel/CarouselItem.cs b/osu.Game/Graphics/Carousel/CarouselItem.cs index 223c8d9869..4904b9f13d 100644 --- a/osu.Game/Graphics/Carousel/CarouselItem.cs +++ b/osu.Game/Graphics/Carousel/CarouselItem.cs @@ -44,6 +44,11 @@ namespace osu.Game.Graphics.Carousel /// public bool IsExpanded { get; set; } + /// + /// The number of nested items underneath this header. Should only be used for headers of groups. + /// + public int NestedItemCount { get; set; } + public CarouselItem(object model) { Model = model; diff --git a/osu.Game/Screens/SelectV2/BeatmapCarouselFilterGrouping.cs b/osu.Game/Screens/SelectV2/BeatmapCarouselFilterGrouping.cs index 3360437544..a628595477 100644 --- a/osu.Game/Screens/SelectV2/BeatmapCarouselFilterGrouping.cs +++ b/osu.Game/Screens/SelectV2/BeatmapCarouselFilterGrouping.cs @@ -47,7 +47,9 @@ namespace osu.Game.Screens.SelectV2 var newItems = new List(); BeatmapInfo? lastBeatmap = null; + GroupDefinition? lastGroup = null; + CarouselItem? lastGroupItem = null; HashSet? currentGroupItems = null; HashSet? currentSetItems = null; @@ -69,7 +71,7 @@ namespace osu.Game.Screens.SelectV2 groupItems[newGroup] = currentGroupItems = new HashSet(); lastGroup = newGroup; - addItem(new CarouselItem(newGroup) + addItem(lastGroupItem = new CarouselItem(newGroup) { DrawHeight = PanelGroup.HEIGHT, DepthLayer = -2, @@ -84,6 +86,9 @@ namespace osu.Game.Screens.SelectV2 { setItems[beatmap.BeatmapSet!] = currentSetItems = new HashSet(); + if (lastGroupItem != null) + lastGroupItem.NestedItemCount++; + addItem(new CarouselItem(beatmap.BeatmapSet!) { DrawHeight = PanelBeatmapSet.HEIGHT, @@ -91,6 +96,11 @@ namespace osu.Game.Screens.SelectV2 }); } } + else + { + if (lastGroupItem != null) + lastGroupItem.NestedItemCount++; + } addItem(item); lastBeatmap = beatmap; diff --git a/osu.Game/Screens/SelectV2/PanelGroup.cs b/osu.Game/Screens/SelectV2/PanelGroup.cs index ac4857d2f3..4370146dbc 100644 --- a/osu.Game/Screens/SelectV2/PanelGroup.cs +++ b/osu.Game/Screens/SelectV2/PanelGroup.cs @@ -26,6 +26,7 @@ namespace osu.Game.Screens.SelectV2 private Drawable iconContainer = null!; private OsuSpriteText titleText = null!; private TrianglesV2 triangles = null!; + private OsuSpriteText countText = null!; private Box glow = null!; [Resolved] @@ -99,13 +100,11 @@ namespace osu.Game.Screens.SelectV2 RelativeSizeAxes = Axes.Both, Colour = Color4.Black.Opacity(0.7f), }, - new OsuSpriteText + countText = new OsuSpriteText { Anchor = Anchor.Centre, Origin = Anchor.Centre, Font = OsuFont.Style.Caption1.With(weight: FontWeight.Bold), - // TODO: requires Carousel/CarouselItem-side implementation - Text = "43", UseFullGlyphHeight = false, } }, @@ -144,6 +143,7 @@ namespace osu.Game.Screens.SelectV2 GroupDefinition group = (GroupDefinition)Item.Model; titleText.Text = group.Title; + countText.Text = Item.NestedItemCount.ToString("N0"); } } } From 6d258d4ed5182341b66fe1e425dbebc1a81d0567 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 18 Apr 2025 12:11:42 +0200 Subject: [PATCH 117/164] Remove unnecessary interface --- .../Gameplay/TestSceneGameplayLeaderboard.cs | 30 +-------- .../Play/HUD/DrawableGameplayLeaderboard.cs | 6 +- .../HUD/DrawableGameplayLeaderboardScore.cs | 3 +- .../Play/HUD/IGameplayLeaderboardScore.cs | 67 ------------------- .../Leaderboards/GameplayLeaderboardScore.cs | 58 +++++++++++++++- .../IGameplayLeaderboardProvider.cs | 3 +- .../MultiplayerLeaderboardProvider.cs | 5 +- .../SoloGameplayLeaderboardProvider.cs | 5 +- 8 files changed, 69 insertions(+), 108 deletions(-) delete mode 100644 osu.Game/Screens/Play/HUD/IGameplayLeaderboardScore.cs diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneGameplayLeaderboard.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneGameplayLeaderboard.cs index e8b5326244..bef43b3108 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneGameplayLeaderboard.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneGameplayLeaderboard.cs @@ -1,7 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using System; using System.Collections.Generic; using System.Linq; using NUnit.Framework; @@ -16,10 +15,8 @@ using osu.Framework.Testing; using osu.Framework.Utils; using osu.Game.Online.API; using osu.Game.Online.API.Requests.Responses; -using osu.Game.Rulesets.Scoring; using osu.Game.Screens.Play.HUD; using osu.Game.Screens.Select.Leaderboards; -using osu.Game.Users; using osuTK; namespace osu.Game.Tests.Visual.Gameplay @@ -238,7 +235,7 @@ namespace osu.Game.Tests.Visual.Gameplay private void createLeaderboardScore(BindableLong score, APIUser user, bool isTracked = false) { - var leaderboardScore = new TestDrawableGameplayLeaderboardScore(user, isTracked, score); + var leaderboardScore = new GameplayLeaderboardScore(user, isTracked, score); leaderboardProvider.Scores.Add(leaderboardScore); } @@ -261,30 +258,9 @@ namespace osu.Game.Tests.Visual.Gameplay private class TestGameplayLeaderboardProvider : IGameplayLeaderboardProvider { - IBindableList IGameplayLeaderboardProvider.Scores => Scores; - public BindableList Scores { get; } = new BindableList(); + IBindableList IGameplayLeaderboardProvider.Scores => Scores; + public BindableList Scores { get; } = new BindableList(); public bool IsPartial { get; set; } } - - private class TestDrawableGameplayLeaderboardScore : IGameplayLeaderboardScore - { - public IUser User { get; } - public bool Tracked { get; } - public BindableLong TotalScore { get; } = new BindableLong(); - public BindableDouble Accuracy { get; } = new BindableDouble(); - public BindableInt Combo { get; } = new BindableInt(); - public BindableBool HasQuit { get; } = new BindableBool(); - public Bindable DisplayOrder { get; } = new BindableLong(); - public Func GetDisplayScore { get; set; } - public Colour4? TeamColour => null; - - public TestDrawableGameplayLeaderboardScore(IUser user, bool isTracked, Bindable totalScore) - { - User = user; - Tracked = isTracked; - TotalScore.BindTo(totalScore); - GetDisplayScore = _ => TotalScore.Value; - } - } } } diff --git a/osu.Game/Screens/Play/HUD/DrawableGameplayLeaderboard.cs b/osu.Game/Screens/Play/HUD/DrawableGameplayLeaderboard.cs index 7cfdb9631b..f60d12d84f 100644 --- a/osu.Game/Screens/Play/HUD/DrawableGameplayLeaderboard.cs +++ b/osu.Game/Screens/Play/HUD/DrawableGameplayLeaderboard.cs @@ -33,7 +33,7 @@ namespace osu.Game.Screens.Play.HUD [Resolved] private IGameplayLeaderboardProvider? leaderboardProvider { get; set; } - private readonly IBindableList scores = new BindableList(); + private readonly IBindableList scores = new BindableList(); private const int max_panels = 8; @@ -85,7 +85,7 @@ namespace osu.Game.Screens.Play.HUD /// /// Adds a player to the leaderboard. /// - public void Add(IGameplayLeaderboardScore score) + public void Add(GameplayLeaderboardScore score) { var drawable = CreateLeaderboardScoreDrawable(score); @@ -115,7 +115,7 @@ namespace osu.Game.Screens.Play.HUD scroll.ScrollToStart(false); } - protected virtual DrawableGameplayLeaderboardScore CreateLeaderboardScoreDrawable(IGameplayLeaderboardScore score) => + protected virtual DrawableGameplayLeaderboardScore CreateLeaderboardScoreDrawable(GameplayLeaderboardScore score) => new DrawableGameplayLeaderboardScore(score); protected override void Update() diff --git a/osu.Game/Screens/Play/HUD/DrawableGameplayLeaderboardScore.cs b/osu.Game/Screens/Play/HUD/DrawableGameplayLeaderboardScore.cs index f04d3ee492..b14e31983c 100644 --- a/osu.Game/Screens/Play/HUD/DrawableGameplayLeaderboardScore.cs +++ b/osu.Game/Screens/Play/HUD/DrawableGameplayLeaderboardScore.cs @@ -14,6 +14,7 @@ using osu.Game.Graphics; using osu.Game.Graphics.Sprites; using osu.Game.Online.API; using osu.Game.Rulesets.Scoring; +using osu.Game.Screens.Select.Leaderboards; using osu.Game.Users; using osu.Game.Users.Drawables; using osu.Game.Utils; @@ -114,7 +115,7 @@ namespace osu.Game.Screens.Play.HUD /// /// Creates a new . /// - public DrawableGameplayLeaderboardScore(IGameplayLeaderboardScore score) + public DrawableGameplayLeaderboardScore(GameplayLeaderboardScore score) { User = score.User; Tracked = score.Tracked; diff --git a/osu.Game/Screens/Play/HUD/IGameplayLeaderboardScore.cs b/osu.Game/Screens/Play/HUD/IGameplayLeaderboardScore.cs deleted file mode 100644 index 20c7b16d79..0000000000 --- a/osu.Game/Screens/Play/HUD/IGameplayLeaderboardScore.cs +++ /dev/null @@ -1,67 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -using System; -using osu.Framework.Bindables; -using osu.Framework.Graphics; -using osu.Game.Rulesets.Scoring; -using osu.Game.Users; - -namespace osu.Game.Screens.Play.HUD -{ - /// - /// Represents a score shown on a gameplay leaderboard. - /// The score is expected to update itself as gameplay progresses. - /// - public interface IGameplayLeaderboardScore - { - /// - /// The user playing. - /// - IUser User { get; } - - /// - /// Whether the score is being tracked. - /// Generally understood as true when this score is the score of the local user currently playing. - /// - bool Tracked { get; } - - /// - /// The current total of the score. - /// - BindableLong TotalScore { get; } - - /// - /// The current accuracy of the score. - /// - BindableDouble Accuracy { get; } - - /// - /// The current combo of the score. - /// - BindableInt Combo { get; } - - /// - /// Whether the user playing has quit. - /// - BindableBool HasQuit { get; } - - /// - /// An optional value to guarantee stable ordering. - /// Lower numbers will appear higher in cases of ties. - /// - Bindable DisplayOrder { get; } - - /// - /// A custom function which handles converting a score to a display score using a provide . - /// - /// - /// If no function is provided, will be used verbatim. - Func GetDisplayScore { get; set; } - - /// - /// The colour of the team that the user playing is on, if any. - /// - Colour4? TeamColour { get; } - } -} diff --git a/osu.Game/Screens/Select/Leaderboards/GameplayLeaderboardScore.cs b/osu.Game/Screens/Select/Leaderboards/GameplayLeaderboardScore.cs index ba3e4f728b..2655fd8dba 100644 --- a/osu.Game/Screens/Select/Leaderboards/GameplayLeaderboardScore.cs +++ b/osu.Game/Screens/Select/Leaderboards/GameplayLeaderboardScore.cs @@ -8,21 +8,64 @@ using osu.Game.Online.Spectator; using osu.Game.Rulesets.Scoring; using osu.Game.Scoring; using osu.Game.Scoring.Legacy; -using osu.Game.Screens.Play.HUD; using osu.Game.Users; namespace osu.Game.Screens.Select.Leaderboards { - public class GameplayLeaderboardScore : IGameplayLeaderboardScore + /// + /// Represents a score shown on a gameplay leaderboard. + /// The score is expected to update itself as gameplay progresses. + /// + public class GameplayLeaderboardScore { + /// + /// The user playing. + /// public IUser User { get; } + + /// + /// Whether the score is being tracked. + /// Generally understood as true when this score is the score of the local user currently playing. + /// public bool Tracked { get; } + + /// + /// The current total of the score. + /// public BindableLong TotalScore { get; } = new BindableLong(); + + /// + /// The current accuracy of the score. + /// public BindableDouble Accuracy { get; } = new BindableDouble(); + + /// + /// The current combo of the score. + /// public BindableInt Combo { get; } = new BindableInt(); + + /// + /// Whether the user playing has quit. + /// public BindableBool HasQuit { get; } = new BindableBool(); + + /// + /// An optional value to guarantee stable ordering. + /// Lower numbers will appear higher in cases of ties. + /// public Bindable DisplayOrder { get; } = new BindableLong(); + + /// + /// A custom function which handles converting a score to a display score using a provided . + /// + /// + /// If no function is provided, will be used verbatim. + /// public Func GetDisplayScore { get; set; } + + /// + /// The colour of the team that the user playing is on, if any. + /// public Colour4? TeamColour { get; init; } public GameplayLeaderboardScore(IUser user, ScoreProcessor scoreProcessor, bool tracked) @@ -55,5 +98,16 @@ namespace osu.Game.Screens.Select.Leaderboards DisplayOrder.Value = scoreInfo.OnlineID > 0 ? scoreInfo.OnlineID : scoreInfo.Date.ToUnixTimeSeconds(); GetDisplayScore = scoreInfo.GetDisplayScore; } + + /// + /// Used for testing. + /// + internal GameplayLeaderboardScore(IUser user, bool tracked, Bindable displayScore) + { + User = user; + Tracked = tracked; + TotalScore.BindTarget = displayScore; + GetDisplayScore = _ => displayScore.Value; + } } } diff --git a/osu.Game/Screens/Select/Leaderboards/IGameplayLeaderboardProvider.cs b/osu.Game/Screens/Select/Leaderboards/IGameplayLeaderboardProvider.cs index 0d88e7bf6c..4399c422b4 100644 --- a/osu.Game/Screens/Select/Leaderboards/IGameplayLeaderboardProvider.cs +++ b/osu.Game/Screens/Select/Leaderboards/IGameplayLeaderboardProvider.cs @@ -2,7 +2,6 @@ // See the LICENCE file in the repository root for full licence text. using osu.Framework.Bindables; -using osu.Game.Screens.Play.HUD; namespace osu.Game.Screens.Select.Leaderboards { @@ -14,7 +13,7 @@ namespace osu.Game.Screens.Select.Leaderboards /// /// List of all scores to display on the leaderboard. /// - public IBindableList Scores { get; } + public IBindableList Scores { get; } /// /// Whether this leaderboard is a partial leaderboard (e.g. contains only the top 50 of all scores), diff --git a/osu.Game/Screens/Select/Leaderboards/MultiplayerLeaderboardProvider.cs b/osu.Game/Screens/Select/Leaderboards/MultiplayerLeaderboardProvider.cs index 1c2b400164..edfccd0e7e 100644 --- a/osu.Game/Screens/Select/Leaderboards/MultiplayerLeaderboardProvider.cs +++ b/osu.Game/Screens/Select/Leaderboards/MultiplayerLeaderboardProvider.cs @@ -21,7 +21,6 @@ using osu.Game.Online.Multiplayer; using osu.Game.Online.Multiplayer.MatchTypes.TeamVersus; using osu.Game.Online.Spectator; using osu.Game.Rulesets.Scoring; -using osu.Game.Screens.Play.HUD; using osuTK.Graphics; namespace osu.Game.Screens.Select.Leaderboards @@ -29,8 +28,8 @@ namespace osu.Game.Screens.Select.Leaderboards [LongRunningLoad] public partial class MultiplayerLeaderboardProvider : CompositeComponent, IGameplayLeaderboardProvider { - public IBindableList Scores => scores; - private readonly BindableList scores = new BindableList(); + public IBindableList Scores => scores; + private readonly BindableList scores = new BindableList(); protected readonly Dictionary UserScores = new Dictionary(); public readonly SortedDictionary TeamScores = new SortedDictionary(); diff --git a/osu.Game/Screens/Select/Leaderboards/SoloGameplayLeaderboardProvider.cs b/osu.Game/Screens/Select/Leaderboards/SoloGameplayLeaderboardProvider.cs index 216fda8d9f..ac94d307c6 100644 --- a/osu.Game/Screens/Select/Leaderboards/SoloGameplayLeaderboardProvider.cs +++ b/osu.Game/Screens/Select/Leaderboards/SoloGameplayLeaderboardProvider.cs @@ -7,7 +7,6 @@ using osu.Framework.Graphics; using osu.Game.Online.Leaderboards; using osu.Game.Scoring; using osu.Game.Screens.Play; -using osu.Game.Screens.Play.HUD; namespace osu.Game.Screens.Select.Leaderboards { @@ -15,8 +14,8 @@ namespace osu.Game.Screens.Select.Leaderboards { public bool IsPartial { get; private set; } - public IBindableList Scores => scores; - private readonly BindableList scores = new BindableList(); + public IBindableList Scores => scores; + private readonly BindableList scores = new BindableList(); [BackgroundDependencyLoader] private void load(LeaderboardManager? leaderboardManager, GameplayState? gameplayState) From f480765bf44b0ea797f52cc9d52cfaa56a3c50d8 Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Wed, 16 Apr 2025 06:53:46 -0400 Subject: [PATCH 118/164] Add drawable wrapper for shear alignment purposes --- .../TestSceneShearAligningWrapper.cs | 132 ++++++++++++++++++ .../Containers/ShearAligningWrapper.cs | 49 +++++++ 2 files changed, 181 insertions(+) create mode 100644 osu.Game.Tests/Visual/UserInterface/TestSceneShearAligningWrapper.cs create mode 100644 osu.Game/Graphics/Containers/ShearAligningWrapper.cs diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneShearAligningWrapper.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneShearAligningWrapper.cs new file mode 100644 index 0000000000..eb65de8fdc --- /dev/null +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneShearAligningWrapper.cs @@ -0,0 +1,132 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using NUnit.Framework; +using osu.Framework.Allocation; +using osu.Framework.Extensions.ObjectExtensions; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; +using osu.Framework.Testing; +using osu.Game.Graphics; +using osu.Game.Graphics.Containers; +using osu.Game.Graphics.Sprites; +using osu.Game.Overlays; +using osuTK; +using osuTK.Graphics; + +namespace osu.Game.Tests.Visual.UserInterface +{ + public partial class TestSceneShearAligningWrapper : OsuTestScene + { + private readonly OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Aquamarine); + + private ShearedBox first = null!; + private ShearedBox second = null!; + private ShearedBox third = null!; + + [SetUp] + public void SetUp() => Schedule(() => + { + Child = new Container + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Width = 200f, + AutoSizeAxes = Axes.Y, + Shear = OsuGame.SHEAR, + CornerRadius = 10f, + Masking = true, + Children = new Drawable[] + { + new Box + { + RelativeSizeAxes = Axes.Both, + Colour = colourProvider.Background6, + }, + new FillFlowContainer + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Direction = FillDirection.Vertical, + Spacing = new Vector2(0f, 10f), + Children = new Drawable[] + { + new ShearAligningWrapper(first = new ShearedBox("Text 1", OsuColour.Gray(0.4f)) + { + RelativeSizeAxes = Axes.X, + Height = 30, + }), + new ShearAligningWrapper(second = new ShearedBox("Text 2", OsuColour.Gray(0.3f)) + { + RelativeSizeAxes = Axes.X, + Height = 30, + }), + new ShearAligningWrapper(third = new ShearedBox("Text 3", OsuColour.Gray(0.2f)) + { + RelativeSizeAxes = Axes.X, + Height = 30, + }), + } + } + }, + }; + }); + + [SetUpSteps] + public void SetUpSteps() + { + AddSliderStep("box 1 height", 0, 100, 30, h => + { + if (first.IsNotNull()) + first.Height = h; + }); + AddSliderStep("box 2 height", 0, 100, 30, h => + { + if (second.IsNotNull()) + second.Height = h; + }); + AddSliderStep("box 3 height", 0, 100, 30, h => + { + if (third.IsNotNull()) + third.Height = h; + }); + } + + public partial class ShearedBox : Container + { + private readonly string text; + private readonly Color4 boxColour; + + public ShearedBox(string text, Color4 boxColour) + { + this.text = text; + this.boxColour = boxColour; + } + + [BackgroundDependencyLoader] + private void load() + { + CornerRadius = 10; + Masking = true; + + InternalChildren = new Drawable[] + { + new Box + { + RelativeSizeAxes = Axes.Both, + Colour = boxColour, + }, + new OsuSpriteText + { + Text = text, + Colour = Color4.White, + Shear = -OsuGame.SHEAR, + Font = OsuFont.Torus.With(size: 24), + Margin = new MarginPadding { Left = 50 }, + } + }; + } + } + } +} diff --git a/osu.Game/Graphics/Containers/ShearAligningWrapper.cs b/osu.Game/Graphics/Containers/ShearAligningWrapper.cs new file mode 100644 index 0000000000..d720120b4f --- /dev/null +++ b/osu.Game/Graphics/Containers/ShearAligningWrapper.cs @@ -0,0 +1,49 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Layout; +using osuTK; + +namespace osu.Game.Graphics.Containers +{ + /// + /// Adds left padding based on direct parent to make sheared pieces in a vertical flow aligned appropriately. + /// + /// + /// See associated test scene for further demonstration. + /// + public partial class ShearAligningWrapper : CompositeDrawable + { + private readonly LayoutValue layout = new LayoutValue(Invalidation.MiscGeometry); + + public ShearAligningWrapper(Drawable drawable) + { + RelativeSizeAxes = drawable.RelativeSizeAxes; + AutoSizeAxes = Axes.Both & ~drawable.RelativeSizeAxes; + + InternalChild = drawable; + + AddLayout(layout); + } + + protected override void Update() + { + base.Update(); + + if (!layout.IsValid) + { + updateLayout(); + layout.Validate(); + } + } + + private void updateLayout() + { + float shearWidth = OsuGame.SHEAR.X * Parent!.DrawHeight; + float relativeY = Parent!.DrawHeight == 0 ? 0 : InternalChild.ToSpaceOfOtherDrawable(Vector2.Zero, Parent).Y / Parent!.DrawHeight; + Padding = new MarginPadding { Left = shearWidth * relativeY }; + } + } +} From dfd226394de4f2e3bff6d1a38767846c1c282497 Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Fri, 18 Apr 2025 05:05:56 -0400 Subject: [PATCH 119/164] Add beatmap title wedge statistic display --- .../TestSceneBeatmapTitleWedgeStatistic.cs | 74 +++++++++ .../SelectV2/BeatmapTitleWedge_Statistic.cs | 151 ++++++++++++++++++ .../BeatmapTitleWedge_StatisticPlayCount.cs | 144 +++++++++++++++++ 3 files changed, 369 insertions(+) create mode 100644 osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapTitleWedgeStatistic.cs create mode 100644 osu.Game/Screens/SelectV2/BeatmapTitleWedge_Statistic.cs create mode 100644 osu.Game/Screens/SelectV2/BeatmapTitleWedge_StatisticPlayCount.cs diff --git a/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapTitleWedgeStatistic.cs b/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapTitleWedgeStatistic.cs new file mode 100644 index 0000000000..96eab3e8ec --- /dev/null +++ b/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapTitleWedgeStatistic.cs @@ -0,0 +1,74 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using NUnit.Framework; +using osu.Framework.Extensions.IEnumerableExtensions; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Testing; +using osu.Game.Graphics; +using osu.Game.Overlays; +using osu.Game.Screens.SelectV2; +using osu.Game.Tests.Visual.UserInterface; + +namespace osu.Game.Tests.Visual.SongSelectV2 +{ + public partial class TestSceneBeatmapTitleWedgeStatistic : ThemeComparisonTestScene + { + private BeatmapTitleWedge.StatisticPlayCount playCount = null!; + private BeatmapTitleWedge.Statistic statistic2 = null!; + private BeatmapTitleWedge.Statistic statistic3 = null!; + private BeatmapTitleWedge.Statistic statistic4 = null!; + + public TestSceneBeatmapTitleWedgeStatistic() + : base(false) + { + } + + [Test] + public void TestLoading() + { + AddStep("setup", () => CreateThemedContent(OverlayColourScheme.Aquamarine)); + AddStep("set loading", () => this.ChildrenOfType().ForEach(s => s.Value = null)); + AddWaitStep("wait", 3); + AddStep("set values", () => + { + playCount.Value = new BeatmapTitleWedge.StatisticPlayCount.Data(1234, 12); + statistic2.Value = "3,234"; + statistic3.Value = "12:34"; + statistic4.Value = "123"; + }); + } + + protected override Drawable CreateContent() => new FillFlowContainer + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Direction = FillDirection.Horizontal, + AutoSizeAxes = Axes.Both, + Children = new[] + { + playCount = new BeatmapTitleWedge.StatisticPlayCount(true, minSize: 50) + { + Value = new BeatmapTitleWedge.StatisticPlayCount.Data(1234, 12), + }, + statistic2 = new BeatmapTitleWedge.Statistic(OsuIcon.Clock, true, minSize: 30) + { + Value = "3,234", + TooltipText = "Statistic 2", + }, + statistic3 = new BeatmapTitleWedge.Statistic(OsuIcon.Metronome) + { + Value = "12:34", + Margin = new MarginPadding { Right = 10f }, + TooltipText = "Statistic 3", + }, + statistic4 = new BeatmapTitleWedge.Statistic(OsuIcon.Graphics) + { + Value = "123", + TooltipText = "Statistic 4", + }, + }, + }; + } +} diff --git a/osu.Game/Screens/SelectV2/BeatmapTitleWedge_Statistic.cs b/osu.Game/Screens/SelectV2/BeatmapTitleWedge_Statistic.cs new file mode 100644 index 0000000000..b4ec72761f --- /dev/null +++ b/osu.Game/Screens/SelectV2/BeatmapTitleWedge_Statistic.cs @@ -0,0 +1,151 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Allocation; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Cursor; +using osu.Framework.Graphics.Shapes; +using osu.Framework.Graphics.Sprites; +using osu.Framework.Localisation; +using osu.Game.Graphics; +using osu.Game.Graphics.Sprites; +using osu.Game.Graphics.UserInterface; +using osu.Game.Overlays; +using osuTK; +using osuTK.Graphics; + +namespace osu.Game.Screens.SelectV2 +{ + public partial class BeatmapTitleWedge + { + public partial class Statistic : CompositeDrawable, IHasTooltip + { + private readonly IconUsage icon; + private readonly bool background; + private readonly float leftPadding; + private readonly float? minSize; + + private OsuSpriteText valueText = null!; + private LoadingSpinner loading = null!; + + private LocalisableString? value; + + public LocalisableString? Value + { + get => value; + set + { + this.value = value; + + Schedule(() => + { + loading.State.Value = value != null ? Visibility.Hidden : Visibility.Visible; + + if (value != null) + { + valueText.Text = value.Value; + valueText.FadeIn(120, Easing.OutQuint); + } + else + valueText.FadeOut(120, Easing.OutQuint); + }); + } + } + + public LocalisableString TooltipText { get; set; } + + public Statistic(IconUsage icon, bool background = false, float leftPadding = 10f, float? minSize = null) + { + this.icon = icon; + this.background = background; + this.leftPadding = leftPadding; + this.minSize = minSize; + + AutoSizeAxes = Axes.Both; + } + + [BackgroundDependencyLoader] + private void load(OverlayColourProvider colourProvider) + { + Masking = true; + CornerRadius = 5; + Shear = background ? OsuGame.SHEAR : Vector2.Zero; + + InternalChildren = new Drawable[] + { + new Box + { + RelativeSizeAxes = Axes.Both, + Colour = Color4.Black, + Alpha = background ? 0.2f : 0f, + }, + new FillFlowContainer + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + AutoSizeAxes = Axes.Both, + Direction = FillDirection.Horizontal, + Margin = new MarginPadding { Left = background ? leftPadding : 0, Right = background ? 10f : 0f, Vertical = 5f }, + Spacing = new Vector2(4f, 0f), + Shear = background ? -OsuGame.SHEAR : Vector2.Zero, + Children = new Drawable[] + { + new SpriteIcon + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + Icon = icon, + Size = new Vector2(OsuFont.Style.Heading2.Size), + Colour = colourProvider.Content2, + }, + new Container + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + AutoSizeAxes = Axes.X, + Height = 20, + Children = new Drawable[] + { + loading = new LoadingSpinner + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Size = new Vector2(14f), + State = { Value = Visibility.Visible }, + }, + new GridContainer + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + AutoSizeAxes = Axes.Both, + RowDimensions = new[] { new Dimension(GridSizeMode.AutoSize) }, + ColumnDimensions = new[] + { + new Dimension(GridSizeMode.AutoSize, minSize: minSize ?? 0), + }, + Content = new[] + { + new[] + { + valueText = new OsuSpriteText + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Font = OsuFont.Style.Heading2, + Colour = colourProvider.Content2, + Margin = new MarginPadding { Bottom = 2f }, + AlwaysPresent = true, + }, + } + } + }, + }, + }, + }, + } + }; + } + } + } +} diff --git a/osu.Game/Screens/SelectV2/BeatmapTitleWedge_StatisticPlayCount.cs b/osu.Game/Screens/SelectV2/BeatmapTitleWedge_StatisticPlayCount.cs new file mode 100644 index 0000000000..2d480ad5f4 --- /dev/null +++ b/osu.Game/Screens/SelectV2/BeatmapTitleWedge_StatisticPlayCount.cs @@ -0,0 +1,144 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Allocation; +using osu.Framework.Extensions.Color4Extensions; +using osu.Framework.Extensions.LocalisationExtensions; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Cursor; +using osu.Framework.Graphics.Effects; +using osu.Framework.Graphics.Shapes; +using osu.Game.Graphics; +using osu.Game.Graphics.Sprites; +using osu.Game.Overlays; +using osuTK; +using osuTK.Graphics; + +namespace osu.Game.Screens.SelectV2 +{ + public partial class BeatmapTitleWedge + { + public partial class StatisticPlayCount : Statistic, IHasCustomTooltip + { + public new Data? Value + { + set + { + base.Value = value?.Total < 0 ? "-" : value?.Total.ToLocalisableString("N0"); + TooltipContent = value; + } + } + + public Data? TooltipContent { get; private set; } + + [Resolved] + private OverlayColourProvider colourProvider { get; set; } = null!; + + public StatisticPlayCount(bool background = false, float leftPadding = 10, float? minSize = null) + : base(OsuIcon.Play, background, leftPadding, minSize) + { + } + + ITooltip IHasCustomTooltip.GetCustomTooltip() => new PlayCountTooltip(colourProvider); + + public record Data(int Total, int User); + + private partial class PlayCountTooltip : VisibilityContainer, ITooltip + { + private readonly OverlayColourProvider colourProvider; + + private OsuSpriteText totalPlaysText = null!; + private OsuSpriteText personalPlaysText = null!; + + public PlayCountTooltip(OverlayColourProvider colourProvider) + { + this.colourProvider = colourProvider; + } + + [BackgroundDependencyLoader] + private void load() + { + AutoSizeAxes = Axes.Both; + CornerRadius = 10; + Masking = true; + + EdgeEffect = new EdgeEffectParameters + { + Type = EdgeEffectType.Shadow, + Colour = Color4.Black.Opacity(0.25f), + Radius = 10f, + }; + + InternalChildren = new Drawable[] + { + new Box + { + RelativeSizeAxes = Axes.Both, + Colour = colourProvider.Background4, + }, + new FillFlowContainer + { + AutoSizeAxes = Axes.Both, + Margin = new MarginPadding(10), + Direction = FillDirection.Horizontal, + Spacing = new Vector2(16f, 0f), + Children = new[] + { + new FillFlowContainer + { + AutoSizeAxes = Axes.Both, + Direction = FillDirection.Vertical, + Children = new[] + { + new OsuSpriteText + { + Colour = colourProvider.Content2, + Font = OsuFont.Style.Caption1.With(weight: FontWeight.SemiBold), + Text = "Total Plays", + }, + totalPlaysText = new OsuSpriteText + { + Colour = colourProvider.Content1, + Font = OsuFont.Style.Heading1.With(weight: FontWeight.Regular), + }, + } + }, + new FillFlowContainer + { + AutoSizeAxes = Axes.Both, + Direction = FillDirection.Vertical, + Children = new[] + { + new OsuSpriteText + { + Colour = colourProvider.Content2, + Font = OsuFont.Style.Caption1.With(weight: FontWeight.SemiBold), + Text = "Personal Plays", + }, + personalPlaysText = new OsuSpriteText + { + Colour = colourProvider.Content1, + Font = OsuFont.Style.Heading1.With(weight: FontWeight.Regular), + }, + } + }, + } + }, + }; + } + + public void SetContent(Data content) + { + totalPlaysText.Text = content.Total < 0 ? "-" : content.Total.ToLocalisableString("N0"); + personalPlaysText.Text = content.User < 0 ? "-" : content.User.ToLocalisableString("N0"); + } + + public void Move(Vector2 pos) => Position = pos; + + protected override void PopIn() => this.FadeIn(300, Easing.OutQuint); + protected override void PopOut() => this.FadeOut(300, Easing.OutQuint); + } + } + } +} From a870a71b4b61c5d0888a16aa71835b2d0f57cb0c Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Fri, 18 Apr 2025 05:09:16 -0400 Subject: [PATCH 120/164] Add beatmap title wedge difficulty statistics --- .../TestSceneDifficultyStatisticsDisplay.cs | 166 ++++++++++++++ ...pTitleWedge_DifficultyStatisticsDisplay.cs | 205 ++++++++++++++++++ .../BeatmapTitleWedge_StatisticDifficulty.cs | 196 +++++++++++++++++ 3 files changed, 567 insertions(+) create mode 100644 osu.Game.Tests/Visual/SongSelectV2/TestSceneDifficultyStatisticsDisplay.cs create mode 100644 osu.Game/Screens/SelectV2/BeatmapTitleWedge_DifficultyStatisticsDisplay.cs create mode 100644 osu.Game/Screens/SelectV2/BeatmapTitleWedge_StatisticDifficulty.cs diff --git a/osu.Game.Tests/Visual/SongSelectV2/TestSceneDifficultyStatisticsDisplay.cs b/osu.Game.Tests/Visual/SongSelectV2/TestSceneDifficultyStatisticsDisplay.cs new file mode 100644 index 0000000000..3dd6fed708 --- /dev/null +++ b/osu.Game.Tests/Visual/SongSelectV2/TestSceneDifficultyStatisticsDisplay.cs @@ -0,0 +1,166 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using System.Linq; +using NUnit.Framework; +using osu.Framework.Allocation; +using osu.Framework.Extensions.ObjectExtensions; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; +using osu.Framework.Testing; +using osu.Game.Overlays; +using osu.Game.Screens.SelectV2; +using osuTK.Graphics; + +namespace osu.Game.Tests.Visual.SongSelectV2 +{ + public partial class TestSceneDifficultyStatisticsDisplay : OsuTestScene + { + private Container displayContainer = null!; + private BeatmapTitleWedge.DifficultyStatisticsDisplay display = null!; + + [Cached] + private readonly OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Aquamarine); + + [SetUpSteps] + public void SetUpSteps() + { + AddStep("setup", () => + { + Child = displayContainer = new Container + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Width = 300, + AutoSizeAxes = Axes.Y, + Children = new Drawable[] + { + new Box + { + RelativeSizeAxes = Axes.Both, + Colour = Color4.Black, + }, + display = new BeatmapTitleWedge.DifficultyStatisticsDisplay + { + RelativeSizeAxes = Axes.X, + Statistics = new[] + { + new BeatmapTitleWedge.StatisticDifficulty.Data("Statistic 1", 0.5f, 0.5f, 1f), + new BeatmapTitleWedge.StatisticDifficulty.Data("Statistic 2", 0.8f, 0.8f, 1f), + new BeatmapTitleWedge.StatisticDifficulty.Data("Statistic 3", 0.7f, 0.7f, 1f), + } + } + } + }; + }); + AddSliderStep("display width", 0, 300, 300, v => + { + if (displayContainer.IsNotNull()) + displayContainer.Width = v; + }); + } + + [Test] + public void TestEmpty() + { + AddStep("set empty", () => display.Statistics = Array.Empty()); + AddAssert("no statistics", () => !display.ChildrenOfType().Any()); + AddAssert("no tiny statistics", () => !display.ChildrenOfType().Single().Content.Any()); + } + + [Test] + public void TestDisplay() + { + AddStep("change data with same labels", () => display.Statistics = new[] + { + new BeatmapTitleWedge.StatisticDifficulty.Data("Statistic 1", 0.2f, 0.2f, 1f), + new BeatmapTitleWedge.StatisticDifficulty.Data("Statistic 2", 0.7f, 0.7f, 1f), + new BeatmapTitleWedge.StatisticDifficulty.Data("Statistic 3", 0.4f, 0.8f, 1f), + }); + + AddStep("change data with different labels", () => display.Statistics = new[] + { + new BeatmapTitleWedge.StatisticDifficulty.Data("Statistic 4", 0.3f, 0.3f, 1f), + new BeatmapTitleWedge.StatisticDifficulty.Data("Statistic 5", 0.8f, 0.8f, 1f), + new BeatmapTitleWedge.StatisticDifficulty.Data("Statistic 6", 0.5f, 0.5f, 1f), + }); + + AddAssert("statistics visible", () => display.ChildrenOfType().First().Parent!.Alpha == 1); + AddAssert("tiny statistics hidden", () => display.ChildrenOfType().Last().Alpha == 0); + + AddStep("shrink width", () => displayContainer.Width = 100); + AddAssert("statistics hidden", () => display.ChildrenOfType().First().Parent!.Alpha == 0); + AddUntilStep("tiny statistics displayed", () => display.ChildrenOfType().Last().Alpha == 1); + } + + [Test] + public void TestContraction() + { + AddAssert("statistics visible", () => display.ChildrenOfType().First().Parent!.Alpha == 1); + AddAssert("tiny statistics hidden", () => display.ChildrenOfType().Last().Alpha == 0); + + AddStep("set too many statistics", () => display.Statistics = new[] + { + new BeatmapTitleWedge.StatisticDifficulty.Data("Statistic 1", 0.2f, 0.2f, 1f), + new BeatmapTitleWedge.StatisticDifficulty.Data("Statistic 2", 0.7f, 0.7f, 1f), + new BeatmapTitleWedge.StatisticDifficulty.Data("Statistic 3", 0.4f, 0.8f, 1f), + new BeatmapTitleWedge.StatisticDifficulty.Data("Statistic 4", 0.3f, 0.3f, 1f), + new BeatmapTitleWedge.StatisticDifficulty.Data("Statistic 5", 0.8f, 0.8f, 1f), + new BeatmapTitleWedge.StatisticDifficulty.Data("Statistic 6", 0.5f, 0.5f, 1f), + }); + + AddAssert("statistics hidden", () => display.ChildrenOfType().First().Parent!.Alpha == 0); + AddUntilStep("tiny statistics displayed", () => display.ChildrenOfType().Last().Alpha == 1); + + AddStep("set less statistics", () => display.Statistics = new[] + { + new BeatmapTitleWedge.StatisticDifficulty.Data("Statistic 1", 0.2f, 0.2f, 1f), + }); + + AddAssert("tiny statistics hidden", () => display.ChildrenOfType().Last().Alpha == 0); + AddUntilStep("statistics visible", () => display.ChildrenOfType().First().Parent!.Alpha == 1); + } + + [Test] + public void TestAutoSize() + { + AddStep("setup auto size", () => Child = display = new BeatmapTitleWedge.DifficultyStatisticsDisplay(true) + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Statistics = new[] + { + new BeatmapTitleWedge.StatisticDifficulty.Data("Statistic 1", 0.5f, 0.5f, 1f), + new BeatmapTitleWedge.StatisticDifficulty.Data("Statistic 2", 0.8f, 0.8f, 1f), + new BeatmapTitleWedge.StatisticDifficulty.Data("Statistic 3", 0.7f, 0.7f, 1f), + } + }); + + AddAssert("statistics visible", () => display.ChildrenOfType().First().Parent!.Alpha == 1); + AddAssert("tiny statistics hidden", () => display.ChildrenOfType().Last().Alpha == 0); + + AddStep("set too many statistics", () => display.Statistics = new[] + { + new BeatmapTitleWedge.StatisticDifficulty.Data("Statistic 1", 0.2f, 0.2f, 1f), + new BeatmapTitleWedge.StatisticDifficulty.Data("Statistic 2", 0.7f, 0.7f, 1f), + new BeatmapTitleWedge.StatisticDifficulty.Data("Statistic 3", 0.4f, 0.8f, 1f), + new BeatmapTitleWedge.StatisticDifficulty.Data("Statistic 4", 0.3f, 0.3f, 1f), + new BeatmapTitleWedge.StatisticDifficulty.Data("Statistic 5", 0.8f, 0.8f, 1f), + new BeatmapTitleWedge.StatisticDifficulty.Data("Statistic 6", 0.5f, 0.5f, 1f), + }); + + AddAssert("statistics still visible", () => display.ChildrenOfType().First().Parent!.Alpha == 1); + AddAssert("tiny statistics still hidden", () => display.ChildrenOfType().Last().Alpha == 0); + + AddStep("set less statistics", () => display.Statistics = new[] + { + new BeatmapTitleWedge.StatisticDifficulty.Data("Statistic 1", 0.2f, 0.2f, 1f), + }); + + AddAssert("statistics still visible", () => display.ChildrenOfType().First().Parent!.Alpha == 1); + AddAssert("tiny statistics still hidden", () => display.ChildrenOfType().Last().Alpha == 0); + } + } +} diff --git a/osu.Game/Screens/SelectV2/BeatmapTitleWedge_DifficultyStatisticsDisplay.cs b/osu.Game/Screens/SelectV2/BeatmapTitleWedge_DifficultyStatisticsDisplay.cs new file mode 100644 index 0000000000..1cafe1c6db --- /dev/null +++ b/osu.Game/Screens/SelectV2/BeatmapTitleWedge_DifficultyStatisticsDisplay.cs @@ -0,0 +1,205 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using System.Collections.Generic; +using System.Linq; +using osu.Framework.Allocation; +using osu.Framework.Bindables; +using osu.Framework.Extensions.LocalisationExtensions; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Layout; +using osu.Framework.Localisation; +using osu.Game.Graphics; +using osu.Game.Graphics.Sprites; +using osu.Game.Overlays; +using osuTK; +using osuTK.Graphics; + +namespace osu.Game.Screens.SelectV2 +{ + public partial class BeatmapTitleWedge + { + public partial class DifficultyStatisticsDisplay : CompositeDrawable, IHasAccentColour + { + private readonly bool autoSize; + private readonly FillFlowContainer statisticsFlow; + private readonly GridContainer tinyStatisticsGrid; + + private IReadOnlyList statistics = Array.Empty(); + + public IReadOnlyList Statistics + { + get => statistics; + set + { + statistics = value; + + if (IsLoaded) + { + updateStatistics(); + updateTinyStatistics(); + } + } + } + + private Color4 accentColour; + + public Color4 AccentColour + { + get => accentColour; + set + { + accentColour = value; + + foreach (var statistic in statisticsFlow) + statistic.AccentColour = value; + } + } + + private readonly LayoutValue drawSizeLayout = new LayoutValue(Invalidation.DrawSize); + + [Resolved] + private OverlayColourProvider colourProvider { get; set; } = null!; + + public DifficultyStatisticsDisplay(bool autoSize = false) + { + this.autoSize = autoSize; + + if (autoSize) + AutoSizeAxes = Axes.Both; + else + AutoSizeAxes = Axes.Y; + + InternalChildren = new Drawable[] + { + statisticsFlow = new FillFlowContainer + { + AutoSizeAxes = Axes.Both, + Spacing = new Vector2(8f, 0f), + Direction = FillDirection.Horizontal, + AlwaysPresent = true, + }, + tinyStatisticsGrid = new GridContainer + { + Alpha = 0f, + AutoSizeAxes = Axes.Both, + ColumnDimensions = new[] + { + new Dimension(GridSizeMode.AutoSize), + new Dimension(GridSizeMode.Absolute, 8), + new Dimension(GridSizeMode.AutoSize), + } + }, + }; + + AddLayout(drawSizeLayout); + } + + [Resolved] + private LocalisationManager localisations { get; set; } = null!; + + private IBindable? localisationParameters; + + protected override void LoadComplete() + { + base.LoadComplete(); + + localisationParameters = localisations.CurrentParameters.GetBoundCopy(); + localisationParameters.BindValueChanged(_ => updateStatisticsSizing()); + + updateStatistics(); + updateTinyStatistics(); + } + + protected override void Update() + { + base.Update(); + + if (!drawSizeLayout.IsValid) + { + updateLayout(); + drawSizeLayout.Validate(); + } + } + + private bool displayedTinyStatistics; + + private void updateLayout() + { + if (statisticsFlow.Count == 0) + return; + + float flowWidth = statisticsFlow[0].Width * statisticsFlow.Count + statisticsFlow.Spacing.X * (statisticsFlow.Count - 1); + bool tiny = !autoSize && DrawWidth < flowWidth; + + if (displayedTinyStatistics != tiny) + { + if (tiny) + { + statisticsFlow.Hide(); + tinyStatisticsGrid.FadeIn(200, Easing.InQuint); + } + else + { + tinyStatisticsGrid.Hide(); + statisticsFlow.FadeIn(200, Easing.InQuint); + } + + displayedTinyStatistics = tiny; + } + } + + private void updateStatisticsSizing() => SchedulerAfterChildren.AddOnce(() => + { + if (statisticsFlow.Count == 0) + return; + + float statisticWidth = Math.Max(65, statisticsFlow.Max(s => s.LabelWidth)); + + foreach (var statistic in statisticsFlow) + statistic.Width = statisticWidth; + + drawSizeLayout.Invalidate(); + }); + + private void updateStatistics() + { + var oldStatistics = statisticsFlow.Select(s => s.Value).ToArray(); + + if (oldStatistics.Select(s => s.Label).SequenceEqual(statistics.Select(s => s.Label))) + { + for (int i = 0; i < statistics.Count; i++) + statisticsFlow[i].Value = statistics[i]; + } + else + { + statisticsFlow.ChildrenEnumerable = statistics.Select(d => new StatisticDifficulty { Value = d }); + updateStatisticsSizing(); + } + } + + private void updateTinyStatistics() + { + tinyStatisticsGrid.RowDimensions = statistics.Select(_ => new Dimension(GridSizeMode.AutoSize)).ToArray(); + tinyStatisticsGrid.Content = statistics.Select(s => new[] + { + new OsuSpriteText + { + Text = s.Label, + Font = OsuFont.Style.Caption2.With(weight: FontWeight.SemiBold), + Colour = colourProvider.Content2, + }, + Empty(), + new OsuSpriteText + { + Font = OsuFont.Style.Caption2.With(weight: FontWeight.SemiBold), + Text = s.Content ?? s.Value.ToLocalisableString("0.##"), + Colour = colourProvider.Content1, + }, + }).ToArray(); + } + } + } +} diff --git a/osu.Game/Screens/SelectV2/BeatmapTitleWedge_StatisticDifficulty.cs b/osu.Game/Screens/SelectV2/BeatmapTitleWedge_StatisticDifficulty.cs new file mode 100644 index 0000000000..b533d21c1e --- /dev/null +++ b/osu.Game/Screens/SelectV2/BeatmapTitleWedge_StatisticDifficulty.cs @@ -0,0 +1,196 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using osu.Framework.Allocation; +using osu.Framework.Extensions.LocalisationExtensions; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Colour; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; +using osu.Framework.Graphics.Sprites; +using osu.Framework.Localisation; +using osu.Game.Graphics; +using osu.Game.Graphics.Sprites; +using osu.Game.Overlays; +using osuTK; +using osuTK.Graphics; + +namespace osu.Game.Screens.SelectV2 +{ + public partial class BeatmapTitleWedge + { + public partial class StatisticDifficulty : CompositeDrawable, IHasAccentColour + { + private Data value = new Data(string.Empty, 0, 0, 0); + + public Data Value + { + get => value; + set + { + this.value = value; + + if (IsLoaded) + updateDisplay(); + } + } + + public float LabelWidth => labelText.DrawWidth; + + private readonly Circle bar; + private readonly Circle adjustedBar; + private readonly OsuSpriteText labelText; + private readonly OsuSpriteText valueText; + private readonly SpriteIcon valueIcon; + private readonly Container bars; + + public Color4 AccentColour + { + get => bar.Colour; + set => bar.Colour = value; + } + + [Resolved] + private OsuColour colours { get; set; } = null!; + + public StatisticDifficulty() + { + AutoSizeAxes = Axes.Y; + + InternalChild = new FillFlowContainer + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Direction = FillDirection.Vertical, + Children = new Drawable[] + { + bars = new Container + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Children = new[] + { + new Circle + { + RelativeSizeAxes = Axes.X, + Height = 2f, + Colour = Color4.Black, + Masking = true, + CornerRadius = 1f, + Depth = float.MaxValue, + }, + bar = new Circle + { + RelativeSizeAxes = Axes.X, + Width = 0f, + Height = 2f, + Masking = true, + CornerRadius = 1f, + }, + adjustedBar = new Circle + { + RelativeSizeAxes = Axes.X, + Width = 0f, + Height = 2f, + Masking = true, + CornerRadius = 1f, + }, + }, + }, + labelText = new OsuSpriteText + { + Margin = new MarginPadding { Top = 2f }, + Font = OsuFont.Style.Caption1.With(weight: FontWeight.SemiBold), + }, + new FillFlowContainer + { + AutoSizeAxes = Axes.Both, + Direction = FillDirection.Horizontal, + Children = new Drawable[] + { + valueText = new OsuSpriteText + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + Font = OsuFont.Style.Body, + }, + valueIcon = new SpriteIcon + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + Margin = new MarginPadding + { + Top = -4f, + Left = 2, + }, + Size = new Vector2(8), + } + }, + }, + }, + }; + } + + [BackgroundDependencyLoader] + private void load(OverlayColourProvider colourProvider) + { + labelText.Colour = colourProvider.Content2; + valueText.Colour = colourProvider.Content1; + } + + protected override void LoadComplete() + { + base.LoadComplete(); + updateDisplay(); + } + + private void updateDisplay() + { + bar.ResizeWidthTo(value.Maximum == 0 ? 0 : Math.Clamp(value.Value / value.Maximum, 0, 1), 300, Easing.OutQuint); + adjustedBar.ResizeWidthTo(value.Maximum == 0 ? 0 : Math.Clamp(value.AdjustedValue / value.Maximum, 0, 1), 300, Easing.OutQuint); + + labelText.Text = value.Label; + valueText.Text = value.Content ?? value.AdjustedValue.ToLocalisableString("0.##"); + + if (value.Value == value.AdjustedValue) + { + adjustedBar.FadeColour(Color4.Transparent, 300, Easing.OutQuint); + bar.FadeIn(300, Easing.OutQuint); + + valueText.FadeColour(Color4.White, 300, Easing.OutQuint); + valueIcon.Hide(); + } + else + { + bool difficultyIncrease = value.Value < value.AdjustedValue; + + if (difficultyIncrease) + { + bars.ChangeChildDepth(adjustedBar, 1); + bar.FadeIn(300, Easing.OutQuint); + adjustedBar.FadeColour(ColourInfo.GradientHorizontal(Color4.Black, colours.Red1), 300, Easing.OutQuint); + + valueText.FadeColour(colours.Red1, 300, Easing.OutQuint); + valueIcon.Show(); + valueIcon.Colour = colours.Red1; + valueIcon.Icon = FontAwesome.Solid.SortUp; + } + else + { + bar.FadeTo(0.5f, 300, Easing.OutQuint); + bars.ChangeChildDepth(adjustedBar, -1); + adjustedBar.FadeColour(colours.Lime1, 300, Easing.OutQuint); + + valueText.FadeColour(colours.Lime1, 300, Easing.OutQuint); + valueIcon.Show(); + valueIcon.Colour = colours.Lime1; + valueIcon.Icon = FontAwesome.Solid.SortDown; + } + } + } + + public record Data(LocalisableString Label, float Value, float AdjustedValue, float Maximum, string? Content = null); + } + } +} From 04fa95d924ffa6a0598aa4e8f7b6a84525f5c7ae Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Fri, 18 Apr 2025 05:09:23 -0400 Subject: [PATCH 121/164] Add beatmap title wedge --- .../TestSceneBeatmapTitleWedge.cs | 161 +++++++ .../Mods/AdjustedAttributesTooltip.cs | 8 +- .../Screens/SelectV2/BeatmapTitleWedge.cs | 324 +++++++++++++++ .../BeatmapTitleWedge_DifficultyDisplay.cs | 392 ++++++++++++++++++ 4 files changed, 884 insertions(+), 1 deletion(-) create mode 100644 osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapTitleWedge.cs create mode 100644 osu.Game/Screens/SelectV2/BeatmapTitleWedge.cs create mode 100644 osu.Game/Screens/SelectV2/BeatmapTitleWedge_DifficultyDisplay.cs diff --git a/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapTitleWedge.cs b/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapTitleWedge.cs new file mode 100644 index 0000000000..8a674d43a5 --- /dev/null +++ b/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapTitleWedge.cs @@ -0,0 +1,161 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Linq; +using NUnit.Framework; +using osu.Framework.Allocation; +using osu.Framework.Bindables; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Testing; +using osu.Game.Beatmaps; +using osu.Game.Beatmaps.ControlPoints; +using osu.Game.Resources.Localisation.Web; +using osu.Game.Rulesets; +using osu.Game.Rulesets.Osu; +using osu.Game.Rulesets.Osu.Mods; +using osu.Game.Screens.SelectV2; +using osu.Game.Tests.Visual.SongSelect; + +namespace osu.Game.Tests.Visual.SongSelectV2 +{ + public partial class TestSceneBeatmapTitleWedge : SongSelectComponentsTestScene + { + private RulesetStore rulesets = null!; + + private BeatmapTitleWedge titleWedge = null!; + private BeatmapTitleWedge.DifficultyDisplay difficultyDisplay => titleWedge.ChildrenOfType().Single(); + + [BackgroundDependencyLoader] + private void load(RulesetStore rulesets) + { + this.rulesets = rulesets; + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + AddRange(new Drawable[] + { + new Container + { + RelativeSizeAxes = Axes.Both, + Children = new Drawable[] + { + titleWedge = new BeatmapTitleWedge + { + State = { Value = Visibility.Visible }, + }, + }, + } + }); + + AddSliderStep("change star difficulty", 0, 11.9, 4.18, v => + { + ((BindableDouble)difficultyDisplay.DisplayedStars).Value = v; + }); + } + + [Test] + public void TestNullBeatmap() + { + selectBeatmap(null); + AddAssert("check default title", () => titleWedge.DisplayedTitle == Beatmap.Default.BeatmapInfo.Metadata.Title); + AddAssert("check default artist", () => titleWedge.DisplayedArtist == Beatmap.Default.BeatmapInfo.Metadata.Artist); + AddAssert("check empty version", () => string.IsNullOrEmpty(difficultyDisplay.DisplayedVersion.ToString())); + AddAssert("check empty author", () => string.IsNullOrEmpty(difficultyDisplay.DisplayedAuthor.ToString())); + AddAssert("check no statistics", () => difficultyDisplay.ChildrenOfType().All(d => !d.Statistics.Any())); + } + + [Test] + public void TestBPMUpdates() + { + const double bpm = 120; + IBeatmap beatmap = TestSceneBeatmapInfoWedge.CreateTestBeatmap(new OsuRuleset().RulesetInfo); + beatmap.ControlPointInfo.Add(0, new TimingControlPoint { BeatLength = 60 * 1000 / bpm }); + + OsuModDoubleTime doubleTime = null!; + + selectBeatmap(beatmap); + checkDisplayedBPM($"{bpm}"); + + AddStep("select DT", () => SelectedMods.Value = new[] { doubleTime = new OsuModDoubleTime() }); + checkDisplayedBPM($"{bpm * 1.5f}"); + + AddStep("change DT rate", () => doubleTime.SpeedChange.Value = 2); + checkDisplayedBPM($"{bpm * 2}"); + + AddStep("select HT", () => SelectedMods.Value = new[] { new OsuModHalfTime() }); + checkDisplayedBPM($"{bpm * 0.75f}"); + } + + [Test] + public void TestRulesetChange() + { + selectBeatmap(Beatmap.Value.Beatmap); + + AddWaitStep("wait for select", 3); + + foreach (var rulesetInfo in rulesets.AvailableRulesets) + { + var testBeatmap = TestSceneBeatmapInfoWedge.CreateTestBeatmap(rulesetInfo); + + setRuleset(rulesetInfo); + selectBeatmap(testBeatmap); + } + } + + [Test] + public void TestWedgeVisibility() + { + AddStep("hide", () => { titleWedge.Hide(); }); + AddWaitStep("wait for hide", 3); + AddAssert("check visibility", () => titleWedge.Alpha == 0); + AddStep("show", () => { titleWedge.Show(); }); + AddWaitStep("wait for show", 1); + AddAssert("check visibility", () => titleWedge.Alpha > 0); + } + + [TestCase(120, 125, null, "120-125 (mostly 120)")] + [TestCase(120, 120.6, null, "120-121 (mostly 120)")] + [TestCase(120, 120.4, null, "120")] + [TestCase(120, 120.6, "DT", "180-182 (mostly 180)")] + [TestCase(120, 120.4, "DT", "180")] + public void TestVaryingBPM(double commonBpm, double otherBpm, string? mod, string expectedDisplay) + { + IBeatmap beatmap = TestSceneBeatmapInfoWedge.CreateTestBeatmap(new OsuRuleset().RulesetInfo); + beatmap.ControlPointInfo.Add(0, new TimingControlPoint { BeatLength = 60 * 1000 / commonBpm }); + beatmap.ControlPointInfo.Add(100, new TimingControlPoint { BeatLength = 60 * 1000 / otherBpm }); + beatmap.ControlPointInfo.Add(200, new TimingControlPoint { BeatLength = 60 * 1000 / commonBpm }); + + if (mod != null) + AddStep($"select {mod}", () => SelectedMods.Value = new[] { Ruleset.Value.CreateInstance().CreateModFromAcronym(mod) }); + + selectBeatmap(beatmap); + checkDisplayedBPM(expectedDisplay); + } + + private void setRuleset(RulesetInfo rulesetInfo) + { + AddStep("set ruleset", () => Ruleset.Value = rulesetInfo); + } + + private void selectBeatmap(IBeatmap? b) + { + AddStep($"select {b?.Metadata.Title ?? "null"} beatmap", () => + { + Beatmap.Value = b == null ? Beatmap.Default : CreateWorkingBeatmap(b); + }); + } + + private void checkDisplayedBPM(string target) + { + AddUntilStep($"displayed bpm is {target}", () => + { + var label = titleWedge.ChildrenOfType().Single(l => l.TooltipText == BeatmapsetsStrings.ShowStatsBpm); + return label.Value == target; + }); + } + } +} diff --git a/osu.Game/Overlays/Mods/AdjustedAttributesTooltip.cs b/osu.Game/Overlays/Mods/AdjustedAttributesTooltip.cs index 957ee23e3b..bdb10a477c 100644 --- a/osu.Game/Overlays/Mods/AdjustedAttributesTooltip.cs +++ b/osu.Game/Overlays/Mods/AdjustedAttributesTooltip.cs @@ -18,6 +18,7 @@ namespace osu.Game.Overlays.Mods { public partial class AdjustedAttributesTooltip : VisibilityContainer, ITooltip { + private readonly OverlayColourProvider? colourProvider; private FillFlowContainer attributesFillFlow = null!; private Container content = null!; @@ -27,6 +28,11 @@ namespace osu.Game.Overlays.Mods [Resolved] private OsuColour colours { get; set; } = null!; + public AdjustedAttributesTooltip(OverlayColourProvider? colourProvider = null) + { + this.colourProvider = colourProvider; + } + [BackgroundDependencyLoader] private void load() { @@ -45,7 +51,7 @@ namespace osu.Game.Overlays.Mods new Box { RelativeSizeAxes = Axes.Both, - Colour = colours.Gray3, + Colour = colourProvider?.Background4 ?? colours.Gray3, }, new FillFlowContainer { diff --git a/osu.Game/Screens/SelectV2/BeatmapTitleWedge.cs b/osu.Game/Screens/SelectV2/BeatmapTitleWedge.cs new file mode 100644 index 0000000000..9d1be2fc37 --- /dev/null +++ b/osu.Game/Screens/SelectV2/BeatmapTitleWedge.cs @@ -0,0 +1,324 @@ +// 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.Extensions.LocalisationExtensions; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Localisation; +using osu.Game.Beatmaps; +using osu.Game.Beatmaps.Drawables; +using osu.Game.Configuration; +using osu.Game.Extensions; +using osu.Game.Graphics; +using osu.Game.Graphics.Containers; +using osu.Game.Graphics.Sprites; +using osu.Game.Online.API; +using osu.Game.Online.API.Requests; +using osu.Game.Online.API.Requests.Responses; +using osu.Game.Resources.Localisation.Web; +using osu.Game.Rulesets; +using osu.Game.Rulesets.Mods; +using osu.Game.Utils; +using osuTK; + +namespace osu.Game.Screens.SelectV2 +{ + public partial class BeatmapTitleWedge : VisibilityContainer + { + private const float corner_radius = 10; + + [Resolved] + private IBindable beatmap { get; set; } = null!; + + [Resolved] + private IBindable ruleset { get; set; } = null!; + + [Resolved] + private IBindable> mods { get; set; } = null!; + + private ModSettingChangeTracker? settingChangeTracker; + + private BeatmapSetOnlineStatusPill statusPill = null!; + private Container titleContainer = null!; + private OsuHoverContainer titleLink = null!; + private OsuSpriteText titleLabel = null!; + private Container artistContainer = null!; + private OsuHoverContainer artistLink = null!; + private OsuSpriteText artistLabel = null!; + + internal string DisplayedTitle => titleLabel.Text.ToString(); + internal string DisplayedArtist => artistLabel.Text.ToString(); + + private StatisticPlayCount playCount = null!; + private Statistic favouritesStatistic = null!; + private Statistic lengthStatistic = null!; + private Statistic bpmStatistic = null!; + + [Resolved] + private SongSelect? songSelect { get; set; } + + [Resolved] + private LocalisationManager localisation { get; set; } = null!; + + [Resolved] + private IAPIProvider api { get; set; } = null!; + + private APIBeatmapSet? currentOnlineBeatmapSet; + private GetBeatmapSetRequest? currentRequest; + + public BeatmapTitleWedge() + { + RelativeSizeAxes = Axes.X; + AutoSizeAxes = Axes.Y; + } + + [BackgroundDependencyLoader] + private void load() + { + Shear = OsuGame.SHEAR; + Masking = true; + CornerRadius = corner_radius; + + InternalChildren = new Drawable[] + { + new WedgeBackground(), + new FillFlowContainer + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Direction = FillDirection.Vertical, + Padding = new MarginPadding + { + Top = SongSelect.WEDGE_CONTENT_MARGIN, + Left = SongSelect.WEDGE_CONTENT_MARGIN + }, + Spacing = new Vector2(0f, 4f), + Children = new Drawable[] + { + new ShearAligningWrapper(statusPill = new BeatmapSetOnlineStatusPill + { + Shear = -OsuGame.SHEAR, + ShowUnknownStatus = true, + TextSize = OsuFont.Style.Caption1.Size, + TextPadding = new MarginPadding { Horizontal = 6, Vertical = 1 }, + }), + new ShearAligningWrapper(titleContainer = new Container + { + Shear = -OsuGame.SHEAR, + RelativeSizeAxes = Axes.X, + Height = OsuFont.Style.Title.Size, + Margin = new MarginPadding { Bottom = -4f }, + Child = titleLink = new OsuHoverContainer + { + AutoSizeAxes = Axes.Both, + Child = titleLabel = new TruncatingSpriteText + { + Shadow = true, + Font = OsuFont.Style.Title, + }, + } + }), + new ShearAligningWrapper(artistContainer = new Container + { + Shear = -OsuGame.SHEAR, + RelativeSizeAxes = Axes.X, + Height = OsuFont.Style.Heading2.Size, + Margin = new MarginPadding { Left = 1f }, + Child = artistLink = new OsuHoverContainer + { + AutoSizeAxes = Axes.Both, + Child = artistLabel = new TruncatingSpriteText + { + Shadow = true, + Font = OsuFont.Style.Heading2, + }, + } + }), + new ShearAligningWrapper(new FillFlowContainer + { + Shear = -OsuGame.SHEAR, + AutoSizeAxes = Axes.Both, + Direction = FillDirection.Horizontal, + Spacing = new Vector2(2f, 0f), + AutoSizeDuration = 100, + AutoSizeEasing = Easing.OutQuint, + Children = new Drawable[] + { + playCount = new StatisticPlayCount(background: true, leftPadding: SongSelect.WEDGE_CONTENT_MARGIN, minSize: 50f) + { + Margin = new MarginPadding { Left = -SongSelect.WEDGE_CONTENT_MARGIN }, + }, + favouritesStatistic = new Statistic(OsuIcon.Heart, background: true, minSize: 25f) + { + TooltipText = BeatmapsStrings.StatusFavourites, + }, + lengthStatistic = new Statistic(OsuIcon.Clock), + bpmStatistic = new Statistic(OsuIcon.Metronome) + { + TooltipText = BeatmapsetsStrings.ShowStatsBpm, + Margin = new MarginPadding { Left = 5f }, + }, + }, + }), + new ShearAligningWrapper(new Container + { + Shear = -OsuGame.SHEAR, + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Margin = new MarginPadding { Left = -SongSelect.WEDGE_CONTENT_MARGIN }, + Padding = new MarginPadding { Right = -SongSelect.WEDGE_CONTENT_MARGIN }, + Child = new DifficultyDisplay(), + }), + }, + } + }; + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + beatmap.BindValueChanged(_ => updateDisplay()); + ruleset.BindValueChanged(_ => updateDisplay()); + + mods.BindValueChanged(m => + { + settingChangeTracker?.Dispose(); + + updateLengthAndBpmStatistics(); + + settingChangeTracker = new ModSettingChangeTracker(m.NewValue); + settingChangeTracker.SettingChanged += _ => updateLengthAndBpmStatistics(); + }); + + updateDisplay(); + + FinishTransforms(true); + } + + protected override void PopIn() + { + this.MoveToX(0, SongSelect.ENTER_DURATION, Easing.OutQuint) + .FadeIn(SongSelect.ENTER_DURATION / 3, Easing.In); + } + + protected override void PopOut() + { + this.MoveToX(-150, SongSelect.ENTER_DURATION, Easing.OutQuint) + .FadeOut(SongSelect.ENTER_DURATION / 3, Easing.In); + } + + protected override void Update() + { + base.Update(); + titleLabel.MaxWidth = titleContainer.DrawWidth - 20; + artistLabel.MaxWidth = artistContainer.DrawWidth - 20; + } + + private void updateDisplay() + { + var metadata = beatmap.Value.Metadata; + var beatmapInfo = beatmap.Value.BeatmapInfo; + var beatmapSetInfo = beatmap.Value.BeatmapSetInfo; + + statusPill.Status = beatmapInfo.Status; + + var titleText = new RomanisableString(metadata.TitleUnicode, metadata.Title); + titleLabel.Text = titleText; + titleLink.Action = () => songSelect?.Search(titleText.GetPreferred(localisation.CurrentParameters.Value.PreferOriginalScript)); + + var artistText = new RomanisableString(metadata.ArtistUnicode, metadata.Artist); + artistLabel.Text = artistText; + artistLink.Action = () => songSelect?.Search(artistText.GetPreferred(localisation.CurrentParameters.Value.PreferOriginalScript)); + + updateLengthAndBpmStatistics(); + + if (currentOnlineBeatmapSet == null || currentOnlineBeatmapSet.OnlineID != beatmapSetInfo.OnlineID) + refetchBeatmapSet(); + + updateOnlineDisplay(); + } + + private void updateLengthAndBpmStatistics() + { + var beatmapInfo = beatmap.Value.BeatmapInfo; + + double rate = ModUtils.CalculateRateWithMods(mods.Value); + + int bpmMax = FormatUtils.RoundBPM(beatmap.Value.Beatmap.ControlPointInfo.BPMMaximum, rate); + int bpmMin = FormatUtils.RoundBPM(beatmap.Value.Beatmap.ControlPointInfo.BPMMinimum, rate); + int mostCommonBPM = FormatUtils.RoundBPM(60000 / beatmap.Value.Beatmap.GetMostCommonBeatLength(), rate); + + double drainLength = Math.Round(beatmap.Value.Beatmap.CalculateDrainLength() / rate); + double hitLength = Math.Round(beatmapInfo.Length / rate); + + lengthStatistic.Value = hitLength.ToFormattedDuration(); + lengthStatistic.TooltipText = BeatmapsetsStrings.ShowStatsTotalLength(drainLength.ToFormattedDuration()); + + bpmStatistic.Value = bpmMin == bpmMax + ? $"{bpmMin}" + : $"{bpmMin}-{bpmMax} (mostly {mostCommonBPM})"; + } + + private void refetchBeatmapSet() + { + var beatmapSetInfo = beatmap.Value.BeatmapSetInfo; + + currentRequest?.Cancel(); + currentRequest = null; + currentOnlineBeatmapSet = null; + + if (beatmapSetInfo.OnlineID >= 1) + { + // todo: consider introducing a BeatmapSetLookupCache for caching benefits. + currentRequest = new GetBeatmapSetRequest(beatmapSetInfo.OnlineID); + currentRequest.Failure += _ => updateOnlineDisplay(); + currentRequest.Success += s => + { + currentOnlineBeatmapSet = s; + updateOnlineDisplay(); + }; + + api.Queue(currentRequest); + } + } + + private void updateOnlineDisplay() + { + if (currentRequest?.CompletionState == APIRequestCompletionState.Waiting) + { + playCount.Value = null; + favouritesStatistic.Value = null; + } + else if (currentOnlineBeatmapSet == null) + { + playCount.Value = new StatisticPlayCount.Data(-1, -1); + favouritesStatistic.Value = "-"; + } + else + { + var onlineBeatmapSet = currentOnlineBeatmapSet; + var onlineBeatmap = currentOnlineBeatmapSet.Beatmaps.SingleOrDefault(b => b.OnlineID == beatmap.Value.BeatmapInfo.OnlineID); + + if (onlineBeatmap != null) + { + playCount.FadeIn(300, Easing.OutQuint); + playCount.Value = new StatisticPlayCount.Data(onlineBeatmap.PlayCount, onlineBeatmap.UserPlayCount); + } + else + { + playCount.FadeOut(300, Easing.OutQuint); + playCount.Value = null; + } + + favouritesStatistic.FadeIn(300, Easing.OutQuint); + favouritesStatistic.Value = onlineBeatmapSet.FavouriteCount.ToLocalisableString(@"N0"); + } + } + } +} diff --git a/osu.Game/Screens/SelectV2/BeatmapTitleWedge_DifficultyDisplay.cs b/osu.Game/Screens/SelectV2/BeatmapTitleWedge_DifficultyDisplay.cs new file mode 100644 index 0000000000..e8b2ccb04a --- /dev/null +++ b/osu.Game/Screens/SelectV2/BeatmapTitleWedge_DifficultyDisplay.cs @@ -0,0 +1,392 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using osu.Framework.Allocation; +using osu.Framework.Bindables; +using osu.Framework.Extensions; +using osu.Framework.Extensions.Color4Extensions; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Cursor; +using osu.Framework.Graphics.Shapes; +using osu.Framework.Localisation; +using osu.Game.Beatmaps; +using osu.Game.Beatmaps.Drawables; +using osu.Game.Configuration; +using osu.Game.Graphics; +using osu.Game.Graphics.Containers; +using osu.Game.Graphics.Sprites; +using osu.Game.Localisation; +using osu.Game.Online; +using osu.Game.Online.Chat; +using osu.Game.Overlays; +using osu.Game.Overlays.Mods; +using osu.Game.Resources.Localisation.Web; +using osu.Game.Rulesets; +using osu.Game.Rulesets.Mods; +using osu.Game.Utils; +using osuTK.Graphics; + +namespace osu.Game.Screens.SelectV2 +{ + public partial class BeatmapTitleWedge + { + public partial class DifficultyDisplay : CompositeDrawable + { + private const float border_weight = 2; + + [Resolved] + private IBindable beatmap { get; set; } = null!; + + [Resolved] + private IBindable ruleset { get; set; } = null!; + + [Resolved] + private IBindable> mods { get; set; } = null!; + + private ModSettingChangeTracker? settingChangeTracker; + + [Resolved] + private BeatmapDifficultyCache difficultyCache { get; set; } = null!; + + [Resolved] + private OsuColour colours { get; set; } = null!; + + private StarRatingDisplay starRatingDisplay = null!; + private FillFlowContainer nameLine = null!; + private OsuSpriteText difficultyText = null!; + private OsuSpriteText mappedByText = null!; + private OsuHoverContainer mapperLink = null!; + private OsuSpriteText mapperText = null!; + + internal LocalisableString DisplayedVersion => difficultyText.Text; + internal LocalisableString DisplayedAuthor => mapperText.Text; + + private GridContainer ratingAndNameContainer = null!; + private DifficultyStatisticsDisplay countStatisticsDisplay = null!; + private AdjustableDifficultyStatisticsDisplay difficultyStatisticsDisplay = null!; + + private CancellationTokenSource? cancellationSource; + + public IBindable DisplayedStars => displayedStars; + + private readonly Bindable displayedStars = new BindableDouble(); + + public DifficultyDisplay() + { + RelativeSizeAxes = Axes.X; + AutoSizeAxes = Axes.Y; + } + + [BackgroundDependencyLoader] + private void load(OverlayColourProvider colourProvider) + { + Masking = true; + CornerRadius = 10; + Shear = OsuGame.SHEAR; + + InternalChildren = new Drawable[] + { + new WedgeBackground(), + new FillFlowContainer + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Direction = FillDirection.Vertical, + Children = new Drawable[] + { + new ShearAligningWrapper(ratingAndNameContainer = new GridContainer + { + Shear = -OsuGame.SHEAR, + AlwaysPresent = true, + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Margin = new MarginPadding { Vertical = 5f }, + Padding = new MarginPadding { Left = SongSelect.WEDGE_CONTENT_MARGIN }, + RowDimensions = new[] { new Dimension(GridSizeMode.AutoSize) }, + ColumnDimensions = new[] + { + new Dimension(GridSizeMode.AutoSize), + new Dimension(GridSizeMode.Absolute, 6), + new Dimension(), + }, + Content = new[] + { + new[] + { + starRatingDisplay = new StarRatingDisplay(default, animated: true) + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + }, + Empty(), + nameLine = new FillFlowContainer + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Direction = FillDirection.Horizontal, + Margin = new MarginPadding { Bottom = 2f }, + Children = new Drawable[] + { + difficultyText = new TruncatingSpriteText + { + Anchor = Anchor.BottomLeft, + Origin = Anchor.BottomLeft, + Font = OsuFont.Style.Body.With(weight: FontWeight.SemiBold), + }, + mappedByText = new OsuSpriteText + { + Anchor = Anchor.BottomLeft, + Origin = Anchor.BottomLeft, + Text = " mapped by ", + Font = OsuFont.Style.Body, + }, + mapperLink = new MapperLinkContainer + { + AutoSizeAxes = Axes.Both, + Anchor = Anchor.BottomLeft, + Origin = Anchor.BottomLeft, + Child = mapperText = new TruncatingSpriteText + { + Shadow = true, + Font = OsuFont.Style.Body.With(weight: FontWeight.SemiBold), + }, + }, + }, + }, + } + }, + }), + new ShearAligningWrapper(new Container + { + Shear = -OsuGame.SHEAR, + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Padding = new MarginPadding { Bottom = border_weight, Right = border_weight }, + Child = new Container + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Masking = true, + CornerRadius = 10 - border_weight, + Shear = OsuGame.SHEAR, + Children = new Drawable[] + { + new Box + { + RelativeSizeAxes = Axes.Both, + Colour = colourProvider.Background5.Opacity(0.8f), + }, + new GridContainer + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Padding = new MarginPadding { Left = SongSelect.WEDGE_CONTENT_MARGIN, Right = 20f, Vertical = 7.5f }, + Shear = -OsuGame.SHEAR, + RowDimensions = new[] { new Dimension(GridSizeMode.AutoSize) }, + ColumnDimensions = new[] + { + new Dimension(), + new Dimension(GridSizeMode.Absolute, 30), + new Dimension(GridSizeMode.AutoSize), + }, + Content = new[] + { + new[] + { + countStatisticsDisplay = new DifficultyStatisticsDisplay + { + RelativeSizeAxes = Axes.X, + }, + Empty(), + difficultyStatisticsDisplay = new AdjustableDifficultyStatisticsDisplay(autoSize: true), + } + }, + } + }, + } + }), + } + }, + }; + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + beatmap.BindValueChanged(_ => updateDisplay()); + ruleset.BindValueChanged(_ => updateDisplay()); + + mods.BindValueChanged(m => + { + settingChangeTracker?.Dispose(); + + updateDifficultyStatistics(); + + settingChangeTracker = new ModSettingChangeTracker(m.NewValue); + settingChangeTracker.SettingChanged += _ => updateDifficultyStatistics(); + }); + + updateDisplay(); + + displayedStars.BindValueChanged(_ => updateStars(), true); + FinishTransforms(true); + } + + [Resolved] + private ILinkHandler? linkHandler { get; set; } + + private void updateDisplay() + { + cancellationSource?.Cancel(); + cancellationSource = new CancellationTokenSource(); + + computeStarDifficulty(cancellationSource.Token); + + if (beatmap.IsDefault) + { + ratingAndNameContainer.FadeOut(300, Easing.OutQuint); + difficultyText.Text = string.Empty; + mapperText.Text = string.Empty; + countStatisticsDisplay.Statistics = Array.Empty(); + } + else + { + ratingAndNameContainer.FadeIn(300, Easing.OutQuint); + difficultyText.Text = beatmap.Value.BeatmapInfo.DifficultyName; + mapperLink.Action = () => linkHandler?.HandleLink(new LinkDetails(LinkAction.OpenUserProfile, beatmap.Value.Metadata.Author)); + mapperText.Text = beatmap.Value.Metadata.Author.Username; + + var playableBeatmap = beatmap.Value.GetPlayableBeatmap(ruleset.Value); + + countStatisticsDisplay.Statistics = playableBeatmap.GetStatistics() + .Select(s => new StatisticDifficulty.Data(s.Name, s.BarDisplayLength ?? 0, s.BarDisplayLength ?? 0, 1, s.Content)) + .ToList(); + } + + updateDifficultyStatistics(); + } + + private void updateDifficultyStatistics() => Scheduler.AddOnce(() => + { + if (beatmap.IsDefault) + { + difficultyStatisticsDisplay.TooltipContent = null; + difficultyStatisticsDisplay.Statistics = Array.Empty(); + return; + } + + BeatmapDifficulty baseDifficulty = beatmap.Value.BeatmapInfo.Difficulty; + BeatmapDifficulty originalDifficulty = new BeatmapDifficulty(baseDifficulty); + + foreach (var mod in mods.Value.OfType()) + mod.ApplyToDifficulty(originalDifficulty); + + var rateAdjustedDifficulty = originalDifficulty; + + if (ruleset.Value != null) + { + double rate = ModUtils.CalculateRateWithMods(mods.Value); + + rateAdjustedDifficulty = ruleset.Value.CreateInstance().GetRateAdjustedDisplayDifficulty(originalDifficulty, rate); + difficultyStatisticsDisplay.TooltipContent = new AdjustedAttributesTooltip.Data(originalDifficulty, rateAdjustedDifficulty); + } + + StatisticDifficulty.Data firstStatistic; + + switch (ruleset.Value?.OnlineID) + { + case 3: + // Account for mania differences locally for now. + // Eventually this should be handled in a more modular way, allowing rulesets to return arbitrary difficulty attributes. + ILegacyRuleset legacyRuleset = (ILegacyRuleset)ruleset.Value.CreateInstance(); + + // For the time being, the key count is static no matter what, because: + // a) The method doesn't have knowledge of the active keymods. Doing so may require considerations for filtering. + // b) Using the difficulty adjustment mod to adjust OD doesn't have an effect on conversion. + int keyCount = legacyRuleset.GetKeyCount(beatmap.Value.BeatmapInfo, mods.Value); + + firstStatistic = new StatisticDifficulty.Data(BeatmapsetsStrings.ShowStatsCsMania, keyCount, keyCount, 10); + break; + + default: + firstStatistic = new StatisticDifficulty.Data(BeatmapsetsStrings.ShowStatsCs, baseDifficulty.CircleSize, rateAdjustedDifficulty.CircleSize, 10); + break; + } + + difficultyStatisticsDisplay.Statistics = new[] + { + firstStatistic, + new StatisticDifficulty.Data(BeatmapsetsStrings.ShowStatsAccuracy, baseDifficulty.OverallDifficulty, rateAdjustedDifficulty.OverallDifficulty, 10), + new StatisticDifficulty.Data(BeatmapsetsStrings.ShowStatsDrain, baseDifficulty.DrainRate, rateAdjustedDifficulty.DrainRate, 10), + new StatisticDifficulty.Data(BeatmapsetsStrings.ShowStatsAr, baseDifficulty.ApproachRate, rateAdjustedDifficulty.ApproachRate, 10), + }; + }); + + private void updateStars() + { + starRatingDisplay.Current.Value = new StarDifficulty(displayedStars.Value, 0); + + Color4 colour = displayedStars.Value >= 6.5f ? colours.Orange1 : colours.ForStarDifficulty(displayedStars.Value); + difficultyText.FadeColour(colour, 300, Easing.OutQuint); + mappedByText.FadeColour(colour, 300, Easing.OutQuint); + countStatisticsDisplay.TransformTo(nameof(countStatisticsDisplay.AccentColour), colour, 300, Easing.OutQuint); + difficultyStatisticsDisplay.TransformTo(nameof(difficultyStatisticsDisplay.AccentColour), colour, 300, Easing.OutQuint); + } + + private void computeStarDifficulty(CancellationToken cancellationToken) + { + difficultyCache.GetDifficultyAsync(beatmap.Value.BeatmapInfo, ruleset.Value, mods.Value, cancellationToken) + .ContinueWith(task => + { + Schedule(() => + { + if (cancellationToken.IsCancellationRequested) + return; + + var result = task.GetResultSafely() ?? default; + displayedStars.Value = result.Stars; + }); + }, cancellationToken); + } + + protected override void Update() + { + base.Update(); + difficultyText.MaxWidth = Math.Max(nameLine.DrawWidth - mappedByText.DrawWidth - mapperText.DrawWidth - 20, 0); + } + + private partial class MapperLinkContainer : OsuHoverContainer + { + [BackgroundDependencyLoader] + private void load(OverlayColourProvider? overlayColourProvider, OsuColour colours) + { + TooltipText = ContextMenuStrings.ViewProfile; + IdleColour = overlayColourProvider?.Light2 ?? colours.Blue; + } + } + + private partial class AdjustableDifficultyStatisticsDisplay : DifficultyStatisticsDisplay, IHasCustomTooltip + { + [Resolved] + private OverlayColourProvider colourProvider { get; set; } = null!; + + public ITooltip GetCustomTooltip() => new AdjustedAttributesTooltip(colourProvider); + + public AdjustedAttributesTooltip.Data? TooltipContent { get; set; } + + public AdjustableDifficultyStatisticsDisplay(bool autoSize) + : base(autoSize) + { + } + } + } + } +} From 856f907c864ea2e728c665479db512c89018624a Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Fri, 18 Apr 2025 05:27:16 -0400 Subject: [PATCH 122/164] Add beatmap metadata wedge --- .../TestSceneBeatmapMetadataWedge.cs | 165 +++++++++ .../Screens/SelectV2/BeatmapMetadataWedge.cs | 337 ++++++++++++++++++ .../BeatmapMetadataWedge_FailRetryDisplay.cs | 195 ++++++++++ .../BeatmapMetadataWedge_MetadataDisplay.cs | 174 +++++++++ ...eatmapMetadataWedge_RatingSpreadDisplay.cs | 123 +++++++ ...BeatmapMetadataWedge_SuccessRateDisplay.cs | 112 ++++++ .../SelectV2/BeatmapMetadataWedge_TagsLine.cs | 223 ++++++++++++ .../BeatmapMetadataWedge_UserRatingDisplay.cs | 130 +++++++ 8 files changed, 1459 insertions(+) create mode 100644 osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapMetadataWedge.cs create mode 100644 osu.Game/Screens/SelectV2/BeatmapMetadataWedge.cs create mode 100644 osu.Game/Screens/SelectV2/BeatmapMetadataWedge_FailRetryDisplay.cs create mode 100644 osu.Game/Screens/SelectV2/BeatmapMetadataWedge_MetadataDisplay.cs create mode 100644 osu.Game/Screens/SelectV2/BeatmapMetadataWedge_RatingSpreadDisplay.cs create mode 100644 osu.Game/Screens/SelectV2/BeatmapMetadataWedge_SuccessRateDisplay.cs create mode 100644 osu.Game/Screens/SelectV2/BeatmapMetadataWedge_TagsLine.cs create mode 100644 osu.Game/Screens/SelectV2/BeatmapMetadataWedge_UserRatingDisplay.cs diff --git a/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapMetadataWedge.cs b/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapMetadataWedge.cs new file mode 100644 index 0000000000..769188eb71 --- /dev/null +++ b/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapMetadataWedge.cs @@ -0,0 +1,165 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using System.Linq; +using NUnit.Framework; +using osu.Framework.Graphics.Containers; +using osu.Game.Beatmaps; +using osu.Game.Models; +using osu.Game.Online.API; +using osu.Game.Online.API.Requests; +using osu.Game.Online.API.Requests.Responses; +using osu.Game.Screens.SelectV2; + +namespace osu.Game.Tests.Visual.SongSelectV2 +{ + public partial class TestSceneBeatmapMetadataWedge : SongSelectComponentsTestScene + { + private APIBeatmapSet? currentOnlineSet; + + protected override void LoadComplete() + { + base.LoadComplete(); + + ((DummyAPIAccess)API).HandleRequest = request => + { + switch (request) + { + case GetBeatmapSetRequest set: + if (set.ID == currentOnlineSet?.OnlineID) + { + set.TriggerSuccess(currentOnlineSet); + return true; + } + + return false; + + default: + return false; + } + }; + + Child = new BeatmapMetadataWedge + { + State = { Value = Visibility.Visible }, + }; + } + + [Test] + public void TestDisplay() + { + AddStep("null beatmap", () => Beatmap.SetDefault()); + AddStep("all metrics", () => + { + var (working, onlineSet) = createTestBeatmap(); + currentOnlineSet = onlineSet; + Beatmap.Value = working; + }); + AddStep("no source", () => + { + var (working, onlineSet) = createTestBeatmap(); + + working.Metadata.Source = string.Empty; + + currentOnlineSet = onlineSet; + Beatmap.Value = working; + }); + AddStep("no success rate", () => + { + var (working, onlineSet) = createTestBeatmap(); + + onlineSet.Beatmaps.Single().PlayCount = 0; + onlineSet.Beatmaps.Single().PassCount = 0; + + currentOnlineSet = onlineSet; + Beatmap.Value = working; + }); + AddStep("no user ratings", () => + { + var (working, onlineSet) = createTestBeatmap(); + + onlineSet.Ratings = Array.Empty(); + + currentOnlineSet = onlineSet; + Beatmap.Value = working; + }); + AddStep("no fail times", () => + { + var (working, onlineSet) = createTestBeatmap(); + + onlineSet.Beatmaps.Single().FailTimes = null; + + currentOnlineSet = onlineSet; + Beatmap.Value = working; + }); + AddStep("no metrics", () => + { + var (working, onlineSet) = createTestBeatmap(); + + onlineSet.Ratings = Array.Empty(); + onlineSet.Beatmaps.Single().FailTimes = null; + + currentOnlineSet = onlineSet; + Beatmap.Value = working; + }); + AddStep("local beatmap", () => + { + var (working, onlineSet) = createTestBeatmap(); + + working.BeatmapInfo.OnlineID = 0; + + currentOnlineSet = onlineSet; + Beatmap.Value = working; + }); + } + + [Test] + public void TestTruncation() + { + AddStep("long text", () => + { + var (working, onlineSet) = createTestBeatmap(); + + working.BeatmapInfo.Metadata.Author = new RealmUser { Username = "Verrrrryyyy llooonngggggg author" }; + working.BeatmapInfo.Metadata.Source = "Verrrrryyyy llooonngggggg source"; + working.BeatmapInfo.Metadata.Tags = string.Join(' ', Enumerable.Repeat(working.BeatmapInfo.Metadata.Tags, 3)); + onlineSet.Genre = new BeatmapSetOnlineGenre { Id = 12, Name = "Verrrrryyyy llooonngggggg genre" }; + onlineSet.Language = new BeatmapSetOnlineLanguage { Id = 12, Name = "Verrrrryyyy llooonngggggg language" }; + + currentOnlineSet = onlineSet; + Beatmap.Value = working; + }); + } + + private (WorkingBeatmap, APIBeatmapSet) createTestBeatmap() + { + var working = CreateWorkingBeatmap(Ruleset.Value); + var onlineSet = new APIBeatmapSet + { + OnlineID = working.BeatmapSetInfo.OnlineID, + Genre = new BeatmapSetOnlineGenre { Id = 15, Name = "Pop" }, + Language = new BeatmapSetOnlineLanguage { Id = 15, Name = "English" }, + Ratings = Enumerable.Range(0, 11).ToArray(), + Beatmaps = new[] + { + new APIBeatmap + { + OnlineID = working.BeatmapInfo.OnlineID, + PlayCount = 10000, + PassCount = 4567, + FailTimes = new APIFailTimes + { + Fails = Enumerable.Range(1, 100).Select(i => i % 12 - 6).ToArray(), + Retries = Enumerable.Range(-2, 100).Select(i => i % 12 - 6).ToArray(), + }, + }, + } + }; + + working.BeatmapSetInfo.DateSubmitted = DateTimeOffset.Now; + working.BeatmapSetInfo.DateRanked = DateTimeOffset.Now; + return (working, onlineSet); + } + } +} diff --git a/osu.Game/Screens/SelectV2/BeatmapMetadataWedge.cs b/osu.Game/Screens/SelectV2/BeatmapMetadataWedge.cs new file mode 100644 index 0000000000..a83ec51b11 --- /dev/null +++ b/osu.Game/Screens/SelectV2/BeatmapMetadataWedge.cs @@ -0,0 +1,337 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Linq; +using osu.Framework.Allocation; +using osu.Framework.Bindables; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Game.Beatmaps; +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.Online.Chat; +using osuTK; + +namespace osu.Game.Screens.SelectV2 +{ + public partial class BeatmapMetadataWedge : VisibilityContainer + { + private MetadataDisplay creator = null!; + private MetadataDisplay source = null!; + private MetadataDisplay genre = null!; + private MetadataDisplay language = null!; + private MetadataDisplay tag = null!; + private MetadataDisplay submitted = null!; + private MetadataDisplay ranked = null!; + + private Drawable ratingsWedge = null!; + private SuccessRateDisplay successRateDisplay = null!; + private UserRatingDisplay userRatingDisplay = null!; + private RatingSpreadDisplay ratingSpreadDisplay = null!; + + private Drawable failRetryWedge = null!; + private FailRetryDisplay failRetryDisplay = null!; + + [Resolved] + private IBindable beatmap { get; set; } = null!; + + [Resolved] + private IAPIProvider api { get; set; } = null!; + + private IBindable apiState = null!; + + [Resolved] + private ILinkHandler? linkHandler { get; set; } + + [Resolved] + private SongSelect? songSelect { get; set; } + + [BackgroundDependencyLoader] + private void load() + { + RelativeSizeAxes = Axes.X; + AutoSizeAxes = Axes.Y; + Padding = new MarginPadding { Top = 4f }; + + Width = 0.9f; + + InternalChild = new FillFlowContainer + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Direction = FillDirection.Vertical, + Spacing = new Vector2(0f, 4f), + Shear = OsuGame.SHEAR, + Children = new[] + { + new ShearAligningWrapper(new Container + { + CornerRadius = 10, + Masking = true, + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Children = new Drawable[] + { + new WedgeBackground(), + new Container + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Shear = -OsuGame.SHEAR, + Padding = new MarginPadding { Left = SongSelect.WEDGE_CONTENT_MARGIN, Right = 35, Vertical = 16 }, + Children = new Drawable[] + { + new FillFlowContainer + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Direction = FillDirection.Vertical, + Spacing = new Vector2(0f, 10f), + Children = new Drawable[] + { + new GridContainer + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + RowDimensions = new[] { new Dimension(GridSizeMode.AutoSize) }, + ColumnDimensions = new[] + { + new Dimension(), + new Dimension(), + new Dimension(), + }, + Content = new[] + { + new[] + { + new FillFlowContainer + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Direction = FillDirection.Vertical, + Spacing = new Vector2(0f, 10f), + Children = new[] + { + creator = new MetadataDisplay("Creator"), + genre = new MetadataDisplay("Genre"), + }, + }, + new FillFlowContainer + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Direction = FillDirection.Vertical, + Spacing = new Vector2(0f, 10f), + Children = new[] + { + source = new MetadataDisplay("Source"), + language = new MetadataDisplay("Language"), + }, + }, + new FillFlowContainer + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Direction = FillDirection.Vertical, + Spacing = new Vector2(0f, 10f), + Children = new[] + { + submitted = new MetadataDisplay("Submitted"), + ranked = new MetadataDisplay("Ranked"), + }, + }, + }, + }, + }, + tag = new MetadataDisplay("Tags"), + }, + }, + }, + }, + }, + }), + new ShearAligningWrapper(ratingsWedge = new Container + { + Alpha = 0f, + CornerRadius = 10, + Masking = true, + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Children = new Drawable[] + { + new WedgeBackground(), + new GridContainer + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Shear = -OsuGame.SHEAR, + RowDimensions = new[] { new Dimension(GridSizeMode.AutoSize) }, + ColumnDimensions = new[] + { + new Dimension(), + new Dimension(GridSizeMode.Absolute, 10), + new Dimension(), + new Dimension(GridSizeMode.Absolute, 10), + new Dimension(), + }, + Padding = new MarginPadding { Left = SongSelect.WEDGE_CONTENT_MARGIN, Right = 40f, Vertical = 16 }, + Content = new[] + { + new[] + { + successRateDisplay = new SuccessRateDisplay(), + Empty(), + userRatingDisplay = new UserRatingDisplay(), + Empty(), + ratingSpreadDisplay = new RatingSpreadDisplay(), + }, + }, + }, + } + }), + new ShearAligningWrapper(failRetryWedge = new Container + { + Alpha = 0f, + CornerRadius = 10, + Masking = true, + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Children = new Drawable[] + { + new WedgeBackground(), + new Container + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Shear = -OsuGame.SHEAR, + Padding = new MarginPadding { Left = SongSelect.WEDGE_CONTENT_MARGIN, Right = 40f, Vertical = 16 }, + Child = failRetryDisplay = new FailRetryDisplay(), + }, + }, + }), + } + }; + } + + protected override void LoadComplete() + { + base.LoadComplete(); + beatmap.BindValueChanged(_ => updateDisplay()); + + apiState = api.State.GetBoundCopy(); + apiState.BindValueChanged(_ => Scheduler.AddOnce(updateDisplay), true); + } + + protected override void PopIn() + { + this.FadeIn(300, Easing.OutQuint) + .MoveToX(0, 300, Easing.OutQuint); + } + + protected override void PopOut() + { + this.FadeOut(300, Easing.OutQuint) + .MoveToX(-100, 300, Easing.OutQuint); + } + + private void updateDisplay() + { + var metadata = beatmap.Value.Metadata; + var beatmapSetInfo = beatmap.Value.BeatmapSetInfo; + + creator.Data = (metadata.Author.Username, () => linkHandler?.HandleLink(new LinkDetails(LinkAction.OpenUserProfile, metadata.Author))); + + if (!string.IsNullOrEmpty(metadata.Source)) + source.Data = (metadata.Source, () => songSelect?.Search(metadata.Source)); + else + source.Data = ("-", null); + + tag.Tags = (metadata.Tags.Split(' '), t => songSelect?.Search(t)); + submitted.Date = beatmapSetInfo.DateSubmitted; + ranked.Date = beatmapSetInfo.DateRanked; + + if (currentOnlineBeatmapSet == null || currentOnlineBeatmapSet.OnlineID != beatmapSetInfo.OnlineID) + refetchBeatmapSet(); + + updateOnlineDisplay(); + } + + private APIBeatmapSet? currentOnlineBeatmapSet; + private GetBeatmapSetRequest? currentRequest; + + private void refetchBeatmapSet() + { + var beatmapSetInfo = beatmap.Value.BeatmapSetInfo; + + currentRequest?.Cancel(); + currentRequest = null; + currentOnlineBeatmapSet = null; + + if (beatmapSetInfo.OnlineID >= 1) + { + // todo: consider introducing a BeatmapSetLookupCache for caching benefits. + currentRequest = new GetBeatmapSetRequest(beatmapSetInfo.OnlineID); + currentRequest.Failure += _ => updateOnlineDisplay(); + currentRequest.Success += s => + { + currentOnlineBeatmapSet = s; + updateOnlineDisplay(); + }; + + api.Queue(currentRequest); + } + } + + private void updateOnlineDisplay() + { + if (currentRequest?.CompletionState == APIRequestCompletionState.Waiting) + { + genre.Data = null; + language.Data = null; + } + else if (currentOnlineBeatmapSet == null) + { + genre.Data = ("-", null); + language.Data = ("-", null); + + ratingsWedge.FadeOut(300, Easing.OutQuint); + ratingsWedge.MoveToX(-50, 300, Easing.OutQuint); + failRetryWedge.FadeOut(300, Easing.OutQuint); + failRetryWedge.MoveToX(-50, 300, Easing.OutQuint); + } + else + { + var beatmapInfo = beatmap.Value.BeatmapInfo; + + var onlineBeatmapSet = currentOnlineBeatmapSet; + var onlineBeatmap = onlineBeatmapSet.Beatmaps.SingleOrDefault(b => b.OnlineID == beatmapInfo.OnlineID); + + genre.Data = (onlineBeatmapSet.Genre.Name, () => songSelect?.Search(onlineBeatmapSet.Genre.Name)); + language.Data = (onlineBeatmapSet.Language.Name, () => songSelect?.Search(onlineBeatmapSet.Language.Name)); + + if (onlineBeatmap != null) + { + ratingsWedge.FadeIn(300, Easing.OutQuint); + ratingsWedge.MoveToX(0, 300, Easing.OutQuint); + failRetryWedge.FadeIn(300, Easing.OutQuint); + failRetryWedge.MoveToX(0, 300, Easing.OutQuint); + + userRatingDisplay.Data = onlineBeatmapSet.Ratings; + ratingSpreadDisplay.Data = onlineBeatmapSet.Ratings; + successRateDisplay.Data = (onlineBeatmap.PassCount, onlineBeatmap.PlayCount); + failRetryDisplay.Data = onlineBeatmap.FailTimes ?? new APIFailTimes(); + } + else + { + ratingsWedge.FadeOut(300, Easing.OutQuint); + ratingsWedge.MoveToX(-50, 300, Easing.OutQuint); + failRetryWedge.FadeOut(300, Easing.OutQuint); + failRetryWedge.MoveToX(-50, 300, Easing.OutQuint); + } + } + } + } +} diff --git a/osu.Game/Screens/SelectV2/BeatmapMetadataWedge_FailRetryDisplay.cs b/osu.Game/Screens/SelectV2/BeatmapMetadataWedge_FailRetryDisplay.cs new file mode 100644 index 0000000000..048ec3c40d --- /dev/null +++ b/osu.Game/Screens/SelectV2/BeatmapMetadataWedge_FailRetryDisplay.cs @@ -0,0 +1,195 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using System.Linq; +using osu.Framework.Allocation; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Primitives; +using osu.Framework.Graphics.Rendering; +using osu.Framework.Utils; +using osu.Game.Beatmaps; +using osu.Game.Graphics; +using osu.Game.Graphics.Sprites; +using osu.Game.Resources.Localisation.Web; +using osuTK; + +namespace osu.Game.Screens.SelectV2 +{ + public partial class BeatmapMetadataWedge + { + private partial class FailRetryDisplay : CompositeDrawable + { + private readonly GraphDrawable retriesGraph; + private readonly GraphDrawable failsGraph; + + public APIFailTimes Data + { + set + { + int[] retries = value.Retries ?? Array.Empty(); + int[] fails = value.Fails ?? Array.Empty(); + int[] total = retries.Zip(fails, (r, f) => r + f).ToArray(); + + int maximum = total.DefaultIfEmpty(0).Max(); + + retriesGraph.Data = total.Select(r => maximum == 0 ? 0 : (float)r / maximum).ToArray(); + failsGraph.Data = fails.Select(r => maximum == 0 ? 0 : (float)r / maximum).ToArray(); + } + } + + public FailRetryDisplay() + { + RelativeSizeAxes = Axes.X; + AutoSizeAxes = Axes.Y; + + InternalChild = new FillFlowContainer + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Direction = FillDirection.Vertical, + Spacing = new Vector2(0f, 4f), + Children = new Drawable[] + { + new OsuSpriteText + { + Text = BeatmapsetsStrings.ShowInfoPointsOfFailure, + Font = OsuFont.Style.Caption1.With(weight: FontWeight.SemiBold), + Margin = new MarginPadding { Bottom = 4f }, + }, + new Container + { + RelativeSizeAxes = Axes.X, + Height = 65f, + Children = new[] + { + retriesGraph = new GraphDrawable { RelativeSizeAxes = Axes.Both, Y = -1f }, + failsGraph = new GraphDrawable { RelativeSizeAxes = Axes.Both }, + }, + }, + }, + }; + } + + [BackgroundDependencyLoader] + private void load(OsuColour colours) + { + retriesGraph.Colour = colours.Orange1; + failsGraph.Colour = colours.DarkOrange2; + } + + private partial class GraphDrawable : Drawable + { + private readonly float[] displayedData = new float[100]; + + private float[] data = new float[100]; + + public float[] Data + { + get => data; + set + { + data = value; + Invalidate(Invalidation.DrawNode); + } + } + + protected override void Update() + { + base.Update(); + + bool changed = false; + + for (int i = 0; i < displayedData.Length; i++) + { + float before = displayedData[i]; + float value = data.ElementAtOrDefault(i); + displayedData[i] = (float)Interpolation.DampContinuously(displayedData[i], value, 40, Time.Elapsed); + changed |= displayedData[i] != before; + } + + if (changed) + Invalidate(Invalidation.DrawNode); + } + + protected override DrawNode CreateDrawNode() => new GraphDrawNode(this); + + // todo: consider integrating this with BarGraph + // this is different from BarGraph since this displays each bar with corner radii applied. + private class GraphDrawNode : DrawNode + { + private readonly GraphDrawable source; + + private Vector2 drawSize; + private float[] displayedData = null!; + + public GraphDrawNode(GraphDrawable source) + : base(source) + { + this.source = source; + } + + public override void ApplyState() + { + base.ApplyState(); + + drawSize = source.DrawSize; + displayedData = source.displayedData; + } + + protected override void Draw(IRenderer renderer) + { + base.Draw(renderer); + + const float spacing_constant = 1.5f; + + float position = 0; + float barWidth = drawSize.X / displayedData.Length / spacing_constant; + + float totalSpacing = drawSize.X - barWidth * displayedData.Length; + float spacing = totalSpacing / (displayedData.Length - 1); + + for (int i = 0; i < displayedData.Length; i++) + { + float barHeight = MathF.Max(drawSize.Y * displayedData[i], barWidth); + + drawBar(renderer, position, barWidth, barHeight); + + position += barWidth + spacing; + } + } + + private void drawBar(IRenderer renderer, float position, float width, float height) + { + float cornerRadius = width / 2f; + + Vector3 scale = DrawInfo.MatrixInverse.ExtractScale(); + float blendRange = (scale.X + scale.Y) / 2; + + RectangleF drawRectangle = new RectangleF(new Vector2(position, drawSize.Y - height), new Vector2(width, height)); + Quad screenSpaceDrawQuad = Quad.FromRectangle(drawRectangle) * DrawInfo.Matrix; + + renderer.PushMaskingInfo(new MaskingInfo + { + ScreenSpaceAABB = screenSpaceDrawQuad.AABB, + MaskingRect = drawRectangle.Normalize(), + ConservativeScreenSpaceQuad = screenSpaceDrawQuad, + ToMaskingSpace = DrawInfo.MatrixInverse, + CornerRadius = cornerRadius, + CornerExponent = 2f, + // We are setting the linear blend range to the approximate size of a _pixel_ here. + // This results in the optimal trade-off between crispness and smoothness of the + // edges of the masked region according to sampling theory. + BlendRange = blendRange, + AlphaExponent = 1, + }); + + renderer.DrawQuad(renderer.WhitePixel, screenSpaceDrawQuad, DrawColourInfo.Colour); + renderer.PopMaskingInfo(); + } + } + } + } + } +} diff --git a/osu.Game/Screens/SelectV2/BeatmapMetadataWedge_MetadataDisplay.cs b/osu.Game/Screens/SelectV2/BeatmapMetadataWedge_MetadataDisplay.cs new file mode 100644 index 0000000000..897349b9cb --- /dev/null +++ b/osu.Game/Screens/SelectV2/BeatmapMetadataWedge_MetadataDisplay.cs @@ -0,0 +1,174 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using osu.Framework.Allocation; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Localisation; +using osu.Game.Graphics; +using osu.Game.Graphics.Containers; +using osu.Game.Graphics.Sprites; +using osu.Game.Graphics.UserInterface; +using osu.Game.Overlays; +using osuTK; + +namespace osu.Game.Screens.SelectV2 +{ + public partial class BeatmapMetadataWedge + { + private partial class MetadataDisplay : FillFlowContainer + { + private readonly OsuSpriteText labelText; + private readonly OsuSpriteText contentText; + private readonly OsuSpriteText contentLinkText; + private readonly OsuHoverContainer contentLink; + private readonly DrawableDate contentDate; + private readonly TagsLine contentTags; + private readonly LoadingSpinner contentLoading; + + private (LocalisableString value, Action? linkAction)? data; + + public (LocalisableString value, Action? linkAction)? Data + { + get => data; + set + { + data = value; + + if (value?.linkAction != null) + setLink(value.Value.value, value.Value.linkAction); + else if (value.HasValue) + setText(value.Value.value); + else + setLoading(); + } + } + + public DateTimeOffset? Date + { + set + { + if (value != null) + setDate(value.Value); + else + setText("-"); + } + } + + public (string[] tags, Action linkAction) Tags + { + set => setTags(value.tags, value.linkAction); + } + + public MetadataDisplay(LocalisableString label) + { + RelativeSizeAxes = Axes.X; + AutoSizeAxes = Axes.Y; + + Padding = new MarginPadding { Right = 10 }; + + InternalChildren = new Drawable[] + { + labelText = new OsuSpriteText + { + Text = label, + Font = OsuFont.Style.Caption1.With(weight: FontWeight.SemiBold), + }, + new Container + { + RelativeSizeAxes = Axes.X, + Height = OsuFont.Style.Caption1.Size, + Children = new Drawable[] + { + contentText = new TruncatingSpriteText + { + RelativeSizeAxes = Axes.X, + Font = OsuFont.Style.Caption1, + }, + contentLink = new OsuHoverContainer + { + AutoSizeAxes = Axes.Both, + Child = contentLinkText = new TruncatingSpriteText + { + Font = OsuFont.Style.Caption1, + }, + }, + contentDate = new DrawableDate(default, OsuFont.Style.Caption1.Size, false), + contentTags = new TagsLine(), + contentLoading = new LoadingSpinner + { + Anchor = Anchor.TopLeft, + Origin = Anchor.TopLeft, + Size = new Vector2(10), + Margin = new MarginPadding { Top = 3f }, + State = { Value = Visibility.Visible }, + } + }, + }, + }; + } + + [BackgroundDependencyLoader] + private void load(OverlayColourProvider colourProvider) + { + labelText.Colour = colourProvider.Content1; + contentText.Colour = colourProvider.Content2; + contentLink.IdleColour = colourProvider.Light2; + } + + protected override void Update() + { + base.Update(); + contentLinkText.MaxWidth = ChildSize.X; + } + + private void clear() + { + contentText.Text = string.Empty; + contentLinkText.Text = string.Empty; + contentDate.Hide(); + contentTags.Tags = Array.Empty(); + contentLoading.Hide(); + } + + private void setText(LocalisableString text) + { + clear(); + + contentText.Text = text; + } + + private void setLink(LocalisableString text, Action action) => Schedule(() => + { + clear(); + + contentLinkText.Text = text; + contentLink.Action = action; + }); + + private void setDate(DateTimeOffset date) + { + clear(); + + contentDate.Show(); + contentDate.Date = date; + } + + private void setTags(string[] tags, Action link) + { + clear(); + + contentTags.Tags = tags; + contentTags.Action = link; + } + + private void setLoading() + { + clear(); + + contentLoading.Show(); + } + } + } +} diff --git a/osu.Game/Screens/SelectV2/BeatmapMetadataWedge_RatingSpreadDisplay.cs b/osu.Game/Screens/SelectV2/BeatmapMetadataWedge_RatingSpreadDisplay.cs new file mode 100644 index 0000000000..ee938ecdd9 --- /dev/null +++ b/osu.Game/Screens/SelectV2/BeatmapMetadataWedge_RatingSpreadDisplay.cs @@ -0,0 +1,123 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Linq; +using osu.Framework.Allocation; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Colour; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; +using osu.Framework.Utils; +using osu.Game.Graphics; +using osu.Game.Graphics.Sprites; +using osu.Game.Resources.Localisation.Web; +using osuTK; + +namespace osu.Game.Screens.SelectV2 +{ + public partial class BeatmapMetadataWedge + { + private partial class RatingSpreadDisplay : CompositeDrawable + { + private const float min_height = 4f; + private const float max_height = 32f; + + private const int rating_range = 10; + + private readonly GraphBar[] graph; + + public int[] Data + { + set + { + if (!value.Any()) + { + foreach (var bar in graph) + bar.ResizeHeightTo(min_height, 300, Easing.OutQuint); + } + else + { + var usableRange = value.Skip(1).Take(rating_range); // adjust for API returning weird empty data at 0. + int maxRating = usableRange.Max(); + + for (int i = 0; i < graph.Length; i++) + graph[i].ResizeHeightTo(min_height + (max_height - min_height) * (maxRating == 0 ? 0 : usableRange.ElementAt(i) / (float)maxRating), 300, Easing.OutQuint); + } + } + } + + public RatingSpreadDisplay() + { + RelativeSizeAxes = Axes.X; + AutoSizeAxes = Axes.Y; + + graph = Enumerable.Range(0, rating_range).Select(_ => new GraphBar()).ToArray(); + + InternalChildren = new[] + { + new FillFlowContainer + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Direction = FillDirection.Vertical, + Spacing = new Vector2(0f, 1f), + Children = new Drawable[] + { + new OsuSpriteText + { + Text = BeatmapsetsStrings.ShowStatsRatingSpread, + Font = OsuFont.Style.Caption1.With(weight: FontWeight.SemiBold), + }, + new GridContainer + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + RowDimensions = new[] { new Dimension(GridSizeMode.Absolute, max_height) }, + ColumnDimensions = graph.SkipLast(1).Select(_ => new[] + { + new Dimension(), + new Dimension(GridSizeMode.Absolute, 1f), + }).SelectMany(d => d).Append(new Dimension()).ToArray(), + Content = new[] + { + graph.SkipLast(1).Select(g => new[] + { + g, + Empty() + }).SelectMany(g => g).Append(graph[^1]).ToArray() + }, + } + }, + } + }; + } + + [BackgroundDependencyLoader] + private void load(OsuColour colours) + { + for (int i = 0; i < 10; i++) + { + var left = Interpolation.ValueAt(i, colours.Blue4, colours.Blue0, 0, 10); + var right = Interpolation.ValueAt(i + 1, colours.Blue4, colours.Blue0, 0, 10); + graph[i].Colour = ColourInfo.GradientHorizontal(left, right); + } + } + + private partial class GraphBar : CompositeDrawable + { + [BackgroundDependencyLoader] + private void load() + { + Anchor = Anchor.BottomLeft; + Origin = Anchor.BottomLeft; + + RelativeSizeAxes = Axes.X; + CornerRadius = 2f; + Masking = true; + + InternalChild = new Box { RelativeSizeAxes = Axes.Both }; + } + } + } + } +} diff --git a/osu.Game/Screens/SelectV2/BeatmapMetadataWedge_SuccessRateDisplay.cs b/osu.Game/Screens/SelectV2/BeatmapMetadataWedge_SuccessRateDisplay.cs new file mode 100644 index 0000000000..6118547274 --- /dev/null +++ b/osu.Game/Screens/SelectV2/BeatmapMetadataWedge_SuccessRateDisplay.cs @@ -0,0 +1,112 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using osu.Framework.Allocation; +using osu.Framework.Extensions.LocalisationExtensions; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Cursor; +using osu.Framework.Graphics.Shapes; +using osu.Framework.Localisation; +using osu.Game.Graphics; +using osu.Game.Graphics.Sprites; +using osu.Game.Overlays; +using osu.Game.Resources.Localisation.Web; +using osuTK; + +namespace osu.Game.Screens.SelectV2 +{ + public partial class BeatmapMetadataWedge + { + private partial class SuccessRateDisplay : CompositeDrawable, IHasTooltip + { + private readonly OsuSpriteText valueText; + private readonly Circle backgroundBar; + private readonly Circle valueBar; + + private (int passes, int plays) data; + + public (int passes, int plays) Data + { + get => data; + set + { + data = value; + + float ratio = value.plays == 0 ? 0 : (float)value.passes / value.plays; + + valueText.Text = ratio.ToLocalisableString(@"0.##%"); + valueText.MoveToX(Math.Clamp(ratio, 0.05f, 0.95f), 300, Easing.OutQuint); + valueBar.ResizeWidthTo(ratio, 300, Easing.OutQuint); + } + } + + public LocalisableString TooltipText => $"{data.passes:N0} / {data.plays:N0}"; + + public SuccessRateDisplay() + { + RelativeSizeAxes = Axes.X; + AutoSizeAxes = Axes.Y; + + InternalChildren = new[] + { + new FillFlowContainer + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Direction = FillDirection.Vertical, + Spacing = new Vector2(0f, 2f), + Children = new Drawable[] + { + new OsuSpriteText + { + Text = BeatmapsetsStrings.ShowInfoSuccessRate, + Font = OsuFont.Style.Caption1.With(weight: FontWeight.SemiBold), + }, + new Container + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Margin = new MarginPadding { Top = 10f }, + Child = valueText = new OsuSpriteText + { + Origin = Anchor.TopCentre, + RelativePositionAxes = Axes.X, + Font = OsuFont.Style.Caption1, + } + }, + new Container + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Children = new[] + { + backgroundBar = new Circle + { + RelativeSizeAxes = Axes.X, + Height = 4f, + }, + valueBar = new Circle + { + RelativeSizeAxes = Axes.X, + Width = 0f, + Height = 4f, + }, + }, + } + }, + } + }; + } + + [BackgroundDependencyLoader] + private void load(OsuColour colours, OverlayColourProvider colourProvider) + { + backgroundBar.Colour = colourProvider.Background6; + valueBar.Colour = colours.Lime1; + valueText.Colour = colourProvider.Content2; + } + } + } +} diff --git a/osu.Game/Screens/SelectV2/BeatmapMetadataWedge_TagsLine.cs b/osu.Game/Screens/SelectV2/BeatmapMetadataWedge_TagsLine.cs new file mode 100644 index 0000000000..56b83a2578 --- /dev/null +++ b/osu.Game/Screens/SelectV2/BeatmapMetadataWedge_TagsLine.cs @@ -0,0 +1,223 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using System.Diagnostics; +using System.Linq; +using osu.Framework.Allocation; +using osu.Framework.Extensions; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Cursor; +using osu.Framework.Graphics.Shapes; +using osu.Framework.Graphics.Sprites; +using osu.Framework.Graphics.UserInterface; +using osu.Framework.Input.Events; +using osu.Framework.Layout; +using osu.Game.Graphics; +using osu.Game.Graphics.Containers; +using osu.Game.Graphics.Sprites; +using osu.Game.Graphics.UserInterfaceV2; +using osu.Game.Overlays; +using osuTK; + +namespace osu.Game.Screens.SelectV2 +{ + public partial class BeatmapMetadataWedge + { + private partial class TagsLine : FillFlowContainer + { + private readonly LayoutValue drawSizeLayout = new LayoutValue(Invalidation.DrawSize); + + private string[] tags = Array.Empty(); + + private TagsOverflowButton? overflowButton; + + public string[] Tags + { + get => tags; + set + { + tags = value; + updateTags(); + } + } + + public Action? Action; + + [Resolved] + private OverlayColourProvider colourProvider { get; set; } = null!; + + public TagsLine() + { + RelativeSizeAxes = Axes.X; + AutoSizeAxes = Axes.Y; + Direction = FillDirection.Horizontal; + Spacing = new Vector2(4, 0); + + AddLayout(drawSizeLayout); + } + + protected override void UpdateAfterChildren() + { + base.UpdateAfterChildren(); + + if (!drawSizeLayout.IsValid) + { + updateLayout(); + drawSizeLayout.Validate(); + } + } + + private void updateLayout() + { + if (tags.Length == 0) + return; + + Debug.Assert(overflowButton != null); + + float limit = DrawWidth - overflowButton.DrawWidth - 5; + bool showOverflow = false; + + foreach (var text in Children) + { + if (text.X + text.DrawWidth < limit) + text.Show(); + else + { + showOverflow = true; + text.AlwaysPresent = false; + text.Hide(); + } + } + + if (showOverflow) + overflowButton.Show(); + else + overflowButton.Hide(); + } + + private void updateTags() + { + ChildrenEnumerable = tags.Select(t => new OsuHoverContainer + { + AutoSizeAxes = Axes.Both, + Action = () => Action?.Invoke(t), + IdleColour = colourProvider.Light2, + AlwaysPresent = true, + Alpha = 0f, + Child = new OsuSpriteText + { + Text = t, + Font = OsuFont.Style.Caption1, + }, + }); + + Add(overflowButton = new TagsOverflowButton(tags) + { + Alpha = 0f, + }); + + drawSizeLayout.Invalidate(); + } + + private partial class TagsOverflowButton : CompositeDrawable, IHasPopover, IHasLineBaseHeight + { + private readonly string[] tags; + + private Box box = null!; + private OsuSpriteText text = null!; + + [Resolved] + private OverlayColourProvider colourProvider { get; set; } = null!; + + [Resolved] + private SongSelect? songSelect { get; set; } + + public float LineBaseHeight => text.LineBaseHeight; + + public TagsOverflowButton(string[] tags) + { + this.tags = tags; + } + + [BackgroundDependencyLoader] + private void load() + { + Size = new Vector2(OsuFont.Style.Caption1.Size); + CornerRadius = 1.5f; + Masking = true; + + InternalChildren = new Drawable[] + { + box = new Box + { + Colour = colourProvider.Light1, + RelativeSizeAxes = Axes.Both, + }, + text = new OsuSpriteText + { + Y = -2, + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre, + Text = "...", + Colour = colourProvider.Background4, + Font = OsuFont.Style.Caption1.With(weight: FontWeight.Bold), + } + }; + } + + protected override bool OnHover(HoverEvent e) + { + box.FadeColour(colourProvider.Content2, 300, Easing.OutQuint); + return base.OnHover(e); + } + + protected override void OnHoverLost(HoverLostEvent e) + { + box.FadeColour(colourProvider.Light1, 300, Easing.OutQuint); + base.OnHoverLost(e); + } + + protected override bool OnClick(ClickEvent e) + { + box.FlashColour(colourProvider.Content1, 300, Easing.OutQuint); + this.ShowPopover(); + return true; + } + + public Popover GetPopover() => new TagsOverflowPopover(tags, songSelect); + } + + public partial class TagsOverflowPopover : OsuPopover + { + private readonly string[] tags; + private readonly SongSelect? songSelect; + + public TagsOverflowPopover(string[] tags, SongSelect? songSelect) + { + this.tags = tags; + this.songSelect = songSelect; + } + + [BackgroundDependencyLoader] + private void load() + { + LinkFlowContainer textFlow; + + Child = textFlow = new LinkFlowContainer(t => t.Font = OsuFont.Style.Caption1) + { + Width = 200, + AutoSizeAxes = Axes.Y, + }; + + foreach (string tag in tags) + { + textFlow.AddLink(tag, () => songSelect?.Search(tag)); + textFlow.AddText(" "); + } + } + } + } + } +} diff --git a/osu.Game/Screens/SelectV2/BeatmapMetadataWedge_UserRatingDisplay.cs b/osu.Game/Screens/SelectV2/BeatmapMetadataWedge_UserRatingDisplay.cs new file mode 100644 index 0000000000..2f38079577 --- /dev/null +++ b/osu.Game/Screens/SelectV2/BeatmapMetadataWedge_UserRatingDisplay.cs @@ -0,0 +1,130 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Linq; +using osu.Framework.Allocation; +using osu.Framework.Extensions.LocalisationExtensions; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; +using osu.Game.Graphics; +using osu.Game.Graphics.Sprites; +using osu.Game.Overlays; +using osu.Game.Resources.Localisation.Web; +using osuTK; + +namespace osu.Game.Screens.SelectV2 +{ + public partial class BeatmapMetadataWedge + { + private partial class UserRatingDisplay : CompositeDrawable + { + private readonly OsuSpriteText negativeText; + private readonly OsuSpriteText positiveText; + private readonly Circle backgroundBar; + private readonly Circle positiveBar; + + public int[] Data + { + set + { + const int rating_range = 10; + + if (!value.Any()) + { + negativeText.Text = 0.ToLocalisableString(@"N0"); + positiveText.Text = 0.ToLocalisableString(@"N0"); + positiveBar.ResizeWidthTo(0, 300, Easing.OutQuint); + } + else + { + var usableRange = value.Skip(1).Take(rating_range); // adjust for API returning weird empty data at 0. + + int positiveCount = usableRange.Skip(rating_range / 2).Sum(); + int totalCount = usableRange.Sum(); + + negativeText.Text = (totalCount - positiveCount).ToLocalisableString(@"N0"); + positiveText.Text = positiveCount.ToLocalisableString(@"N0"); + positiveBar.ResizeWidthTo(totalCount == 0 ? 0 : (float)positiveCount / totalCount, 300, Easing.OutQuint); + } + } + } + + public UserRatingDisplay() + { + RelativeSizeAxes = Axes.X; + AutoSizeAxes = Axes.Y; + + InternalChildren = new[] + { + new FillFlowContainer + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Direction = FillDirection.Vertical, + Spacing = new Vector2(0f, 2f), + Children = new Drawable[] + { + new OsuSpriteText + { + Text = BeatmapsetsStrings.ShowStatsUserRating, + Font = OsuFont.Style.Caption1.With(weight: FontWeight.SemiBold), + }, + new Container + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Margin = new MarginPadding { Top = 10f }, + Children = new[] + { + negativeText = new OsuSpriteText + { + Anchor = Anchor.TopLeft, + Origin = Anchor.TopLeft, + Font = OsuFont.Style.Caption1, + }, + positiveText = new OsuSpriteText + { + Anchor = Anchor.TopRight, + Origin = Anchor.TopRight, + Font = OsuFont.Style.Caption1, + }, + }, + }, + new Container + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Children = new[] + { + backgroundBar = new Circle + { + RelativeSizeAxes = Axes.X, + Height = 4f, + }, + positiveBar = new Circle + { + Anchor = Anchor.TopRight, + Origin = Anchor.TopRight, + RelativeSizeAxes = Axes.X, + Width = 0f, + Height = 4f, + }, + }, + } + }, + } + }; + } + + [BackgroundDependencyLoader] + private void load(OsuColour colours, OverlayColourProvider colourProvider) + { + backgroundBar.Colour = colours.DarkOrange2; + positiveBar.Colour = colours.Lime1; + negativeText.Colour = colourProvider.Content2; + positiveText.Colour = colourProvider.Content2; + } + } + } +} From 5791375b38bb16838e897f8935c4564661425cdf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 18 Apr 2025 14:03:32 +0200 Subject: [PATCH 123/164] Fix rate adjust no longer showing the rate if custom "Accidentally" removed in 6e635f124aee13d3d95d26ba10a08c321360ceb7 apparently. --- osu.Game/Rulesets/Mods/ModRateAdjust.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/osu.Game/Rulesets/Mods/ModRateAdjust.cs b/osu.Game/Rulesets/Mods/ModRateAdjust.cs index 358034541c..a824731830 100644 --- a/osu.Game/Rulesets/Mods/ModRateAdjust.cs +++ b/osu.Game/Rulesets/Mods/ModRateAdjust.cs @@ -34,5 +34,7 @@ namespace osu.Game.Rulesets.Mods yield return ("Speed change", $"{SpeedChange.Value:N2}x"); } } + + public override string ExtendedIconInformation => SpeedChange.IsDefault ? string.Empty : FormattableString.Invariant($"{SpeedChange.Value:N2}x"); } } From 20b2cc8251b7ad468a7e9d9b00b494822bd54b66 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 18 Apr 2025 14:21:43 +0200 Subject: [PATCH 124/164] Add failing test coverage --- osu.Game.Tests/Visual/Gameplay/TestSceneStoryboard.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneStoryboard.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneStoryboard.cs index 5da60966b2..4b90bec784 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneStoryboard.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneStoryboard.cs @@ -2,12 +2,14 @@ // 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.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; +using osu.Framework.Testing; using osu.Framework.Timing; using osu.Framework.Utils; using osu.Game.Beatmaps; @@ -48,7 +50,7 @@ namespace osu.Game.Tests.Visual.Gameplay } [Test] - public void TestVideoSize() + public void TestVideo() { AddStep("load storyboard with only video", () => { @@ -56,6 +58,7 @@ namespace osu.Game.Tests.Visual.Gameplay loadStoryboard("storyboard_only_video.osu", s => s.Beatmap.WidescreenStoryboard = false); }); + AddAssert("storyboard video present in hierarchy", () => this.ChildrenOfType().Any()); AddAssert("storyboard is correct width", () => Precision.AlmostEquals(storyboard?.Width ?? 0f, 480 * 16 / 9f)); } From 2761ee005dafb9f2f5eea1c5a958e2c1cdb64bb8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 18 Apr 2025 14:23:43 +0200 Subject: [PATCH 125/164] Fix storyboard videos not displaying Regressed with 102085668f84bd80f1717f101adc22fc7075e7fa because the stupid magic alpha transform addition was also implicitly changing the value of `IsDrawable` from false to true because that property checks for presence of any commands. Apparently past me, in his infinite wisdom, did not decide it pertinent to test that change against, you know, *a beatmap with a storyboard*. Great job, past me, good show all around. --- osu.Game/Storyboards/StoryboardSprite.cs | 2 +- osu.Game/Storyboards/StoryboardVideo.cs | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/osu.Game/Storyboards/StoryboardSprite.cs b/osu.Game/Storyboards/StoryboardSprite.cs index e10edfefe1..5b3e7c3919 100644 --- a/osu.Game/Storyboards/StoryboardSprite.cs +++ b/osu.Game/Storyboards/StoryboardSprite.cs @@ -17,7 +17,7 @@ namespace osu.Game.Storyboards private readonly List triggerGroups = new List(); public string Path { get; } - public bool IsDrawable => HasCommands; + public virtual bool IsDrawable => HasCommands; public Anchor Origin; public Vector2 InitialPosition; diff --git a/osu.Game/Storyboards/StoryboardVideo.cs b/osu.Game/Storyboards/StoryboardVideo.cs index fb4ac56e98..5a9eb533c6 100644 --- a/osu.Game/Storyboards/StoryboardVideo.cs +++ b/osu.Game/Storyboards/StoryboardVideo.cs @@ -19,6 +19,8 @@ namespace osu.Game.Storyboards public override double StartTime { get; } + public override bool IsDrawable => true; + public override Drawable CreateDrawable() => new DrawableStoryboardVideo(this); } } From c29f59fcdb964288ec988eb7c41e74771848bdc5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 18 Apr 2025 20:02:34 +0200 Subject: [PATCH 126/164] Fix gameplay leaderboard showing scores from wrong beatmaps Kind of a big oversight this. In wanting to get the leaderboard refactors to move forward I sort of didn't realise the fact that all of the error handling related to online status and such in `BeatmapLeaderboard` kind of... can't stay there... It's also an all-or-nothing business too - moving this stuff can't really be done only in part. Not sure whether tests are warranted if it's more or less moving logic across? --- .../Online/Leaderboards/LeaderboardManager.cs | 55 +++++++++++++++++-- .../Online/Leaderboards/LeaderboardState.cs | 15 ++--- .../Select/Leaderboards/BeatmapLeaderboard.cs | 49 ++--------------- 3 files changed, 63 insertions(+), 56 deletions(-) diff --git a/osu.Game/Online/Leaderboards/LeaderboardManager.cs b/osu.Game/Online/Leaderboards/LeaderboardManager.cs index ff3fe39a96..6629781d2c 100644 --- a/osu.Game/Online/Leaderboards/LeaderboardManager.cs +++ b/osu.Game/Online/Leaderboards/LeaderboardManager.cs @@ -54,6 +54,9 @@ namespace osu.Game.Online.Leaderboards lastFetchCompletionSource?.TrySetCanceled(); scores.Value = null; + if (newCriteria.Beatmap == null || newCriteria.Ruleset == null) + return Task.FromResult(scores.Value = LeaderboardScores.Failure(LeaderboardFailState.NoneSelected)); + switch (newCriteria.Scope) { case BeatmapLeaderboardScope.Local: @@ -72,6 +75,21 @@ namespace osu.Game.Online.Leaderboards default: { + if (!api.IsLoggedIn) + return Task.FromResult(scores.Value = LeaderboardScores.Failure(LeaderboardFailState.NotLoggedIn)); + + if (!newCriteria.Ruleset.IsLegacyRuleset()) + return Task.FromResult(scores.Value = LeaderboardScores.Failure(LeaderboardFailState.RulesetUnavailable)); + + if (newCriteria.Beatmap.OnlineID <= 0 || newCriteria.Beatmap.Status <= BeatmapOnlineStatus.Pending) + return Task.FromResult(scores.Value = LeaderboardScores.Failure(LeaderboardFailState.BeatmapUnavailable)); + + if ((newCriteria.Scope.RequiresSupporter(newCriteria.ExactMods != null)) && !api.LocalUser.Value.IsSupporter) + return Task.FromResult(scores.Value = LeaderboardScores.Failure(LeaderboardFailState.NotSupporter)); + + if (newCriteria.Scope == BeatmapLeaderboardScope.Team && api.LocalUser.Value.Team == null) + return Task.FromResult(scores.Value = LeaderboardScores.Failure(LeaderboardFailState.NoTeam)); + var onlineFetchCompletionSource = new TaskCompletionSource(); lastFetchCompletionSource = onlineFetchCompletionSource; @@ -92,7 +110,7 @@ namespace osu.Game.Online.Leaderboards if (inFlightOnlineRequest != null && !newRequest.Equals(inFlightOnlineRequest)) return; - var result = new LeaderboardScores + var result = LeaderboardScores.Success ( response.Scores.Select(s => s.ToScoreInfo(rulesets, newCriteria.Beatmap)).OrderByTotalScore(), response.UserScore?.CreateScoreInfo(rulesets, newCriteria.Beatmap) @@ -101,7 +119,7 @@ namespace osu.Game.Online.Leaderboards if (onlineFetchCompletionSource.TrySetResult(result)) scores.Value = result; }; - newRequest.Failure += ex => onlineFetchCompletionSource.TrySetException(ex); + newRequest.Failure += _ => onlineFetchCompletionSource.TrySetResult(LeaderboardScores.Failure(LeaderboardFailState.NetworkFailure)); api.Queue(inFlightOnlineRequest = newRequest); return onlineFetchCompletionSource.Task; } @@ -138,7 +156,7 @@ namespace osu.Game.Online.Leaderboards newScores = newScores.Detach().OrderByTotalScore(); - scores.Value = new LeaderboardScores(newScores, null); + scores.Value = LeaderboardScores.Success(newScores, null); if (localFetchCompletionSource != null && localFetchCompletionSource == lastFetchCompletionSource) { @@ -149,14 +167,18 @@ namespace osu.Game.Online.Leaderboards } public record LeaderboardCriteria( - BeatmapInfo Beatmap, - RulesetInfo Ruleset, + BeatmapInfo? Beatmap, + RulesetInfo? Ruleset, BeatmapLeaderboardScope Scope, Mod[]? ExactMods ); - public record LeaderboardScores(IEnumerable TopScores, ScoreInfo? UserScore) + public record LeaderboardScores { + public IEnumerable TopScores { get; } + public ScoreInfo? UserScore { get; } + public LeaderboardFailState? FailState { get; } + public IEnumerable AllScores { get @@ -168,5 +190,26 @@ namespace osu.Game.Online.Leaderboards yield return UserScore; } } + + private LeaderboardScores(IEnumerable topScores, ScoreInfo? userScore, LeaderboardFailState? failState) + { + TopScores = topScores; + UserScore = userScore; + FailState = failState; + } + + public static LeaderboardScores Success(IEnumerable topScores, ScoreInfo? userScore) => new LeaderboardScores(topScores, userScore, null); + public static LeaderboardScores Failure(LeaderboardFailState failState) => new LeaderboardScores([], null, failState); + } + + public enum LeaderboardFailState + { + NetworkFailure = -1, + BeatmapUnavailable = -2, + RulesetUnavailable = -3, + NoneSelected = -4, + NotLoggedIn = -5, + NotSupporter = -6, + NoTeam = -7 } } diff --git a/osu.Game/Online/Leaderboards/LeaderboardState.cs b/osu.Game/Online/Leaderboards/LeaderboardState.cs index dbd982acf2..b0b45ef04e 100644 --- a/osu.Game/Online/Leaderboards/LeaderboardState.cs +++ b/osu.Game/Online/Leaderboards/LeaderboardState.cs @@ -7,13 +7,14 @@ namespace osu.Game.Online.Leaderboards { Success, Retrieving, - NetworkFailure, - BeatmapUnavailable, - RulesetUnavailable, - NoneSelected, NoScores, - NotLoggedIn, - NotSupporter, - NoTeam + + NetworkFailure = LeaderboardFailState.NetworkFailure, + BeatmapUnavailable = LeaderboardFailState.BeatmapUnavailable, + RulesetUnavailable = LeaderboardFailState.RulesetUnavailable, + NoneSelected = LeaderboardFailState.NoneSelected, + NotLoggedIn = LeaderboardFailState.NotLoggedIn, + NotSupporter = LeaderboardFailState.NotSupporter, + NoTeam = LeaderboardFailState.NoTeam, } } diff --git a/osu.Game/Screens/Select/Leaderboards/BeatmapLeaderboard.cs b/osu.Game/Screens/Select/Leaderboards/BeatmapLeaderboard.cs index 2896e7eab4..f5fefa52b5 100644 --- a/osu.Game/Screens/Select/Leaderboards/BeatmapLeaderboard.cs +++ b/osu.Game/Screens/Select/Leaderboards/BeatmapLeaderboard.cs @@ -8,7 +8,6 @@ using System.Threading; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Game.Beatmaps; -using osu.Game.Extensions; using osu.Game.Online.API; using osu.Game.Online.Leaderboards; using osu.Game.Rulesets; @@ -71,9 +70,6 @@ namespace osu.Game.Screens.Select.Leaderboards [Resolved] private IBindable> mods { get; set; } = null!; - [Resolved] - private IAPIProvider api { get; set; } = null!; - [Resolved] private LeaderboardManager leaderboardManager { get; set; } = null!; @@ -94,44 +90,7 @@ namespace osu.Game.Screens.Select.Leaderboards protected override APIRequest? FetchScores(CancellationToken cancellationToken) { var fetchBeatmapInfo = BeatmapInfo; - - if (fetchBeatmapInfo == null) - { - SetErrorState(LeaderboardState.NoneSelected); - return null; - } - - var fetchRuleset = ruleset.Value ?? fetchBeatmapInfo.Ruleset; - - if (!api.IsLoggedIn && IsOnlineScope) - { - SetErrorState(LeaderboardState.NotLoggedIn); - return null; - } - - if (!fetchRuleset.IsLegacyRuleset()) - { - SetErrorState(LeaderboardState.RulesetUnavailable); - return null; - } - - if ((fetchBeatmapInfo.OnlineID <= 0 || fetchBeatmapInfo.Status <= BeatmapOnlineStatus.Pending) && IsOnlineScope) - { - SetErrorState(LeaderboardState.BeatmapUnavailable); - return null; - } - - if (Scope.RequiresSupporter(filterMods) && !api.LocalUser.Value.IsSupporter) - { - SetErrorState(LeaderboardState.NotSupporter); - return null; - } - - if (Scope == BeatmapLeaderboardScope.Team && api.LocalUser.Value.Team == null) - { - SetErrorState(LeaderboardState.NoTeam); - return null; - } + var fetchRuleset = ruleset.Value ?? fetchBeatmapInfo?.Ruleset; leaderboardManager.FetchWithCriteriaAsync(new LeaderboardCriteria(fetchBeatmapInfo, fetchRuleset, Scope, filterMods ? mods.Value.ToArray() : null)) .ContinueWith(t => @@ -145,8 +104,12 @@ namespace osu.Game.Screens.Select.Leaderboards fetchedScores.UnbindEvents(); fetchedScores.BindValueChanged(scores => { - if (scores.NewValue != null) + if (scores.NewValue == null) return; + + if (scores.NewValue.FailState == null) Schedule(() => SetScores(scores.NewValue.TopScores, scores.NewValue.UserScore)); + else + Schedule(() => SetErrorState((LeaderboardState)scores.NewValue.FailState)); }, true); }, cancellationToken); From d1f7afc8edbb4e88939b3b283dae1d6fb5e0f504 Mon Sep 17 00:00:00 2001 From: schiavoanto Date: Sat, 19 Apr 2025 09:00:53 +0200 Subject: [PATCH 127/164] Change "Delete Difficulty" editor menu item type to destructive --- osu.Game/Screens/Edit/Editor.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/Edit/Editor.cs b/osu.Game/Screens/Edit/Editor.cs index 572c4ce283..e238abbb25 100644 --- a/osu.Game/Screens/Edit/Editor.cs +++ b/osu.Game/Screens/Edit/Editor.cs @@ -1266,7 +1266,7 @@ namespace osu.Game.Screens.Edit yield return createDifficultyCreationMenu(); yield return createDifficultySwitchMenu(); yield return new OsuMenuItemSpacer(); - yield return new EditorMenuItem(EditorStrings.DeleteDifficulty, MenuItemType.Standard, deleteDifficulty) { Action = { Disabled = Beatmap.Value.BeatmapSetInfo.Beatmaps.Count < 2 } }; + yield return new EditorMenuItem(EditorStrings.DeleteDifficulty, MenuItemType.Destructive, deleteDifficulty) { Action = { Disabled = Beatmap.Value.BeatmapSetInfo.Beatmaps.Count < 2 } }; yield return new OsuMenuItemSpacer(); var save = new EditorMenuItem(WebCommonStrings.ButtonsSave, MenuItemType.Standard, () => attemptMutationOperation(Save)) { Hotkey = new Hotkey(PlatformAction.Save) }; From 99e882bfbc63b6dc17d65f6dde5738b9ccbe2263 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sun, 20 Apr 2025 00:11:26 +0900 Subject: [PATCH 128/164] 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 98ad145482..5bca6cc497 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 6949aea22e..d988adb6cf 100644 --- a/osu.iOS.props +++ b/osu.iOS.props @@ -17,6 +17,6 @@ -all - + From 267dccdd9afee0bad9742f5272684e1f10a36c2a Mon Sep 17 00:00:00 2001 From: schiavoanto Date: Sun, 20 Apr 2025 09:43:45 +0200 Subject: [PATCH 129/164] Fix slider tooltip text not updating with current value --- osu.Game/Graphics/UserInterface/OsuSliderBar.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Graphics/UserInterface/OsuSliderBar.cs b/osu.Game/Graphics/UserInterface/OsuSliderBar.cs index 24b0e7b0f5..ca95d45042 100644 --- a/osu.Game/Graphics/UserInterface/OsuSliderBar.cs +++ b/osu.Game/Graphics/UserInterface/OsuSliderBar.cs @@ -83,6 +83,6 @@ namespace osu.Game.Graphics.UserInterface channel.Play(); } - public LocalisableString GetDisplayableValue(T value) => CurrentNumber.Value.ToStandardFormattedString(max_decimal_digits, DisplayAsPercentage); + public LocalisableString GetDisplayableValue(T value) => value.ToStandardFormattedString(max_decimal_digits, DisplayAsPercentage); } } From d8df499e728cb827c4b90f75d7233d5ba75cd739 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 21 Apr 2025 08:49:27 +0200 Subject: [PATCH 130/164] Allow toggling leaderboard visibility in replays Closes https://github.com/ppy/osu/issues/31744 I guess. This isn't the resolution that I had in mind for this but my hand has been basically forced by user feedback to do this, at least in the short-term. --- .../Visual/Gameplay/TestSceneSoloGameplayLeaderboard.cs | 7 ------- osu.Game/Screens/Play/HUD/SoloGameplayLeaderboard.cs | 9 +-------- osu.Game/Screens/Play/ReplayPlayer.cs | 1 - osu.Game/Screens/Play/SoloPlayer.cs | 1 - 4 files changed, 1 insertion(+), 17 deletions(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSoloGameplayLeaderboard.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSoloGameplayLeaderboard.cs index dbd14db818..6b2f5767f8 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneSoloGameplayLeaderboard.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSoloGameplayLeaderboard.cs @@ -57,7 +57,6 @@ namespace osu.Game.Tests.Visual.Gameplay Scores = { BindTarget = scores }, Anchor = Anchor.Centre, Origin = Anchor.Centre, - AlwaysVisible = { Value = false }, Expanded = { Value = true }, }; }); @@ -101,12 +100,6 @@ namespace osu.Game.Tests.Visual.Gameplay AddStep("set config visible false", () => configVisibility.Value = false); AddUntilStep("leaderboard not visible", () => leaderboard.Alpha == 0); - - AddStep("set always visible", () => leaderboard.AlwaysVisible.Value = true); - AddUntilStep("leaderboard visible", () => leaderboard.Alpha == 1); - - AddStep("set config visible true", () => configVisibility.Value = true); - AddAssert("leaderboard still visible", () => leaderboard.Alpha == 1); } private static List createSampleScores() diff --git a/osu.Game/Screens/Play/HUD/SoloGameplayLeaderboard.cs b/osu.Game/Screens/Play/HUD/SoloGameplayLeaderboard.cs index e9bb1d2101..b06c9b7be8 100644 --- a/osu.Game/Screens/Play/HUD/SoloGameplayLeaderboard.cs +++ b/osu.Game/Screens/Play/HUD/SoloGameplayLeaderboard.cs @@ -30,12 +30,6 @@ namespace osu.Game.Screens.Play.HUD [Resolved] private ScoreProcessor scoreProcessor { get; set; } = null!; - /// - /// Whether the leaderboard should be visible regardless of the configuration value. - /// This is true by default, but can be changed. - /// - public readonly Bindable AlwaysVisible = new Bindable(true); - public SoloGameplayLeaderboard(IUser trackingUser) { this.trackingUser = trackingUser; @@ -57,7 +51,6 @@ namespace osu.Game.Screens.Play.HUD // Alpha will be updated via `updateVisibility` below. Alpha = 0; - AlwaysVisible.BindValueChanged(_ => updateVisibility()); configVisibility.BindValueChanged(_ => updateVisibility(), true); } @@ -103,6 +96,6 @@ namespace osu.Game.Screens.Play.HUD } private void updateVisibility() => - this.FadeTo(AlwaysVisible.Value || configVisibility.Value ? 1 : 0, duration); + this.FadeTo(configVisibility.Value ? 1 : 0, duration); } } diff --git a/osu.Game/Screens/Play/ReplayPlayer.cs b/osu.Game/Screens/Play/ReplayPlayer.cs index a5952f3ff3..39f5d28e64 100644 --- a/osu.Game/Screens/Play/ReplayPlayer.cs +++ b/osu.Game/Screens/Play/ReplayPlayer.cs @@ -121,7 +121,6 @@ namespace osu.Game.Screens.Play protected override GameplayLeaderboard CreateGameplayLeaderboard() => new SoloGameplayLeaderboard(Score.ScoreInfo.User) { - AlwaysVisible = { Value = true }, Scores = { BindTarget = localScores } }; diff --git a/osu.Game/Screens/Play/SoloPlayer.cs b/osu.Game/Screens/Play/SoloPlayer.cs index ed5dea98cd..eae710bd1f 100644 --- a/osu.Game/Screens/Play/SoloPlayer.cs +++ b/osu.Game/Screens/Play/SoloPlayer.cs @@ -68,7 +68,6 @@ namespace osu.Game.Screens.Play protected override GameplayLeaderboard CreateGameplayLeaderboard() => new SoloGameplayLeaderboard(Score.ScoreInfo.User) { - AlwaysVisible = { Value = false }, Scores = { BindTarget = localScores } }; From 4d08c81e8d8c2597a198f8284a1f69c3189af525 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 21 Apr 2025 09:05:54 +0200 Subject: [PATCH 131/164] Move bindable list population to load complete to fix threading woes --- .../Leaderboards/SoloGameplayLeaderboardProvider.cs | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/Select/Leaderboards/SoloGameplayLeaderboardProvider.cs b/osu.Game/Screens/Select/Leaderboards/SoloGameplayLeaderboardProvider.cs index ac94d307c6..5cbbb3f3b0 100644 --- a/osu.Game/Screens/Select/Leaderboards/SoloGameplayLeaderboardProvider.cs +++ b/osu.Game/Screens/Select/Leaderboards/SoloGameplayLeaderboardProvider.cs @@ -17,9 +17,16 @@ namespace osu.Game.Screens.Select.Leaderboards public IBindableList Scores => scores; private readonly BindableList scores = new BindableList(); - [BackgroundDependencyLoader] - private void load(LeaderboardManager? leaderboardManager, GameplayState? gameplayState) + [Resolved] + private LeaderboardManager? leaderboardManager { get; set; } + + [Resolved] + private GameplayState? gameplayState { get; set; } + + protected override void LoadComplete() { + base.LoadComplete(); + var globalScores = leaderboardManager?.Scores.Value; IsPartial = leaderboardManager?.CurrentCriteria?.Scope != BeatmapLeaderboardScope.Local && globalScores?.TopScores.Count >= 50; From da1fc1013e07b8dafb0c409354f9d1cef971e449 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 21 Apr 2025 09:15:44 +0200 Subject: [PATCH 132/164] Bring back reading from config value --- .../OnlinePlay/Multiplayer/GameplayChatDisplay.cs | 3 ++- osu.Game/Screens/Play/HUD/DrawableGameplayLeaderboard.cs | 9 +++++++++ 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/GameplayChatDisplay.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/GameplayChatDisplay.cs index 65f667b929..c7b65856e6 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/GameplayChatDisplay.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/GameplayChatDisplay.cs @@ -31,14 +31,15 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer private readonly Bindable expandedFromTextBoxFocus = new Bindable(); private const float height = 100; + private const float width = 260; public override bool PropagateNonPositionalInputSubTree => true; public GameplayChatDisplay(Room room) : base(room, leaveChannelOnDispose: false) { - RelativeSizeAxes = Axes.X; Background.Alpha = 0.2f; + Width = width; } [BackgroundDependencyLoader] diff --git a/osu.Game/Screens/Play/HUD/DrawableGameplayLeaderboard.cs b/osu.Game/Screens/Play/HUD/DrawableGameplayLeaderboard.cs index f60d12d84f..005cd784c4 100644 --- a/osu.Game/Screens/Play/HUD/DrawableGameplayLeaderboard.cs +++ b/osu.Game/Screens/Play/HUD/DrawableGameplayLeaderboard.cs @@ -10,6 +10,7 @@ using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Colour; using osu.Framework.Graphics.Containers; +using osu.Game.Configuration; using osu.Game.Graphics.Containers; using osu.Game.Screens.Select.Leaderboards; using osuTK; @@ -34,6 +35,7 @@ namespace osu.Game.Screens.Play.HUD private IGameplayLeaderboardProvider? leaderboardProvider { get; set; } private readonly IBindableList scores = new BindableList(); + private readonly Bindable configVisibility = new Bindable(); private const int max_panels = 8; @@ -64,6 +66,12 @@ namespace osu.Game.Screens.Play.HUD }; } + [BackgroundDependencyLoader] + private void load(OsuConfigManager config) + { + config.BindWith(OsuSetting.GameplayLeaderboard, configVisibility); + } + protected override void LoadComplete() { base.LoadComplete(); @@ -80,6 +88,7 @@ namespace osu.Game.Screens.Play.HUD } Scheduler.AddDelayed(sort, 1000, true); + configVisibility.BindValueChanged(_ => this.FadeTo(configVisibility.Value ? 1 : 0, 100, Easing.OutQuint), true); } /// From 78d9bd7fb4e3faea758fb1fd49184bd30442ee0f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 21 Apr 2025 10:00:22 +0200 Subject: [PATCH 133/164] Fix slider repeat arrows appearing too early in editor when hit markers are enabled Closes https://github.com/ppy/osu/issues/32880 Broke in conjunction with https://github.com/ppy/osu/pull/32638 because of transforms not being applied to `DrawableSliderRepeat` but its individual pieces instead. In cross-checking with stable (visual only) the early fade in of the arrow should still apply, it just shouldn't be instantaneous as is currently ends up being with how the code is structured. --- .../Objects/Drawables/DrawableSliderRepeat.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderRepeat.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderRepeat.cs index 9368c69ebd..8205483f82 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderRepeat.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderRepeat.cs @@ -176,10 +176,9 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables // More or less matches stable (see https://github.com/peppy/osu-stable-reference/blob/bb57924c1552adbed11ee3d96cdcde47cf96f2b6/osu!/GameplayElements/HitObjects/Osu/HitCircleOsu.cs#L336-L338) AccentColour.Value = Color4.White; Alpha = Interpolation.ValueAt(Time.Current, 1f, 0f, HitStateUpdateTime, HitStateUpdateTime + 700); + Arrow.Alpha = 0; } - Arrow.Alpha = hit ? 0 : 1; - LifetimeEnd = HitStateUpdateTime + 700; } From 5895a8ac498d995e71650fa54de18295f88172a1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 22 Apr 2025 08:52:01 +0200 Subject: [PATCH 134/164] Fix daily challenge marker text spacing Closes https://github.com/ppy/osu/issues/32908. Have you ever been in a situation wherein you find out you fixed a bug that you didn't know existed, but that makes *another* bug appear because it was relying on the other bug? This is where I'm at right now. But, to start from the top. `TextFlowContainer.Text` (the setter) is a convenience property that you use to set the text in one go. Internally it uses `AddText()`: https://github.com/ppy/osu-framework/blob/681900ffb70adfeede4e3fa32a69da66252691ee/osu.Framework/Graphics/Containers/TextFlowContainer.cs#L81-L94 `AddText()`'s xmldoc says: The \n character will create a new paragraph, not just a line break. If you need \n to be a line break, use instead. https://github.com/ppy/osu-framework/blob/681900ffb70adfeede4e3fa32a69da66252691ee/osu.Framework/Graphics/Containers/TextFlowContainer.cs#L226-L239 That's right. This portion of xmldoc was *straight up false* and *silently broken* before https://github.com/ppy/osu-framework/pull/6556. If you want to check that out yourself, apply the following patch to framework: diff --git a/osu.Framework.Tests/Visual/Containers/TestSceneTextFlowContainer.cs b/osu.Framework.Tests/Visual/Containers/TestSceneTextFlowContainer.cs index 464f47c2c..e1ad521a7 100644 --- a/osu.Framework.Tests/Visual/Containers/TestSceneTextFlowContainer.cs +++ b/osu.Framework.Tests/Visual/Containers/TestSceneTextFlowContainer.cs @@ -180,6 +180,22 @@ public void TestAlignmentIsCorrectWhenLineBreaksAtLastWordOfParagraph(Anchor tex }); } + [Test] + public void TestSetTextWithNewLine() + { + AddStep("set text", () => textContainer.Text = "this text\nhas a newline"); + AddStep("clear and add text", () => + { + textContainer.Clear(); + textContainer.AddText("this text\nhas a newline"); + }); + AddStep("clear and add paragraph", () => + { + textContainer.Clear(); + textContainer.AddParagraph("this text\nhas a newline"); + }); + } + private void assertSpriteTextCount(int count) => AddAssert($"text flow has {count} sprite texts", () => textContainer.ChildrenOfType().Count() == count); On `master`, there will be a difference between the first two steps, and the third. On 2025.321.0, *there will be none*. My working theory as to why this was always busted is that the corresponding code that was there before in https://github.com/bdach/osu-framework/blob/c31a48178889ca2f9b4d257d2d64915eee90338a/osu.Framework/Graphics/Containers/TextFlowContainer.cs#L454-L458 just straight up ran too late. *The height of the container is being changed after the flow has laid itself out, without adjusting subsequent children in any way.* There is potentially a discussion to be had as to whether the emergent behaviour of `TextFlowContainer.Text` with respect to `\n` character is correct, but I'm just going to start with this diff and see what the reaction is. --- .../Header/Components/DailyChallengeStatsDisplay.cs | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/osu.Game/Overlays/Profile/Header/Components/DailyChallengeStatsDisplay.cs b/osu.Game/Overlays/Profile/Header/Components/DailyChallengeStatsDisplay.cs index a3dce89ad4..d1be7cecce 100644 --- a/osu.Game/Overlays/Profile/Header/Components/DailyChallengeStatsDisplay.cs +++ b/osu.Game/Overlays/Profile/Header/Components/DailyChallengeStatsDisplay.cs @@ -44,6 +44,8 @@ namespace osu.Game.Overlays.Profile.Header.Components { AutoSizeAxes = Axes.Both; + OsuTextFlowContainer label; + InternalChildren = new Drawable[] { content = new Container @@ -69,12 +71,9 @@ namespace osu.Game.Overlays.Profile.Header.Components Direction = FillDirection.Horizontal, Children = new Drawable[] { - new OsuTextFlowContainer(s => s.Font = OsuFont.GetFont(size: 12)) + label = new OsuTextFlowContainer(s => s.Font = OsuFont.GetFont(size: 12)) { AutoSizeAxes = Axes.Both, - // can't use this because osu-web does weird stuff with \\n. - // Text = UsersStrings.ShowDailyChallengeTitle., - Text = "Daily\nChallenge", Margin = new MarginPadding { Horizontal = 5f, Bottom = 2f }, }, new Container @@ -129,6 +128,10 @@ namespace osu.Game.Overlays.Profile.Header.Components } }, }; + + // can't use this because osu-web does weird stuff with \\n. + // Text = UsersStrings.ShowDailyChallengeTitle., + label.AddParagraph("Daily\nChallenge"); } protected override void LoadComplete() From ec854f7b7ffeca0aec3a42b1b1355fca1ad3204c Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 22 Apr 2025 16:56:20 +0900 Subject: [PATCH 135/164] Adjust namespaces and naming --- .../TestSceneCollectionDropdown.cs | 2 +- .../TestSceneManageCollectionsDialog.cs | 2 +- .../TestSceneCollectionDropdown.cs} | 23 ++++++++++--------- .../Music/NowPlayingCollectionDropdown.cs | 2 +- .../SelectV2/CollectionDropdown.cs} | 7 +++--- 5 files changed, 19 insertions(+), 17 deletions(-) rename osu.Game.Tests/Visual/{Collections => SongSelect}/TestSceneCollectionDropdown.cs (99%) rename osu.Game.Tests/Visual/{Collections => SongSelect}/TestSceneManageCollectionsDialog.cs (99%) rename osu.Game.Tests/Visual/{Collections/TestSceneShearedCollectionDropdown.cs => SongSelectV2/TestSceneCollectionDropdown.cs} (90%) rename osu.Game/{Collections/ShearedCollectionDropdown.cs => Screens/SelectV2/CollectionDropdown.cs} (97%) diff --git a/osu.Game.Tests/Visual/Collections/TestSceneCollectionDropdown.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneCollectionDropdown.cs similarity index 99% rename from osu.Game.Tests/Visual/Collections/TestSceneCollectionDropdown.cs rename to osu.Game.Tests/Visual/SongSelect/TestSceneCollectionDropdown.cs index a47f3c5108..fe2bf6ff5d 100644 --- a/osu.Game.Tests/Visual/Collections/TestSceneCollectionDropdown.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneCollectionDropdown.cs @@ -22,7 +22,7 @@ using osu.Game.Tests.Resources; using osuTK.Input; using Realms; -namespace osu.Game.Tests.Visual.Collections +namespace osu.Game.Tests.Visual.SongSelect { public partial class TestSceneCollectionDropdown : OsuManualInputManagerTestScene { diff --git a/osu.Game.Tests/Visual/Collections/TestSceneManageCollectionsDialog.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneManageCollectionsDialog.cs similarity index 99% rename from osu.Game.Tests/Visual/Collections/TestSceneManageCollectionsDialog.cs rename to osu.Game.Tests/Visual/SongSelect/TestSceneManageCollectionsDialog.cs index 60675018e9..4c895faf27 100644 --- a/osu.Game.Tests/Visual/Collections/TestSceneManageCollectionsDialog.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneManageCollectionsDialog.cs @@ -20,7 +20,7 @@ using osu.Game.Tests.Resources; using osuTK; using osuTK.Input; -namespace osu.Game.Tests.Visual.Collections +namespace osu.Game.Tests.Visual.SongSelect { public partial class TestSceneManageCollectionsDialog : OsuManualInputManagerTestScene { diff --git a/osu.Game.Tests/Visual/Collections/TestSceneShearedCollectionDropdown.cs b/osu.Game.Tests/Visual/SongSelectV2/TestSceneCollectionDropdown.cs similarity index 90% rename from osu.Game.Tests/Visual/Collections/TestSceneShearedCollectionDropdown.cs rename to osu.Game.Tests/Visual/SongSelectV2/TestSceneCollectionDropdown.cs index f1afdf2019..f3c96861ed 100644 --- a/osu.Game.Tests/Visual/Collections/TestSceneShearedCollectionDropdown.cs +++ b/osu.Game.Tests/Visual/SongSelectV2/TestSceneCollectionDropdown.cs @@ -21,13 +21,14 @@ using osu.Game.Rulesets; using osu.Game.Tests.Resources; using osuTK.Input; using Realms; +using CollectionDropdown = osu.Game.Screens.SelectV2.CollectionDropdown; -namespace osu.Game.Tests.Visual.Collections +namespace osu.Game.Tests.Visual.SongSelectV2 { - public partial class TestSceneShearedCollectionDropdown : OsuManualInputManagerTestScene + public partial class TestSceneCollectionDropdown : OsuManualInputManagerTestScene { private BeatmapManager beatmapManager = null!; - private ShearedCollectionDropdown dropdown = null!; + private CollectionDropdown dropdown = null!; [Cached] private readonly OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Aquamarine); @@ -51,7 +52,7 @@ namespace osu.Game.Tests.Visual.Collections { RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y, - Child = dropdown = new ShearedCollectionDropdown + Child = dropdown = new CollectionDropdown { Width = 300, Y = 100, @@ -84,11 +85,11 @@ namespace osu.Game.Tests.Visual.Collections AddStep("add collection", () => writeAndRefresh(r => r.Add(new BeatmapCollection(name: "2")))); AddStep("add collection", () => writeAndRefresh(r => r.Add(new BeatmapCollection(name: "3")))); - AddAssert("check count 5", () => dropdown.ChildrenOfType().Single().ChildrenOfType().Count(), () => Is.EqualTo(5)); + AddAssert("check count 5", () => dropdown.ChildrenOfType().Single().ChildrenOfType().Count(), () => Is.EqualTo(5)); AddStep("delete all collections", () => writeAndRefresh(r => r.RemoveAll())); - AddAssert("check count 2", () => dropdown.ChildrenOfType().Single().ChildrenOfType().Count(), () => Is.EqualTo(2)); + AddAssert("check count 2", () => dropdown.ChildrenOfType().Single().ChildrenOfType().Count(), () => Is.EqualTo(2)); } [Test] @@ -212,12 +213,12 @@ namespace osu.Game.Tests.Visual.Collections AddStep("watch for filter requests", () => { received = false; - dropdown.ChildrenOfType().First().RequestFilter = () => received = true; + dropdown.ChildrenOfType().First().RequestFilter = () => received = true; }); AddStep("click manage collections filter", () => { - int lastItemIndex = dropdown.ChildrenOfType().Single().Items.Count() - 1; + int lastItemIndex = dropdown.ChildrenOfType().Single().Items.Count() - 1; InputManager.MoveMouseTo(getCollectionDropdownItemAt(lastItemIndex)); InputManager.Click(MouseButton.Left); }); @@ -237,7 +238,7 @@ namespace osu.Game.Tests.Visual.Collections private void assertCollectionHeaderDisplays(string collectionName, bool shouldDisplay = true) => AddUntilStep($"collection dropdown header displays '{collectionName}'", - () => shouldDisplay == dropdown.ChildrenOfType().Any(h => h.ChildrenOfType().Any(t => t.Text == collectionName))); + () => shouldDisplay == dropdown.ChildrenOfType().Any(h => h.ChildrenOfType().Any(t => t.Text == collectionName))); private void assertFirstButtonIs(IconUsage icon) => AddUntilStep($"button is {icon.Icon.ToString()}", () => getAddOrRemoveButton(1).Icon.Equals(icon)); @@ -251,7 +252,7 @@ namespace osu.Game.Tests.Visual.Collections private void addExpandHeaderStep() => AddStep("expand header", () => { - InputManager.MoveMouseTo(dropdown.ChildrenOfType().Single()); + InputManager.MoveMouseTo(dropdown.ChildrenOfType().Single()); InputManager.Click(MouseButton.Left); }); @@ -264,7 +265,7 @@ namespace osu.Game.Tests.Visual.Collections private Menu.DrawableMenuItem getCollectionDropdownItemAt(int index) { // todo: we should be able to use Items, but apparently that's not guaranteed to be ordered... see: https://github.com/ppy/osu-framework/pull/6079 - CollectionFilterMenuItem item = dropdown.ChildrenOfType().Single().ItemSource.ElementAt(index); + CollectionFilterMenuItem item = dropdown.ChildrenOfType().Single().ItemSource.ElementAt(index); return dropdown.ChildrenOfType().Single(i => i.Item.Text.Value == item.CollectionName); } } diff --git a/osu.Game/Overlays/Music/NowPlayingCollectionDropdown.cs b/osu.Game/Overlays/Music/NowPlayingCollectionDropdown.cs index 0f2e9400d9..2ba222b976 100644 --- a/osu.Game/Overlays/Music/NowPlayingCollectionDropdown.cs +++ b/osu.Game/Overlays/Music/NowPlayingCollectionDropdown.cs @@ -15,7 +15,7 @@ namespace osu.Game.Overlays.Music /// /// A for use in the . /// - public partial class NowPlayingCollectionDropdown : CollectionDropdown + public partial class NowPlayingCollectionDropdown : CollectionDropdown // TODO: class is now unused. if we decide this isn't coming back it can be nuked. { protected override bool ShowManageCollectionsItem => false; diff --git a/osu.Game/Collections/ShearedCollectionDropdown.cs b/osu.Game/Screens/SelectV2/CollectionDropdown.cs similarity index 97% rename from osu.Game/Collections/ShearedCollectionDropdown.cs rename to osu.Game/Screens/SelectV2/CollectionDropdown.cs index 2bb2f5bfe7..a2a2ec1c93 100644 --- a/osu.Game/Collections/ShearedCollectionDropdown.cs +++ b/osu.Game/Screens/SelectV2/CollectionDropdown.cs @@ -13,6 +13,7 @@ using osu.Framework.Graphics.UserInterface; using osu.Framework.Input.Events; using osu.Framework.Localisation; using osu.Game.Beatmaps; +using osu.Game.Collections; using osu.Game.Database; using osu.Game.Graphics.Containers; using osu.Game.Graphics.UserInterface; @@ -20,12 +21,12 @@ using osu.Game.Graphics.UserInterfaceV2; using osuTK; using Realms; -namespace osu.Game.Collections +namespace osu.Game.Screens.SelectV2 { /// /// A dropdown to select the collection to be used to filter results. /// - public partial class ShearedCollectionDropdown : ShearedDropdown + public partial class CollectionDropdown : ShearedDropdown // TODO: partial class under FilterControl? { /// /// Whether to show the "manage collections..." menu item in the dropdown. @@ -46,7 +47,7 @@ namespace osu.Game.Collections private readonly CollectionFilterMenuItem allBeatmapsItem = new AllBeatmapsCollectionFilterMenuItem(); - public ShearedCollectionDropdown() + public CollectionDropdown() : base("Collection") { ItemSource = filters; From 57e693e0c779076668384a72f3b5526b58ca85b8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 22 Apr 2025 11:17:52 +0200 Subject: [PATCH 136/164] Add failing test --- .../TestScenePlaylistsSongSelect.cs | 36 +++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/osu.Game.Tests/Visual/Multiplayer/TestScenePlaylistsSongSelect.cs b/osu.Game.Tests/Visual/Multiplayer/TestScenePlaylistsSongSelect.cs index 7c73fb8321..77fe96310f 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestScenePlaylistsSongSelect.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestScenePlaylistsSongSelect.cs @@ -6,8 +6,12 @@ using System.Linq; using NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Audio; +using osu.Framework.Bindables; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Sprites; using osu.Framework.Platform; using osu.Framework.Screens; +using osu.Framework.Testing; using osu.Framework.Utils; using osu.Game.Beatmaps; using osu.Game.Database; @@ -16,10 +20,12 @@ using osu.Game.Rulesets; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Osu; using osu.Game.Rulesets.Osu.Mods; +using osu.Game.Screens.OnlinePlay; using osu.Game.Screens.OnlinePlay.Components; using osu.Game.Screens.OnlinePlay.Playlists; using osu.Game.Tests.Resources; using osu.Game.Tests.Visual.OnlinePlay; +using osuTK.Input; namespace osu.Game.Tests.Visual.Multiplayer { @@ -153,10 +159,40 @@ namespace osu.Game.Tests.Visual.Multiplayer }); } + [Test] + public void TestFreeModSelectionDisable() + { + FooterButtonFreeMods freeMods = null!; + + AddAssert("freestyle enabled", () => songSelect.Freestyle.Value, () => Is.True); + AddStep("click icon in free mods button", () => + { + freeMods = this.ChildrenOfType().Single(); + InputManager.MoveMouseTo(freeMods.ChildrenOfType().Single()); + InputManager.Click(MouseButton.Left); + }); + AddAssert("mod select not visible", () => this.ChildrenOfType().Single().State.Value, () => Is.EqualTo(Visibility.Hidden)); + + AddStep("toggle freestyle off", () => + { + InputManager.MoveMouseTo(this.ChildrenOfType().Single()); + InputManager.Click(MouseButton.Left); + }); + AddAssert("freestyle disabled", () => songSelect.Freestyle.Value, () => Is.False); + AddStep("click icon in free mods button", () => + { + InputManager.MoveMouseTo(freeMods.ChildrenOfType().Single()); + InputManager.Click(MouseButton.Left); + }); + AddAssert("mod select visible", () => this.ChildrenOfType().Single().State.Value, () => Is.EqualTo(Visibility.Visible)); + } + private partial class TestPlaylistsSongSelect : PlaylistsSongSelect { public new MatchBeatmapDetailArea BeatmapDetails => (MatchBeatmapDetailArea)base.BeatmapDetails; + public new IBindable Freestyle => base.Freestyle; + public TestPlaylistsSongSelect(Room room) : base(room) { From fea1b73c173be0a81ea6f5a07547356b747e0798 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 22 Apr 2025 11:25:09 +0200 Subject: [PATCH 137/164] Fix free mod selection sub-button being clickable even if the main button isn't Noticed in passing when testing https://github.com/ppy/osu/pull/32674. --- osu.Game/Screens/OnlinePlay/FooterButtonFreeMods.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/osu.Game/Screens/OnlinePlay/FooterButtonFreeMods.cs b/osu.Game/Screens/OnlinePlay/FooterButtonFreeMods.cs index 3605412b2b..ad780cd27d 100644 --- a/osu.Game/Screens/OnlinePlay/FooterButtonFreeMods.cs +++ b/osu.Game/Screens/OnlinePlay/FooterButtonFreeMods.cs @@ -80,6 +80,7 @@ namespace osu.Game.Screens.OnlinePlay Origin = Anchor.Centre, Scale = new Vector2(0.8f), Icon = FontAwesome.Solid.Bars, + Enabled = { BindTarget = Enabled }, Action = () => freeModSelectOverlay.ToggleVisibility() } }); From 7e6e082bac2907150f2a47a2519161888f62b47d Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 22 Apr 2025 19:13:17 +0900 Subject: [PATCH 138/164] Avoid clearing global cache on entering song select --- osu.Game/Screens/Select/Leaderboards/BeatmapLeaderboard.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/osu.Game/Screens/Select/Leaderboards/BeatmapLeaderboard.cs b/osu.Game/Screens/Select/Leaderboards/BeatmapLeaderboard.cs index f5fefa52b5..61abe3bd86 100644 --- a/osu.Game/Screens/Select/Leaderboards/BeatmapLeaderboard.cs +++ b/osu.Game/Screens/Select/Leaderboards/BeatmapLeaderboard.cs @@ -92,6 +92,10 @@ namespace osu.Game.Screens.Select.Leaderboards var fetchBeatmapInfo = BeatmapInfo; var fetchRuleset = ruleset.Value ?? fetchBeatmapInfo?.Ruleset; + // Without this check, an initial fetch will be performed and clear global cache. + if (fetchBeatmapInfo == null) + return null; + leaderboardManager.FetchWithCriteriaAsync(new LeaderboardCriteria(fetchBeatmapInfo, fetchRuleset, Scope, filterMods ? mods.Value.ToArray() : null)) .ContinueWith(t => { From f7d1809cb7a59d458ab652231aaa7f94ebcb59a4 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 22 Apr 2025 19:00:45 +0900 Subject: [PATCH 139/164] Remove `LeaderboardManager` return value and simplify flow further The rationale for this change is that the return value was mostly useless, and at worst, misleading. When using `LeaderboardManager`, it's assumed that a consumer will bind to the global `Scores` list to ensure they receive updates for things like local score changes via the internal realm subscription. If one decides to instead use the return value of the task, it will be a static snapshot that potentially becomes stale in the future. I fell into this trap when refactoring the new leaderboard component (while attempting to assert correctness that the values we are displaying were in fact from the fetch operation we requested). In the interest of keeping things simple, removing the return value seems to be the best path forward. --- .../Online/Leaderboards/LeaderboardManager.cs | 50 +++++++++++++++---- osu.Game/OsuGame.cs | 7 +-- .../Select/Leaderboards/BeatmapLeaderboard.cs | 44 ++++++++-------- 3 files changed, 63 insertions(+), 38 deletions(-) diff --git a/osu.Game/Online/Leaderboards/LeaderboardManager.cs b/osu.Game/Online/Leaderboards/LeaderboardManager.cs index cd77a28893..75f2972f29 100644 --- a/osu.Game/Online/Leaderboards/LeaderboardManager.cs +++ b/osu.Game/Online/Leaderboards/LeaderboardManager.cs @@ -9,6 +9,7 @@ using System.Threading.Tasks; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; +using osu.Framework.Logging; using osu.Game.Beatmaps; using osu.Game.Database; using osu.Game.Extensions; @@ -43,10 +44,14 @@ namespace osu.Game.Online.Leaderboards [Resolved] private RulesetStore rulesets { get; set; } = null!; - public Task FetchWithCriteriaAsync(LeaderboardCriteria newCriteria) + /// + /// Fetch leaderboard content with the new criteria specified in the background. + /// On completion, will be updated with the results from this call (unless a more recent call with a different criteria has completed). + /// + public void FetchWithCriteria(LeaderboardCriteria newCriteria) { if (CurrentCriteria?.Equals(newCriteria) == true && lastFetchCompletionSource?.Task.IsFaulted == false) - return lastFetchCompletionSource?.Task ?? Task.FromResult(Scores.Value); + return; CurrentCriteria = newCriteria; localScoreSubscription?.Dispose(); @@ -55,7 +60,10 @@ namespace osu.Game.Online.Leaderboards scores.Value = null; if (newCriteria.Beatmap == null || newCriteria.Ruleset == null) - return Task.FromResult(scores.Value = LeaderboardScores.Failure(LeaderboardFailState.NoneSelected)); + { + scores.Value = LeaderboardScores.Failure(LeaderboardFailState.NoneSelected); + return; + } switch (newCriteria.Scope) { @@ -70,25 +78,40 @@ namespace osu.Game.Online.Leaderboards + $" AND {nameof(ScoreInfo.Ruleset)}.{nameof(RulesetInfo.ShortName)} == $1" + $" AND {nameof(ScoreInfo.DeletePending)} == false" , newCriteria.Beatmap.ID, newCriteria.Ruleset.ShortName), localScoresChanged); - return localFetchCompletionSource.Task; + return; } default: { if (!api.IsLoggedIn) - return Task.FromResult(scores.Value = LeaderboardScores.Failure(LeaderboardFailState.NotLoggedIn)); + { + scores.Value = LeaderboardScores.Failure(LeaderboardFailState.NotLoggedIn); + return; + } if (!newCriteria.Ruleset.IsLegacyRuleset()) - return Task.FromResult(scores.Value = LeaderboardScores.Failure(LeaderboardFailState.RulesetUnavailable)); + { + scores.Value = LeaderboardScores.Failure(LeaderboardFailState.RulesetUnavailable); + return; + } if (newCriteria.Beatmap.OnlineID <= 0 || newCriteria.Beatmap.Status <= BeatmapOnlineStatus.Pending) - return Task.FromResult(scores.Value = LeaderboardScores.Failure(LeaderboardFailState.BeatmapUnavailable)); + { + scores.Value = LeaderboardScores.Failure(LeaderboardFailState.BeatmapUnavailable); + return; + } if ((newCriteria.Scope.RequiresSupporter(newCriteria.ExactMods != null)) && !api.LocalUser.Value.IsSupporter) - return Task.FromResult(scores.Value = LeaderboardScores.Failure(LeaderboardFailState.NotSupporter)); + { + scores.Value = LeaderboardScores.Failure(LeaderboardFailState.NotSupporter); + return; + } if (newCriteria.Scope == BeatmapLeaderboardScope.Team && api.LocalUser.Value.Team == null) - return Task.FromResult(scores.Value = LeaderboardScores.Failure(LeaderboardFailState.NoTeam)); + { + scores.Value = LeaderboardScores.Failure(LeaderboardFailState.NoTeam); + return; + } var onlineFetchCompletionSource = new TaskCompletionSource(); lastFetchCompletionSource = onlineFetchCompletionSource; @@ -119,9 +142,14 @@ namespace osu.Game.Online.Leaderboards if (onlineFetchCompletionSource.TrySetResult(result)) scores.Value = result; }; - newRequest.Failure += _ => onlineFetchCompletionSource.TrySetResult(LeaderboardScores.Failure(LeaderboardFailState.NetworkFailure)); + newRequest.Failure += ex => + { + Logger.Log($@"Failed to fetch leaderboards when displaying results: {ex}", LoggingTarget.Network); + onlineFetchCompletionSource.TrySetResult(LeaderboardScores.Failure(LeaderboardFailState.NetworkFailure)); + }; + api.Queue(inFlightOnlineRequest = newRequest); - return onlineFetchCompletionSource.Task; + break; } } } diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs index 0c6a06a8fc..cbb2d44a9a 100644 --- a/osu.Game/OsuGame.cs +++ b/osu.Game/OsuGame.cs @@ -801,12 +801,7 @@ namespace osu.Game var newLeaderboard = currentLeaderboard != null ? currentLeaderboard with { Beatmap = databasedBeatmap, Ruleset = databasedScore.ScoreInfo.Ruleset } : new LeaderboardCriteria(databasedBeatmap, databasedScore.ScoreInfo.Ruleset, BeatmapLeaderboardScope.Global, null); - LeaderboardManager.FetchWithCriteriaAsync(newLeaderboard) - .ContinueWith(t => - { - if (t.Exception != null) - Logger.Log($@"Failed to fetch leaderboards when displaying results: {t.Exception}", LoggingTarget.Network); - }); + LeaderboardManager.FetchWithCriteria(newLeaderboard); } switch (presentType) diff --git a/osu.Game/Screens/Select/Leaderboards/BeatmapLeaderboard.cs b/osu.Game/Screens/Select/Leaderboards/BeatmapLeaderboard.cs index 61abe3bd86..1c62499162 100644 --- a/osu.Game/Screens/Select/Leaderboards/BeatmapLeaderboard.cs +++ b/osu.Game/Screens/Select/Leaderboards/BeatmapLeaderboard.cs @@ -62,7 +62,7 @@ namespace osu.Game.Screens.Select.Leaderboards } } - private readonly Bindable fetchedScores = new Bindable(); + private readonly IBindable fetchedScores = new Bindable(); [Resolved] private IBindable ruleset { get; set; } = null!; @@ -82,9 +82,10 @@ namespace osu.Game.Screens.Select.Leaderboards if (filterMods) RefetchScores(); }; - ((IBindable)fetchedScores).BindTo(leaderboardManager.Scores); } + private bool initialFetchComplete; + protected override bool IsOnlineScope => Scope != BeatmapLeaderboardScope.Local; protected override APIRequest? FetchScores(CancellationToken cancellationToken) @@ -96,30 +97,31 @@ namespace osu.Game.Screens.Select.Leaderboards if (fetchBeatmapInfo == null) return null; - leaderboardManager.FetchWithCriteriaAsync(new LeaderboardCriteria(fetchBeatmapInfo, fetchRuleset, Scope, filterMods ? mods.Value.ToArray() : null)) - .ContinueWith(t => - { - if (t.Exception != null && !t.IsCanceled) - { - Schedule(() => SetErrorState(LeaderboardState.NetworkFailure)); - return; - } + leaderboardManager.FetchWithCriteria(new LeaderboardCriteria(fetchBeatmapInfo, fetchRuleset, Scope, filterMods ? mods.Value.ToArray() : null)); - fetchedScores.UnbindEvents(); - fetchedScores.BindValueChanged(scores => - { - if (scores.NewValue == null) return; - - if (scores.NewValue.FailState == null) - Schedule(() => SetScores(scores.NewValue.TopScores, scores.NewValue.UserScore)); - else - Schedule(() => SetErrorState((LeaderboardState)scores.NewValue.FailState)); - }, true); - }, cancellationToken); + if (!initialFetchComplete) + { + // only bind this after the first fetch to avoid reading stale scores. + fetchedScores.BindTo(leaderboardManager.Scores); + fetchedScores.BindValueChanged(_ => updateScores(), true); + initialFetchComplete = true; + } return null; } + private void updateScores() + { + var scores = fetchedScores.Value; + + if (scores == null) return; + + if (scores.FailState == null) + Schedule(() => SetScores(scores.TopScores, scores.UserScore)); + else + Schedule(() => SetErrorState((LeaderboardState)scores.FailState)); + } + protected override LeaderboardScore CreateDrawableScore(ScoreInfo model, int index) => new LeaderboardScore(model, index, IsOnlineScope, Scope != BeatmapLeaderboardScope.Friend) { Action = () => ScoreSelected?.Invoke(model) From 3472b6af91e15b1634d3ac461fe4c35cf24f76e4 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 18 Apr 2025 19:48:56 +0900 Subject: [PATCH 140/164] Add test coverage of carousel update scenarios --- .../TestSceneBeatmapCarouselUpdateHandling.cs | 137 ++++++++++++++++++ 1 file changed, 137 insertions(+) create mode 100644 osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselUpdateHandling.cs diff --git a/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselUpdateHandling.cs b/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselUpdateHandling.cs new file mode 100644 index 0000000000..236bd59772 --- /dev/null +++ b/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselUpdateHandling.cs @@ -0,0 +1,137 @@ +// 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 NUnit.Framework; +using osu.Framework.Testing; +using osu.Game.Beatmaps; +using osu.Game.Extensions; +using osu.Game.Screens.SelectV2; +using osu.Game.Tests.Resources; + +namespace osu.Game.Tests.Visual.SongSelectV2 +{ + [TestFixture] + public partial class TestSceneBeatmapCarouselUpdateHandling : BeatmapCarouselTestScene + { + private BeatmapSetInfo baseTestBeatmap = null!; + + [SetUpSteps] + public void SetUpSteps() + { + RemoveAllBeatmaps(); + CreateCarousel(); + AddBeatmaps(1, 3); + AddStep("generate and add test beatmap", () => + { + baseTestBeatmap = TestResources.CreateTestBeatmapSetInfo(); + + var metadata = new BeatmapMetadata + { + Artist = "update test", + Title = "beatmap", + }; + + foreach (var b in baseTestBeatmap.Beatmaps) + b.Metadata = metadata; + BeatmapSets.Add(baseTestBeatmap); + }); + + WaitForSorting(); + } + + [Test] + public void TestBeatmapSetUpdatedNoop() + { + List originalDrawables = new List(); + + AddStep("store drawable references", () => + { + originalDrawables.Clear(); + originalDrawables.AddRange(Carousel.ChildrenOfType().ToList()); + }); + + AddStep("update beatmap with same reference", () => BeatmapSets.ReplaceRange(1, 1, [baseTestBeatmap])); + + WaitForSorting(); + AddAssert("drawables unchanged", () => Carousel.ChildrenOfType(), () => Is.EqualTo(originalDrawables)); + } + + [Test] + public void TestBeatmapSetMetadataUpdated() + { + var metadata = new BeatmapMetadata + { + Artist = "updated test", + Title = "new beatmap title", + }; + + List originalDrawables = new List(); + + AddStep("store drawable references", () => + { + originalDrawables.Clear(); + originalDrawables.AddRange(Carousel.ChildrenOfType().ToList()); + }); + + updateBeatmap(b => b.Metadata = metadata); + + WaitForSorting(); + AddAssert("drawables changed", () => Carousel.ChildrenOfType(), () => Is.Not.EqualTo(originalDrawables)); + } + + private void updateBeatmap(Action? updateBeatmap = null, Action? updateSet = null) + { + AddStep("update beatmap with different reference", () => + { + var updatedSet = new BeatmapSetInfo + { + ID = baseTestBeatmap.ID, + OnlineID = baseTestBeatmap.OnlineID, + DateAdded = baseTestBeatmap.DateAdded, + DateSubmitted = baseTestBeatmap.DateSubmitted, + DateRanked = baseTestBeatmap.DateRanked, + Status = baseTestBeatmap.Status, + StatusInt = baseTestBeatmap.StatusInt, + DeletePending = baseTestBeatmap.DeletePending, + Hash = baseTestBeatmap.Hash, + Protected = baseTestBeatmap.Protected, + }; + + updateSet?.Invoke(updatedSet); + + var updatedBeatmaps = baseTestBeatmap.Beatmaps.Select(b => + { + var updatedBeatmap = new BeatmapInfo + { + ID = b.ID, + Metadata = b.Metadata, + Ruleset = b.Ruleset, + DifficultyName = b.DifficultyName, + BeatmapSet = updatedSet, + Status = b.Status, + OnlineID = b.OnlineID, + Length = b.Length, + BPM = b.BPM, + Hash = b.Hash, + StarRating = b.StarRating, + MD5Hash = b.MD5Hash, + OnlineMD5Hash = b.OnlineMD5Hash, + }; + + updateBeatmap?.Invoke(updatedBeatmap); + + return updatedBeatmap; + }).ToList(); + + updatedSet.Beatmaps.AddRange(updatedBeatmaps); + + int originalIndex = BeatmapSets.IndexOf(baseTestBeatmap); + + BeatmapSets.ReplaceRange(originalIndex, 1, [updatedSet]); + }); + } + } +} From ac6747343318f0a03b990f55f115380be9f28f69 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 18 Apr 2025 19:22:09 +0900 Subject: [PATCH 141/164] Change equality to allow non-reference comparisons This is required to hold selection when beatmaps are updates, as one important case. --- osu.Game/Graphics/Carousel/Carousel.cs | 16 ++++++++++++---- osu.Game/Screens/SelectV2/BeatmapCarousel.cs | 15 +++++++++++++-- 2 files changed, 25 insertions(+), 6 deletions(-) diff --git a/osu.Game/Graphics/Carousel/Carousel.cs b/osu.Game/Graphics/Carousel/Carousel.cs index 3a02eb7119..bbd469800c 100644 --- a/osu.Game/Graphics/Carousel/Carousel.cs +++ b/osu.Game/Graphics/Carousel/Carousel.cs @@ -166,6 +166,11 @@ namespace osu.Game.Graphics.Carousel /// protected virtual Task FilterAsync() => filterTask = performFilter(); + /// + /// Check whether two models are the same for display purposes. + /// + protected virtual bool CheckModelEquality(object x, object y) => ReferenceEquals(x, y); + /// /// Create a drawable for the given carousel item so it can be displayed. /// @@ -490,10 +495,10 @@ namespace osu.Game.Graphics.Carousel updateItemYPosition(item, ref lastVisible, ref yPos); - if (ReferenceEquals(item.Model, currentKeyboardSelection.Model)) + if (CheckModelEquality(item.Model, currentKeyboardSelection.Model!)) currentKeyboardSelection = new Selection(item.Model, item, item.CarouselYPosition, i); - if (ReferenceEquals(item.Model, currentSelection.Model)) + if (CheckModelEquality(item.Model, currentSelection.Model!)) currentSelection = new Selection(item.Model, item, item.CarouselYPosition, i); } @@ -578,7 +583,7 @@ namespace osu.Game.Graphics.Carousel panel.X = GetPanelXOffset(panel); - c.Selected.Value = c.Item == currentSelection?.CarouselItem; + c.Selected.Value = currentSelection?.CarouselItem != null && CheckModelEquality(c.Item, currentSelection.CarouselItem); c.KeyboardSelected.Value = c.Item == currentKeyboardSelection?.CarouselItem; c.Expanded.Value = c.Item.IsExpanded; } @@ -644,7 +649,10 @@ namespace osu.Game.Graphics.Carousel // The case where we're intending to display this panel, but it's already displayed. // Note that we **must compare the model here** as the CarouselItems may be fresh instances due to a filter operation. - var existing = toDisplay.FirstOrDefault(i => i.Model == carouselPanel.Item!.Model); + // + // Reference equality is used here instead of CheckModelEquality intentionally. In order to switch to `CheckModelEquality`, + // we need a way to signal to the drawable panels that there is an update. + var existing = toDisplay.FirstOrDefault(i => ReferenceEquals(i.Model, carouselPanel.Item!.Model)); if (existing != null) { diff --git a/osu.Game/Screens/SelectV2/BeatmapCarousel.cs b/osu.Game/Screens/SelectV2/BeatmapCarousel.cs index 9cb7d152de..1e33e4e04b 100644 --- a/osu.Game/Screens/SelectV2/BeatmapCarousel.cs +++ b/osu.Game/Screens/SelectV2/BeatmapCarousel.cs @@ -132,7 +132,7 @@ namespace osu.Game.Screens.SelectV2 return; case BeatmapInfo beatmapInfo: - if (ReferenceEquals(CurrentSelection, beatmapInfo)) + if (CurrentSelection != null && CheckModelEquality(CurrentSelection, beatmapInfo)) { RequestPresentBeatmap?.Invoke(beatmapInfo); return; @@ -155,7 +155,7 @@ namespace osu.Game.Screens.SelectV2 case BeatmapInfo beatmapInfo: // Find any containing group. There should never be too many groups so iterating is efficient enough. - GroupDefinition? containingGroup = grouping.GroupItems.SingleOrDefault(kvp => kvp.Value.Any(i => ReferenceEquals(i.Model, beatmapInfo))).Key; + GroupDefinition? containingGroup = grouping.GroupItems.SingleOrDefault(kvp => kvp.Value.Any(i => CheckModelEquality(i.Model, beatmapInfo))).Key; if (containingGroup != null) setExpandedGroup(containingGroup); @@ -311,6 +311,17 @@ namespace osu.Game.Screens.SelectV2 AddInternal(setPanelPool); } + protected override bool CheckModelEquality(object x, object y) + { + if (x is BeatmapSetInfo beatmapSetX && y is BeatmapSetInfo beatmapSetY) + return beatmapSetX.Equals(beatmapSetY); + + if (x is BeatmapInfo beatmapX && y is BeatmapInfo beatmapY) + return beatmapX.Equals(beatmapY); + + return base.CheckModelEquality(x, y); + } + protected override Drawable GetDrawableForDisplay(CarouselItem item) { switch (item.Model) From 6f97667889e0b5ec320a068a767d12719e3adad7 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 22 Apr 2025 19:46:21 +0900 Subject: [PATCH 142/164] Add basic support for beatmap updates in `BeatmapCarousel` --- osu.Game/Screens/SelectV2/BeatmapCarousel.cs | 47 +++++++++++++++++--- 1 file changed, 41 insertions(+), 6 deletions(-) diff --git a/osu.Game/Screens/SelectV2/BeatmapCarousel.cs b/osu.Game/Screens/SelectV2/BeatmapCarousel.cs index 1e33e4e04b..6b486d7da6 100644 --- a/osu.Game/Screens/SelectV2/BeatmapCarousel.cs +++ b/osu.Game/Screens/SelectV2/BeatmapCarousel.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Generic; using System.Collections.Specialized; +using System.Diagnostics; using System.Linq; using System.Threading; using osu.Framework.Allocation; @@ -74,18 +75,17 @@ namespace osu.Game.Screens.SelectV2 { // TODO: moving management of BeatmapInfo tracking to BeatmapStore might be something we want to consider. // right now we are managing this locally which is a bit of added overhead. - IEnumerable? newBeatmapSets = changed.NewItems?.Cast(); - IEnumerable? beatmapSetInfos = changed.OldItems?.Cast(); + IEnumerable? newItems = changed.NewItems?.Cast(); + IEnumerable? oldItems = changed.OldItems?.Cast(); switch (changed.Action) { case NotifyCollectionChangedAction.Add: - Items.AddRange(newBeatmapSets!.SelectMany(s => s.Beatmaps)); + Items.AddRange(newItems!.SelectMany(s => s.Beatmaps)); break; case NotifyCollectionChangedAction.Remove: - - foreach (var set in beatmapSetInfos!) + foreach (var set in oldItems!) { foreach (var beatmap in set.Beatmaps) Items.RemoveAll(i => i is BeatmapInfo bi && beatmap.Equals(bi)); @@ -94,8 +94,43 @@ namespace osu.Game.Screens.SelectV2 break; case NotifyCollectionChangedAction.Move: + // We can ignore move operations as we are applying our own sort in all cases. + break; + case NotifyCollectionChangedAction.Replace: - throw new NotImplementedException(); + var oldSetBeatmaps = oldItems!.Single().Beatmaps; + var newSetBeatmaps = newItems!.Single().Beatmaps.ToList(); + + // Handling replace operations is a touch manual, as we need to locally diff the beatmaps of each version of the beatmap set. + // Matching is done based on difficulty names as these are the most stable thing between updates (which are usually triggered + // by users editing the beatmap or by difficulty/metadata recomputation). + // + // In the case of difficulty reprocessing, this will trigger multiple times per beatmap as it's always triggering a set update. + // We may want to look to improve this in the future either here or at the source (only trigger an update after all difficulties + // have been processed) if it becomes an issue for animation or performance reasons. + foreach (var beatmap in oldSetBeatmaps) + { + int previousIndex = Items.IndexOf(beatmap); + Debug.Assert(previousIndex >= 0); + + BeatmapInfo? matchingNewBeatmap = newSetBeatmaps.SingleOrDefault(b => b.DifficultyName == beatmap.DifficultyName && b.Ruleset.Equals(beatmap.Ruleset)); + + if (matchingNewBeatmap != null) + { + Items.ReplaceRange(previousIndex, 1, [matchingNewBeatmap]); + newSetBeatmaps.Remove(matchingNewBeatmap); + } + else + { + Items.RemoveAt(previousIndex); + } + } + + // Add any items which weren't found in the previous pass (difficulty names didn't match). + foreach (var beatmap in newSetBeatmaps) + Items.Add(beatmap); + + break; case NotifyCollectionChangedAction.Reset: Items.Clear(); From 4979dd86afb7d8d59f92f0e0c33932d08e32281a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 22 Apr 2025 13:19:53 +0200 Subject: [PATCH 143/164] Simplify even further by removing all of the superfluous task completion sources --- .../Online/Leaderboards/LeaderboardManager.cs | 24 ++++--------------- 1 file changed, 4 insertions(+), 20 deletions(-) diff --git a/osu.Game/Online/Leaderboards/LeaderboardManager.cs b/osu.Game/Online/Leaderboards/LeaderboardManager.cs index 75f2972f29..2144d8e8b3 100644 --- a/osu.Game/Online/Leaderboards/LeaderboardManager.cs +++ b/osu.Game/Online/Leaderboards/LeaderboardManager.cs @@ -5,7 +5,6 @@ using System; using System.Collections.Generic; using System.Diagnostics; using System.Linq; -using System.Threading.Tasks; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; @@ -31,8 +30,6 @@ namespace osu.Game.Online.Leaderboards public LeaderboardCriteria? CurrentCriteria { get; private set; } private IDisposable? localScoreSubscription; - private TaskCompletionSource? localFetchCompletionSource; - private TaskCompletionSource? lastFetchCompletionSource; private GetScoresRequest? inFlightOnlineRequest; [Resolved] @@ -50,13 +47,12 @@ namespace osu.Game.Online.Leaderboards /// public void FetchWithCriteria(LeaderboardCriteria newCriteria) { - if (CurrentCriteria?.Equals(newCriteria) == true && lastFetchCompletionSource?.Task.IsFaulted == false) + if (CurrentCriteria?.Equals(newCriteria) == true && scores.Value?.FailState == null) return; CurrentCriteria = newCriteria; localScoreSubscription?.Dispose(); inFlightOnlineRequest?.Cancel(); - lastFetchCompletionSource?.TrySetCanceled(); scores.Value = null; if (newCriteria.Beatmap == null || newCriteria.Ruleset == null) @@ -69,9 +65,6 @@ namespace osu.Game.Online.Leaderboards { case BeatmapLeaderboardScope.Local: { - // this task completion source will be marked completed in the `localScoresChanged()` below. - // yes it's twisty, but such are the costs of trying to reconcile data-push / subscription and data-pull / explicit fetch flows. - lastFetchCompletionSource = localFetchCompletionSource = new TaskCompletionSource(); localScoreSubscription = realm.RegisterForNotifications(r => r.All().Filter($"{nameof(ScoreInfo.BeatmapInfo)}.{nameof(BeatmapInfo.ID)} == $0" + $" AND {nameof(ScoreInfo.BeatmapInfo)}.{nameof(BeatmapInfo.Hash)} == {nameof(ScoreInfo.BeatmapHash)}" @@ -113,9 +106,6 @@ namespace osu.Game.Online.Leaderboards return; } - var onlineFetchCompletionSource = new TaskCompletionSource(); - lastFetchCompletionSource = onlineFetchCompletionSource; - IReadOnlyList? requestMods = null; if (newCriteria.ExactMods != null) @@ -139,13 +129,13 @@ namespace osu.Game.Online.Leaderboards response.UserScore?.CreateScoreInfo(rulesets, newCriteria.Beatmap) ); inFlightOnlineRequest = null; - if (onlineFetchCompletionSource.TrySetResult(result)) - scores.Value = result; + scores.Value = result; }; newRequest.Failure += ex => { Logger.Log($@"Failed to fetch leaderboards when displaying results: {ex}", LoggingTarget.Network); - onlineFetchCompletionSource.TrySetResult(LeaderboardScores.Failure(LeaderboardFailState.NetworkFailure)); + if (ex is not OperationCanceledException) + scores.Value = LeaderboardScores.Failure(LeaderboardFailState.NetworkFailure); }; api.Queue(inFlightOnlineRequest = newRequest); @@ -185,12 +175,6 @@ namespace osu.Game.Online.Leaderboards newScores = newScores.Detach().OrderByTotalScore(); scores.Value = LeaderboardScores.Success(newScores.ToArray(), null); - - if (localFetchCompletionSource != null && localFetchCompletionSource == lastFetchCompletionSource) - { - localFetchCompletionSource.SetResult(scores.Value); - localFetchCompletionSource = lastFetchCompletionSource = null; - } } } From 615c7b29b59d5f9660d38b6f53c76d37bdc039aa Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 22 Apr 2025 21:01:36 +0900 Subject: [PATCH 144/164] Ensure selection is retained over beatmap update --- .../TestSceneBeatmapCarouselUpdateHandling.cs | 20 +++++++++++++++++-- osu.Game/Graphics/Carousel/Carousel.cs | 4 ++-- osu.Game/Screens/SelectV2/BeatmapCarousel.cs | 6 ++++++ 3 files changed, 26 insertions(+), 4 deletions(-) diff --git a/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselUpdateHandling.cs b/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselUpdateHandling.cs index 236bd59772..d1d73e141a 100644 --- a/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselUpdateHandling.cs +++ b/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselUpdateHandling.cs @@ -26,7 +26,7 @@ namespace osu.Game.Tests.Visual.SongSelectV2 AddBeatmaps(1, 3); AddStep("generate and add test beatmap", () => { - baseTestBeatmap = TestResources.CreateTestBeatmapSetInfo(); + baseTestBeatmap = TestResources.CreateTestBeatmapSetInfo(3); var metadata = new BeatmapMetadata { @@ -82,6 +82,22 @@ namespace osu.Game.Tests.Visual.SongSelectV2 AddAssert("drawables changed", () => Carousel.ChildrenOfType(), () => Is.Not.EqualTo(originalDrawables)); } + [Test] + public void TestSelectionHeld() + { + SelectPrevGroup(); + + WaitForSelection(1, 0); + AddAssert("selection is updateable beatmap", () => Carousel.CurrentSelection, () => Is.EqualTo(baseTestBeatmap.Beatmaps[0])); + AddAssert("visible panel is updateable beatmap", () => GetSelectedPanel()?.Item?.Model, () => Is.EqualTo(baseTestBeatmap.Beatmaps[0])); + + updateBeatmap(); + WaitForSorting(); + + AddAssert("selection is updateable beatmap", () => Carousel.CurrentSelection, () => Is.EqualTo(baseTestBeatmap.Beatmaps[0])); + AddAssert("visible panel is updateable beatmap", () => GetSelectedPanel()?.Item?.Model, () => Is.EqualTo(baseTestBeatmap.Beatmaps[0])); + } + private void updateBeatmap(Action? updateBeatmap = null, Action? updateSet = null) { AddStep("update beatmap with different reference", () => @@ -89,7 +105,7 @@ namespace osu.Game.Tests.Visual.SongSelectV2 var updatedSet = new BeatmapSetInfo { ID = baseTestBeatmap.ID, - OnlineID = baseTestBeatmap.OnlineID, + OnlineID = 99999, // this is just for tracking / debug purposes at the moment. DateAdded = baseTestBeatmap.DateAdded, DateSubmitted = baseTestBeatmap.DateSubmitted, DateRanked = baseTestBeatmap.DateRanked, diff --git a/osu.Game/Graphics/Carousel/Carousel.cs b/osu.Game/Graphics/Carousel/Carousel.cs index bbd469800c..8d8289422b 100644 --- a/osu.Game/Graphics/Carousel/Carousel.cs +++ b/osu.Game/Graphics/Carousel/Carousel.cs @@ -496,10 +496,10 @@ namespace osu.Game.Graphics.Carousel updateItemYPosition(item, ref lastVisible, ref yPos); if (CheckModelEquality(item.Model, currentKeyboardSelection.Model!)) - currentKeyboardSelection = new Selection(item.Model, item, item.CarouselYPosition, i); + currentKeyboardSelection = new Selection(currentKeyboardSelection.Model, item, item.CarouselYPosition, i); if (CheckModelEquality(item.Model, currentSelection.Model!)) - currentSelection = new Selection(item.Model, item, item.CarouselYPosition, i); + currentSelection = new Selection(currentSelection.Model, item, item.CarouselYPosition, i); } // If a keyboard selection is currently made, we want to keep the view stable around the selection. diff --git a/osu.Game/Screens/SelectV2/BeatmapCarousel.cs b/osu.Game/Screens/SelectV2/BeatmapCarousel.cs index 6b486d7da6..3294b9e8a2 100644 --- a/osu.Game/Screens/SelectV2/BeatmapCarousel.cs +++ b/osu.Game/Screens/SelectV2/BeatmapCarousel.cs @@ -117,6 +117,11 @@ namespace osu.Game.Screens.SelectV2 if (matchingNewBeatmap != null) { + // TODO: should this exist in song select instead of here? + // we need to ensure the global beatmap is also updated alongside changes. + if (CurrentSelection != null && CheckModelEquality(beatmap, CurrentSelection)) + CurrentSelection = matchingNewBeatmap; + Items.ReplaceRange(previousIndex, 1, [matchingNewBeatmap]); newSetBeatmaps.Remove(matchingNewBeatmap); } @@ -348,6 +353,7 @@ namespace osu.Game.Screens.SelectV2 protected override bool CheckModelEquality(object x, object y) { + // TODO: this doesn't check online ID. probably need to account for that. if (x is BeatmapSetInfo beatmapSetX && y is BeatmapSetInfo beatmapSetY) return beatmapSetX.Equals(beatmapSetY); From 3b2382ceb0f61589092e7042057ab12b16db1085 Mon Sep 17 00:00:00 2001 From: Shavixinio Date: Tue, 22 Apr 2025 19:49:34 +0200 Subject: [PATCH 145/164] Minor fix to the description text --- osu.Game.Rulesets.Catch/Mods/CatchModEasy.cs | 2 +- osu.Game.Rulesets.Mania/Mods/ManiaModEasy.cs | 2 +- osu.Game.Rulesets.Osu/Mods/OsuModEasy.cs | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Game.Rulesets.Catch/Mods/CatchModEasy.cs b/osu.Game.Rulesets.Catch/Mods/CatchModEasy.cs index cac5b9aa6a..f2c77d6a05 100644 --- a/osu.Game.Rulesets.Catch/Mods/CatchModEasy.cs +++ b/osu.Game.Rulesets.Catch/Mods/CatchModEasy.cs @@ -8,6 +8,6 @@ namespace osu.Game.Rulesets.Catch.Mods { public class CatchModEasy : ModEasyWithExtraLives { - public override LocalisableString Description => @"Larger fruits, more forgiving HP drain, less accuracy required, and three lives!"; + public override LocalisableString Description => @"Larger fruits, more forgiving HP drain, less accuracy required, and extra lives!"; } } diff --git a/osu.Game.Rulesets.Mania/Mods/ManiaModEasy.cs b/osu.Game.Rulesets.Mania/Mods/ManiaModEasy.cs index 5c8cd6a5ae..275643ca44 100644 --- a/osu.Game.Rulesets.Mania/Mods/ManiaModEasy.cs +++ b/osu.Game.Rulesets.Mania/Mods/ManiaModEasy.cs @@ -8,6 +8,6 @@ namespace osu.Game.Rulesets.Mania.Mods { public class ManiaModEasy : ModEasyWithExtraLives { - public override LocalisableString Description => @"More forgiving HP drain, less accuracy required, and three lives!"; + public override LocalisableString Description => @"More forgiving HP drain, less accuracy required, and extra lives!"; } } diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModEasy.cs b/osu.Game.Rulesets.Osu/Mods/OsuModEasy.cs index 281b36e70e..97fe0d0bf2 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModEasy.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModEasy.cs @@ -8,6 +8,6 @@ namespace osu.Game.Rulesets.Osu.Mods { public class OsuModEasy : ModEasyWithExtraLives { - public override LocalisableString Description => @"Larger circles, more forgiving HP drain, less accuracy required, and three lives!"; + public override LocalisableString Description => @"Larger circles, more forgiving HP drain, less accuracy required, and extra lives!"; } } From 83d2189b4f1bceedf890602cef2e8c4b5021f89d Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 23 Apr 2025 13:57:36 +0900 Subject: [PATCH 146/164] Remove loading layer --- osu.Game/Users/UserPanel.cs | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/osu.Game/Users/UserPanel.cs b/osu.Game/Users/UserPanel.cs index 76b7894a9e..fc261163da 100644 --- a/osu.Game/Users/UserPanel.cs +++ b/osu.Game/Users/UserPanel.cs @@ -86,8 +86,6 @@ namespace osu.Game.Users [Resolved] private INotificationOverlay? notifications { get; set; } - private LoadingLayer loading { get; set; } = null!; - [BackgroundDependencyLoader] private void load() { @@ -104,7 +102,6 @@ namespace osu.Game.Users Add(background); Add(CreateLayout()); - Add(loading = new LoadingLayer(true)); base.Action = ViewProfile = () => { @@ -167,8 +164,8 @@ namespace osu.Game.Users })); items.Add(isUserBlocked() - ? new OsuMenuItem(UsersStrings.BlocksButtonUnblock, MenuItemType.Standard, () => blockUser(false)) - : new OsuMenuItem(UsersStrings.BlocksButtonBlock, MenuItemType.Destructive, () => blockUser(true))); + ? new OsuMenuItem(UsersStrings.BlocksButtonUnblock, MenuItemType.Standard, () => toggleBlock(false)) + : new OsuMenuItem(UsersStrings.BlocksButtonBlock, MenuItemType.Destructive, () => toggleBlock(true))); if (isUserOnline()) { @@ -196,15 +193,13 @@ namespace osu.Game.Users } } - private void blockUser(bool block) + private void toggleBlock(bool block) { - loading.Show(); APIRequest req = block ? new BlockUserRequest(User.OnlineID) : new UnblockUserRequest(User.OnlineID); req.Success += () => { api.UpdateLocalBlocks(); - loading.Hide(); }; req.Failure += e => @@ -214,7 +209,6 @@ namespace osu.Game.Users Text = e.Message, Icon = FontAwesome.Solid.Times, }); - loading.Hide(); }; api.Queue(req); From f945abb72eea310c5e15f0757b9e3ed940cd2b53 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 23 Apr 2025 14:51:00 +0900 Subject: [PATCH 147/164] Always refresh leaderboard for now --- osu.Game/Online/Leaderboards/LeaderboardManager.cs | 4 ++-- osu.Game/Screens/Select/Leaderboards/BeatmapLeaderboard.cs | 5 ++++- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/osu.Game/Online/Leaderboards/LeaderboardManager.cs b/osu.Game/Online/Leaderboards/LeaderboardManager.cs index 2144d8e8b3..dd68085103 100644 --- a/osu.Game/Online/Leaderboards/LeaderboardManager.cs +++ b/osu.Game/Online/Leaderboards/LeaderboardManager.cs @@ -45,9 +45,9 @@ namespace osu.Game.Online.Leaderboards /// Fetch leaderboard content with the new criteria specified in the background. /// On completion, will be updated with the results from this call (unless a more recent call with a different criteria has completed). /// - public void FetchWithCriteria(LeaderboardCriteria newCriteria) + public void FetchWithCriteria(LeaderboardCriteria newCriteria, bool forceRefresh = false) { - if (CurrentCriteria?.Equals(newCriteria) == true && scores.Value?.FailState == null) + if (!forceRefresh && CurrentCriteria?.Equals(newCriteria) == true && scores.Value?.FailState == null) return; CurrentCriteria = newCriteria; diff --git a/osu.Game/Screens/Select/Leaderboards/BeatmapLeaderboard.cs b/osu.Game/Screens/Select/Leaderboards/BeatmapLeaderboard.cs index 1c62499162..8197319102 100644 --- a/osu.Game/Screens/Select/Leaderboards/BeatmapLeaderboard.cs +++ b/osu.Game/Screens/Select/Leaderboards/BeatmapLeaderboard.cs @@ -97,7 +97,10 @@ namespace osu.Game.Screens.Select.Leaderboards if (fetchBeatmapInfo == null) return null; - leaderboardManager.FetchWithCriteria(new LeaderboardCriteria(fetchBeatmapInfo, fetchRuleset, Scope, filterMods ? mods.Value.ToArray() : null)); + // For now, we forcefully refresh to keep things simple. + // In the future, removing this requirement may be deemed useful, but will need ample testing of edge case scenarios + // (like returning from gameplay after setting a new score, returning to song select after main menu). + leaderboardManager.FetchWithCriteria(new LeaderboardCriteria(fetchBeatmapInfo, fetchRuleset, Scope, filterMods ? mods.Value.ToArray() : null), forceRefresh: true); if (!initialFetchComplete) { From b9fe5079fc074744197de9bbb20d263e96d549c0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 23 Apr 2025 08:55:29 +0200 Subject: [PATCH 148/164] Fix fps counter test scene being half broken --- osu.Game.Tests/Visual/UserInterface/TestSceneFPSCounter.cs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneFPSCounter.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneFPSCounter.cs index a91e6e3350..f38fa05218 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneFPSCounter.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneFPSCounter.cs @@ -2,10 +2,12 @@ // See the LICENCE file in the repository root for full licence text. using NUnit.Framework; +using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; using osu.Framework.Testing; +using osu.Game.Configuration; using osu.Game.Graphics.UserInterface; using osuTK; using osuTK.Graphics; @@ -14,6 +16,9 @@ namespace osu.Game.Tests.Visual.UserInterface { public partial class TestSceneFPSCounter : OsuTestScene { + [Resolved] + private OsuConfigManager config { get; set; } = null!; + [SetUpSteps] public void SetUpSteps() { @@ -41,6 +46,7 @@ namespace osu.Game.Tests.Visual.UserInterface }, }; }); + AddToggleStep("toggle show", b => config.SetValue(OsuSetting.ShowFpsDisplay, b)); } [Test] From 3f98dd93edd5a18233243766f44009ff01a7771f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 23 Apr 2025 08:55:41 +0200 Subject: [PATCH 149/164] Fix increased spacing on fps counter tooltip --- osu.Game/Graphics/UserInterface/FPSCounterTooltip.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/osu.Game/Graphics/UserInterface/FPSCounterTooltip.cs b/osu.Game/Graphics/UserInterface/FPSCounterTooltip.cs index 17e7be1d8b..e64a4c6c07 100644 --- a/osu.Game/Graphics/UserInterface/FPSCounterTooltip.cs +++ b/osu.Game/Graphics/UserInterface/FPSCounterTooltip.cs @@ -44,7 +44,8 @@ namespace osu.Game.Graphics.UserInterface AutoSizeAxes = Axes.Both, TextAnchor = Anchor.TopRight, Margin = new MarginPadding { Left = 5, Vertical = 10 }, - Text = string.Join('\n', gameHost.Threads.Select(t => t.Name)) + Text = string.Join('\n', gameHost.Threads.Select(t => t.Name)), + ParagraphSpacing = 0, }, textFlow = new OsuTextFlowContainer(cp => { @@ -56,6 +57,7 @@ namespace osu.Game.Graphics.UserInterface Margin = new MarginPadding { Left = 35, Right = 10, Vertical = 10 }, AutoSizeAxes = Axes.Y, TextAnchor = Anchor.TopRight, + ParagraphSpacing = 0, }, }; } From eafb52ffb4767a0a1fff44c384ca17bfc14ba588 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Wed, 23 Apr 2025 16:04:38 +0900 Subject: [PATCH 150/164] Add failing test --- .../TestSceneMultiplayerMatchSubScreen.cs | 25 +++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSubScreen.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSubScreen.cs index 2def7aeb1c..a94f440a01 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSubScreen.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSubScreen.cs @@ -426,6 +426,31 @@ namespace osu.Game.Tests.Visual.Multiplayer AddUntilStep("countdown started", () => MultiplayerClient.ServerRoom!.ActiveCountdowns.Any()); } + [Test] + public void TestSettingsRemainsOpenOnRoomUpdate() + { + AddStep("set playlist", () => + { + room.Playlist = + [ + new PlaylistItem(beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First()).BeatmapInfo) + { + RulesetID = new OsuRuleset().RulesetInfo.OnlineID + } + ]; + }); + + ClickButtonWhenEnabled(); + + AddUntilStep("wait for room join", () => RoomJoined); + + AddStep("open settings", () => this.ChildrenOfType().Single().Show()); + AddAssert("settings opened", () => this.ChildrenOfType().Single().State.Value, () => Is.EqualTo(Visibility.Visible)); + + AddStep("trigger room update", () => MultiplayerClient.AddPlaylistItem(MultiplayerClient.ServerRoom!.Playlist[0].Clone())); + AddAssert("settings still open", () => this.ChildrenOfType().Single().State.Value, () => Is.EqualTo(Visibility.Visible)); + } + private partial class TestMultiplayerMatchSubScreen : MultiplayerMatchSubScreen { [Resolved(canBeNull: true)] From 5ea408f3a13aff27675d6dcb879973e4fba15608 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Wed, 23 Apr 2025 16:05:26 +0900 Subject: [PATCH 151/164] Keep multiplayer settings open during room updates --- .../Multiplayer/MultiplayerMatchSubScreen.cs | 29 +++++++++---------- 1 file changed, 14 insertions(+), 15 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs index 6d271a0077..db1b8262b7 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs @@ -2,7 +2,6 @@ // See the LICENCE file in the repository root for full licence text. using System; -using System.Diagnostics; using System.Linq; using osu.Framework.Allocation; using osu.Framework.Audio; @@ -431,14 +430,25 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer /// private void onRoomUpdated() => Scheduler.AddOnce(() => { - bool newIsRoomJoined = client.Room != null; + bool wasRoomJoined = isRoomJoined; + isRoomJoined = client.Room != null; - if (newIsRoomJoined) + // Creating a room. + if (!wasRoomJoined && !isRoomJoined) + { + roomContent.Hide(); + settingsOverlay.Show(); + } + + // Joining a room. + if (!wasRoomJoined && isRoomJoined) { roomContent.Show(); settingsOverlay.Hide(); } - else if (isRoomJoined) + + // Leaving a room. + if (wasRoomJoined && !isRoomJoined) { Logger.Log($"{this} exiting due to loss of room or connection"); @@ -447,17 +457,6 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer else ValidForResume = false; } - else - { - Debug.Assert(!isRoomJoined && !newIsRoomJoined); - - // A new room is being created. - // The main content should be hidden until the settings overlay is hidden, signaling the room is ready to be displayed. - roomContent.Hide(); - settingsOverlay.Show(); - } - - isRoomJoined = newIsRoomJoined; }); /// From 883df07ff6d5fd8880c4fea079f8939a6996c103 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 23 Apr 2025 16:23:37 +0900 Subject: [PATCH 152/164] Adjust tests and transitions --- .../TestSceneBeatmapMetadataWedge.cs | 22 ++++++- .../Screens/SelectV2/BeatmapMetadataWedge.cs | 64 ++++++++++++------- 2 files changed, 61 insertions(+), 25 deletions(-) diff --git a/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapMetadataWedge.cs b/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapMetadataWedge.cs index 769188eb71..be2e6eb9bf 100644 --- a/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapMetadataWedge.cs +++ b/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapMetadataWedge.cs @@ -18,6 +18,8 @@ namespace osu.Game.Tests.Visual.SongSelectV2 { private APIBeatmapSet? currentOnlineSet; + private BeatmapMetadataWedge wedge = null!; + protected override void LoadComplete() { base.LoadComplete(); @@ -40,22 +42,36 @@ namespace osu.Game.Tests.Visual.SongSelectV2 } }; - Child = new BeatmapMetadataWedge + Child = wedge = new BeatmapMetadataWedge { State = { Value = Visibility.Visible }, }; } [Test] - public void TestDisplay() + public void TestShowHide() { - AddStep("null beatmap", () => Beatmap.SetDefault()); AddStep("all metrics", () => { var (working, onlineSet) = createTestBeatmap(); currentOnlineSet = onlineSet; Beatmap.Value = working; }); + + AddStep("hide wedge", () => wedge.Hide()); + AddStep("show wedge", () => wedge.Show()); + } + + [Test] + public void TestVariousMetrics() + { + AddStep("all metrics", () => + { + var (working, onlineSet) = createTestBeatmap(); + currentOnlineSet = onlineSet; + Beatmap.Value = working; + }); + AddStep("null beatmap", () => Beatmap.SetDefault()); AddStep("no source", () => { var (working, onlineSet) = createTestBeatmap(); diff --git a/osu.Game/Screens/SelectV2/BeatmapMetadataWedge.cs b/osu.Game/Screens/SelectV2/BeatmapMetadataWedge.cs index a83ec51b11..816dfc3f95 100644 --- a/osu.Game/Screens/SelectV2/BeatmapMetadataWedge.cs +++ b/osu.Game/Screens/SelectV2/BeatmapMetadataWedge.cs @@ -35,6 +35,8 @@ namespace osu.Game.Screens.SelectV2 private Drawable failRetryWedge = null!; private FailRetryDisplay failRetryDisplay = null!; + protected override bool StartHidden => true; + [Resolved] private IBindable beatmap { get; set; } = null!; @@ -225,16 +227,47 @@ namespace osu.Game.Screens.SelectV2 apiState.BindValueChanged(_ => Scheduler.AddOnce(updateDisplay), true); } + private const double transition_duration = 300; + protected override void PopIn() { - this.FadeIn(300, Easing.OutQuint) - .MoveToX(0, 300, Easing.OutQuint); + this.FadeIn(transition_duration, Easing.OutQuint) + .MoveToX(0, transition_duration, Easing.OutQuint); + + updateSubWedgeVisibility(); } protected override void PopOut() { - this.FadeOut(300, Easing.OutQuint) - .MoveToX(-100, 300, Easing.OutQuint); + this.FadeOut(transition_duration, Easing.OutQuint) + .MoveToX(-100, transition_duration, Easing.OutQuint); + + updateSubWedgeVisibility(); + } + + private void updateSubWedgeVisibility() + { + // We could consider hiding individual wedges based on zero data in the future. + // Needs some experimentation on what looks good. + + if (State.Value == Visibility.Visible && currentOnlineBeatmapSet != null) + { + ratingsWedge.FadeIn(transition_duration, Easing.OutQuint) + .MoveToX(0, transition_duration, Easing.OutQuint); + + failRetryWedge.Delay(100) + .FadeIn(transition_duration, Easing.OutQuint) + .MoveToX(0, transition_duration, Easing.OutQuint); + } + else + { + ratingsWedge.FadeOut(transition_duration, Easing.OutQuint) + .MoveToX(-50, transition_duration, Easing.OutQuint); + + failRetryWedge.Delay(100) + .FadeOut(transition_duration, Easing.OutQuint) + .MoveToX(-50, transition_duration, Easing.OutQuint); + } } private void updateDisplay() @@ -291,16 +324,13 @@ namespace osu.Game.Screens.SelectV2 { genre.Data = null; language.Data = null; + return; } - else if (currentOnlineBeatmapSet == null) + + if (currentOnlineBeatmapSet == null) { genre.Data = ("-", null); language.Data = ("-", null); - - ratingsWedge.FadeOut(300, Easing.OutQuint); - ratingsWedge.MoveToX(-50, 300, Easing.OutQuint); - failRetryWedge.FadeOut(300, Easing.OutQuint); - failRetryWedge.MoveToX(-50, 300, Easing.OutQuint); } else { @@ -314,24 +344,14 @@ namespace osu.Game.Screens.SelectV2 if (onlineBeatmap != null) { - ratingsWedge.FadeIn(300, Easing.OutQuint); - ratingsWedge.MoveToX(0, 300, Easing.OutQuint); - failRetryWedge.FadeIn(300, Easing.OutQuint); - failRetryWedge.MoveToX(0, 300, Easing.OutQuint); - userRatingDisplay.Data = onlineBeatmapSet.Ratings; ratingSpreadDisplay.Data = onlineBeatmapSet.Ratings; successRateDisplay.Data = (onlineBeatmap.PassCount, onlineBeatmap.PlayCount); failRetryDisplay.Data = onlineBeatmap.FailTimes ?? new APIFailTimes(); } - else - { - ratingsWedge.FadeOut(300, Easing.OutQuint); - ratingsWedge.MoveToX(-50, 300, Easing.OutQuint); - failRetryWedge.FadeOut(300, Easing.OutQuint); - failRetryWedge.MoveToX(-50, 300, Easing.OutQuint); - } } + + updateSubWedgeVisibility(); } } } From 65cf1a4b7c8f31f3dadccd1499c1120244cecdfe Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Wed, 23 Apr 2025 16:28:44 +0900 Subject: [PATCH 153/164] Show true beatmap background when viewing historical multiplayer results --- .../Playlists/PlaylistItemUserBestResultsScreen.cs | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistItemUserBestResultsScreen.cs b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistItemUserBestResultsScreen.cs index 866b094178..c5cea5fef1 100644 --- a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistItemUserBestResultsScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistItemUserBestResultsScreen.cs @@ -2,9 +2,12 @@ // See the LICENCE file in the repository root for full licence text. using System.Linq; +using osu.Framework.Allocation; +using osu.Game.Beatmaps; using osu.Game.Online.API; using osu.Game.Online.Rooms; using osu.Game.Scoring; +using osu.Game.Screens.Backgrounds; namespace osu.Game.Screens.OnlinePlay.Playlists { @@ -14,6 +17,7 @@ namespace osu.Game.Screens.OnlinePlay.Playlists public partial class PlaylistItemUserBestResultsScreen : PlaylistItemResultsScreen { private readonly int userId; + private WorkingBeatmap itemBeatmap = null!; public PlaylistItemUserBestResultsScreen(long roomId, PlaylistItem playlistItem, int userId) : base(null, roomId, playlistItem) @@ -21,6 +25,13 @@ namespace osu.Game.Screens.OnlinePlay.Playlists this.userId = userId; } + [BackgroundDependencyLoader] + private void load(BeatmapManager beatmaps) + { + var localBeatmap = beatmaps.QueryBeatmap($@"{nameof(BeatmapInfo.OnlineID)} == $0 AND {nameof(BeatmapInfo.MD5Hash)} == {nameof(BeatmapInfo.OnlineMD5Hash)}", PlaylistItem.Beatmap.OnlineID); + itemBeatmap = beatmaps.GetWorkingBeatmap(localBeatmap); + } + protected override APIRequest CreateScoreRequest() => new ShowPlaylistUserScoreRequest(RoomId, PlaylistItem.ID, userId); protected override void OnScoresAdded(ScoreInfo[] scores) @@ -30,5 +41,7 @@ namespace osu.Game.Screens.OnlinePlay.Playlists // Prefer selecting the local user's score, or otherwise default to the first visible score. SelectedScore.Value ??= scores.FirstOrDefault(s => s.UserID == userId) ?? scores.FirstOrDefault(); } + + protected override BackgroundScreen CreateBackground() => new BackgroundScreenBeatmap(itemBeatmap); } } From 3f719125e6a4a6c1f8b17d7bc52b45c917593c67 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 23 Apr 2025 16:57:41 +0900 Subject: [PATCH 154/164] Define constant for difficulty colour cutoff --- osu.Game/Beatmaps/Drawables/StarRatingDisplay.cs | 4 ++-- osu.Game/Graphics/OsuColour.cs | 5 +++++ osu.Game/Screens/SelectV2/PanelBeatmap.cs | 2 +- osu.Game/Screens/SelectV2/PanelBeatmapStandalone.cs | 2 +- 4 files changed, 9 insertions(+), 4 deletions(-) diff --git a/osu.Game/Beatmaps/Drawables/StarRatingDisplay.cs b/osu.Game/Beatmaps/Drawables/StarRatingDisplay.cs index 050a78a6b4..eaadf43ad4 100644 --- a/osu.Game/Beatmaps/Drawables/StarRatingDisplay.cs +++ b/osu.Game/Beatmaps/Drawables/StarRatingDisplay.cs @@ -152,8 +152,8 @@ namespace osu.Game.Beatmaps.Drawables background.Colour = colours.ForStarDifficulty(s.NewValue); - starIcon.Colour = s.NewValue >= 6.5 ? colours.Orange1 : colourProvider?.Background5 ?? Color4Extensions.FromHex("303d47"); - starsText.Colour = s.NewValue >= 6.5 ? colours.Orange1 : colourProvider?.Background5 ?? Color4.Black.Opacity(0.75f); + starIcon.Colour = s.NewValue >= OsuColour.STAR_DIFFICULTY_DEFINED_COLOUR_CUTOFF ? colours.Orange1 : colourProvider?.Background5 ?? Color4Extensions.FromHex("303d47"); + starsText.Colour = s.NewValue >= OsuColour.STAR_DIFFICULTY_DEFINED_COLOUR_CUTOFF ? colours.Orange1 : colourProvider?.Background5 ?? Color4.Black.Opacity(0.75f); }, true); } } diff --git a/osu.Game/Graphics/OsuColour.cs b/osu.Game/Graphics/OsuColour.cs index dd5e19e167..ff78e93b5e 100644 --- a/osu.Game/Graphics/OsuColour.cs +++ b/osu.Game/Graphics/OsuColour.cs @@ -20,6 +20,11 @@ namespace osu.Game.Graphics public static Color4 Gray(float amt) => new Color4(amt, amt, amt, 1f); public static Color4 Gray(byte amt) => new Color4(amt, amt, amt, 255); + /// + /// The maximum star rating colour which can be distinguished against a black background. + /// + public const float STAR_DIFFICULTY_DEFINED_COLOUR_CUTOFF = 6.5f; + public static readonly (float, Color4)[] STAR_DIFFICULTY_SPECTRUM = { (0.1f, Color4Extensions.FromHex("aaaaaa")), diff --git a/osu.Game/Screens/SelectV2/PanelBeatmap.cs b/osu.Game/Screens/SelectV2/PanelBeatmap.cs index c8ae443364..20c27dba92 100644 --- a/osu.Game/Screens/SelectV2/PanelBeatmap.cs +++ b/osu.Game/Screens/SelectV2/PanelBeatmap.cs @@ -236,7 +236,7 @@ namespace osu.Game.Screens.SelectV2 starRatingDisplay.Current.Value = starDifficulty; starCounter.Current = (float)starDifficulty.Stars; - difficultyIcon.FadeColour(starDifficulty.Stars > 6.5f ? colours.Orange1 : colourProvider.Background5, duration, Easing.OutQuint); + difficultyIcon.FadeColour(starDifficulty.Stars > OsuColour.STAR_DIFFICULTY_DEFINED_COLOUR_CUTOFF ? colours.Orange1 : colourProvider.Background5, duration, Easing.OutQuint); var starRatingColour = colours.ForStarDifficulty(starDifficulty.Stars); starCounter.FadeColour(starRatingColour, duration, Easing.OutQuint); diff --git a/osu.Game/Screens/SelectV2/PanelBeatmapStandalone.cs b/osu.Game/Screens/SelectV2/PanelBeatmapStandalone.cs index a90a84d115..9a61ce998c 100644 --- a/osu.Game/Screens/SelectV2/PanelBeatmapStandalone.cs +++ b/osu.Game/Screens/SelectV2/PanelBeatmapStandalone.cs @@ -270,7 +270,7 @@ namespace osu.Game.Screens.SelectV2 var starDifficulty = starDifficultyBindable?.Value ?? default; AccentColour = colours.ForStarDifficulty(starDifficulty.Stars); - difficultyIcon.FadeColour(starDifficulty.Stars > 6.5f ? colours.Orange1 : colourProvider.Background5, duration, Easing.OutQuint); + difficultyIcon.FadeColour(starDifficulty.Stars > OsuColour.STAR_DIFFICULTY_DEFINED_COLOUR_CUTOFF ? colours.Orange1 : colourProvider.Background5, duration, Easing.OutQuint); difficultyStarRating.Current.Value = starDifficulty; } } From f6d7e29396286ae60b200a718bbfdadd54732eb2 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 23 Apr 2025 16:59:15 +0900 Subject: [PATCH 155/164] Improve star rating colour animations to match --- .../TestSceneBeatmapTitleWedge.cs | 32 +++++++++---------- .../Beatmaps/Drawables/StarRatingDisplay.cs | 10 +++++- .../BeatmapTitleWedge_DifficultyDisplay.cs | 25 ++++++++------- ...pTitleWedge_DifficultyStatisticsDisplay.cs | 5 ++- 4 files changed, 42 insertions(+), 30 deletions(-) diff --git a/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapTitleWedge.cs b/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapTitleWedge.cs index 8a674d43a5..8454781e32 100644 --- a/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapTitleWedge.cs +++ b/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapTitleWedge.cs @@ -57,6 +57,22 @@ namespace osu.Game.Tests.Visual.SongSelectV2 }); } + [Test] + public void TestRulesetChange() + { + selectBeatmap(Beatmap.Value.Beatmap); + + AddWaitStep("wait for select", 3); + + foreach (var rulesetInfo in rulesets.AvailableRulesets) + { + var testBeatmap = TestSceneBeatmapInfoWedge.CreateTestBeatmap(rulesetInfo); + + setRuleset(rulesetInfo); + selectBeatmap(testBeatmap); + } + } + [Test] public void TestNullBeatmap() { @@ -90,22 +106,6 @@ namespace osu.Game.Tests.Visual.SongSelectV2 checkDisplayedBPM($"{bpm * 0.75f}"); } - [Test] - public void TestRulesetChange() - { - selectBeatmap(Beatmap.Value.Beatmap); - - AddWaitStep("wait for select", 3); - - foreach (var rulesetInfo in rulesets.AvailableRulesets) - { - var testBeatmap = TestSceneBeatmapInfoWedge.CreateTestBeatmap(rulesetInfo); - - setRuleset(rulesetInfo); - selectBeatmap(testBeatmap); - } - } - [Test] public void TestWedgeVisibility() { diff --git a/osu.Game/Beatmaps/Drawables/StarRatingDisplay.cs b/osu.Game/Beatmaps/Drawables/StarRatingDisplay.cs index eaadf43ad4..93d1f5d5c5 100644 --- a/osu.Game/Beatmaps/Drawables/StarRatingDisplay.cs +++ b/osu.Game/Beatmaps/Drawables/StarRatingDisplay.cs @@ -23,6 +23,8 @@ namespace osu.Game.Beatmaps.Drawables /// public partial class StarRatingDisplay : CompositeDrawable, IHasCurrentValue { + public const double TRANSFORM_DURATION = 750; + private readonly bool animated; private readonly Box background; private readonly SpriteIcon starIcon; @@ -36,6 +38,12 @@ namespace osu.Game.Beatmaps.Drawables set => current.Current = value; } + /// + /// The difficulty colour currently displayed. + /// Can be used to have other components match the spectrum animation. + /// + public Color4 DisplayedDifficultyColour => background.Colour; + private readonly Bindable displayedStars = new BindableDouble(); /// @@ -139,7 +147,7 @@ namespace osu.Game.Beatmaps.Drawables Current.BindValueChanged(c => { if (animated) - this.TransformBindableTo(displayedStars, c.NewValue.Stars, 750, Easing.OutQuint); + this.TransformBindableTo(displayedStars, c.NewValue.Stars, TRANSFORM_DURATION, Easing.OutQuint); else displayedStars.Value = c.NewValue.Stars; }); diff --git a/osu.Game/Screens/SelectV2/BeatmapTitleWedge_DifficultyDisplay.cs b/osu.Game/Screens/SelectV2/BeatmapTitleWedge_DifficultyDisplay.cs index e8b2ccb04a..7b6fd81267 100644 --- a/osu.Game/Screens/SelectV2/BeatmapTitleWedge_DifficultyDisplay.cs +++ b/osu.Game/Screens/SelectV2/BeatmapTitleWedge_DifficultyDisplay.cs @@ -236,7 +236,10 @@ namespace osu.Game.Screens.SelectV2 updateDisplay(); - displayedStars.BindValueChanged(_ => updateStars(), true); + displayedStars.BindValueChanged(_ => + { + starRatingDisplay.Current.Value = new StarDifficulty(displayedStars.Value, 0); + }, true); FinishTransforms(true); } @@ -330,17 +333,6 @@ namespace osu.Game.Screens.SelectV2 }; }); - private void updateStars() - { - starRatingDisplay.Current.Value = new StarDifficulty(displayedStars.Value, 0); - - Color4 colour = displayedStars.Value >= 6.5f ? colours.Orange1 : colours.ForStarDifficulty(displayedStars.Value); - difficultyText.FadeColour(colour, 300, Easing.OutQuint); - mappedByText.FadeColour(colour, 300, Easing.OutQuint); - countStatisticsDisplay.TransformTo(nameof(countStatisticsDisplay.AccentColour), colour, 300, Easing.OutQuint); - difficultyStatisticsDisplay.TransformTo(nameof(difficultyStatisticsDisplay.AccentColour), colour, 300, Easing.OutQuint); - } - private void computeStarDifficulty(CancellationToken cancellationToken) { difficultyCache.GetDifficultyAsync(beatmap.Value.BeatmapInfo, ruleset.Value, mods.Value, cancellationToken) @@ -360,7 +352,16 @@ namespace osu.Game.Screens.SelectV2 protected override void Update() { base.Update(); + difficultyText.MaxWidth = Math.Max(nameLine.DrawWidth - mappedByText.DrawWidth - mapperText.DrawWidth - 20, 0); + + // Use difficulty colour until it gets too dark to be visible against dark backgrounds. + Color4 col = starRatingDisplay.DisplayedStars.Value >= OsuColour.STAR_DIFFICULTY_DEFINED_COLOUR_CUTOFF ? colours.Orange1 : starRatingDisplay.DisplayedDifficultyColour; + + difficultyText.Colour = col; + mappedByText.Colour = col; + countStatisticsDisplay.AccentColour = col; + difficultyStatisticsDisplay.AccentColour = col; } private partial class MapperLinkContainer : OsuHoverContainer diff --git a/osu.Game/Screens/SelectV2/BeatmapTitleWedge_DifficultyStatisticsDisplay.cs b/osu.Game/Screens/SelectV2/BeatmapTitleWedge_DifficultyStatisticsDisplay.cs index 1cafe1c6db..aaf3d5f9d6 100644 --- a/osu.Game/Screens/SelectV2/BeatmapTitleWedge_DifficultyStatisticsDisplay.cs +++ b/osu.Game/Screens/SelectV2/BeatmapTitleWedge_DifficultyStatisticsDisplay.cs @@ -21,7 +21,7 @@ namespace osu.Game.Screens.SelectV2 { public partial class BeatmapTitleWedge { - public partial class DifficultyStatisticsDisplay : CompositeDrawable, IHasAccentColour + public partial class DifficultyStatisticsDisplay : CompositeDrawable { private readonly bool autoSize; private readonly FillFlowContainer statisticsFlow; @@ -51,6 +51,9 @@ namespace osu.Game.Screens.SelectV2 get => accentColour; set { + if (accentColour == value) + return; + accentColour = value; foreach (var statistic in statisticsFlow) From c11220df9b2e84248a84b55dc8c0f973c9ccb5f7 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 23 Apr 2025 17:08:48 +0900 Subject: [PATCH 156/164] Remove silly bindable flow that only exists for testing purposes --- .../Visual/SongSelectV2/TestSceneBeatmapTitleWedge.cs | 4 ++-- .../SelectV2/BeatmapTitleWedge_DifficultyDisplay.cs | 11 +---------- 2 files changed, 3 insertions(+), 12 deletions(-) diff --git a/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapTitleWedge.cs b/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapTitleWedge.cs index 8454781e32..c97af5a835 100644 --- a/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapTitleWedge.cs +++ b/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapTitleWedge.cs @@ -4,12 +4,12 @@ using System.Linq; using NUnit.Framework; using osu.Framework.Allocation; -using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Testing; using osu.Game.Beatmaps; using osu.Game.Beatmaps.ControlPoints; +using osu.Game.Beatmaps.Drawables; using osu.Game.Resources.Localisation.Web; using osu.Game.Rulesets; using osu.Game.Rulesets.Osu; @@ -53,7 +53,7 @@ namespace osu.Game.Tests.Visual.SongSelectV2 AddSliderStep("change star difficulty", 0, 11.9, 4.18, v => { - ((BindableDouble)difficultyDisplay.DisplayedStars).Value = v; + difficultyDisplay.ChildrenOfType().Single().Current.Value = new StarDifficulty(v, 0); }); } diff --git a/osu.Game/Screens/SelectV2/BeatmapTitleWedge_DifficultyDisplay.cs b/osu.Game/Screens/SelectV2/BeatmapTitleWedge_DifficultyDisplay.cs index 7b6fd81267..cb5046b227 100644 --- a/osu.Game/Screens/SelectV2/BeatmapTitleWedge_DifficultyDisplay.cs +++ b/osu.Game/Screens/SelectV2/BeatmapTitleWedge_DifficultyDisplay.cs @@ -72,10 +72,6 @@ namespace osu.Game.Screens.SelectV2 private CancellationTokenSource? cancellationSource; - public IBindable DisplayedStars => displayedStars; - - private readonly Bindable displayedStars = new BindableDouble(); - public DifficultyDisplay() { RelativeSizeAxes = Axes.X; @@ -236,10 +232,6 @@ namespace osu.Game.Screens.SelectV2 updateDisplay(); - displayedStars.BindValueChanged(_ => - { - starRatingDisplay.Current.Value = new StarDifficulty(displayedStars.Value, 0); - }, true); FinishTransforms(true); } @@ -343,8 +335,7 @@ namespace osu.Game.Screens.SelectV2 if (cancellationToken.IsCancellationRequested) return; - var result = task.GetResultSafely() ?? default; - displayedStars.Value = result.Stars; + starRatingDisplay.Current.Value = task.GetResultSafely() ?? default; }); }, cancellationToken); } From 9372ba02d1432f3d884f4c8ac1e343a82b233f0c Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 23 Apr 2025 17:18:51 +0900 Subject: [PATCH 157/164] Fix animations and alignment of tiny statistics --- osu.Game/Screens/SelectV2/BeatmapTitleWedge.cs | 11 +++++++---- .../SelectV2/BeatmapTitleWedge_DifficultyDisplay.cs | 2 -- .../BeatmapTitleWedge_DifficultyStatisticsDisplay.cs | 7 ++++--- 3 files changed, 11 insertions(+), 9 deletions(-) diff --git a/osu.Game/Screens/SelectV2/BeatmapTitleWedge.cs b/osu.Game/Screens/SelectV2/BeatmapTitleWedge.cs index 9d1be2fc37..4de896d777 100644 --- a/osu.Game/Screens/SelectV2/BeatmapTitleWedge.cs +++ b/osu.Game/Screens/SelectV2/BeatmapTitleWedge.cs @@ -41,6 +41,8 @@ namespace osu.Game.Screens.SelectV2 [Resolved] private IBindable> mods { get; set; } = null!; + protected override bool StartHidden => true; + private ModSettingChangeTracker? settingChangeTracker; private BeatmapSetOnlineStatusPill statusPill = null!; @@ -71,6 +73,8 @@ namespace osu.Game.Screens.SelectV2 private APIBeatmapSet? currentOnlineBeatmapSet; private GetBeatmapSetRequest? currentRequest; + private FillFlowContainer statisticsFlow = null!; + public BeatmapTitleWedge() { RelativeSizeAxes = Axes.X; @@ -139,14 +143,12 @@ namespace osu.Game.Screens.SelectV2 }, } }), - new ShearAligningWrapper(new FillFlowContainer + new ShearAligningWrapper(statisticsFlow = new FillFlowContainer { Shear = -OsuGame.SHEAR, AutoSizeAxes = Axes.Both, Direction = FillDirection.Horizontal, Spacing = new Vector2(2f, 0f), - AutoSizeDuration = 100, - AutoSizeEasing = Easing.OutQuint, Children = new Drawable[] { playCount = new StatisticPlayCount(background: true, leftPadding: SongSelect.WEDGE_CONTENT_MARGIN, minSize: 50f) @@ -198,7 +200,8 @@ namespace osu.Game.Screens.SelectV2 updateDisplay(); - FinishTransforms(true); + statisticsFlow.AutoSizeDuration = 100; + statisticsFlow.AutoSizeEasing = Easing.OutQuint; } protected override void PopIn() diff --git a/osu.Game/Screens/SelectV2/BeatmapTitleWedge_DifficultyDisplay.cs b/osu.Game/Screens/SelectV2/BeatmapTitleWedge_DifficultyDisplay.cs index cb5046b227..07ec1fdade 100644 --- a/osu.Game/Screens/SelectV2/BeatmapTitleWedge_DifficultyDisplay.cs +++ b/osu.Game/Screens/SelectV2/BeatmapTitleWedge_DifficultyDisplay.cs @@ -231,8 +231,6 @@ namespace osu.Game.Screens.SelectV2 }); updateDisplay(); - - FinishTransforms(true); } [Resolved] diff --git a/osu.Game/Screens/SelectV2/BeatmapTitleWedge_DifficultyStatisticsDisplay.cs b/osu.Game/Screens/SelectV2/BeatmapTitleWedge_DifficultyStatisticsDisplay.cs index aaf3d5f9d6..a185448f36 100644 --- a/osu.Game/Screens/SelectV2/BeatmapTitleWedge_DifficultyStatisticsDisplay.cs +++ b/osu.Game/Screens/SelectV2/BeatmapTitleWedge_DifficultyStatisticsDisplay.cs @@ -88,6 +88,8 @@ namespace osu.Game.Screens.SelectV2 { Alpha = 0f, AutoSizeAxes = Axes.Both, + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, ColumnDimensions = new[] { new Dimension(GridSizeMode.AutoSize), @@ -169,9 +171,8 @@ namespace osu.Game.Screens.SelectV2 private void updateStatistics() { - var oldStatistics = statisticsFlow.Select(s => s.Value).ToArray(); - - if (oldStatistics.Select(s => s.Label).SequenceEqual(statistics.Select(s => s.Label))) + if (statisticsFlow.Select(s => s.Value.Label) + .SequenceEqual(statistics.Select(s => s.Label))) { for (int i = 0; i < statistics.Count; i++) statisticsFlow[i].Value = statistics[i]; From 4290f2d4fd2d74e0fc9cd983f21e94af8b1faee7 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 23 Apr 2025 17:29:37 +0900 Subject: [PATCH 158/164] Simplify and fix naming of statistic class --- .../TestSceneBeatmapTitleWedge.cs | 2 +- .../TestSceneBeatmapTitleWedgeStatistic.cs | 22 ++++++---- .../Screens/SelectV2/BeatmapTitleWedge.cs | 10 ++--- .../SelectV2/BeatmapTitleWedge_Statistic.cs | 41 +++++++++++-------- .../BeatmapTitleWedge_StatisticPlayCount.cs | 11 ++++- 5 files changed, 54 insertions(+), 32 deletions(-) diff --git a/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapTitleWedge.cs b/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapTitleWedge.cs index c97af5a835..6a14ddc147 100644 --- a/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapTitleWedge.cs +++ b/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapTitleWedge.cs @@ -154,7 +154,7 @@ namespace osu.Game.Tests.Visual.SongSelectV2 AddUntilStep($"displayed bpm is {target}", () => { var label = titleWedge.ChildrenOfType().Single(l => l.TooltipText == BeatmapsetsStrings.ShowStatsBpm); - return label.Value == target; + return label.Text == target; }); } } diff --git a/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapTitleWedgeStatistic.cs b/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapTitleWedgeStatistic.cs index 96eab3e8ec..6bf9469021 100644 --- a/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapTitleWedgeStatistic.cs +++ b/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapTitleWedgeStatistic.cs @@ -29,14 +29,22 @@ namespace osu.Game.Tests.Visual.SongSelectV2 public void TestLoading() { AddStep("setup", () => CreateThemedContent(OverlayColourScheme.Aquamarine)); - AddStep("set loading", () => this.ChildrenOfType().ForEach(s => s.Value = null)); + AddStep("set loading", () => this.ChildrenOfType().ForEach(s => s.Text = null)); AddWaitStep("wait", 3); AddStep("set values", () => { playCount.Value = new BeatmapTitleWedge.StatisticPlayCount.Data(1234, 12); - statistic2.Value = "3,234"; - statistic3.Value = "12:34"; - statistic4.Value = "123"; + statistic2.Text = "3,234"; + statistic3.Text = "12:34"; + statistic4.Text = "123"; + }); + + AddStep("set large values", () => + { + playCount.Value = new BeatmapTitleWedge.StatisticPlayCount.Data(134587921, 502); + statistic2.Text = "1,048,576"; + statistic3.Text = "2:50:23"; + statistic4.Text = "1238014"; }); } @@ -54,18 +62,18 @@ namespace osu.Game.Tests.Visual.SongSelectV2 }, statistic2 = new BeatmapTitleWedge.Statistic(OsuIcon.Clock, true, minSize: 30) { - Value = "3,234", + Text = "3,234", TooltipText = "Statistic 2", }, statistic3 = new BeatmapTitleWedge.Statistic(OsuIcon.Metronome) { - Value = "12:34", + Text = "12:34", Margin = new MarginPadding { Right = 10f }, TooltipText = "Statistic 3", }, statistic4 = new BeatmapTitleWedge.Statistic(OsuIcon.Graphics) { - Value = "123", + Text = "123", TooltipText = "Statistic 4", }, }, diff --git a/osu.Game/Screens/SelectV2/BeatmapTitleWedge.cs b/osu.Game/Screens/SelectV2/BeatmapTitleWedge.cs index 4de896d777..d892fcb485 100644 --- a/osu.Game/Screens/SelectV2/BeatmapTitleWedge.cs +++ b/osu.Game/Screens/SelectV2/BeatmapTitleWedge.cs @@ -260,10 +260,10 @@ namespace osu.Game.Screens.SelectV2 double drainLength = Math.Round(beatmap.Value.Beatmap.CalculateDrainLength() / rate); double hitLength = Math.Round(beatmapInfo.Length / rate); - lengthStatistic.Value = hitLength.ToFormattedDuration(); + lengthStatistic.Text = hitLength.ToFormattedDuration(); lengthStatistic.TooltipText = BeatmapsetsStrings.ShowStatsTotalLength(drainLength.ToFormattedDuration()); - bpmStatistic.Value = bpmMin == bpmMax + bpmStatistic.Text = bpmMin == bpmMax ? $"{bpmMin}" : $"{bpmMin}-{bpmMax} (mostly {mostCommonBPM})"; } @@ -296,12 +296,12 @@ namespace osu.Game.Screens.SelectV2 if (currentRequest?.CompletionState == APIRequestCompletionState.Waiting) { playCount.Value = null; - favouritesStatistic.Value = null; + favouritesStatistic.Text = null; } else if (currentOnlineBeatmapSet == null) { playCount.Value = new StatisticPlayCount.Data(-1, -1); - favouritesStatistic.Value = "-"; + favouritesStatistic.Text = "-"; } else { @@ -320,7 +320,7 @@ namespace osu.Game.Screens.SelectV2 } favouritesStatistic.FadeIn(300, Easing.OutQuint); - favouritesStatistic.Value = onlineBeatmapSet.FavouriteCount.ToLocalisableString(@"N0"); + favouritesStatistic.Text = onlineBeatmapSet.FavouriteCount.ToLocalisableString(@"N0"); } } } diff --git a/osu.Game/Screens/SelectV2/BeatmapTitleWedge_Statistic.cs b/osu.Game/Screens/SelectV2/BeatmapTitleWedge_Statistic.cs index b4ec72761f..85a0382360 100644 --- a/osu.Game/Screens/SelectV2/BeatmapTitleWedge_Statistic.cs +++ b/osu.Game/Screens/SelectV2/BeatmapTitleWedge_Statistic.cs @@ -29,27 +29,15 @@ namespace osu.Game.Screens.SelectV2 private OsuSpriteText valueText = null!; private LoadingSpinner loading = null!; - private LocalisableString? value; + private LocalisableString? text; - public LocalisableString? Value + public LocalisableString? Text { - get => value; + get => text; set { - this.value = value; - - Schedule(() => - { - loading.State.Value = value != null ? Visibility.Hidden : Visibility.Visible; - - if (value != null) - { - valueText.Text = value.Value; - valueText.FadeIn(120, Easing.OutQuint); - } - else - valueText.FadeOut(120, Easing.OutQuint); - }); + text = value; + Scheduler.AddOnce(updateDisplay); } } @@ -146,6 +134,25 @@ namespace osu.Game.Screens.SelectV2 } }; } + + protected override void LoadComplete() + { + base.LoadComplete(); + Scheduler.AddOnce(updateDisplay); + } + + private void updateDisplay() + { + loading.State.Value = text != null ? Visibility.Hidden : Visibility.Visible; + + if (text != null) + { + valueText.Text = text.Value; + valueText.FadeIn(120, Easing.OutQuint); + } + else + valueText.FadeOut(120, Easing.OutQuint); + } } } } diff --git a/osu.Game/Screens/SelectV2/BeatmapTitleWedge_StatisticPlayCount.cs b/osu.Game/Screens/SelectV2/BeatmapTitleWedge_StatisticPlayCount.cs index 2d480ad5f4..87f7c30d17 100644 --- a/osu.Game/Screens/SelectV2/BeatmapTitleWedge_StatisticPlayCount.cs +++ b/osu.Game/Screens/SelectV2/BeatmapTitleWedge_StatisticPlayCount.cs @@ -1,6 +1,7 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System; using osu.Framework.Allocation; using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Extensions.LocalisationExtensions; @@ -9,6 +10,7 @@ using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Cursor; using osu.Framework.Graphics.Effects; using osu.Framework.Graphics.Shapes; +using osu.Framework.Localisation; using osu.Game.Graphics; using osu.Game.Graphics.Sprites; using osu.Game.Overlays; @@ -21,15 +23,20 @@ namespace osu.Game.Screens.SelectV2 { public partial class StatisticPlayCount : Statistic, IHasCustomTooltip { - public new Data? Value + public Data? Value { set { - base.Value = value?.Total < 0 ? "-" : value?.Total.ToLocalisableString("N0"); + base.Text = value?.Total < 0 ? "-" : value?.Total.ToLocalisableString("N0"); TooltipContent = value; } } + public new LocalisableString? Text + { + set => throw new InvalidOperationException($"Use {nameof(Value)} instead."); + } + public Data? TooltipContent { get; private set; } [Resolved] From 5ad28a792b683191e5d21bbff04299766b4eb3b5 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 23 Apr 2025 17:34:59 +0900 Subject: [PATCH 159/164] Fix "mapped by" line showing stupid display when switching to default beatmap --- .../Screens/SelectV2/BeatmapTitleWedge_DifficultyDisplay.cs | 2 -- 1 file changed, 2 deletions(-) diff --git a/osu.Game/Screens/SelectV2/BeatmapTitleWedge_DifficultyDisplay.cs b/osu.Game/Screens/SelectV2/BeatmapTitleWedge_DifficultyDisplay.cs index 07ec1fdade..7e3589b001 100644 --- a/osu.Game/Screens/SelectV2/BeatmapTitleWedge_DifficultyDisplay.cs +++ b/osu.Game/Screens/SelectV2/BeatmapTitleWedge_DifficultyDisplay.cs @@ -246,8 +246,6 @@ namespace osu.Game.Screens.SelectV2 if (beatmap.IsDefault) { ratingAndNameContainer.FadeOut(300, Easing.OutQuint); - difficultyText.Text = string.Empty; - mapperText.Text = string.Empty; countStatisticsDisplay.Statistics = Array.Empty(); } else From 414150e9e07c4131c3e6be1814a582552e5071fd Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 23 Apr 2025 17:53:40 +0900 Subject: [PATCH 160/164] Maintain selection using `OnlineID` as a priority --- .../TestSceneBeatmapCarouselUpdateHandling.cs | 34 ++++++++++++++++++- osu.Game/Screens/SelectV2/BeatmapCarousel.cs | 6 ++-- 2 files changed, 37 insertions(+), 3 deletions(-) diff --git a/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselUpdateHandling.cs b/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselUpdateHandling.cs index d1d73e141a..31aa1b6f94 100644 --- a/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselUpdateHandling.cs +++ b/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselUpdateHandling.cs @@ -98,6 +98,38 @@ namespace osu.Game.Tests.Visual.SongSelectV2 AddAssert("visible panel is updateable beatmap", () => GetSelectedPanel()?.Item?.Model, () => Is.EqualTo(baseTestBeatmap.Beatmaps[0])); } + [Test] // Checks that we keep selection based on online ID where possible. + public void TestSelectionHeldDifficultyNameChanged() + { + SelectPrevGroup(); + + WaitForSelection(1, 0); + AddAssert("selection is updateable beatmap", () => Carousel.CurrentSelection, () => Is.EqualTo(baseTestBeatmap.Beatmaps[0])); + AddAssert("visible panel is updateable beatmap", () => GetSelectedPanel()?.Item?.Model, () => Is.EqualTo(baseTestBeatmap.Beatmaps[0])); + + updateBeatmap(b => b.DifficultyName = "new name"); + WaitForSorting(); + + AddAssert("selection is updateable beatmap", () => Carousel.CurrentSelection, () => Is.EqualTo(baseTestBeatmap.Beatmaps[0])); + AddAssert("visible panel is updateable beatmap", () => GetSelectedPanel()?.Item?.Model, () => Is.EqualTo(baseTestBeatmap.Beatmaps[0])); + } + + [Test] // Checks that we fallback to keeping selection based on difficulty name. + public void TestSelectionHeldDifficultyOnlineIDChanged() + { + SelectPrevGroup(); + + WaitForSelection(1, 0); + AddAssert("selection is updateable beatmap", () => Carousel.CurrentSelection, () => Is.EqualTo(baseTestBeatmap.Beatmaps[0])); + AddAssert("visible panel is updateable beatmap", () => GetSelectedPanel()?.Item?.Model, () => Is.EqualTo(baseTestBeatmap.Beatmaps[0])); + + updateBeatmap(b => b.OnlineID = b.OnlineID + 1); + WaitForSorting(); + + AddAssert("selection is updateable beatmap", () => Carousel.CurrentSelection, () => Is.EqualTo(baseTestBeatmap.Beatmaps[0])); + AddAssert("visible panel is updateable beatmap", () => GetSelectedPanel()?.Item?.Model, () => Is.EqualTo(baseTestBeatmap.Beatmaps[0])); + } + private void updateBeatmap(Action? updateBeatmap = null, Action? updateSet = null) { AddStep("update beatmap with different reference", () => @@ -105,7 +137,7 @@ namespace osu.Game.Tests.Visual.SongSelectV2 var updatedSet = new BeatmapSetInfo { ID = baseTestBeatmap.ID, - OnlineID = 99999, // this is just for tracking / debug purposes at the moment. + OnlineID = baseTestBeatmap.OnlineID, DateAdded = baseTestBeatmap.DateAdded, DateSubmitted = baseTestBeatmap.DateSubmitted, DateRanked = baseTestBeatmap.DateRanked, diff --git a/osu.Game/Screens/SelectV2/BeatmapCarousel.cs b/osu.Game/Screens/SelectV2/BeatmapCarousel.cs index 3294b9e8a2..9574a05762 100644 --- a/osu.Game/Screens/SelectV2/BeatmapCarousel.cs +++ b/osu.Game/Screens/SelectV2/BeatmapCarousel.cs @@ -102,7 +102,7 @@ namespace osu.Game.Screens.SelectV2 var newSetBeatmaps = newItems!.Single().Beatmaps.ToList(); // Handling replace operations is a touch manual, as we need to locally diff the beatmaps of each version of the beatmap set. - // Matching is done based on difficulty names as these are the most stable thing between updates (which are usually triggered + // Matching is done based on online IDs, then difficulty names as these are the most stable thing between updates (which are usually triggered // by users editing the beatmap or by difficulty/metadata recomputation). // // In the case of difficulty reprocessing, this will trigger multiple times per beatmap as it's always triggering a set update. @@ -113,7 +113,9 @@ namespace osu.Game.Screens.SelectV2 int previousIndex = Items.IndexOf(beatmap); Debug.Assert(previousIndex >= 0); - BeatmapInfo? matchingNewBeatmap = newSetBeatmaps.SingleOrDefault(b => b.DifficultyName == beatmap.DifficultyName && b.Ruleset.Equals(beatmap.Ruleset)); + BeatmapInfo? matchingNewBeatmap = + newSetBeatmaps.SingleOrDefault(b => b.OnlineID == beatmap.OnlineID) ?? + newSetBeatmaps.SingleOrDefault(b => b.DifficultyName == beatmap.DifficultyName && b.Ruleset.Equals(beatmap.Ruleset)); if (matchingNewBeatmap != null) { From 73773aa69a1c2368c9e9126c87dad860451ec9a0 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 23 Apr 2025 18:13:13 +0900 Subject: [PATCH 161/164] Remove online ID equality TODO and add explanation as to why it's not required --- osu.Game/Screens/SelectV2/BeatmapCarousel.cs | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/osu.Game/Screens/SelectV2/BeatmapCarousel.cs b/osu.Game/Screens/SelectV2/BeatmapCarousel.cs index 9574a05762..80006fddd9 100644 --- a/osu.Game/Screens/SelectV2/BeatmapCarousel.cs +++ b/osu.Game/Screens/SelectV2/BeatmapCarousel.cs @@ -355,7 +355,13 @@ namespace osu.Game.Screens.SelectV2 protected override bool CheckModelEquality(object x, object y) { - // TODO: this doesn't check online ID. probably need to account for that. + // In the confines of the carousel logic, we assume that CurrentSelection (and all items) are using non-stale + // BeatmapInfo reference, and that we can match based on beatmap / beatmapset (GU)IDs. + // + // If there's a case where updates don't come in as expected, diagnosis should start from BeatmapStore, ensuring + // it is doing a Replace operation on the list. If it is, then check the local handling in beatmapSetsChanged + // before changing matching requirements here. + if (x is BeatmapSetInfo beatmapSetX && y is BeatmapSetInfo beatmapSetY) return beatmapSetX.Equals(beatmapSetY); From 1488a49dae187f978313d19e1a43307a233d1eaf Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 23 Apr 2025 18:21:17 +0900 Subject: [PATCH 162/164] Ensure online ID has a valid online value before preferring it --- osu.Game/Screens/SelectV2/BeatmapCarousel.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/SelectV2/BeatmapCarousel.cs b/osu.Game/Screens/SelectV2/BeatmapCarousel.cs index 80006fddd9..4af5e759a7 100644 --- a/osu.Game/Screens/SelectV2/BeatmapCarousel.cs +++ b/osu.Game/Screens/SelectV2/BeatmapCarousel.cs @@ -114,7 +114,7 @@ namespace osu.Game.Screens.SelectV2 Debug.Assert(previousIndex >= 0); BeatmapInfo? matchingNewBeatmap = - newSetBeatmaps.SingleOrDefault(b => b.OnlineID == beatmap.OnlineID) ?? + newSetBeatmaps.SingleOrDefault(b => b.OnlineID > 0 && b.OnlineID == beatmap.OnlineID) ?? newSetBeatmaps.SingleOrDefault(b => b.DifficultyName == beatmap.DifficultyName && b.Ruleset.Equals(beatmap.Ruleset)); if (matchingNewBeatmap != null) From 655861752dad598ca3edc3b8daa444ecdc011d74 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Wed, 23 Apr 2025 18:27:59 +0900 Subject: [PATCH 163/164] Move implementation to base class --- .../Playlists/PlaylistItemResultsScreen.cs | 8 ++++++++ .../Playlists/PlaylistItemUserBestResultsScreen.cs | 13 ------------- 2 files changed, 8 insertions(+), 13 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistItemResultsScreen.cs b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistItemResultsScreen.cs index 0e539936d8..e994299606 100644 --- a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistItemResultsScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistItemResultsScreen.cs @@ -19,6 +19,7 @@ using osu.Game.Online.API.Requests.Responses; using osu.Game.Online.Rooms; using osu.Game.Rulesets; using osu.Game.Scoring; +using osu.Game.Screens.Backgrounds; using osu.Game.Screens.Ranking; namespace osu.Game.Screens.OnlinePlay.Playlists @@ -34,6 +35,7 @@ namespace osu.Game.Screens.OnlinePlay.Playlists private MultiplayerScores? higherScores; private MultiplayerScores? lowerScores; + private WorkingBeatmap itemBeatmap = null!; [Resolved] protected IAPIProvider API { get; private set; } = null!; @@ -60,6 +62,10 @@ namespace osu.Game.Screens.OnlinePlay.Playlists [BackgroundDependencyLoader] private void load() { + var localBeatmap = beatmapManager.QueryBeatmap($@"{nameof(BeatmapInfo.OnlineID)} == $0 AND {nameof(BeatmapInfo.MD5Hash)} == {nameof(BeatmapInfo.OnlineMD5Hash)}", + PlaylistItem.Beatmap.OnlineID); + itemBeatmap = beatmapManager.GetWorkingBeatmap(localBeatmap); + AddInternal(new Container { RelativeSizeAxes = Axes.Both, @@ -307,6 +313,8 @@ namespace osu.Game.Screens.OnlinePlay.Playlists } } + protected override BackgroundScreen CreateBackground() => new BackgroundScreenBeatmap(itemBeatmap); + private partial class PanelListLoadingSpinner : LoadingSpinner { private readonly ScorePanelList list; diff --git a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistItemUserBestResultsScreen.cs b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistItemUserBestResultsScreen.cs index c5cea5fef1..866b094178 100644 --- a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistItemUserBestResultsScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistItemUserBestResultsScreen.cs @@ -2,12 +2,9 @@ // See the LICENCE file in the repository root for full licence text. using System.Linq; -using osu.Framework.Allocation; -using osu.Game.Beatmaps; using osu.Game.Online.API; using osu.Game.Online.Rooms; using osu.Game.Scoring; -using osu.Game.Screens.Backgrounds; namespace osu.Game.Screens.OnlinePlay.Playlists { @@ -17,7 +14,6 @@ namespace osu.Game.Screens.OnlinePlay.Playlists public partial class PlaylistItemUserBestResultsScreen : PlaylistItemResultsScreen { private readonly int userId; - private WorkingBeatmap itemBeatmap = null!; public PlaylistItemUserBestResultsScreen(long roomId, PlaylistItem playlistItem, int userId) : base(null, roomId, playlistItem) @@ -25,13 +21,6 @@ namespace osu.Game.Screens.OnlinePlay.Playlists this.userId = userId; } - [BackgroundDependencyLoader] - private void load(BeatmapManager beatmaps) - { - var localBeatmap = beatmaps.QueryBeatmap($@"{nameof(BeatmapInfo.OnlineID)} == $0 AND {nameof(BeatmapInfo.MD5Hash)} == {nameof(BeatmapInfo.OnlineMD5Hash)}", PlaylistItem.Beatmap.OnlineID); - itemBeatmap = beatmaps.GetWorkingBeatmap(localBeatmap); - } - protected override APIRequest CreateScoreRequest() => new ShowPlaylistUserScoreRequest(RoomId, PlaylistItem.ID, userId); protected override void OnScoresAdded(ScoreInfo[] scores) @@ -41,7 +30,5 @@ namespace osu.Game.Screens.OnlinePlay.Playlists // Prefer selecting the local user's score, or otherwise default to the first visible score. SelectedScore.Value ??= scores.FirstOrDefault(s => s.UserID == userId) ?? scores.FirstOrDefault(); } - - protected override BackgroundScreen CreateBackground() => new BackgroundScreenBeatmap(itemBeatmap); } } From 618ab4fec67c917e3c9416b313ec690d96c439d1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 23 Apr 2025 12:20:17 +0200 Subject: [PATCH 164/164] Fix beatmap wedge test failures Started failing after 5ad28a792b683191e5d21bbff04299766b4eb3b5, I'm guessing. --- .../Visual/SongSelectV2/TestSceneBeatmapTitleWedge.cs | 2 -- 1 file changed, 2 deletions(-) diff --git a/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapTitleWedge.cs b/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapTitleWedge.cs index 6a14ddc147..8b89de5fce 100644 --- a/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapTitleWedge.cs +++ b/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapTitleWedge.cs @@ -79,8 +79,6 @@ namespace osu.Game.Tests.Visual.SongSelectV2 selectBeatmap(null); AddAssert("check default title", () => titleWedge.DisplayedTitle == Beatmap.Default.BeatmapInfo.Metadata.Title); AddAssert("check default artist", () => titleWedge.DisplayedArtist == Beatmap.Default.BeatmapInfo.Metadata.Artist); - AddAssert("check empty version", () => string.IsNullOrEmpty(difficultyDisplay.DisplayedVersion.ToString())); - AddAssert("check empty author", () => string.IsNullOrEmpty(difficultyDisplay.DisplayedAuthor.ToString())); AddAssert("check no statistics", () => difficultyDisplay.ChildrenOfType().All(d => !d.Statistics.Any())); }