mirror of
https://github.com/osukey/osukey.git
synced 2025-08-07 00:23:59 +09:00
Merge pull request #20286 from peppy/gameplay-leaderboards
Add basic gameplay leaderboard display
This commit is contained in:
@ -0,0 +1,72 @@
|
|||||||
|
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. 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.Online.API.Requests.Responses;
|
||||||
|
using osu.Game.Rulesets.Osu;
|
||||||
|
using osu.Game.Rulesets.Scoring;
|
||||||
|
using osu.Game.Scoring;
|
||||||
|
using osu.Game.Screens.Play.HUD;
|
||||||
|
|
||||||
|
namespace osu.Game.Tests.Visual.Gameplay
|
||||||
|
{
|
||||||
|
public class TestSceneSoloGameplayLeaderboard : OsuTestScene
|
||||||
|
{
|
||||||
|
[Cached]
|
||||||
|
private readonly ScoreProcessor scoreProcessor = new ScoreProcessor(new OsuRuleset());
|
||||||
|
|
||||||
|
private readonly BindableList<ScoreInfo> scores = new BindableList<ScoreInfo>();
|
||||||
|
|
||||||
|
[SetUpSteps]
|
||||||
|
public void SetUpSteps()
|
||||||
|
{
|
||||||
|
AddStep("clear scores", () => scores.Clear());
|
||||||
|
|
||||||
|
AddStep("create component", () =>
|
||||||
|
{
|
||||||
|
var trackingUser = new APIUser
|
||||||
|
{
|
||||||
|
Username = "local user",
|
||||||
|
Id = 2,
|
||||||
|
};
|
||||||
|
|
||||||
|
Child = new SoloGameplayLeaderboard(trackingUser)
|
||||||
|
{
|
||||||
|
Scores = { BindTarget = scores },
|
||||||
|
Anchor = Anchor.Centre,
|
||||||
|
Origin = Anchor.Centre,
|
||||||
|
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, 1000, 0, v => scoreProcessor.Combo.Value = v);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static List<ScoreInfo> 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, 50).Select(i => new ScoreInfo { User = new APIUser { Username = $"User {i + 1}" }, TotalScore = 1000000 - i * 10000 })).ToList();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -120,6 +120,6 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
|||||||
=> AddStep($"set user {userId} time {time}", () => clocks[userId].CurrentTime = time);
|
=> AddStep($"set user {userId} time {time}", () => clocks[userId].CurrentTime = time);
|
||||||
|
|
||||||
private void assertCombo(int userId, int expectedCombo)
|
private void assertCombo(int userId, int expectedCombo)
|
||||||
=> AddUntilStep($"player {userId} has {expectedCombo} combo", () => this.ChildrenOfType<GameplayLeaderboardScore>().Single(s => s.User?.Id == userId).Combo.Value == expectedCombo);
|
=> AddUntilStep($"player {userId} has {expectedCombo} combo", () => this.ChildrenOfType<GameplayLeaderboardScore>().Single(s => s.User?.OnlineID == userId).Combo.Value == expectedCombo);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -522,7 +522,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
|||||||
|
|
||||||
private PlayerArea getInstance(int userId) => spectatorScreen.ChildrenOfType<PlayerArea>().Single(p => p.UserId == userId);
|
private PlayerArea getInstance(int userId) => spectatorScreen.ChildrenOfType<PlayerArea>().Single(p => p.UserId == userId);
|
||||||
|
|
||||||
private GameplayLeaderboardScore getLeaderboardScore(int userId) => spectatorScreen.ChildrenOfType<GameplayLeaderboardScore>().Single(s => s.User?.Id == userId);
|
private GameplayLeaderboardScore getLeaderboardScore(int userId) => spectatorScreen.ChildrenOfType<GameplayLeaderboardScore>().Single(s => s.User?.OnlineID == userId);
|
||||||
|
|
||||||
private int[] getPlayerIds(int count) => Enumerable.Range(PLAYER_1_ID, count).ToArray();
|
private int[] getPlayerIds(int count) => Enumerable.Range(PLAYER_1_ID, count).ToArray();
|
||||||
}
|
}
|
||||||
|
@ -39,7 +39,9 @@ namespace osu.Game.Online.Leaderboards
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// The currently displayed scores.
|
/// The currently displayed scores.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public IEnumerable<TScoreInfo> Scores => scores;
|
public IBindableList<TScoreInfo> Scores => scores;
|
||||||
|
|
||||||
|
private readonly BindableList<TScoreInfo> scores = new BindableList<TScoreInfo>();
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Whether the current scope should refetch in response to changes in API connectivity state.
|
/// Whether the current scope should refetch in response to changes in API connectivity state.
|
||||||
@ -68,8 +70,6 @@ namespace osu.Game.Online.Leaderboards
|
|||||||
|
|
||||||
private readonly IBindable<APIState> apiState = new Bindable<APIState>();
|
private readonly IBindable<APIState> apiState = new Bindable<APIState>();
|
||||||
|
|
||||||
private ICollection<TScoreInfo> scores;
|
|
||||||
|
|
||||||
private TScope scope;
|
private TScope scope;
|
||||||
|
|
||||||
public TScope Scope
|
public TScope Scope
|
||||||
@ -169,7 +169,7 @@ namespace osu.Game.Online.Leaderboards
|
|||||||
throw new InvalidOperationException($"State {state} cannot be set by a leaderboard implementation.");
|
throw new InvalidOperationException($"State {state} cannot be set by a leaderboard implementation.");
|
||||||
}
|
}
|
||||||
|
|
||||||
Debug.Assert(scores?.Any() != true);
|
Debug.Assert(!scores.Any());
|
||||||
|
|
||||||
setState(state);
|
setState(state);
|
||||||
}
|
}
|
||||||
@ -181,15 +181,26 @@ namespace osu.Game.Online.Leaderboards
|
|||||||
/// <param name="userScore">The user top score, if any.</param>
|
/// <param name="userScore">The user top score, if any.</param>
|
||||||
protected void SetScores(IEnumerable<TScoreInfo> scores, TScoreInfo userScore = default)
|
protected void SetScores(IEnumerable<TScoreInfo> scores, TScoreInfo userScore = default)
|
||||||
{
|
{
|
||||||
this.scores = scores?.ToList();
|
this.scores.Clear();
|
||||||
userScoreContainer.Score.Value = userScore;
|
if (scores != null)
|
||||||
|
this.scores.AddRange(scores);
|
||||||
|
|
||||||
if (userScore == null)
|
// Schedule needs to be non-delayed here for the weird logic in refetchScores to work.
|
||||||
userScoreContainer.Hide();
|
// If it is removed, the placeholder will be incorrectly updated to "no scores" rather than "retrieving".
|
||||||
else
|
// This whole flow should be refactored in the future.
|
||||||
userScoreContainer.Show();
|
Scheduler.Add(applyNewScores, false);
|
||||||
|
|
||||||
Scheduler.Add(updateScoresDrawables, false);
|
void applyNewScores()
|
||||||
|
{
|
||||||
|
userScoreContainer.Score.Value = userScore;
|
||||||
|
|
||||||
|
if (userScore == null)
|
||||||
|
userScoreContainer.Hide();
|
||||||
|
else
|
||||||
|
userScoreContainer.Show();
|
||||||
|
|
||||||
|
updateScoresDrawables();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@ -209,8 +220,8 @@ namespace osu.Game.Online.Leaderboards
|
|||||||
Debug.Assert(ThreadSafety.IsUpdateThread);
|
Debug.Assert(ThreadSafety.IsUpdateThread);
|
||||||
|
|
||||||
cancelPendingWork();
|
cancelPendingWork();
|
||||||
SetScores(null);
|
|
||||||
|
|
||||||
|
SetScores(null);
|
||||||
setState(LeaderboardState.Retrieving);
|
setState(LeaderboardState.Retrieving);
|
||||||
|
|
||||||
currentFetchCancellationSource = new CancellationTokenSource();
|
currentFetchCancellationSource = new CancellationTokenSource();
|
||||||
@ -247,7 +258,7 @@ namespace osu.Game.Online.Leaderboards
|
|||||||
.Expire();
|
.Expire();
|
||||||
scoreFlowContainer = null;
|
scoreFlowContainer = null;
|
||||||
|
|
||||||
if (scores?.Any() != true)
|
if (!scores.Any())
|
||||||
{
|
{
|
||||||
setState(LeaderboardState.NoScores);
|
setState(LeaderboardState.NoScores);
|
||||||
return;
|
return;
|
||||||
|
@ -9,8 +9,6 @@ using System.Linq;
|
|||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using osu.Framework.Allocation;
|
using osu.Framework.Allocation;
|
||||||
using osu.Framework.Bindables;
|
using osu.Framework.Bindables;
|
||||||
using osu.Framework.Graphics;
|
|
||||||
using osu.Framework.Graphics.Containers;
|
|
||||||
using osu.Framework.Logging;
|
using osu.Framework.Logging;
|
||||||
using osu.Framework.Screens;
|
using osu.Framework.Screens;
|
||||||
using osu.Game.Graphics.UserInterface;
|
using osu.Game.Graphics.UserInterface;
|
||||||
@ -21,7 +19,6 @@ using osu.Game.Screens.Play;
|
|||||||
using osu.Game.Screens.Play.HUD;
|
using osu.Game.Screens.Play.HUD;
|
||||||
using osu.Game.Screens.Ranking;
|
using osu.Game.Screens.Ranking;
|
||||||
using osu.Game.Users;
|
using osu.Game.Users;
|
||||||
using osuTK;
|
|
||||||
|
|
||||||
namespace osu.Game.Screens.OnlinePlay.Multiplayer
|
namespace osu.Game.Screens.OnlinePlay.Multiplayer
|
||||||
{
|
{
|
||||||
@ -41,14 +38,11 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
|
|||||||
|
|
||||||
private readonly TaskCompletionSource<bool> resultsReady = new TaskCompletionSource<bool>();
|
private readonly TaskCompletionSource<bool> resultsReady = new TaskCompletionSource<bool>();
|
||||||
|
|
||||||
private MultiplayerGameplayLeaderboard leaderboard;
|
|
||||||
|
|
||||||
private readonly MultiplayerRoomUser[] users;
|
private readonly MultiplayerRoomUser[] users;
|
||||||
|
|
||||||
private readonly Bindable<bool> leaderboardExpanded = new BindableBool();
|
|
||||||
|
|
||||||
private LoadingLayer loadingDisplay;
|
private LoadingLayer loadingDisplay;
|
||||||
private FillFlowContainer leaderboardFlow;
|
|
||||||
|
private MultiplayerGameplayLeaderboard multiplayerLeaderboard;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Construct a multiplayer player.
|
/// Construct a multiplayer player.
|
||||||
@ -62,7 +56,8 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
|
|||||||
AllowPause = false,
|
AllowPause = false,
|
||||||
AllowRestart = false,
|
AllowRestart = false,
|
||||||
AllowSkipping = room.AutoSkip.Value,
|
AllowSkipping = room.AutoSkip.Value,
|
||||||
AutomaticallySkipIntro = room.AutoSkip.Value
|
AutomaticallySkipIntro = room.AutoSkip.Value,
|
||||||
|
AlwaysShowLeaderboard = true,
|
||||||
})
|
})
|
||||||
{
|
{
|
||||||
this.users = users;
|
this.users = users;
|
||||||
@ -74,45 +69,33 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
|
|||||||
if (!LoadedBeatmapSuccessfully)
|
if (!LoadedBeatmapSuccessfully)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
HUDOverlay.Add(leaderboardFlow = new FillFlowContainer
|
|
||||||
{
|
|
||||||
AutoSizeAxes = Axes.Both,
|
|
||||||
Direction = FillDirection.Vertical,
|
|
||||||
Spacing = new Vector2(5)
|
|
||||||
});
|
|
||||||
|
|
||||||
HUDOverlay.HoldingForHUD.BindValueChanged(_ => updateLeaderboardExpandedState());
|
|
||||||
LocalUserPlaying.BindValueChanged(_ => updateLeaderboardExpandedState(), true);
|
|
||||||
|
|
||||||
// todo: this should be implemented via a custom HUD implementation, and correctly masked to the main content area.
|
|
||||||
LoadComponentAsync(leaderboard = new MultiplayerGameplayLeaderboard(users), l =>
|
|
||||||
{
|
|
||||||
if (!LoadedBeatmapSuccessfully)
|
|
||||||
return;
|
|
||||||
|
|
||||||
leaderboard.Expanded.BindTo(leaderboardExpanded);
|
|
||||||
|
|
||||||
leaderboardFlow.Insert(0, l);
|
|
||||||
|
|
||||||
if (leaderboard.TeamScores.Count >= 2)
|
|
||||||
{
|
|
||||||
LoadComponentAsync(new GameplayMatchScoreDisplay
|
|
||||||
{
|
|
||||||
Team1Score = { BindTarget = leaderboard.TeamScores.First().Value },
|
|
||||||
Team2Score = { BindTarget = leaderboard.TeamScores.Last().Value },
|
|
||||||
Expanded = { BindTarget = HUDOverlay.ShowHud },
|
|
||||||
}, scoreDisplay => leaderboardFlow.Insert(1, scoreDisplay));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
LoadComponentAsync(new GameplayChatDisplay(Room)
|
LoadComponentAsync(new GameplayChatDisplay(Room)
|
||||||
{
|
{
|
||||||
Expanded = { BindTarget = leaderboardExpanded },
|
Expanded = { BindTarget = LeaderboardExpandedState },
|
||||||
}, chat => leaderboardFlow.Insert(2, chat));
|
}, chat => HUDOverlay.LeaderboardFlow.Insert(2, chat));
|
||||||
|
|
||||||
HUDOverlay.Add(loadingDisplay = new LoadingLayer(true) { Depth = float.MaxValue });
|
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(new GameplayMatchScoreDisplay
|
||||||
|
{
|
||||||
|
Team1Score = { BindTarget = multiplayerLeaderboard.TeamScores.First().Value },
|
||||||
|
Team2Score = { BindTarget = multiplayerLeaderboard.TeamScores.Last().Value },
|
||||||
|
Expanded = { BindTarget = HUDOverlay.ShowHud },
|
||||||
|
}, scoreDisplay => HUDOverlay.LeaderboardFlow.Insert(1, scoreDisplay));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
protected override void LoadAsyncComplete()
|
protected override void LoadAsyncComplete()
|
||||||
{
|
{
|
||||||
base.LoadAsyncComplete();
|
base.LoadAsyncComplete();
|
||||||
@ -167,9 +150,6 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void updateLeaderboardExpandedState() =>
|
|
||||||
leaderboardExpanded.Value = !LocalUserPlaying.Value || HUDOverlay.HoldingForHUD.Value;
|
|
||||||
|
|
||||||
private void failAndBail(string message = null)
|
private void failAndBail(string message = null)
|
||||||
{
|
{
|
||||||
if (!string.IsNullOrEmpty(message))
|
if (!string.IsNullOrEmpty(message))
|
||||||
@ -178,23 +158,6 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
|
|||||||
Schedule(() => PerformExit(false));
|
Schedule(() => PerformExit(false));
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override void Update()
|
|
||||||
{
|
|
||||||
base.Update();
|
|
||||||
|
|
||||||
if (!LoadedBeatmapSuccessfully)
|
|
||||||
return;
|
|
||||||
|
|
||||||
adjustLeaderboardPosition();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void adjustLeaderboardPosition()
|
|
||||||
{
|
|
||||||
const float padding = 44; // enough margin to avoid the hit error display.
|
|
||||||
|
|
||||||
leaderboardFlow.Position = new Vector2(padding, padding + HUDOverlay.TopScoringElementsHeight);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void onGameplayStarted() => Scheduler.Add(() =>
|
private void onGameplayStarted() => Scheduler.Add(() =>
|
||||||
{
|
{
|
||||||
if (!this.IsCurrentScreen())
|
if (!this.IsCurrentScreen())
|
||||||
@ -232,8 +195,8 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
|
|||||||
{
|
{
|
||||||
Debug.Assert(Room.RoomID.Value != null);
|
Debug.Assert(Room.RoomID.Value != null);
|
||||||
|
|
||||||
return leaderboard.TeamScores.Count == 2
|
return multiplayerLeaderboard.TeamScores.Count == 2
|
||||||
? new MultiplayerTeamResultsScreen(score, Room.RoomID.Value.Value, PlaylistItem, leaderboard.TeamScores)
|
? new MultiplayerTeamResultsScreen(score, Room.RoomID.Value.Value, PlaylistItem, multiplayerLeaderboard.TeamScores)
|
||||||
: new MultiplayerResultsScreen(score, Room.RoomID.Value.Value, PlaylistItem);
|
: new MultiplayerResultsScreen(score, Room.RoomID.Value.Value, PlaylistItem);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,11 +1,8 @@
|
|||||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||||
// See the LICENCE file in the repository root for full licence text.
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
#nullable disable
|
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using JetBrains.Annotations;
|
|
||||||
using osu.Framework.Bindables;
|
using osu.Framework.Bindables;
|
||||||
using osu.Framework.Caching;
|
using osu.Framework.Caching;
|
||||||
using osu.Framework.Extensions.Color4Extensions;
|
using osu.Framework.Extensions.Color4Extensions;
|
||||||
@ -13,15 +10,14 @@ using osu.Framework.Graphics;
|
|||||||
using osu.Framework.Graphics.Colour;
|
using osu.Framework.Graphics.Colour;
|
||||||
using osu.Framework.Graphics.Containers;
|
using osu.Framework.Graphics.Containers;
|
||||||
using osu.Game.Graphics.Containers;
|
using osu.Game.Graphics.Containers;
|
||||||
using osu.Game.Online.API.Requests.Responses;
|
using osu.Game.Users;
|
||||||
using osuTK;
|
using osuTK;
|
||||||
using osuTK.Graphics;
|
using osuTK.Graphics;
|
||||||
|
|
||||||
namespace osu.Game.Screens.Play.HUD
|
namespace osu.Game.Screens.Play.HUD
|
||||||
{
|
{
|
||||||
public class GameplayLeaderboard : CompositeDrawable
|
public abstract class GameplayLeaderboard : CompositeDrawable
|
||||||
{
|
{
|
||||||
private readonly int maxPanels;
|
|
||||||
private readonly Cached sorting = new Cached();
|
private readonly Cached sorting = new Cached();
|
||||||
|
|
||||||
public Bindable<bool> Expanded = new Bindable<bool>();
|
public Bindable<bool> Expanded = new Bindable<bool>();
|
||||||
@ -31,16 +27,15 @@ namespace osu.Game.Screens.Play.HUD
|
|||||||
private bool requiresScroll;
|
private bool requiresScroll;
|
||||||
private readonly OsuScrollContainer scroll;
|
private readonly OsuScrollContainer scroll;
|
||||||
|
|
||||||
private GameplayLeaderboardScore trackedScore;
|
private GameplayLeaderboardScore? trackedScore;
|
||||||
|
|
||||||
|
private const int max_panels = 8;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Create a new leaderboard.
|
/// Create a new leaderboard.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="maxPanels">The maximum panels to show at once. Defines the maximum height of this component.</param>
|
protected GameplayLeaderboard()
|
||||||
public GameplayLeaderboard(int maxPanels = 8)
|
|
||||||
{
|
{
|
||||||
this.maxPanels = maxPanels;
|
|
||||||
|
|
||||||
Width = GameplayLeaderboardScore.EXTENDED_WIDTH + GameplayLeaderboardScore.SHEAR_WIDTH;
|
Width = GameplayLeaderboardScore.EXTENDED_WIDTH + GameplayLeaderboardScore.SHEAR_WIDTH;
|
||||||
|
|
||||||
InternalChildren = new Drawable[]
|
InternalChildren = new Drawable[]
|
||||||
@ -77,7 +72,7 @@ namespace osu.Game.Screens.Play.HUD
|
|||||||
/// Whether the player should be tracked on the leaderboard.
|
/// Whether the player should be tracked on the leaderboard.
|
||||||
/// Set to <c>true</c> for the local player or a player whose replay is currently being played.
|
/// Set to <c>true</c> for the local player or a player whose replay is currently being played.
|
||||||
/// </param>
|
/// </param>
|
||||||
public ILeaderboardScore Add([CanBeNull] APIUser user, bool isTracked)
|
public ILeaderboardScore Add(IUser? user, bool isTracked)
|
||||||
{
|
{
|
||||||
var drawable = CreateLeaderboardScoreDrawable(user, isTracked);
|
var drawable = CreateLeaderboardScoreDrawable(user, isTracked);
|
||||||
|
|
||||||
@ -93,8 +88,9 @@ namespace osu.Game.Screens.Play.HUD
|
|||||||
|
|
||||||
Flow.Add(drawable);
|
Flow.Add(drawable);
|
||||||
drawable.TotalScore.BindValueChanged(_ => sorting.Invalidate(), true);
|
drawable.TotalScore.BindValueChanged(_ => sorting.Invalidate(), true);
|
||||||
|
drawable.DisplayOrder.BindValueChanged(_ => sorting.Invalidate(), true);
|
||||||
|
|
||||||
int displayCount = Math.Min(Flow.Count, maxPanels);
|
int displayCount = Math.Min(Flow.Count, max_panels);
|
||||||
Height = displayCount * (GameplayLeaderboardScore.PANEL_HEIGHT + Flow.Spacing.Y);
|
Height = displayCount * (GameplayLeaderboardScore.PANEL_HEIGHT + Flow.Spacing.Y);
|
||||||
// Add extra margin space to flow equal to height of leaderboard.
|
// Add extra margin space to flow equal to height of leaderboard.
|
||||||
// This ensures the content is always on screen, but also accounts for the fact that scroll operations
|
// This ensures the content is always on screen, but also accounts for the fact that scroll operations
|
||||||
@ -116,7 +112,7 @@ namespace osu.Game.Screens.Play.HUD
|
|||||||
scroll.ScrollToStart(false);
|
scroll.ScrollToStart(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected virtual GameplayLeaderboardScore CreateLeaderboardScoreDrawable(APIUser user, bool isTracked) =>
|
protected virtual GameplayLeaderboardScore CreateLeaderboardScoreDrawable(IUser? user, bool isTracked) =>
|
||||||
new GameplayLeaderboardScore(user, isTracked);
|
new GameplayLeaderboardScore(user, isTracked);
|
||||||
|
|
||||||
protected override void Update()
|
protected override void Update()
|
||||||
@ -173,7 +169,10 @@ namespace osu.Game.Screens.Play.HUD
|
|||||||
if (sorting.IsValid)
|
if (sorting.IsValid)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
var orderedByScore = Flow.OrderByDescending(i => i.TotalScore.Value).ToList();
|
var orderedByScore = Flow
|
||||||
|
.OrderByDescending(i => i.TotalScore.Value)
|
||||||
|
.ThenBy(i => i.DisplayOrder.Value)
|
||||||
|
.ToList();
|
||||||
|
|
||||||
for (int i = 0; i < Flow.Count; i++)
|
for (int i = 0; i < Flow.Count; i++)
|
||||||
{
|
{
|
||||||
|
@ -12,7 +12,7 @@ using osu.Framework.Graphics.Containers;
|
|||||||
using osu.Framework.Graphics.Shapes;
|
using osu.Framework.Graphics.Shapes;
|
||||||
using osu.Game.Graphics;
|
using osu.Game.Graphics;
|
||||||
using osu.Game.Graphics.Sprites;
|
using osu.Game.Graphics.Sprites;
|
||||||
using osu.Game.Online.API.Requests.Responses;
|
using osu.Game.Users;
|
||||||
using osu.Game.Users.Drawables;
|
using osu.Game.Users.Drawables;
|
||||||
using osu.Game.Utils;
|
using osu.Game.Utils;
|
||||||
using osuTK;
|
using osuTK;
|
||||||
@ -55,6 +55,7 @@ namespace osu.Game.Screens.Play.HUD
|
|||||||
public BindableDouble Accuracy { get; } = new BindableDouble(1);
|
public BindableDouble Accuracy { get; } = new BindableDouble(1);
|
||||||
public BindableInt Combo { get; } = new BindableInt();
|
public BindableInt Combo { get; } = new BindableInt();
|
||||||
public BindableBool HasQuit { get; } = new BindableBool();
|
public BindableBool HasQuit { get; } = new BindableBool();
|
||||||
|
public Bindable<long> DisplayOrder { get; } = new Bindable<long>();
|
||||||
|
|
||||||
public Color4? BackgroundColour { get; set; }
|
public Color4? BackgroundColour { get; set; }
|
||||||
|
|
||||||
@ -81,7 +82,7 @@ namespace osu.Game.Screens.Play.HUD
|
|||||||
}
|
}
|
||||||
|
|
||||||
[CanBeNull]
|
[CanBeNull]
|
||||||
public APIUser User { get; }
|
public IUser User { get; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Whether this score is the local user or a replay player (and should be focused / always visible).
|
/// Whether this score is the local user or a replay player (and should be focused / always visible).
|
||||||
@ -103,7 +104,7 @@ namespace osu.Game.Screens.Play.HUD
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="user">The score's player.</param>
|
/// <param name="user">The score's player.</param>
|
||||||
/// <param name="tracked">Whether the player is the local user or a replay player.</param>
|
/// <param name="tracked">Whether the player is the local user or a replay player.</param>
|
||||||
public GameplayLeaderboardScore([CanBeNull] APIUser user, bool tracked)
|
public GameplayLeaderboardScore([CanBeNull] IUser user, bool tracked)
|
||||||
{
|
{
|
||||||
User = user;
|
User = user;
|
||||||
Tracked = tracked;
|
Tracked = tracked;
|
||||||
|
@ -14,5 +14,11 @@ namespace osu.Game.Screens.Play.HUD
|
|||||||
BindableInt Combo { get; }
|
BindableInt Combo { get; }
|
||||||
|
|
||||||
BindableBool HasQuit { get; }
|
BindableBool HasQuit { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// An optional value to guarantee stable ordering.
|
||||||
|
/// Lower numbers will appear higher in cases of <see cref="TotalScore"/> ties.
|
||||||
|
/// </summary>
|
||||||
|
Bindable<long> DisplayOrder { get; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -21,6 +21,7 @@ using osu.Game.Online.Multiplayer;
|
|||||||
using osu.Game.Online.Multiplayer.MatchTypes.TeamVersus;
|
using osu.Game.Online.Multiplayer.MatchTypes.TeamVersus;
|
||||||
using osu.Game.Online.Spectator;
|
using osu.Game.Online.Spectator;
|
||||||
using osu.Game.Rulesets.Scoring;
|
using osu.Game.Rulesets.Scoring;
|
||||||
|
using osu.Game.Users;
|
||||||
using osuTK.Graphics;
|
using osuTK.Graphics;
|
||||||
|
|
||||||
namespace osu.Game.Screens.Play.HUD
|
namespace osu.Game.Screens.Play.HUD
|
||||||
@ -125,11 +126,11 @@ namespace osu.Game.Screens.Play.HUD
|
|||||||
playingUserIds.BindCollectionChanged(playingUsersChanged);
|
playingUserIds.BindCollectionChanged(playingUsersChanged);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override GameplayLeaderboardScore CreateLeaderboardScoreDrawable(APIUser user, bool isTracked)
|
protected override GameplayLeaderboardScore CreateLeaderboardScoreDrawable(IUser user, bool isTracked)
|
||||||
{
|
{
|
||||||
var leaderboardScore = base.CreateLeaderboardScoreDrawable(user, isTracked);
|
var leaderboardScore = base.CreateLeaderboardScoreDrawable(user, isTracked);
|
||||||
|
|
||||||
if (UserScores[user.Id].Team is int team)
|
if (UserScores[user.OnlineID].Team is int team)
|
||||||
{
|
{
|
||||||
leaderboardScore.BackgroundColour = getTeamColour(team).Lighten(1.2f);
|
leaderboardScore.BackgroundColour = getTeamColour(team).Lighten(1.2f);
|
||||||
leaderboardScore.TextColour = Color4.White;
|
leaderboardScore.TextColour = Color4.White;
|
||||||
|
73
osu.Game/Screens/Play/HUD/SoloGameplayLeaderboard.cs
Normal file
73
osu.Game/Screens/Play/HUD/SoloGameplayLeaderboard.cs
Normal file
@ -0,0 +1,73 @@
|
|||||||
|
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||||
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using osu.Framework.Allocation;
|
||||||
|
using osu.Framework.Bindables;
|
||||||
|
using osu.Game.Rulesets.Scoring;
|
||||||
|
using osu.Game.Scoring;
|
||||||
|
using osu.Game.Users;
|
||||||
|
|
||||||
|
namespace osu.Game.Screens.Play.HUD
|
||||||
|
{
|
||||||
|
public class SoloGameplayLeaderboard : GameplayLeaderboard
|
||||||
|
{
|
||||||
|
private readonly IUser trackingUser;
|
||||||
|
|
||||||
|
public readonly IBindableList<ScoreInfo> Scores = new BindableList<ScoreInfo>();
|
||||||
|
|
||||||
|
// hold references to ensure bindables are updated.
|
||||||
|
private readonly List<Bindable<long>> scoreBindables = new List<Bindable<long>>();
|
||||||
|
|
||||||
|
[Resolved]
|
||||||
|
private ScoreProcessor scoreProcessor { get; set; } = null!;
|
||||||
|
|
||||||
|
[Resolved]
|
||||||
|
private ScoreManager scoreManager { get; set; } = null!;
|
||||||
|
|
||||||
|
public SoloGameplayLeaderboard(IUser trackingUser)
|
||||||
|
{
|
||||||
|
this.trackingUser = trackingUser;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void LoadComplete()
|
||||||
|
{
|
||||||
|
base.LoadComplete();
|
||||||
|
Scores.BindCollectionChanged((_, _) => Scheduler.AddOnce(showScores), true);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void showScores()
|
||||||
|
{
|
||||||
|
Clear();
|
||||||
|
scoreBindables.Clear();
|
||||||
|
|
||||||
|
if (!Scores.Any())
|
||||||
|
return;
|
||||||
|
|
||||||
|
foreach (var s in Scores)
|
||||||
|
{
|
||||||
|
var score = Add(s.User, false);
|
||||||
|
|
||||||
|
var bindableTotal = scoreManager.GetBindableTotalScore(s);
|
||||||
|
|
||||||
|
// Direct binding not possible due to differing types (see https://github.com/ppy/osu/issues/20298).
|
||||||
|
bindableTotal.BindValueChanged(total => score.TotalScore.Value = total.NewValue, true);
|
||||||
|
scoreBindables.Add(bindableTotal);
|
||||||
|
|
||||||
|
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.TotalScore.BindTarget = scoreProcessor.TotalScore;
|
||||||
|
local.Accuracy.BindTarget = scoreProcessor.Accuracy;
|
||||||
|
local.Combo.BindTarget = scoreProcessor.Combo;
|
||||||
|
|
||||||
|
// Local score should always show lower than any existing scores in cases of ties.
|
||||||
|
local.DisplayOrder.Value = long.MaxValue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -9,7 +9,6 @@ using System.Linq;
|
|||||||
using osu.Framework.Allocation;
|
using osu.Framework.Allocation;
|
||||||
using osu.Framework.Bindables;
|
using osu.Framework.Bindables;
|
||||||
using osu.Framework.Extensions.EnumExtensions;
|
using osu.Framework.Extensions.EnumExtensions;
|
||||||
using osu.Framework.Extensions.IEnumerableExtensions;
|
|
||||||
using osu.Framework.Graphics;
|
using osu.Framework.Graphics;
|
||||||
using osu.Framework.Graphics.Containers;
|
using osu.Framework.Graphics.Containers;
|
||||||
using osu.Framework.Input.Bindings;
|
using osu.Framework.Input.Bindings;
|
||||||
@ -35,11 +34,6 @@ namespace osu.Game.Screens.Play
|
|||||||
|
|
||||||
public const Easing FADE_EASING = Easing.OutQuint;
|
public const Easing FADE_EASING = Easing.OutQuint;
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// The total height of all the top of screen scoring elements.
|
|
||||||
/// </summary>
|
|
||||||
public float TopScoringElementsHeight { get; private set; }
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The total height of all the bottom of screen scoring elements.
|
/// The total height of all the bottom of screen scoring elements.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@ -80,9 +74,15 @@ namespace osu.Game.Screens.Play
|
|||||||
|
|
||||||
private readonly SkinnableTargetContainer mainComponents;
|
private readonly SkinnableTargetContainer mainComponents;
|
||||||
|
|
||||||
private IEnumerable<Drawable> hideTargets => new Drawable[] { mainComponents, KeyCounter, topRightElements };
|
/// <summary>
|
||||||
|
/// A flow which sits at the left side of the screen to house leaderboard (and related) components.
|
||||||
|
/// Will automatically be positioned to avoid colliding with top scoring elements.
|
||||||
|
/// </summary>
|
||||||
|
public readonly FillFlowContainer LeaderboardFlow;
|
||||||
|
|
||||||
public HUDOverlay(DrawableRuleset drawableRuleset, IReadOnlyList<Mod> mods)
|
private readonly List<Drawable> hideTargets;
|
||||||
|
|
||||||
|
public HUDOverlay(DrawableRuleset drawableRuleset, IReadOnlyList<Mod> mods, bool alwaysShowLeaderboard = true)
|
||||||
{
|
{
|
||||||
this.drawableRuleset = drawableRuleset;
|
this.drawableRuleset = drawableRuleset;
|
||||||
this.mods = mods;
|
this.mods = mods;
|
||||||
@ -127,8 +127,20 @@ namespace osu.Game.Screens.Play
|
|||||||
HoldToQuit = CreateHoldForMenuButton(),
|
HoldToQuit = CreateHoldForMenuButton(),
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
clicksPerSecondCalculator = new ClicksPerSecondCalculator()
|
LeaderboardFlow = new FillFlowContainer
|
||||||
|
{
|
||||||
|
AutoSizeAxes = Axes.Both,
|
||||||
|
Direction = FillDirection.Vertical,
|
||||||
|
Padding = new MarginPadding(44), // enough margin to avoid the hit error display
|
||||||
|
Spacing = new Vector2(5)
|
||||||
|
},
|
||||||
|
clicksPerSecondCalculator = new ClicksPerSecondCalculator(),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
hideTargets = new List<Drawable> { mainComponents, KeyCounter, topRightElements };
|
||||||
|
|
||||||
|
if (!alwaysShowLeaderboard)
|
||||||
|
hideTargets.Add(LeaderboardFlow);
|
||||||
}
|
}
|
||||||
|
|
||||||
[BackgroundDependencyLoader(true)]
|
[BackgroundDependencyLoader(true)]
|
||||||
@ -177,22 +189,36 @@ namespace osu.Game.Screens.Play
|
|||||||
{
|
{
|
||||||
base.Update();
|
base.Update();
|
||||||
|
|
||||||
Vector2? lowestTopScreenSpace = null;
|
float? lowestTopScreenSpaceLeft = null;
|
||||||
|
float? lowestTopScreenSpaceRight = null;
|
||||||
|
|
||||||
Vector2? highestBottomScreenSpace = null;
|
Vector2? highestBottomScreenSpace = null;
|
||||||
|
|
||||||
// LINQ cast can be removed when IDrawable interface includes Anchor / RelativeSizeAxes.
|
// LINQ cast can be removed when IDrawable interface includes Anchor / RelativeSizeAxes.
|
||||||
foreach (var element in mainComponents.Components.Cast<Drawable>())
|
foreach (var element in mainComponents.Components.Cast<Drawable>())
|
||||||
{
|
{
|
||||||
// for now align top-right components with the bottom-edge of the lowest top-anchored hud element.
|
// for now align some top components with the bottom-edge of the lowest top-anchored hud element.
|
||||||
if (element.Anchor.HasFlagFast(Anchor.TopRight) || (element.Anchor.HasFlagFast(Anchor.y0) && element.RelativeSizeAxes == Axes.X))
|
if (element.Anchor.HasFlagFast(Anchor.y0))
|
||||||
{
|
{
|
||||||
// health bars are excluded for the sake of hacky legacy skins which extend the health bar to take up the full screen area.
|
// health bars are excluded for the sake of hacky legacy skins which extend the health bar to take up the full screen area.
|
||||||
if (element is LegacyHealthDisplay)
|
if (element is LegacyHealthDisplay)
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
var bottomRight = element.ScreenSpaceDrawQuad.BottomRight;
|
float bottom = element.ScreenSpaceDrawQuad.BottomRight.Y;
|
||||||
if (lowestTopScreenSpace == null || bottomRight.Y > lowestTopScreenSpace.Value.Y)
|
|
||||||
lowestTopScreenSpace = bottomRight;
|
bool isRelativeX = element.RelativeSizeAxes == Axes.X;
|
||||||
|
|
||||||
|
if (element.Anchor.HasFlagFast(Anchor.TopRight) || isRelativeX)
|
||||||
|
{
|
||||||
|
if (lowestTopScreenSpaceRight == null || bottom > lowestTopScreenSpaceRight.Value)
|
||||||
|
lowestTopScreenSpaceRight = bottom;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (element.Anchor.HasFlagFast(Anchor.TopLeft) || isRelativeX)
|
||||||
|
{
|
||||||
|
if (lowestTopScreenSpaceLeft == null || bottom > lowestTopScreenSpaceLeft.Value)
|
||||||
|
lowestTopScreenSpaceLeft = bottom;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
// and align bottom-right components with the top-edge of the highest bottom-anchored hud element.
|
// and align bottom-right components with the top-edge of the highest bottom-anchored hud element.
|
||||||
else if (element.Anchor.HasFlagFast(Anchor.BottomRight) || (element.Anchor.HasFlagFast(Anchor.y2) && element.RelativeSizeAxes == Axes.X))
|
else if (element.Anchor.HasFlagFast(Anchor.BottomRight) || (element.Anchor.HasFlagFast(Anchor.y2) && element.RelativeSizeAxes == Axes.X))
|
||||||
@ -203,11 +229,16 @@ namespace osu.Game.Screens.Play
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (lowestTopScreenSpace.HasValue)
|
if (lowestTopScreenSpaceRight.HasValue)
|
||||||
topRightElements.Y = TopScoringElementsHeight = MathHelper.Clamp(ToLocalSpace(lowestTopScreenSpace.Value).Y, 0, DrawHeight - topRightElements.DrawHeight);
|
topRightElements.Y = MathHelper.Clamp(ToLocalSpace(new Vector2(0, lowestTopScreenSpaceRight.Value)).Y, 0, DrawHeight - topRightElements.DrawHeight);
|
||||||
else
|
else
|
||||||
topRightElements.Y = 0;
|
topRightElements.Y = 0;
|
||||||
|
|
||||||
|
if (lowestTopScreenSpaceLeft.HasValue)
|
||||||
|
LeaderboardFlow.Y = MathHelper.Clamp(ToLocalSpace(new Vector2(0, lowestTopScreenSpaceLeft.Value)).Y, 0, DrawHeight - LeaderboardFlow.DrawHeight);
|
||||||
|
else
|
||||||
|
LeaderboardFlow.Y = 0;
|
||||||
|
|
||||||
if (highestBottomScreenSpace.HasValue)
|
if (highestBottomScreenSpace.HasValue)
|
||||||
bottomRightElements.Y = BottomScoringElementsHeight = -MathHelper.Clamp(DrawHeight - ToLocalSpace(highestBottomScreenSpace.Value).Y, 0, DrawHeight - bottomRightElements.DrawHeight);
|
bottomRightElements.Y = BottomScoringElementsHeight = -MathHelper.Clamp(DrawHeight - ToLocalSpace(highestBottomScreenSpace.Value).Y, 0, DrawHeight - bottomRightElements.DrawHeight);
|
||||||
else
|
else
|
||||||
|
@ -9,6 +9,7 @@ using System.IO;
|
|||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
using JetBrains.Annotations;
|
||||||
using osu.Framework.Allocation;
|
using osu.Framework.Allocation;
|
||||||
using osu.Framework.Audio;
|
using osu.Framework.Audio;
|
||||||
using osu.Framework.Audio.Sample;
|
using osu.Framework.Audio.Sample;
|
||||||
@ -34,6 +35,7 @@ using osu.Game.Rulesets.Scoring;
|
|||||||
using osu.Game.Rulesets.UI;
|
using osu.Game.Rulesets.UI;
|
||||||
using osu.Game.Scoring;
|
using osu.Game.Scoring;
|
||||||
using osu.Game.Scoring.Legacy;
|
using osu.Game.Scoring.Legacy;
|
||||||
|
using osu.Game.Screens.Play.HUD;
|
||||||
using osu.Game.Screens.Ranking;
|
using osu.Game.Screens.Ranking;
|
||||||
using osu.Game.Skinning;
|
using osu.Game.Skinning;
|
||||||
using osu.Game.Users;
|
using osu.Game.Users;
|
||||||
@ -375,6 +377,8 @@ namespace osu.Game.Screens.Play
|
|||||||
|
|
||||||
if (Configuration.AutomaticallySkipIntro)
|
if (Configuration.AutomaticallySkipIntro)
|
||||||
skipIntroOverlay.SkipWhenReady();
|
skipIntroOverlay.SkipWhenReady();
|
||||||
|
|
||||||
|
loadLeaderboard();
|
||||||
}
|
}
|
||||||
|
|
||||||
protected virtual GameplayClockContainer CreateGameplayClockContainer(WorkingBeatmap beatmap, double gameplayStart) => new MasterGameplayClockContainer(beatmap, gameplayStart);
|
protected virtual GameplayClockContainer CreateGameplayClockContainer(WorkingBeatmap beatmap, double gameplayStart) => new MasterGameplayClockContainer(beatmap, gameplayStart);
|
||||||
@ -417,7 +421,7 @@ namespace osu.Game.Screens.Play
|
|||||||
// display the cursor above some HUD elements.
|
// display the cursor above some HUD elements.
|
||||||
DrawableRuleset.Cursor?.CreateProxy() ?? new Container(),
|
DrawableRuleset.Cursor?.CreateProxy() ?? new Container(),
|
||||||
DrawableRuleset.ResumeOverlay?.CreateProxy() ?? new Container(),
|
DrawableRuleset.ResumeOverlay?.CreateProxy() ?? new Container(),
|
||||||
HUDOverlay = new HUDOverlay(DrawableRuleset, GameplayState.Mods)
|
HUDOverlay = new HUDOverlay(DrawableRuleset, GameplayState.Mods, Configuration.AlwaysShowLeaderboard)
|
||||||
{
|
{
|
||||||
HoldToQuit =
|
HoldToQuit =
|
||||||
{
|
{
|
||||||
@ -820,6 +824,41 @@ namespace osu.Game.Screens.Play
|
|||||||
return mouseWheelDisabled.Value && !e.AltPressed;
|
return mouseWheelDisabled.Value && !e.AltPressed;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#region Gameplay leaderboard
|
||||||
|
|
||||||
|
protected readonly Bindable<bool> LeaderboardExpandedState = new BindableBool();
|
||||||
|
|
||||||
|
private void loadLeaderboard()
|
||||||
|
{
|
||||||
|
HUDOverlay.HoldingForHUD.BindValueChanged(_ => updateLeaderboardExpandedState());
|
||||||
|
LocalUserPlaying.BindValueChanged(_ => updateLeaderboardExpandedState(), true);
|
||||||
|
|
||||||
|
var gameplayLeaderboard = CreateGameplayLeaderboard();
|
||||||
|
|
||||||
|
if (gameplayLeaderboard != null)
|
||||||
|
{
|
||||||
|
LoadComponentAsync(gameplayLeaderboard, leaderboard =>
|
||||||
|
{
|
||||||
|
if (!LoadedBeatmapSuccessfully)
|
||||||
|
return;
|
||||||
|
|
||||||
|
leaderboard.Expanded.BindTo(LeaderboardExpandedState);
|
||||||
|
|
||||||
|
AddLeaderboardToHUD(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;
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
#region Fail Logic
|
#region Fail Logic
|
||||||
|
|
||||||
protected FailOverlay FailOverlay { get; private set; }
|
protected FailOverlay FailOverlay { get; private set; }
|
||||||
|
@ -36,5 +36,10 @@ namespace osu.Game.Screens.Play
|
|||||||
/// Whether the intro should be skipped by default.
|
/// Whether the intro should be skipped by default.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public bool AutomaticallySkipIntro { get; set; }
|
public bool AutomaticallySkipIntro { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Whether the gameplay leaderboard should always be shown (usually in a contracted state).
|
||||||
|
/// </summary>
|
||||||
|
public bool AlwaysShowLeaderboard { get; set; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -7,6 +7,7 @@ using System;
|
|||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
using osu.Framework.Bindables;
|
||||||
using osu.Framework.Input.Bindings;
|
using osu.Framework.Input.Bindings;
|
||||||
using osu.Framework.Input.Events;
|
using osu.Framework.Input.Events;
|
||||||
using osu.Game.Beatmaps;
|
using osu.Game.Beatmaps;
|
||||||
@ -14,6 +15,7 @@ using osu.Game.Input.Bindings;
|
|||||||
using osu.Game.Rulesets.Mods;
|
using osu.Game.Rulesets.Mods;
|
||||||
using osu.Game.Rulesets.Objects;
|
using osu.Game.Rulesets.Objects;
|
||||||
using osu.Game.Scoring;
|
using osu.Game.Scoring;
|
||||||
|
using osu.Game.Screens.Play.HUD;
|
||||||
using osu.Game.Screens.Ranking;
|
using osu.Game.Screens.Ranking;
|
||||||
|
|
||||||
namespace osu.Game.Screens.Play
|
namespace osu.Game.Screens.Play
|
||||||
@ -55,6 +57,14 @@ namespace osu.Game.Screens.Play
|
|||||||
// Don't re-import replay scores as they're already present in the database.
|
// Don't re-import replay scores as they're already present in the database.
|
||||||
protected override Task ImportScore(Score score) => Task.CompletedTask;
|
protected override Task ImportScore(Score score) => Task.CompletedTask;
|
||||||
|
|
||||||
|
public readonly BindableList<ScoreInfo> LeaderboardScores = new BindableList<ScoreInfo>();
|
||||||
|
|
||||||
|
protected override GameplayLeaderboard CreateGameplayLeaderboard() =>
|
||||||
|
new SoloGameplayLeaderboard(Score.ScoreInfo.User)
|
||||||
|
{
|
||||||
|
Scores = { BindTarget = LeaderboardScores }
|
||||||
|
};
|
||||||
|
|
||||||
protected override ResultsScreen CreateResults(ScoreInfo score) => new SoloResultsScreen(score, false);
|
protected override ResultsScreen CreateResults(ScoreInfo score) => new SoloResultsScreen(score, false);
|
||||||
|
|
||||||
public bool OnPressed(KeyBindingPressEvent<GlobalAction> e)
|
public bool OnPressed(KeyBindingPressEvent<GlobalAction> e)
|
||||||
|
@ -5,12 +5,15 @@
|
|||||||
|
|
||||||
using System;
|
using System;
|
||||||
using System.Diagnostics;
|
using System.Diagnostics;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using osu.Framework.Bindables;
|
||||||
using osu.Game.Beatmaps;
|
using osu.Game.Beatmaps;
|
||||||
using osu.Game.Extensions;
|
using osu.Game.Extensions;
|
||||||
using osu.Game.Online.API;
|
using osu.Game.Online.API;
|
||||||
using osu.Game.Online.Rooms;
|
using osu.Game.Online.Rooms;
|
||||||
using osu.Game.Online.Solo;
|
using osu.Game.Online.Solo;
|
||||||
using osu.Game.Scoring;
|
using osu.Game.Scoring;
|
||||||
|
using osu.Game.Screens.Play.HUD;
|
||||||
|
|
||||||
namespace osu.Game.Screens.Play
|
namespace osu.Game.Screens.Play
|
||||||
{
|
{
|
||||||
@ -40,8 +43,26 @@ namespace osu.Game.Screens.Play
|
|||||||
return new CreateSoloScoreRequest(Beatmap.Value.BeatmapInfo, rulesetId, Game.VersionHash);
|
return new CreateSoloScoreRequest(Beatmap.Value.BeatmapInfo, rulesetId, Game.VersionHash);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public readonly BindableList<ScoreInfo> LeaderboardScores = new BindableList<ScoreInfo>();
|
||||||
|
|
||||||
|
protected override GameplayLeaderboard CreateGameplayLeaderboard() =>
|
||||||
|
new SoloGameplayLeaderboard(Score.ScoreInfo.User)
|
||||||
|
{
|
||||||
|
Scores = { BindTarget = LeaderboardScores }
|
||||||
|
};
|
||||||
|
|
||||||
protected override bool HandleTokenRetrievalFailure(Exception exception) => false;
|
protected override bool HandleTokenRetrievalFailure(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).
|
||||||
|
LeaderboardScores.UnbindBindings();
|
||||||
|
|
||||||
|
return base.ImportScore(score);
|
||||||
|
}
|
||||||
|
|
||||||
protected override APIRequest<MultiplayerScore> CreateSubmissionRequest(Score score, long token)
|
protected override APIRequest<MultiplayerScore> CreateSubmissionRequest(Score score, long token)
|
||||||
{
|
{
|
||||||
IBeatmapInfo beatmap = score.ScoreInfo.BeatmapInfo;
|
IBeatmapInfo beatmap = score.ScoreInfo.BeatmapInfo;
|
||||||
|
@ -41,6 +41,11 @@ namespace osu.Game.Screens.Select.Leaderboards
|
|||||||
return;
|
return;
|
||||||
|
|
||||||
beatmapInfo = value;
|
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();
|
RefetchScores();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -148,12 +153,10 @@ namespace osu.Game.Screens.Select.Leaderboards
|
|||||||
|
|
||||||
var req = new GetScoresRequest(fetchBeatmapInfo, fetchRuleset, Scope, requestMods);
|
var req = new GetScoresRequest(fetchBeatmapInfo, fetchRuleset, Scope, requestMods);
|
||||||
|
|
||||||
req.Success += r => Schedule(() =>
|
req.Success += r => SetScores(
|
||||||
{
|
scoreManager.OrderByTotalScore(r.Scores.Select(s => s.ToScoreInfo(rulesets, fetchBeatmapInfo))),
|
||||||
SetScores(
|
r.UserScore?.CreateScoreInfo(rulesets, fetchBeatmapInfo)
|
||||||
scoreManager.OrderByTotalScore(r.Scores.Select(s => s.ToScoreInfo(rulesets, fetchBeatmapInfo))),
|
);
|
||||||
r.UserScore?.CreateScoreInfo(rulesets, fetchBeatmapInfo));
|
|
||||||
});
|
|
||||||
|
|
||||||
return req;
|
return req;
|
||||||
}
|
}
|
||||||
@ -208,7 +211,7 @@ namespace osu.Game.Screens.Select.Leaderboards
|
|||||||
|
|
||||||
scores = scoreManager.OrderByTotalScore(scores.Detach());
|
scores = scoreManager.OrderByTotalScore(scores.Detach());
|
||||||
|
|
||||||
Schedule(() => SetScores(scores));
|
SetScores(scores);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,8 +1,6 @@
|
|||||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||||
// See the LICENCE file in the repository root for full licence text.
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
#nullable disable
|
|
||||||
|
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using osu.Framework.Allocation;
|
using osu.Framework.Allocation;
|
||||||
@ -24,27 +22,38 @@ namespace osu.Game.Screens.Select
|
|||||||
{
|
{
|
||||||
public class PlaySongSelect : SongSelect
|
public class PlaySongSelect : SongSelect
|
||||||
{
|
{
|
||||||
private OsuScreen playerLoader;
|
private OsuScreen? playerLoader;
|
||||||
|
|
||||||
[Resolved(CanBeNull = true)]
|
[Resolved(CanBeNull = true)]
|
||||||
private INotificationOverlay notifications { get; set; }
|
private INotificationOverlay? notifications { get; set; }
|
||||||
|
|
||||||
public override bool AllowExternalScreenChange => true;
|
public override bool AllowExternalScreenChange => true;
|
||||||
|
|
||||||
protected override UserActivity InitialActivity => new UserActivity.ChoosingBeatmap();
|
protected override UserActivity InitialActivity => new UserActivity.ChoosingBeatmap();
|
||||||
|
|
||||||
|
private PlayBeatmapDetailArea playBeatmapDetailArea = null!;
|
||||||
|
|
||||||
[BackgroundDependencyLoader]
|
[BackgroundDependencyLoader]
|
||||||
private void load(OsuColour colours)
|
private void load(OsuColour colours)
|
||||||
{
|
{
|
||||||
BeatmapOptions.AddButton(@"Edit", @"beatmap", FontAwesome.Solid.PencilAlt, colours.Yellow, () => Edit());
|
BeatmapOptions.AddButton(@"Edit", @"beatmap", FontAwesome.Solid.PencilAlt, colours.Yellow, () => Edit());
|
||||||
|
|
||||||
((PlayBeatmapDetailArea)BeatmapDetails).Leaderboard.ScoreSelected += PresentScore;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected void PresentScore(ScoreInfo score) =>
|
protected void PresentScore(ScoreInfo score) =>
|
||||||
FinaliseSelection(score.BeatmapInfo, score.Ruleset, () => this.Push(new SoloResultsScreen(score, false)));
|
FinaliseSelection(score.BeatmapInfo, score.Ruleset, () => this.Push(new SoloResultsScreen(score, false)));
|
||||||
|
|
||||||
protected override BeatmapDetailArea CreateBeatmapDetailArea() => new PlayBeatmapDetailArea();
|
protected override BeatmapDetailArea CreateBeatmapDetailArea()
|
||||||
|
{
|
||||||
|
playBeatmapDetailArea = new PlayBeatmapDetailArea
|
||||||
|
{
|
||||||
|
Leaderboard =
|
||||||
|
{
|
||||||
|
ScoreSelected = PresentScore
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return playBeatmapDetailArea;
|
||||||
|
}
|
||||||
|
|
||||||
protected override bool OnKeyDown(KeyDownEvent e)
|
protected override bool OnKeyDown(KeyDownEvent e)
|
||||||
{
|
{
|
||||||
@ -61,9 +70,9 @@ namespace osu.Game.Screens.Select
|
|||||||
return base.OnKeyDown(e);
|
return base.OnKeyDown(e);
|
||||||
}
|
}
|
||||||
|
|
||||||
private IReadOnlyList<Mod> modsAtGameplayStart;
|
private IReadOnlyList<Mod>? modsAtGameplayStart;
|
||||||
|
|
||||||
private ModAutoplay getAutoplayMod() => Ruleset.Value.CreateInstance().GetAutoplayMod();
|
private ModAutoplay? getAutoplayMod() => Ruleset.Value.CreateInstance().GetAutoplayMod();
|
||||||
|
|
||||||
protected override bool OnStart()
|
protected override bool OnStart()
|
||||||
{
|
{
|
||||||
@ -100,14 +109,26 @@ namespace osu.Game.Screens.Select
|
|||||||
|
|
||||||
Player createPlayer()
|
Player createPlayer()
|
||||||
{
|
{
|
||||||
|
Player player;
|
||||||
|
|
||||||
var replayGeneratingMod = Mods.Value.OfType<ICreateReplayData>().FirstOrDefault();
|
var replayGeneratingMod = Mods.Value.OfType<ICreateReplayData>().FirstOrDefault();
|
||||||
|
|
||||||
if (replayGeneratingMod != null)
|
if (replayGeneratingMod != null)
|
||||||
{
|
{
|
||||||
return new ReplayPlayer((beatmap, mods) => replayGeneratingMod.CreateScoreFromReplayData(beatmap, mods));
|
player = new ReplayPlayer((beatmap, mods) => replayGeneratingMod.CreateScoreFromReplayData(beatmap, mods))
|
||||||
|
{
|
||||||
|
LeaderboardScores = { BindTarget = playBeatmapDetailArea.Leaderboard.Scores }
|
||||||
|
};
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
player = new SoloPlayer
|
||||||
|
{
|
||||||
|
LeaderboardScores = { BindTarget = playBeatmapDetailArea.Leaderboard.Scores }
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
return new SoloPlayer();
|
return player;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -14,13 +14,13 @@ namespace osu.Game.Users.Drawables
|
|||||||
[LongRunningLoad]
|
[LongRunningLoad]
|
||||||
public class DrawableAvatar : Sprite
|
public class DrawableAvatar : Sprite
|
||||||
{
|
{
|
||||||
private readonly APIUser user;
|
private readonly IUser user;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// A simple, non-interactable avatar sprite for the specified user.
|
/// A simple, non-interactable avatar sprite for the specified user.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="user">The user. A null value will get a placeholder avatar.</param>
|
/// <param name="user">The user. A null value will get a placeholder avatar.</param>
|
||||||
public DrawableAvatar(APIUser user = null)
|
public DrawableAvatar(IUser user = null)
|
||||||
{
|
{
|
||||||
this.user = user;
|
this.user = user;
|
||||||
|
|
||||||
@ -33,10 +33,10 @@ namespace osu.Game.Users.Drawables
|
|||||||
[BackgroundDependencyLoader]
|
[BackgroundDependencyLoader]
|
||||||
private void load(LargeTextureStore textures)
|
private void load(LargeTextureStore textures)
|
||||||
{
|
{
|
||||||
if (user != null && user.Id > 1)
|
if (user != null && user.OnlineID > 1)
|
||||||
// TODO: The fallback here should not need to exist. Users should be looked up and populated via UserLookupCache or otherwise
|
// TODO: The fallback here should not need to exist. Users should be looked up and populated via UserLookupCache or otherwise
|
||||||
// in remaining cases where this is required (chat tabs, local leaderboard), at which point this should be removed.
|
// in remaining cases where this is required (chat tabs, local leaderboard), at which point this should be removed.
|
||||||
Texture = textures.Get(user.AvatarUrl ?? $@"https://a.ppy.sh/{user.Id}");
|
Texture = textures.Get((user as APIUser)?.AvatarUrl ?? $@"https://a.ppy.sh/{user.OnlineID}");
|
||||||
|
|
||||||
Texture ??= textures.Get(@"Online/avatar-guest");
|
Texture ??= textures.Get(@"Online/avatar-guest");
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user