1
0
mirror of https://github.com/ppy/osu.git synced 2025-03-13 07:17:28 +08:00
osu-lazer/osu.Game/Screens/OnlinePlay/Playlists/PlaylistItemResultsScreen.cs

Ignoring revisions in .git-blame-ignore-revs. Click here to bypass and see the normal blame view.

319 lines
13 KiB
C#
Raw Normal View History

// 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;
using System.Collections.Generic;
2020-07-22 20:24:55 +09:00
using System.Diagnostics;
using System.Linq;
using System.Threading.Tasks;
2020-06-09 18:53:55 +09:00
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Logging;
using osu.Game.Beatmaps;
using osu.Game.Database;
2020-06-09 18:53:55 +09:00
using osu.Game.Graphics.UserInterface;
2025-02-26 15:04:43 +09:00
using osu.Game.Models;
using osu.Game.Online.API;
using osu.Game.Online.API.Requests.Responses;
2020-12-25 13:38:11 +09:00
using osu.Game.Online.Rooms;
using osu.Game.Rulesets;
using osu.Game.Scoring;
using osu.Game.Screens.Ranking;
namespace osu.Game.Screens.OnlinePlay.Playlists
{
public abstract partial class PlaylistItemResultsScreen : ResultsScreen
{
protected readonly long RoomId;
protected readonly PlaylistItem PlaylistItem;
protected LoadingSpinner LeftSpinner { get; private set; } = null!;
protected LoadingSpinner CentreSpinner { get; private set; } = null!;
protected LoadingSpinner RightSpinner { get; private set; } = null!;
2020-07-31 21:39:50 +09:00
private MultiplayerScores? higherScores;
private MultiplayerScores? lowerScores;
[Resolved]
protected IAPIProvider API { get; private set; } = null!;
2020-06-09 18:53:55 +09:00
[Resolved]
protected ScoreManager ScoreManager { get; private set; } = null!;
[Resolved]
protected RulesetStore Rulesets { get; private set; } = null!;
[Resolved]
private BeatmapLookupCache beatmapLookupCache { get; set; } = null!;
2025-02-26 14:49:38 +09:00
[Resolved]
private BeatmapManager beatmapManager { get; set; } = null!;
protected PlaylistItemResultsScreen(ScoreInfo? score, long roomId, PlaylistItem playlistItem)
: base(score)
{
RoomId = roomId;
PlaylistItem = playlistItem;
}
2020-06-09 18:53:55 +09:00
[BackgroundDependencyLoader]
private void load()
{
2020-07-31 19:57:05 +09:00
AddInternal(new Container
2020-06-09 18:53:55 +09:00
{
2020-07-31 19:57:05 +09:00
RelativeSizeAxes = Axes.Both,
Padding = new MarginPadding { Bottom = TwoLayerButton.SIZE_EXTENDED.Y },
Children = new Drawable[]
{
2020-07-31 21:39:50 +09:00
LeftSpinner = new PanelListLoadingSpinner(ScorePanelList)
2020-07-31 19:57:05 +09:00
{
Anchor = Anchor.CentreLeft,
Origin = Anchor.Centre,
},
2020-07-31 21:39:50 +09:00
CentreSpinner = new PanelListLoadingSpinner(ScorePanelList)
2020-07-31 19:57:05 +09:00
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
State = { Value = Score == null ? Visibility.Visible : Visibility.Hidden },
},
2020-07-31 21:39:50 +09:00
RightSpinner = new PanelListLoadingSpinner(ScorePanelList)
2020-07-31 19:57:05 +09:00
{
Anchor = Anchor.CentreRight,
Origin = Anchor.Centre,
},
}
2020-06-09 18:53:55 +09:00
});
}
protected abstract APIRequest<MultiplayerScore> CreateScoreRequest();
2025-02-25 22:55:55 +09:00
protected override async Task<ScoreInfo[]> FetchScores()
{
// This performs two requests:
// 1. A request to show the relevant score (and scores around).
// 2. If that fails, a request to index the room starting from the highest score.
var requestTaskSource = new TaskCompletionSource<MultiplayerScore>();
var userScoreReq = CreateScoreRequest();
userScoreReq.Success += requestTaskSource.SetResult;
userScoreReq.Failure += requestTaskSource.SetException;
API.Queue(userScoreReq);
2020-06-09 18:53:55 +09:00
try
2020-06-09 18:53:55 +09:00
{
2025-02-25 22:56:38 +09:00
var userScore = await requestTaskSource.Task.ConfigureAwait(false);
var allScores = new List<MultiplayerScore> { userScore };
// Other scores could have arrived between score submission and entering the results screen. Ensure the local player score position is up to date.
if (Score != null)
{
Score.Position = userScore.Position;
ScorePanelList.GetPanelForScore(Score).ScorePosition.Value = userScore.Position;
}
if (userScore.ScoresAround?.Higher != null)
{
allScores.AddRange(userScore.ScoresAround.Higher.Scores);
higherScores = userScore.ScoresAround.Higher;
Debug.Assert(userScore.Position != null);
setPositions(higherScores, userScore.Position.Value, -1);
}
if (userScore.ScoresAround?.Lower != null)
{
allScores.AddRange(userScore.ScoresAround.Lower.Scores);
lowerScores = userScore.ScoresAround.Lower;
Debug.Assert(userScore.Position != null);
setPositions(lowerScores, userScore.Position.Value, 1);
}
2025-02-25 22:56:38 +09:00
return await transformScores(allScores).ConfigureAwait(false);
}
catch
{
2025-02-25 22:56:38 +09:00
return await fetchScoresAround().ConfigureAwait(false);
}
2020-07-22 20:24:55 +09:00
}
2025-02-25 22:55:55 +09:00
protected override async Task<ScoreInfo[]> FetchNextPage(int direction)
2020-07-22 20:24:55 +09:00
{
Debug.Assert(direction == 1 || direction == -1);
MultiplayerScores? pivot = direction == -1 ? higherScores : lowerScores;
if (pivot?.Cursor == null)
return [];
2020-07-22 20:24:55 +09:00
Schedule(() =>
{
if (pivot == higherScores)
LeftSpinner.Show();
else
RightSpinner.Show();
});
2020-07-31 19:57:05 +09:00
2025-02-25 22:56:38 +09:00
return await fetchScoresAround(pivot).ConfigureAwait(false);
2020-07-31 17:58:48 +09:00
}
/// <summary>
/// Creates a <see cref="IndexPlaylistScoresRequest"/> with an optional score pivot.
/// </summary>
/// <remarks>Does not queue the request.</remarks>
/// <param name="pivot">An optional score pivot to retrieve scores around. Can be null to retrieve scores from the highest score.</param>
2025-02-25 22:55:55 +09:00
private async Task<ScoreInfo[]> fetchScoresAround(MultiplayerScores? pivot = null)
2020-07-31 17:58:48 +09:00
{
var requestTaskSource = new TaskCompletionSource<IndexedMultiplayerScores>();
2020-07-31 17:58:48 +09:00
var indexReq = pivot != null
? new IndexPlaylistScoresRequest(RoomId, PlaylistItem.ID, pivot.Cursor, pivot.Params)
: new IndexPlaylistScoresRequest(RoomId, PlaylistItem.ID);
indexReq.Success += requestTaskSource.SetResult;
indexReq.Failure += requestTaskSource.SetException;
API.Queue(indexReq);
2020-07-22 20:24:55 +09:00
try
2020-07-22 20:24:55 +09:00
{
2025-02-25 22:56:38 +09:00
var index = await requestTaskSource.Task.ConfigureAwait(false);
2020-07-31 17:58:48 +09:00
if (pivot == lowerScores)
{
lowerScores = index;
setPositions(index, pivot, 1);
}
2020-07-31 17:58:48 +09:00
else
{
higherScores = index;
setPositions(index, pivot, -1);
}
2025-02-25 22:56:38 +09:00
return await transformScores(index.Scores).ConfigureAwait(false);
}
catch (Exception ex)
{
Logger.Log($"Failed to fetch scores (room: {RoomId}, item: {PlaylistItem.ID}): {ex}");
return [];
}
2020-07-22 20:24:55 +09:00
}
2020-07-31 17:58:48 +09:00
/// <summary>
/// Transforms returned <see cref="MultiplayerScores"/> into <see cref="ScoreInfo"/>s.
2020-07-31 17:58:48 +09:00
/// </summary>
/// <param name="scores">The <see cref="MultiplayerScore"/>s that were retrieved from <see cref="APIRequest"/>s.</param>
private async Task<ScoreInfo[]> transformScores(List<MultiplayerScore> scores)
2020-07-22 20:24:55 +09:00
{
2025-02-26 14:49:38 +09:00
int[] allBeatmapIds = scores.Select(s => s.BeatmapId).Distinct().ToArray();
BeatmapInfo[] localBeatmaps = allBeatmapIds.Select(id => beatmapManager.QueryBeatmap(b => b.OnlineID == id))
.Where(b => b != null)
.ToArray()!;
int[] missingBeatmapIds = allBeatmapIds.Except(localBeatmaps.Select(b => b.OnlineID)).ToArray();
APIBeatmap[] onlineBeatmaps = (await beatmapLookupCache.GetBeatmapsAsync(missingBeatmapIds).ConfigureAwait(false)).Where(b => b != null).ToArray()!;
Dictionary<int, BeatmapInfo> beatmapsById = new Dictionary<int, BeatmapInfo>();
2025-02-26 14:49:38 +09:00
foreach (var beatmap in localBeatmaps)
beatmapsById[beatmap.OnlineID] = beatmap;
foreach (var beatmap in onlineBeatmaps)
{
2025-02-26 14:49:38 +09:00
// Minimal data required to get various components in this screen to display correctly.
beatmapsById[beatmap.OnlineID] = new BeatmapInfo
{
Difficulty = new BeatmapDifficulty(beatmap.Difficulty),
2025-02-26 15:04:43 +09:00
Metadata =
{
Author = new RealmUser
{
Username = beatmap.Metadata.Author.Username,
OnlineID = beatmap.Metadata.Author.OnlineID,
}
},
2025-02-26 14:49:38 +09:00
DifficultyName = beatmap.DifficultyName,
StarRating = beatmap.StarRating,
Length = beatmap.Length,
BPM = beatmap.BPM
};
}
// Validate that we have all beatmaps we need.
foreach (int id in allBeatmapIds)
{
if (!beatmapsById.ContainsKey(id))
{
Logger.Log($"Failed to fetch beatmap {id} to display scores for playlist item {PlaylistItem.ID}");
beatmapsById[id] = Beatmap.Value.BeatmapInfo;
}
2025-02-26 14:49:38 +09:00
}
// Exclude the score provided to this screen since it's added already.
return scores
.Where(s => s.ID != Score?.OnlineID)
2025-02-26 14:49:38 +09:00
.Select(s => s.CreateScoreInfo(ScoreManager, Rulesets, beatmapsById[s.BeatmapId]))
.OrderByTotalScore()
.ToArray();
}
2020-07-31 19:57:05 +09:00
protected override void OnScoresAdded(ScoreInfo[] scores)
2020-07-31 19:57:05 +09:00
{
base.OnScoresAdded(scores);
2020-07-31 19:57:05 +09:00
CentreSpinner.Hide();
RightSpinner.Hide();
LeftSpinner.Hide();
2020-07-31 19:57:05 +09:00
}
/// <summary>
2020-07-31 23:16:55 +09:00
/// Applies positions to all <see cref="MultiplayerScore"/>s referenced to a given pivot.
/// </summary>
/// <param name="scores">The <see cref="MultiplayerScores"/> to set positions on.</param>
/// <param name="pivot">The pivot.</param>
/// <param name="increment">The amount to increment the pivot position by for each <see cref="MultiplayerScore"/> in <paramref name="scores"/>.</param>
private static void setPositions(MultiplayerScores scores, MultiplayerScores? pivot, int increment)
=> setPositions(scores, pivot?.Scores[^1].Position ?? 0, increment);
/// <summary>
2020-07-31 23:16:55 +09:00
/// Applies positions to all <see cref="MultiplayerScore"/>s referenced to a given pivot.
/// </summary>
/// <param name="scores">The <see cref="MultiplayerScores"/> to set positions on.</param>
/// <param name="pivotPosition">The pivot position.</param>
/// <param name="increment">The amount to increment the pivot position by for each <see cref="MultiplayerScore"/> in <paramref name="scores"/>.</param>
private static void setPositions(MultiplayerScores scores, int pivotPosition, int increment)
{
foreach (var s in scores.Scores)
{
pivotPosition += increment;
s.Position = pivotPosition;
}
}
2020-07-31 19:57:05 +09:00
private partial class PanelListLoadingSpinner : LoadingSpinner
{
private readonly ScorePanelList list;
/// <summary>
/// Creates a new <see cref="PanelListLoadingSpinner"/>.
/// </summary>
/// <param name="list">The list to track.</param>
/// <param name="withBox">Whether the spinner should have a surrounding black box for visibility.</param>
public PanelListLoadingSpinner(ScorePanelList list, bool withBox = true)
: base(withBox)
{
this.list = list;
}
protected override void Update()
{
base.Update();
float panelOffset = list.DrawWidth / 2 - ScorePanel.EXPANDED_WIDTH;
if ((Anchor & Anchor.x0) > 0)
X = (float)(panelOffset - list.Current);
else if ((Anchor & Anchor.x2) > 0)
X = (float)(list.ScrollableExtent - list.Current - panelOffset);
}
}
}
}