Path: blob/master/osu.Game/Screens/Select/Leaderboards/MultiplayerLeaderboardProvider.cs
4610 views
// Copyright (c) ppy Pty Ltd <[email protected]>. Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. using System.Collections.Generic; using System.Collections.Specialized; using System.Diagnostics; using System.Linq; using System.Threading; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Caching; using osu.Framework.Extensions; using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Extensions.ObjectExtensions; using osu.Framework.Graphics.Containers; using osu.Game.Configuration; using osu.Game.Database; using osu.Game.Graphics; using osu.Game.Online.API; using osu.Game.Online.API.Requests.Responses; using osu.Game.Online.Multiplayer; using osu.Game.Online.Multiplayer.MatchTypes.TeamVersus; using osu.Game.Online.Spectator; using osu.Game.Rulesets.Scoring; using osuTK.Graphics; namespace osu.Game.Screens.Select.Leaderboards { [LongRunningLoad] public partial class MultiplayerLeaderboardProvider : CompositeComponent, IGameplayLeaderboardProvider { public IBindableList<GameplayLeaderboardScore> Scores => scores; private readonly BindableList<GameplayLeaderboardScore> scores = new BindableList<GameplayLeaderboardScore>(); protected readonly Dictionary<int, TrackedUserData> UserScores = new Dictionary<int, TrackedUserData>(); public readonly SortedDictionary<int, BindableLong> TeamScores = new SortedDictionary<int, BindableLong>(); public bool HasTeams => TeamScores.Count > 0; private readonly MultiplayerRoomUser[] users; private readonly Bindable<ScoringMode> scoringMode = new Bindable<ScoringMode>(); private readonly IBindableList<int> playingUserIds = new BindableList<int>(); [Resolved] private UserLookupCache userLookupCache { get; set; } = null!; [Resolved] private SpectatorClient spectatorClient { get; set; } = null!; [Resolved] private MultiplayerClient multiplayerClient { get; set; } = null!; [Resolved] private OsuColour colours { get; set; } = null!; private readonly Cached sorting = new Cached(); public MultiplayerLeaderboardProvider(MultiplayerRoomUser[] users) { this.users = users; } [BackgroundDependencyLoader] private void load(OsuConfigManager config, IAPIProvider api, CancellationToken cancellationToken) { config.BindWith(OsuSetting.ScoreDisplayMode, scoringMode); foreach (var user in users) { var scoreProcessor = new SpectatorScoreProcessor(user.UserID); scoreProcessor.Mode.BindTo(scoringMode); scoreProcessor.TotalScore.BindValueChanged(_ => Scheduler.AddOnce(updateTotals)); AddInternal(scoreProcessor); var trackedUser = new TrackedUserData(user, scoreProcessor); UserScores[user.UserID] = trackedUser; if (trackedUser.Team is int team && !TeamScores.ContainsKey(team)) TeamScores.Add(team, new BindableLong()); } userLookupCache.GetUsersAsync(users.Select(u => u.UserID).ToArray(), cancellationToken) .ContinueWith(task => { Schedule(() => { var lookedUpUsers = task.GetResultSafely(); for (int i = 0; i < lookedUpUsers.Length; i++) { var user = lookedUpUsers[i] ?? new APIUser { Id = users[i].UserID, Username = "Unknown user", }; var trackedUser = UserScores[user.Id]; var leaderboardScore = new GameplayLeaderboardScore( user, trackedUser.ScoreProcessor, user.Id == api.LocalUser.Value.Id, GameplayLeaderboardScore.ComboDisplayMode.Current) { HasQuit = { BindTarget = trackedUser.UserQuit }, TeamColour = UserScores[user.OnlineID].Team is int team ? getTeamColour(team) : null, }; leaderboardScore.TotalScore.BindValueChanged(_ => sorting.Invalidate()); leaderboardScore.DisplayOrder.BindValueChanged(_ => sorting.Invalidate(), true); scores.Add(leaderboardScore); } }); }, cancellationToken); } protected override void LoadComplete() { base.LoadComplete(); // BindableList handles binding in a really bad way (Clear then AddRange) so we need to do this manually.. foreach (var user in users) { spectatorClient.WatchUser(user.UserID); if (!multiplayerClient.CurrentMatchPlayingUserIds.Contains(user.UserID)) playingUsersChanged(this, new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Remove, new[] { user.UserID })); } // bind here is to support players leaving the match. // new players are not supported. playingUserIds.BindTo(multiplayerClient.CurrentMatchPlayingUserIds); playingUserIds.BindCollectionChanged(playingUsersChanged); Scheduler.AddDelayed(sort, 1000, true); } private void playingUsersChanged(object? sender, NotifyCollectionChangedEventArgs e) { switch (e.Action) { case NotifyCollectionChangedAction.Remove: Debug.Assert(e.OldItems != null); foreach (int userId in e.OldItems.OfType<int>()) { spectatorClient.StopWatchingUser(userId); if (UserScores.TryGetValue(userId, out var trackedData)) trackedData.MarkUserQuit(); } break; } } private void updateTotals() { if (!HasTeams) return; foreach (var teamTotal in TeamScores.Values) teamTotal.Value = 0; foreach (var u in UserScores.Values) { if (u.Team == null) continue; if (TeamScores.TryGetValue(u.Team.Value, out var team)) team.Value += u.ScoreProcessor.TotalScore.Value; } } private Color4 getTeamColour(int team) { switch (team) { case 0: return colours.TeamColourRed.Lighten(1.2f); default: return colours.TeamColourBlue.Lighten(1.2f); } } private void sort() { if (sorting.IsValid) return; var orderedByScore = scores .OrderByDescending(i => i.TotalScore.Value) .ThenBy(i => i.TotalScoreTiebreaker) .ToList(); for (int i = 0; i < orderedByScore.Count; i++) { var score = orderedByScore[i]; score.DisplayOrder.Value = i; score.Position.Value = i + 1; } sorting.Validate(); } protected override void Dispose(bool isDisposing) { base.Dispose(isDisposing); if (spectatorClient.IsNotNull()) { foreach (var user in users) spectatorClient.StopWatchingUser(user.UserID); } } protected class TrackedUserData { public readonly MultiplayerRoomUser User; public readonly SpectatorScoreProcessor ScoreProcessor; public readonly BindableBool UserQuit = new BindableBool(); public int? Team => (User.MatchState as TeamVersusUserState)?.TeamID; public TrackedUserData(MultiplayerRoomUser user, SpectatorScoreProcessor scoreProcessor) { User = user; ScoreProcessor = scoreProcessor; } public void MarkUserQuit() => UserQuit.Value = true; } } }