diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchLeaderboard.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchLeaderboard.cs index 38522db4d428..a88c6139803c 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchLeaderboard.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchLeaderboard.cs @@ -63,13 +63,21 @@ public override void SetUpSteps() { SelectedRoom.Value = new Room { RoomID = 3 }; - Child = new MatchLeaderboard(SelectedRoom.Value) + MatchLeaderboardScoresProvider scoresProvider; + + AddRange(new Drawable[] { - Origin = Anchor.Centre, - Anchor = Anchor.Centre, - Size = new Vector2(550f, 450f), - Scope = MatchLeaderboardScope.Overall, - }; + scoresProvider = new MatchLeaderboardScoresProvider(SelectedRoom.Value) + { + Scope = MatchLeaderboardScope.Overall, + }, + new MatchLeaderboard(scoresProvider) + { + Origin = Anchor.Centre, + Anchor = Anchor.Centre, + Size = new Vector2(550f, 450f), + } + }); }); } } diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapLeaderboard.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapLeaderboard.cs index c234cc8a9cc0..b6b5fc673bad 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapLeaderboard.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapLeaderboard.cs @@ -29,7 +29,8 @@ namespace osu.Game.Tests.Visual.SongSelect { public partial class TestSceneBeatmapLeaderboard : OsuTestScene { - private readonly FailableLeaderboard leaderboard; + private readonly BeatmapLeaderboard leaderboard; + private readonly FailableScoresProvider scoresProvider; [Cached(typeof(IDialogOverlay))] private readonly DialogOverlay dialogOverlay; @@ -58,12 +59,15 @@ public TestSceneBeatmapLeaderboard() { Depth = -1 }, - leaderboard = new FailableLeaderboard + scoresProvider = new FailableScoresProvider + { + Scope = BeatmapLeaderboardScope.Global + }, + leaderboard = new BeatmapLeaderboard(scoresProvider) { Origin = Anchor.Centre, Anchor = Anchor.Centre, Size = new Vector2(550f, 450f), - Scope = BeatmapLeaderboardScope.Global, } }); } @@ -73,14 +77,14 @@ public void TestLocalScoresDisplay() { BeatmapInfo beatmapInfo = null!; - AddStep(@"Set scope", () => leaderboard.Scope = BeatmapLeaderboardScope.Local); + AddStep(@"Set scope", () => scoresProvider.Scope = BeatmapLeaderboardScope.Local); AddStep(@"Set beatmap", () => { beatmapManager.Import(TestResources.GetQuickTestBeatmapForImport()).WaitSafely(); beatmapInfo = beatmapManager.GetAllUsableBeatmapSets().First().Beatmaps.First(); - leaderboard.BeatmapInfo = beatmapInfo; + scoresProvider.BeatmapInfo = beatmapInfo; }); clearScores(); @@ -102,14 +106,14 @@ public void TestLocalScoresDisplayOnBeatmapEdit() BeatmapInfo beatmapInfo = null!; string originalHash = string.Empty; - AddStep(@"Set scope", () => leaderboard.Scope = BeatmapLeaderboardScope.Local); + AddStep(@"Set scope", () => scoresProvider.Scope = BeatmapLeaderboardScope.Local); AddStep(@"Import beatmap", () => { beatmapManager.Import(TestResources.GetQuickTestBeatmapForImport()).WaitSafely(); beatmapInfo = beatmapManager.GetAllUsableBeatmapSets().First().Beatmaps.First(); - leaderboard.BeatmapInfo = beatmapInfo; + scoresProvider.BeatmapInfo = beatmapInfo; }); clearScores(); @@ -163,8 +167,8 @@ public void TestLocalScoresDisplayOnBeatmapEdit() [Test] public void TestGlobalScoresDisplay() { - AddStep(@"Set scope", () => leaderboard.Scope = BeatmapLeaderboardScope.Global); - AddStep(@"New Scores", () => leaderboard.SetScores(generateSampleScores(new BeatmapInfo()))); + AddStep(@"Set scope", () => scoresProvider.Scope = BeatmapLeaderboardScope.Global); + AddStep(@"New Scores", () => scoresProvider.SetScores(generateSampleScores(new BeatmapInfo()))); } [Test] @@ -177,19 +181,19 @@ public void TestPersonalBest() [Test] public void TestPlaceholderStates() { - AddStep("ensure no scores displayed", () => leaderboard.SetScores(null)); - - AddStep(@"Network failure", () => leaderboard.SetErrorState(LeaderboardState.NetworkFailure)); - AddStep(@"No supporter", () => leaderboard.SetErrorState(LeaderboardState.NotSupporter)); - AddStep(@"Not logged in", () => leaderboard.SetErrorState(LeaderboardState.NotLoggedIn)); - AddStep(@"Ruleset unavailable", () => leaderboard.SetErrorState(LeaderboardState.RulesetUnavailable)); - AddStep(@"Beatmap unavailable", () => leaderboard.SetErrorState(LeaderboardState.BeatmapUnavailable)); - AddStep(@"None selected", () => leaderboard.SetErrorState(LeaderboardState.NoneSelected)); + AddStep("ensure no scores displayed", () => scoresProvider.SetScores(null)); + + AddStep(@"Network failure", () => scoresProvider.SetErrorState(LeaderboardState.NetworkFailure)); + AddStep(@"No supporter", () => scoresProvider.SetErrorState(LeaderboardState.NotSupporter)); + AddStep(@"Not logged in", () => scoresProvider.SetErrorState(LeaderboardState.NotLoggedIn)); + AddStep(@"Ruleset unavailable", () => scoresProvider.SetErrorState(LeaderboardState.RulesetUnavailable)); + AddStep(@"Beatmap unavailable", () => scoresProvider.SetErrorState(LeaderboardState.BeatmapUnavailable)); + AddStep(@"None selected", () => scoresProvider.SetErrorState(LeaderboardState.NoneSelected)); } private void showPersonalBestWithNullPosition() { - leaderboard.SetScores(leaderboard.Scores, new ScoreInfo + scoresProvider.SetScores(scoresProvider.Scores, new ScoreInfo { Rank = ScoreRank.XH, Accuracy = 1, @@ -208,7 +212,7 @@ private void showPersonalBestWithNullPosition() private void showPersonalBest() { - leaderboard.SetScores(leaderboard.Scores, new ScoreInfo + scoresProvider.SetScores(scoresProvider.Scores, new ScoreInfo { Position = 999, Rank = ScoreRank.XH, @@ -458,10 +462,15 @@ private static ScoreInfo[] generateSampleScores(BeatmapInfo beatmapInfo) }; } - private partial class FailableLeaderboard : BeatmapLeaderboard + private partial class FailableScoresProvider : BeatmapLeaderboardScoresProvider { public new void SetErrorState(LeaderboardState state) => base.SetErrorState(state); - public new void SetScores(IEnumerable? scores, ScoreInfo? userScore = null) => base.SetScores(scores, userScore); + + public new void SetScores(IEnumerable? scores, ScoreInfo? userScore = null) + { + SetState(LeaderboardState.Retrieving); + base.SetScores(scores, userScore); + } } } } diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneDeleteLocalScore.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneDeleteLocalScore.cs index f7bdda6b575d..14dd2e2d4234 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneDeleteLocalScore.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneDeleteLocalScore.cs @@ -35,6 +35,7 @@ namespace osu.Game.Tests.Visual.UserInterface public partial class TestSceneDeleteLocalScore : OsuManualInputManagerTestScene { private readonly ContextMenuContainer contextMenuContainer; + private readonly BeatmapLeaderboardScoresProvider scoresProvider; private readonly BeatmapLeaderboard leaderboard; private BeatmapManager beatmapManager; @@ -54,13 +55,19 @@ public TestSceneDeleteLocalScore() contextMenuContainer = new OsuContextMenuContainer { RelativeSizeAxes = Axes.Both, - Child = leaderboard = new BeatmapLeaderboard + Children = new Drawable[] { - Origin = Anchor.Centre, - Anchor = Anchor.Centre, - Size = new Vector2(550f, 450f), - Scope = BeatmapLeaderboardScope.Local, - BeatmapInfo = TestResources.CreateTestBeatmapSetInfo().Beatmaps.First() + scoresProvider = new BeatmapLeaderboardScoresProvider + { + Scope = BeatmapLeaderboardScope.Local, + BeatmapInfo = TestResources.CreateTestBeatmapSetInfo().Beatmaps.First() + }, + leaderboard = new BeatmapLeaderboard(scoresProvider) + { + Origin = Anchor.Centre, + Anchor = Anchor.Centre, + Size = new Vector2(550f, 450f), + } } }, dialogOverlay = new DialogOverlay() @@ -123,8 +130,8 @@ public void SetupSteps() }); AddStep("set up leaderboard", () => { - leaderboard.BeatmapInfo = beatmapInfo; - leaderboard.RefetchScores(); // Required in the case that the beatmap hasn't changed + scoresProvider.BeatmapInfo = beatmapInfo; + scoresProvider.RefetchScores(); // Required in the case that the beatmap hasn't changed }); // Ensure the leaderboard items have finished showing up @@ -164,8 +171,8 @@ public void TestDeleteViaRightClick() InputManager.PressButton(MouseButton.Left); }); - AddUntilStep("wait for fetch", () => leaderboard.Scores.Any()); - AddUntilStep("score removed from leaderboard", () => leaderboard.Scores.All(s => s.OnlineID != scoreBeingDeleted.OnlineID)); + AddUntilStep("wait for fetch", () => scoresProvider.Scores.Any()); + AddUntilStep("score removed from leaderboard", () => scoresProvider.Scores.All(s => s.OnlineID != scoreBeingDeleted.OnlineID)); // "Clean up" AddStep("release left mouse button", () => InputManager.ReleaseButton(MouseButton.Left)); @@ -175,8 +182,8 @@ public void TestDeleteViaRightClick() public void TestDeleteViaDatabase() { AddStep("delete top score", () => scoreManager.Delete(importedScores[0])); - AddUntilStep("wait for fetch", () => leaderboard.Scores.Any()); - AddUntilStep("score removed from leaderboard", () => leaderboard.Scores.All(s => s.OnlineID != importedScores[0].OnlineID)); + AddUntilStep("wait for fetch", () => scoresProvider.Scores.Any()); + AddUntilStep("score removed from leaderboard", () => scoresProvider.Scores.All(s => s.OnlineID != importedScores[0].OnlineID)); } } } diff --git a/osu.Game/Online/Leaderboards/Leaderboard.cs b/osu.Game/Online/Leaderboards/Leaderboard.cs index d76da54adf31..f0335e6d0dbd 100644 --- a/osu.Game/Online/Leaderboards/Leaderboard.cs +++ b/osu.Game/Online/Leaderboards/Leaderboard.cs @@ -2,13 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using System; -using System.Collections.Generic; -using System.Diagnostics; using System.Linq; -using System.Threading; -using osu.Framework.Allocation; -using osu.Framework.Bindables; -using osu.Framework.Development; using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Colour; @@ -17,11 +11,11 @@ using osu.Game.Graphics.Containers; using osu.Game.Graphics.Cursor; using osu.Game.Graphics.UserInterface; -using osu.Game.Online.API; using osu.Game.Online.Placeholders; using osuTK; using osuTK.Graphics; using osu.Game.Localisation; +using System.Threading; namespace osu.Game.Online.Leaderboards { @@ -33,17 +27,7 @@ namespace osu.Game.Online.Leaderboards /// The score model class. public abstract partial class Leaderboard : CompositeDrawable { - /// - /// The currently displayed scores. - /// - public IBindableList Scores => scores; - - private readonly BindableList scores = new BindableList(); - - /// - /// Whether the current scope should refetch in response to changes in API connectivity state. - /// - protected abstract bool IsOnlineScope { get; } + protected LeaderboardScoresProvider LeaderboardScoresProvider; private const double fade_duration = 300; @@ -55,35 +39,12 @@ public abstract partial class Leaderboard : CompositeDrawabl private readonly LoadingSpinner loading; - private CancellationTokenSource? currentFetchCancellationSource; private CancellationTokenSource? currentScoresAsyncLoadCancellationSource; - private APIRequest? fetchScoresRequest; - - private LeaderboardState state; - - [Resolved(CanBeNull = true)] - private IAPIProvider? api { get; set; } - - private readonly IBindable apiState = new Bindable(); - - private TScope scope = default!; - - public TScope Scope + protected Leaderboard(LeaderboardScoresProvider leaderboardScoresProvider) { - get => scope; - set - { - if (EqualityComparer.Default.Equals(value, scope)) - return; + LeaderboardScoresProvider = leaderboardScoresProvider; - scope = value; - RefetchScores(); - } - } - - protected Leaderboard() - { InternalChildren = new Drawable[] { new OsuContextMenuContainer @@ -127,137 +88,31 @@ protected override void LoadComplete() { base.LoadComplete(); - if (api != null) - { - apiState.BindTo(api.State); - apiState.BindValueChanged(state => - { - switch (state.NewValue) - { - case APIState.Online: - case APIState.Offline: - if (IsOnlineScope) - RefetchScores(); - - break; - } - }); - } - - RefetchScores(); + LeaderboardScoresProvider.State.BindValueChanged(state => onStateChange(state.NewValue)); } - /// - /// Perform a full refetch of scores using current criteria. - /// - public void RefetchScores() => Scheduler.AddOnce(refetchScores); - - /// - /// Clear all scores from the display. - /// - public void ClearScores() + private void onStateChange(LeaderboardState state) { - cancelPendingWork(); - SetScores(null); - } - - /// - /// Call when a retrieval or display failure happened to show a relevant message to the user. - /// - /// The state to display. - protected void SetErrorState(LeaderboardState state) - { - switch (state) - { - case LeaderboardState.NoScores: - case LeaderboardState.Retrieving: - case LeaderboardState.Success: - throw new InvalidOperationException($"State {state} cannot be set by a leaderboard implementation."); - } - - Debug.Assert(!scores.Any()); - - setState(state); - } - - /// - /// Call when retrieved scores are ready to be displayed. - /// - /// The scores to display. - /// The user top score, if any. - protected void SetScores(IEnumerable? scores, TScoreInfo? userScore = default) - { - this.scores.Clear(); - if (scores != null) - this.scores.AddRange(scores); - - // Non-delayed schedule may potentially run inline (due to IsMainThread check passing) after leaderboard is disposed. - // This is guarded against in BeatmapLeaderboard via web request cancellation, but let's be extra safe. - if (!IsDisposed) - { - // Schedule needs to be non-delayed here for the weird logic in refetchScores to work. - // If it is removed, the placeholder will be incorrectly updated to "no scores" rather than "retrieving". - // This whole flow should be refactored in the future. - Scheduler.Add(applyNewScores, false); - } + Schedule(applyNewScores); void applyNewScores() { - userScoreContainer.Score.Value = userScore; + userScoreContainer.Score.Value = LeaderboardScoresProvider.UserScore; - if (userScore == null) + if (LeaderboardScoresProvider.UserScore == null) userScoreContainer.Hide(); else userScoreContainer.Show(); - updateScoresDrawables(); + updateScoresDrawables(state); } } - /// - /// Performs a fetch/refresh of scores to be displayed. - /// - /// - /// An responsible for the fetch operation. This will be queued and performed automatically. - protected abstract APIRequest? FetchScores(CancellationToken cancellationToken); - protected abstract LeaderboardScore CreateDrawableScore(TScoreInfo model, int index); protected abstract LeaderboardScore CreateDrawableTopScore(TScoreInfo model); - private void refetchScores() - { - Debug.Assert(ThreadSafety.IsUpdateThread); - - ClearScores(); - setState(LeaderboardState.Retrieving); - - currentFetchCancellationSource = new CancellationTokenSource(); - - fetchScoresRequest = FetchScores(currentFetchCancellationSource.Token); - - if (fetchScoresRequest == null) - return; - - fetchScoresRequest.Failure += e => Schedule(() => - { - if (e is OperationCanceledException || currentFetchCancellationSource.IsCancellationRequested) - return; - - SetErrorState(LeaderboardState.NetworkFailure); - }); - - api?.Queue(fetchScoresRequest); - } - - private void cancelPendingWork() - { - currentFetchCancellationSource?.Cancel(); - currentScoresAsyncLoadCancellationSource?.Cancel(); - fetchScoresRequest?.Cancel(); - } - - private void updateScoresDrawables() + private void updateScoresDrawables(LeaderboardState state) { currentScoresAsyncLoadCancellationSource?.Cancel(); @@ -266,9 +121,9 @@ private void updateScoresDrawables() .Expire(); scoreFlowContainer = null; - if (!scores.Any()) + if (!LeaderboardScoresProvider.Scores.Any()) { - setState(LeaderboardState.NoScores); + setPlaceholder(state); return; } @@ -278,10 +133,10 @@ private void updateScoresDrawables() AutoSizeAxes = Axes.Y, Spacing = new Vector2(0f, 5f), Padding = new MarginPadding { Top = 10, Bottom = 5 }, - ChildrenEnumerable = scores.Select((s, index) => CreateDrawableScore(s, index + 1)) + ChildrenEnumerable = LeaderboardScoresProvider.Scores.Select((s, index) => CreateDrawableScore(s, index + 1)) }, newFlow => { - setState(LeaderboardState.Success); + setPlaceholder(state); scrollContainer.Add(scoreFlowContainer = newFlow); @@ -303,18 +158,13 @@ private void updateScoresDrawables() private Placeholder? placeholder; - private void setState(LeaderboardState state) + private void setPlaceholder(LeaderboardState state) { - if (state == this.state) - return; - if (state == LeaderboardState.Retrieving) loading.Show(); else loading.Hide(); - this.state = state; - placeholder?.FadeOut(150, Easing.OutQuint).Expire(); placeholder = getPlaceholderFor(state); @@ -335,7 +185,7 @@ private void setState(LeaderboardState state) case LeaderboardState.NetworkFailure: return new ClickablePlaceholder(LeaderboardStrings.CouldntFetchScores, FontAwesome.Solid.Sync) { - Action = RefetchScores + Action = LeaderboardScoresProvider.RefetchScores }; case LeaderboardState.NoneSelected: diff --git a/osu.Game/Online/Leaderboards/LeaderboardScoresProvider.cs b/osu.Game/Online/Leaderboards/LeaderboardScoresProvider.cs new file mode 100644 index 000000000000..0c484dda7c7c --- /dev/null +++ b/osu.Game/Online/Leaderboards/LeaderboardScoresProvider.cs @@ -0,0 +1,179 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Threading; +using osu.Framework.Allocation; +using osu.Framework.Bindables; +using osu.Framework.Development; +using osu.Framework.Graphics; +using osu.Game.Online.API; + +namespace osu.Game.Online.Leaderboards +{ + public abstract partial class LeaderboardScoresProvider : Component + { + public IBindableList Scores => scores; + + public TScoreInfo? UserScore { get; private set; } + + private readonly BindableList scores = new BindableList(); + + public IBindable State => state; + + private readonly Bindable state = new Bindable(); + + /// + /// Whether the current scope should refetch in response to changes in API connectivity state. + /// + public abstract bool IsOnlineScope { get; } + + private APIRequest? fetchScoresRequest; + + [Resolved(CanBeNull = true)] + private IAPIProvider? api { get; set; } + + private readonly IBindable apiState = new Bindable(); + + private CancellationTokenSource? currentFetchCancellationSource; + + private TScope scope = default!; + + public TScope Scope + { + get => scope; + set + { + if (EqualityComparer.Default.Equals(value, scope)) + return; + + scope = value; + RefetchScores(); + } + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + if (api != null) + { + apiState.BindTo(api.State); + apiState.BindValueChanged(state => + { + switch (state.NewValue) + { + case APIState.Online: + case APIState.Offline: + if (IsOnlineScope) + RefetchScores(); + + break; + } + }); + } + + RefetchScores(); + } + + /// + /// Perform a full refetch of scores using current criteria. + /// + public void RefetchScores() => Scheduler.AddOnce(refetchScores); + + private void refetchScores() + { + Debug.Assert(ThreadSafety.IsUpdateThread); + + PrepareScoresRetrieval(); + + currentFetchCancellationSource = new CancellationTokenSource(); + + fetchScoresRequest = FetchScores(currentFetchCancellationSource.Token); + + if (fetchScoresRequest == null) + return; + + fetchScoresRequest.Failure += e => Schedule(() => + { + if (e is OperationCanceledException || currentFetchCancellationSource.IsCancellationRequested) + return; + + SetErrorState(LeaderboardState.NetworkFailure); + }); + + api?.Queue(fetchScoresRequest); + } + + /// + /// Call when a retrieval of scores failed. + /// + /// The error state. + protected void SetErrorState(LeaderboardState state) + { + switch (state) + { + case LeaderboardState.NoScores: + case LeaderboardState.Retrieving: + case LeaderboardState.Success: + throw new InvalidOperationException($"State {state} cannot be set by a leaderboard implementation."); + } + + Debug.Assert(!scores.Any()); + + SetState(state); + } + + /// + /// Performs a fetch/refresh of scores of the leaderboard. + /// + /// + /// An responsible for the fetch operation. This will be queued and performed automatically. + protected abstract APIRequest? FetchScores(CancellationToken cancellationToken); + + protected void PrepareScoresRetrieval() + { + cancelPendingWork(); + + UserScore = default; + scores.Clear(); + SetState(LeaderboardState.Retrieving); + } + + private void cancelPendingWork() + { + currentFetchCancellationSource?.Cancel(); + fetchScoresRequest?.Cancel(); + } + + protected void SetState(LeaderboardState state) + { + if (state == this.state.Value) + return; + + this.state.Value = state; + } + + /// + /// Call when retrieved scores are ready to use by a leaderboard. + /// + /// The scores to use. + /// The user top score, if any. + protected void SetScores(IEnumerable? scores, TScoreInfo? userScore = default) + { + this.scores.Clear(); + if (scores != null) + this.scores.AddRange(scores); + + UserScore = userScore; + + if (!this.scores.Any()) + SetState(LeaderboardState.NoScores); + else + SetState(LeaderboardState.Success); + } + } +} diff --git a/osu.Game/Screens/OnlinePlay/Match/Components/MatchLeaderboard.cs b/osu.Game/Screens/OnlinePlay/Match/Components/MatchLeaderboard.cs index a7148abcdea1..ca49f9423e72 100644 --- a/osu.Game/Screens/OnlinePlay/Match/Components/MatchLeaderboard.cs +++ b/osu.Game/Screens/OnlinePlay/Match/Components/MatchLeaderboard.cs @@ -1,76 +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.ComponentModel; -using System.Threading; -using osu.Game.Online.API; using osu.Game.Online.API.Requests.Responses; using osu.Game.Online.Leaderboards; -using osu.Game.Online.Rooms; namespace osu.Game.Screens.OnlinePlay.Match.Components { public partial class MatchLeaderboard : Leaderboard { - private readonly Room room; - - public MatchLeaderboard(Room room) - { - this.room = room; - } - - protected override void LoadComplete() - { - base.LoadComplete(); - - room.PropertyChanged += onRoomPropertyChanged; - fetchInitialScores(); - } - - private void onRoomPropertyChanged(object? sender, PropertyChangedEventArgs e) - { - if (e.PropertyName == nameof(Room.RoomID)) - fetchInitialScores(); - } - - private void fetchInitialScores() + public MatchLeaderboard(MatchLeaderboardScoresProvider leaderboardScoresProvider) + : base(leaderboardScoresProvider) { - if (room.RoomID == null) - return; - - SetScores(null); - RefetchScores(); - } - - protected override bool IsOnlineScope => true; - - protected override APIRequest? FetchScores(CancellationToken cancellationToken) - { - if (room.RoomID == null) - return null; - - var req = new GetRoomLeaderboardRequest(room.RoomID.Value); - - req.Success += r => Schedule(() => - { - if (cancellationToken.IsCancellationRequested) - return; - - SetScores(r.Leaderboard, r.UserScore); - }); - - return req; } protected override LeaderboardScore CreateDrawableScore(APIUserScoreAggregate model, int index) => new MatchLeaderboardScore(model, index); protected override LeaderboardScore CreateDrawableTopScore(APIUserScoreAggregate model) => new MatchLeaderboardScore(model, model.Position, false); - - protected override void Dispose(bool isDisposing) - { - base.Dispose(isDisposing); - room.PropertyChanged -= onRoomPropertyChanged; - } } public enum MatchLeaderboardScope diff --git a/osu.Game/Screens/OnlinePlay/Match/Components/MatchLeaderboardScoresProvider.cs b/osu.Game/Screens/OnlinePlay/Match/Components/MatchLeaderboardScoresProvider.cs new file mode 100644 index 000000000000..299495cf99b4 --- /dev/null +++ b/osu.Game/Screens/OnlinePlay/Match/Components/MatchLeaderboardScoresProvider.cs @@ -0,0 +1,71 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.ComponentModel; +using System.Threading; +using osu.Game.Online.API; +using osu.Game.Online.API.Requests.Responses; +using osu.Game.Online.Leaderboards; +using osu.Game.Online.Rooms; + +namespace osu.Game.Screens.OnlinePlay.Match.Components +{ + public partial class MatchLeaderboardScoresProvider : LeaderboardScoresProvider + { + private readonly Room room; + + public MatchLeaderboardScoresProvider(Room room) + { + this.room = room; + } + + public override bool IsOnlineScope => true; + + protected override void LoadComplete() + { + base.LoadComplete(); + + room.PropertyChanged += onRoomPropertyChanged; + fetchInitialScores(); + } + + private void onRoomPropertyChanged(object? sender, PropertyChangedEventArgs e) + { + if (e.PropertyName == nameof(Room.RoomID)) + fetchInitialScores(); + } + + private void fetchInitialScores() + { + if (room.RoomID == null) + return; + + PrepareScoresRetrieval(); + RefetchScores(); + } + + protected override APIRequest? FetchScores(CancellationToken cancellationToken) + { + if (room.RoomID == null) + return null; + + var req = new GetRoomLeaderboardRequest(room.RoomID.Value); + + req.Success += r => Schedule(() => + { + if (cancellationToken.IsCancellationRequested) + return; + + SetScores(r.Leaderboard, r.UserScore); + }); + + return req; + } + + protected override void Dispose(bool isDisposing) + { + base.Dispose(isDisposing); + room.PropertyChanged -= onRoomPropertyChanged; + } + } +} diff --git a/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs b/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs index 4ef31c02c3ac..503143f6eac6 100644 --- a/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs @@ -95,6 +95,7 @@ public abstract partial class RoomSubScreen : OnlinePlaySubScreen, IPreviewTrack private IDisposable? userModsSelectOverlayRegistration; private RoomSettingsOverlay settingsOverlay = null!; private Drawable mainContent = null!; + protected MatchLeaderboardScoresProvider MatchScoresProvider = null!; /// /// Creates a new . @@ -119,6 +120,7 @@ private void load(AudioManager audio) RelativeSizeAxes = Axes.Both, Children = new Drawable[] { + MatchScoresProvider = new MatchLeaderboardScoresProvider(Room), beatmapAvailabilityTracker, new MultiplayerRoomSounds(), new GridContainer diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSongSelect.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSongSelect.cs index 4e03c190957e..95f801b4b981 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSongSelect.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSongSelect.cs @@ -120,7 +120,7 @@ protected override bool SelectItem(PlaylistItem item) return true; } - protected override BeatmapDetailArea CreateBeatmapDetailArea() => new PlayBeatmapDetailArea(); + protected override BeatmapDetailArea CreateBeatmapDetailArea() => new PlayBeatmapDetailArea(ScoresProvider); protected override bool IsValidMod(Mod mod) => base.IsValidMod(mod) && mod.ValidForMultiplayer; diff --git a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs index 9b4630ac0b09..d612b989ad27 100644 --- a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs @@ -41,7 +41,6 @@ public partial class PlaylistsRoomSubScreen : RoomSubScreen [Resolved(CanBeNull = true)] private IdleTracker? idleTracker { get; set; } - private MatchLeaderboard leaderboard = null!; private SelectionPollingComponent selectionPollingComponent = null!; private FillFlowContainer progressSection = null!; private DrawableRoomPlaylist drawablePlaylist = null!; @@ -226,7 +225,7 @@ private void updateRoomPlaylist() { new OverlinedHeader("Leaderboard") }, - new Drawable[] { leaderboard = new MatchLeaderboard(Room) { RelativeSizeAxes = Axes.Both }, }, + new Drawable[] { new MatchLeaderboard(MatchScoresProvider) { RelativeSizeAxes = Axes.Both }, }, }, RowDimensions = new[] { @@ -294,7 +293,7 @@ protected override Screen CreateGameplayScreen(PlaylistItem selectedItem) { return new PlayerLoader(() => new PlaylistsPlayer(Room, selectedItem) { - Exited = () => leaderboard.RefetchScores() + Exited = () => MatchScoresProvider.RefetchScores() }); } diff --git a/osu.Game/Screens/Play/SoloSpectatorPlayer.cs b/osu.Game/Screens/Play/SoloSpectatorPlayer.cs index be83a4c6b5ba..3b3f8f493da3 100644 --- a/osu.Game/Screens/Play/SoloSpectatorPlayer.cs +++ b/osu.Game/Screens/Play/SoloSpectatorPlayer.cs @@ -2,10 +2,12 @@ // See the LICENCE file in the repository root for full licence text. using osu.Framework.Allocation; +using osu.Framework.Bindables; using osu.Framework.Extensions.ObjectExtensions; using osu.Framework.Screens; using osu.Game.Online.Spectator; using osu.Game.Scoring; +using osu.Game.Screens.Play.HUD; using osu.Game.Users; namespace osu.Game.Screens.Play @@ -14,6 +16,8 @@ public partial class SoloSpectatorPlayer : SpectatorPlayer { private readonly Score score; + public readonly IBindableList LeaderboardScores = new BindableList(); + protected override UserActivity InitialActivity => new UserActivity.SpectatingUser(Score.ScoreInfo); public SoloSpectatorPlayer(Score score) @@ -45,6 +49,13 @@ private void userBeganPlaying(int userId, SpectatorState state) }); } + protected override GameplayLeaderboard CreateGameplayLeaderboard() => + new SoloGameplayLeaderboard(Score.ScoreInfo.User) + { + AlwaysVisible = { Value = true }, + Scores = { BindTarget = LeaderboardScores } + }; + protected override void Dispose(bool isDisposing) { base.Dispose(isDisposing); diff --git a/osu.Game/Screens/Play/SoloSpectatorScreen.cs b/osu.Game/Screens/Play/SoloSpectatorScreen.cs index 269bc3bb92a7..debbc7a735a1 100644 --- a/osu.Game/Screens/Play/SoloSpectatorScreen.cs +++ b/osu.Game/Screens/Play/SoloSpectatorScreen.cs @@ -24,6 +24,7 @@ using osu.Game.Overlays; using osu.Game.Overlays.Settings; using osu.Game.Screens.OnlinePlay.Match.Components; +using osu.Game.Screens.Select.Leaderboards; using osu.Game.Screens.Spectate; using osu.Game.Users; using osuTK; @@ -54,6 +55,8 @@ public partial class SoloSpectatorScreen : SpectatorScreen, IPreviewTrackOwner private readonly APIUser targetUser; + private BeatmapLeaderboardScoresProvider scoresProvider = null!; + /// /// The player's immediate online gameplay state. /// This doesn't always reflect the gameplay state being watched. @@ -157,6 +160,11 @@ private void load(OsuConfigManager config) } } }; + + AddInternal(scoresProvider = new BeatmapLeaderboardScoresProvider + { + Scope = BeatmapLeaderboardScope.Global + }); } protected override void LoadComplete() @@ -236,7 +244,14 @@ void start() Beatmap.Value = spectatorGameplayState.Beatmap; Ruleset.Value = spectatorGameplayState.Ruleset.RulesetInfo; - this.Push(new SpectatorPlayerLoader(spectatorGameplayState.Score, () => new SoloSpectatorPlayer(spectatorGameplayState.Score))); + scoresProvider.BeatmapInfo = Beatmap.Value is DummyWorkingBeatmap ? null : Beatmap.Value.BeatmapInfo; + + this.Push(new SpectatorPlayerLoader(spectatorGameplayState.Score, createPlayer)); + + SoloSpectatorPlayer createPlayer() => new SoloSpectatorPlayer(spectatorGameplayState.Score) + { + LeaderboardScores = { BindTarget = scoresProvider.Scores } + }; } } diff --git a/osu.Game/Screens/Select/Leaderboards/BeatmapLeaderboard.cs b/osu.Game/Screens/Select/Leaderboards/BeatmapLeaderboard.cs index 58c14b15b975..ba38d65e813c 100644 --- a/osu.Game/Screens/Select/Leaderboards/BeatmapLeaderboard.cs +++ b/osu.Game/Screens/Select/Leaderboards/BeatmapLeaderboard.cs @@ -2,22 +2,8 @@ // See the LICENCE file in the repository root for full licence text. using System; -using System.Collections.Generic; -using System.Diagnostics; -using System.Linq; -using System.Threading; -using osu.Framework.Allocation; -using osu.Framework.Bindables; -using osu.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 { @@ -25,151 +11,12 @@ public partial class BeatmapLeaderboard : Leaderboard? ScoreSelected; - private BeatmapInfo? beatmapInfo; - - public BeatmapInfo? BeatmapInfo - { - get => beatmapInfo; - set - { - if (beatmapInfo == null && value == null) - return; - - if (beatmapInfo?.Equals(value) == true) - return; - - beatmapInfo = value; - - // Refetch is scheduled, which can cause scores to be outdated if the leaderboard is not currently updating. - // As scores are potentially used by other components, clear them eagerly to ensure a more correct state. - SetScores(null); - - RefetchScores(); - } - } - - private bool filterMods; - - /// - /// Whether to apply the game's currently selected mods as a filter when retrieving scores. - /// - public bool FilterMods + public BeatmapLeaderboard(BeatmapLeaderboardScoresProvider leaderboardScoresProvider) + : base(leaderboardScoresProvider) { - get => filterMods; - set - { - if (value == filterMods) - return; - - filterMods = value; - - RefetchScores(); - } - } - - [Resolved] - private IBindable ruleset { get; set; } = null!; - - [Resolved] - private IBindable> mods { get; set; } = null!; - - [Resolved] - 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; - - [BackgroundDependencyLoader] - private void load() - { - ruleset.ValueChanged += _ => RefetchScores(); - mods.ValueChanged += _ => - { - if (filterMods) - RefetchScores(); - }; - } - - protected override bool IsOnlineScope => Scope != BeatmapLeaderboardScope.Local; - - protected override APIRequest? FetchScores(CancellationToken cancellationToken) - { - scoreRetrievalRequest?.Cancel(); - scoreRetrievalRequest = null; - - var fetchBeatmapInfo = BeatmapInfo; - - if (fetchBeatmapInfo == null) - { - SetErrorState(LeaderboardState.NoneSelected); - return null; - } - - var fetchRuleset = ruleset.Value ?? fetchBeatmapInfo.Ruleset; - - if (Scope == BeatmapLeaderboardScope.Local) - { - subscribeToLocalScores(fetchBeatmapInfo, cancellationToken); - return null; - } - - if (!api.IsLoggedIn) - { - SetErrorState(LeaderboardState.NotLoggedIn); - return null; - } - - if (!fetchRuleset.IsLegacyRuleset()) - { - SetErrorState(LeaderboardState.RulesetUnavailable); - return null; - } - - if (fetchBeatmapInfo.OnlineID <= 0 || fetchBeatmapInfo.Status <= BeatmapOnlineStatus.Pending) - { - SetErrorState(LeaderboardState.BeatmapUnavailable); - return null; - } - - if (!api.LocalUser.Value.IsSupporter && (Scope != BeatmapLeaderboardScope.Global || filterMods)) - { - SetErrorState(LeaderboardState.NotSupporter); - return null; - } - - IReadOnlyList? requestMods = null; - - 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; } - protected override LeaderboardScore CreateDrawableScore(ScoreInfo model, int index) => new LeaderboardScore(model, index, IsOnlineScope) + protected override LeaderboardScore CreateDrawableScore(ScoreInfo model, int index) => new LeaderboardScore(model, index, LeaderboardScoresProvider.IsOnlineScope) { Action = () => ScoreSelected?.Invoke(model) }; @@ -178,59 +25,5 @@ private void load() { 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(); - } } } diff --git a/osu.Game/Screens/Select/Leaderboards/BeatmapLeaderboardScoresProvider.cs b/osu.Game/Screens/Select/Leaderboards/BeatmapLeaderboardScoresProvider.cs new file mode 100644 index 000000000000..a8d2d091dfda --- /dev/null +++ b/osu.Game/Screens/Select/Leaderboards/BeatmapLeaderboardScoresProvider.cs @@ -0,0 +1,226 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Threading; +using osu.Framework.Allocation; +using osu.Framework.Bindables; +using osu.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 +{ + public partial class BeatmapLeaderboardScoresProvider : LeaderboardScoresProvider + { + private BeatmapInfo? beatmapInfo; + + public BeatmapInfo? BeatmapInfo + { + get => beatmapInfo; + set + { + if (beatmapInfo == null && value == null) + return; + + if (beatmapInfo?.Equals(value) == true) + return; + + beatmapInfo = value; + + // Refetch is scheduled, which can cause scores to be outdated if the leaderboard is not currently updating. + // As scores are potentially used by other components, clear them eagerly to ensure a more correct state. + PrepareScoresRetrieval(); + + RefetchScores(); + } + } + + private bool filterMods; + + /// + /// Whether to apply the game's currently selected mods as a filter when retrieving scores. + /// + public bool FilterMods + { + get => filterMods; + set + { + if (value == filterMods) + return; + + filterMods = value; + + RefetchScores(); + } + } + + public override bool IsOnlineScope => Scope != BeatmapLeaderboardScope.Local; + + [Resolved] + private IBindable ruleset { get; set; } = null!; + + [Resolved] + private IBindable> mods { get; set; } = null!; + + [Resolved] + private RulesetStore rulesets { get; set; } = null!; + + [Resolved] + private IAPIProvider api { get; set; } = null!; + + [Resolved] + private RealmAccess realm { get; set; } = null!; + + private GetScoresRequest? scoreRetrievalRequest; + + private IDisposable? scoreSubscription; + + [BackgroundDependencyLoader] + private void load() + { + ruleset.ValueChanged += _ => RefetchScores(); + mods.ValueChanged += _ => + { + if (filterMods) + RefetchScores(); + }; + } + + protected override APIRequest? FetchScores(CancellationToken cancellationToken) + { + scoreRetrievalRequest?.Cancel(); + scoreRetrievalRequest = null; + + var fetchBeatmapInfo = BeatmapInfo; + + if (fetchBeatmapInfo == null) + { + SetErrorState(LeaderboardState.NoneSelected); + return null; + } + + var fetchRuleset = ruleset.Value ?? fetchBeatmapInfo.Ruleset; + + if (Scope == BeatmapLeaderboardScope.Local) + { + subscribeToLocalScores(fetchBeatmapInfo, cancellationToken); + return null; + } + + if (!api.IsLoggedIn) + { + SetErrorState(LeaderboardState.NotLoggedIn); + return null; + } + + if (!fetchRuleset.IsLegacyRuleset()) + { + SetErrorState(LeaderboardState.RulesetUnavailable); + return null; + } + + if (fetchBeatmapInfo.OnlineID <= 0 || fetchBeatmapInfo.Status <= BeatmapOnlineStatus.Pending) + { + SetErrorState(LeaderboardState.BeatmapUnavailable); + return null; + } + + if (!api.LocalUser.Value.IsSupporter && (Scope != BeatmapLeaderboardScope.Global || filterMods)) + { + SetErrorState(LeaderboardState.NotSupporter); + return null; + } + + IReadOnlyList? requestMods = null; + + 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; + } + + 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; + + SetState(LeaderboardState.Retrieving); + + 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(); + } + } +} diff --git a/osu.Game/Screens/Select/PlayBeatmapDetailArea.cs b/osu.Game/Screens/Select/PlayBeatmapDetailArea.cs index deb1100dfc15..0af9176556f1 100644 --- a/osu.Game/Screens/Select/PlayBeatmapDetailArea.cs +++ b/osu.Game/Screens/Select/PlayBeatmapDetailArea.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System; using System.Linq; using osu.Framework.Allocation; @@ -10,13 +8,16 @@ using osu.Framework.Graphics; using osu.Game.Beatmaps; using osu.Game.Configuration; +using osu.Game.Scoring; using osu.Game.Screens.Select.Leaderboards; namespace osu.Game.Screens.Select { public partial class PlayBeatmapDetailArea : BeatmapDetailArea { - public readonly BeatmapLeaderboard Leaderboard; + private readonly BeatmapLeaderboardScoresProvider scoresProvider; + + private readonly BeatmapLeaderboard leaderboard; public override WorkingBeatmap Beatmap { @@ -25,17 +26,23 @@ public override WorkingBeatmap Beatmap { base.Beatmap = value; - Leaderboard.BeatmapInfo = value is DummyWorkingBeatmap ? null : value?.BeatmapInfo; + scoresProvider.BeatmapInfo = value is DummyWorkingBeatmap ? null : value.BeatmapInfo; } } - private Bindable selectedTab; + private Bindable selectedTab = null!; - private Bindable selectedModsFilter; + private Bindable selectedModsFilter = null!; - public PlayBeatmapDetailArea() + public PlayBeatmapDetailArea(BeatmapLeaderboardScoresProvider scoresProvider, Action? onScoreSelected = null) { - Add(Leaderboard = new BeatmapLeaderboard { RelativeSizeAxes = Axes.Both }); + this.scoresProvider = scoresProvider; + + Add(leaderboard = new BeatmapLeaderboard(scoresProvider) + { + ScoreSelected = onScoreSelected, + RelativeSizeAxes = Axes.Both + }); } [BackgroundDependencyLoader] @@ -55,24 +62,24 @@ public override void Refresh() { base.Refresh(); - Leaderboard.RefetchScores(); + scoresProvider.RefetchScores(); } protected override void OnTabChanged(BeatmapDetailAreaTabItem tab, bool selectedMods) { base.OnTabChanged(tab, selectedMods); - Leaderboard.FilterMods = selectedMods; + scoresProvider.FilterMods = selectedMods; switch (tab) { - case BeatmapDetailAreaLeaderboardTabItem leaderboard: - Leaderboard.Scope = leaderboard.Scope; - Leaderboard.Show(); + case BeatmapDetailAreaLeaderboardTabItem leaderboardTab: + scoresProvider.Scope = leaderboardTab.Scope; + leaderboard.Show(); break; default: - Leaderboard.Hide(); + leaderboard.Hide(); break; } } diff --git a/osu.Game/Screens/Select/PlaySongSelect.cs b/osu.Game/Screens/Select/PlaySongSelect.cs index 7b1479f392a8..754e20aabc26 100644 --- a/osu.Game/Screens/Select/PlaySongSelect.cs +++ b/osu.Game/Screens/Select/PlaySongSelect.cs @@ -58,13 +58,7 @@ protected void PresentScore(ScoreInfo score) => protected override BeatmapDetailArea CreateBeatmapDetailArea() { - playBeatmapDetailArea = new PlayBeatmapDetailArea - { - Leaderboard = - { - ScoreSelected = PresentScore - } - }; + playBeatmapDetailArea = new PlayBeatmapDetailArea(ScoresProvider, PresentScore); return playBeatmapDetailArea; } @@ -131,14 +125,14 @@ Player createPlayer() { player = new ReplayPlayer((beatmap, mods) => replayGeneratingMod.CreateScoreFromReplayData(beatmap, mods)) { - LeaderboardScores = { BindTarget = playBeatmapDetailArea.Leaderboard.Scores } + LeaderboardScores = { BindTarget = ScoresProvider.Scores } }; } else { player = new SoloPlayer { - LeaderboardScores = { BindTarget = playBeatmapDetailArea.Leaderboard.Scores } + LeaderboardScores = { BindTarget = ScoresProvider.Scores } }; } diff --git a/osu.Game/Screens/Select/SongSelect.cs b/osu.Game/Screens/Select/SongSelect.cs index 3dee1b983343..dcbba38b875b 100644 --- a/osu.Game/Screens/Select/SongSelect.cs +++ b/osu.Game/Screens/Select/SongSelect.cs @@ -39,6 +39,7 @@ using osu.Game.Screens.Menu; using osu.Game.Screens.Play; using osu.Game.Screens.Select.Details; +using osu.Game.Screens.Select.Leaderboards; using osu.Game.Screens.Select.Options; using osu.Game.Skinning; using osu.Game.Utils; @@ -151,6 +152,8 @@ public abstract partial class SongSelect : ScreenWithBeatmapBackground, IKeyBind [Resolved] internal IOverlayManager? OverlayManager { get; private set; } + protected BeatmapLeaderboardScoresProvider ScoresProvider = null!; + private Bindable configBackgroundBlur = null!; [BackgroundDependencyLoader(true)] @@ -170,6 +173,7 @@ private void load(AudioManager audio, OsuColour colours, ManageCollectionsDialog AddRangeInternal(new Drawable[] { + ScoresProvider = new BeatmapLeaderboardScoresProvider(), new GlobalScrollAdjustsVolume(), new VerticalMaskingContainer {