1
0
mirror of https://github.com/ppy/osu.git synced 2026-05-26 13:50:33 +08:00
Files
osu-lazer/osu.Game/Screens/Ranking/SoloResultsScreen.cs
T
Dean Herbert 0353b1461f Ensure that task source is not left hanging if work never completes
This could be the case if the results screen goes away and is no longer
running the update/scheduler loop.
2025-08-04 17:01:27 +09:00

165 lines
7.0 KiB
C#

// 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.Diagnostics;
using System.Linq;
using System.Threading.Tasks;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Logging;
using osu.Game.Extensions;
using osu.Game.Online.API;
using osu.Game.Online.Leaderboards;
using osu.Game.Scoring;
using osu.Game.Screens.Select.Leaderboards;
namespace osu.Game.Screens.Ranking
{
public partial class SoloResultsScreen : ResultsScreen
{
private readonly IBindable<LeaderboardScores?> globalScores = new Bindable<LeaderboardScores?>();
private TaskCompletionSource<LeaderboardScores>? requestTaskSource;
[Resolved]
private IAPIProvider api { get; set; } = null!;
[Resolved]
private LeaderboardManager leaderboardManager { get; set; } = null!;
public SoloResultsScreen(ScoreInfo score)
: base(score)
{
}
protected override void LoadComplete()
{
base.LoadComplete();
globalScores.BindTo(leaderboardManager.Scores);
}
protected override void Dispose(bool isDisposing)
{
base.Dispose(isDisposing);
if (requestTaskSource?.Task.IsCompleted == false)
requestTaskSource.SetCanceled();
}
protected override async Task<ScoreInfo[]> FetchScores()
{
Debug.Assert(Score != null);
// sort mode intentionally omitted to default to score - results screen only supports sorting by score, so don't pass any other to avoid confusion
var criteria = new LeaderboardCriteria(
Score.BeatmapInfo!,
Score.Ruleset,
leaderboardManager.CurrentCriteria?.Scope ?? BeatmapLeaderboardScope.Global,
leaderboardManager.CurrentCriteria?.ExactMods
);
Debug.Assert(requestTaskSource == null || requestTaskSource.Task.IsCompleted);
requestTaskSource = new TaskCompletionSource<LeaderboardScores>();
globalScores.BindValueChanged(_ =>
{
if (globalScores.Value != null && leaderboardManager.CurrentCriteria?.Equals(criteria) == true)
requestTaskSource.TrySetResult(globalScores.Value);
});
Schedule(() => leaderboardManager.FetchWithCriteria(criteria, forceRefresh: true));
var result = await requestTaskSource.Task.ConfigureAwait(false);
if (result.FailState != null)
{
Logger.Log($"Failed to fetch scores (beatmap: {Score.BeatmapInfo}, ruleset: {Score.Ruleset}): {result.FailState}");
return [];
}
var clonedScores = result.AllScores.Select(s => s.DeepClone()).ToArray();
List<ScoreInfo> sortedScores = [];
foreach (var clonedScore in clonedScores)
{
// ensure that we do not double up on the score being presented here.
// additionally, ensure that the reference that ends up in `sortedScores` is the `Score` reference specifically.
// this simplifies handling later.
if (clonedScore.Equals(Score) || clonedScore.MatchesOnlineID(Score))
{
// this is a precautionary guard that prevents `Score` from appearing multiple times in the list.
// that can occur in rare cases wherein two local scores have the same online ID but different replay contents
// (this is possible e.g. in cases of client-side vs server-side recorded replays, see https://github.com/ppy/osu-server-spectator/issues/193)
if (sortedScores.Contains(Score))
continue;
Score.Position = clonedScore.Position;
sortedScores.Add(Score);
}
else
{
bool isOnlineLeaderboard = criteria.Scope != BeatmapLeaderboardScope.Local;
bool presentingLocalUserScore = Score.UserID == api.LocalUser.Value.OnlineID;
bool presentedLocalUserScoreIsBetter = presentingLocalUserScore && clonedScore.UserID == api.LocalUser.Value.OnlineID && clonedScore.TotalScore < Score.TotalScore;
if (isOnlineLeaderboard && presentedLocalUserScoreIsBetter)
continue;
sortedScores.Add(clonedScore);
}
}
// if we haven't encountered a match for the presented score, we still need to attach it.
// note that the above block ensuring that the `Score` reference makes it in here makes this valid to write in this way.
if (!sortedScores.Contains(Score))
sortedScores.Add(Score);
sortedScores = sortedScores.OrderByTotalScore().ToList();
int delta = 0;
bool isPartialLeaderboard = leaderboardManager.CurrentCriteria?.Scope != BeatmapLeaderboardScope.Local && result.TopScores.Count >= 50;
for (int i = 0; i < sortedScores.Count; i++)
{
var sortedScore = sortedScores[i];
// see `SoloGameplayLeaderboardProvider.sort()` for another place that does the same thing with slight deviations
// if this code is changed, that code should probably be changed as well
if (!isPartialLeaderboard)
sortedScore.Position = i + 1;
else
{
if (ReferenceEquals(sortedScore, Score) && sortedScore.Position == null)
{
int? previousScorePosition = i > 0 ? sortedScores[i - 1].Position : 0;
int? nextScorePosition = i < result.TopScores.Count - 1 ? sortedScores[i + 1].Position : null;
if (previousScorePosition != null && nextScorePosition != null && previousScorePosition + 1 == nextScorePosition)
{
sortedScore.Position = previousScorePosition + 1;
delta += 1;
}
else
sortedScore.Position = null;
}
else
sortedScore.Position += delta;
}
}
// there's a non-zero chance that the `Score.Position` was mutated above,
// but that is not actually coupled to `ScorePosition` of the relevant score panel in any way,
// so ensure that the drawable panel also receives the updated position.
// note that this is valid to do precisely because we ensured `Score` was in `sortedScores` earlier.
ScorePanelList.GetPanelForScore(Score).ScorePosition.Value = Score.Position;
sortedScores.Remove(Score);
return sortedScores.ToArray();
}
}
}