// 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.Collections.Specialized; using System.Linq; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Game.Configuration; using osu.Game.Database; using osu.Game.Online.API; using osu.Game.Online.Multiplayer; using osu.Game.Online.Spectator; using osu.Game.Rulesets.Scoring; namespace osu.Game.Screens.Play.HUD { [LongRunningLoad] public class MultiplayerGameplayLeaderboard : GameplayLeaderboard { protected readonly Dictionary UserScores = new Dictionary(); [Resolved] private SpectatorClient spectatorClient { get; set; } [Resolved] private MultiplayerClient multiplayerClient { get; set; } [Resolved] private UserLookupCache userLookupCache { get; set; } private readonly ScoreProcessor scoreProcessor; private readonly BindableList playingUsers; private Bindable scoringMode; /// /// Construct a new leaderboard. /// /// A score processor instance to handle score calculation for scores of users in the match. /// IDs of all users in this match. public MultiplayerGameplayLeaderboard(ScoreProcessor scoreProcessor, int[] userIds) { // todo: this will eventually need to be created per user to support different mod combinations. this.scoreProcessor = scoreProcessor; // todo: this will likely be passed in as User instances. playingUsers = new BindableList(userIds); } [BackgroundDependencyLoader] private void load(OsuConfigManager config, IAPIProvider api) { scoringMode = config.GetBindable(OsuSetting.ScoreDisplayMode); foreach (var userId in playingUsers) { var trackedUser = CreateUserData(userId, scoreProcessor); trackedUser.ScoringMode.BindTo(scoringMode); UserScores[userId] = trackedUser; } userLookupCache.GetUsersAsync(playingUsers.ToArray()).ContinueWith(users => Schedule(() => { foreach (var user in users.Result) { if (user == null) continue; var trackedUser = UserScores[user.Id]; var leaderboardScore = AddPlayer(user, user.Id == api.LocalUser.Value.Id); leaderboardScore.Accuracy.BindTo(trackedUser.Accuracy); leaderboardScore.TotalScore.BindTo(trackedUser.Score); leaderboardScore.Combo.BindTo(trackedUser.CurrentCombo); leaderboardScore.HasQuit.BindTo(trackedUser.UserQuit); } })); } 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 (int userId in playingUsers) { spectatorClient.WatchUser(userId); if (!multiplayerClient.CurrentMatchPlayingUserIds.Contains(userId)) usersChanged(this, new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Remove, new[] { userId })); } // bind here is to support players leaving the match. // new players are not supported. playingUsers.BindTo(multiplayerClient.CurrentMatchPlayingUserIds); playingUsers.BindCollectionChanged(usersChanged); // this leaderboard should be guaranteed to be completely loaded before the gameplay starts (is a prerequisite in MultiplayerPlayer). spectatorClient.OnNewFrames += handleIncomingFrames; } private void usersChanged(object sender, NotifyCollectionChangedEventArgs e) { switch (e.Action) { case NotifyCollectionChangedAction.Remove: foreach (var userId in e.OldItems.OfType()) { spectatorClient.StopWatchingUser(userId); if (UserScores.TryGetValue(userId, out var trackedData)) trackedData.MarkUserQuit(); } break; } } private void handleIncomingFrames(int userId, FrameDataBundle bundle) => Schedule(() => { if (!UserScores.TryGetValue(userId, out var trackedData)) return; trackedData.Frames.Add(new TimedFrame(bundle.Frames.First().Time, bundle.Header)); trackedData.UpdateScore(); }); protected virtual TrackedUserData CreateUserData(int userId, ScoreProcessor scoreProcessor) => new TrackedUserData(userId, scoreProcessor); protected override void Dispose(bool isDisposing) { base.Dispose(isDisposing); if (spectatorClient != null) { foreach (var user in playingUsers) { spectatorClient.StopWatchingUser(user); } spectatorClient.OnNewFrames -= handleIncomingFrames; } } protected class TrackedUserData { public readonly int UserId; public readonly ScoreProcessor ScoreProcessor; public readonly BindableDouble Score = new BindableDouble(); public readonly BindableDouble Accuracy = new BindableDouble(1); public readonly BindableInt CurrentCombo = new BindableInt(); public readonly BindableBool UserQuit = new BindableBool(); public readonly IBindable ScoringMode = new Bindable(); public readonly List Frames = new List(); public TrackedUserData(int userId, ScoreProcessor scoreProcessor) { UserId = userId; ScoreProcessor = scoreProcessor; ScoringMode.BindValueChanged(_ => UpdateScore()); } public void MarkUserQuit() => UserQuit.Value = true; public virtual void UpdateScore() { if (Frames.Count == 0) return; SetFrame(Frames.Last()); } protected void SetFrame(TimedFrame frame) { var header = frame.Header; Score.Value = ScoreProcessor.GetImmediateScore(ScoringMode.Value, header.MaxCombo, header.Statistics); Accuracy.Value = header.Accuracy; CurrentCombo.Value = header.Combo; } } protected class TimedFrame : IComparable { public readonly double Time; public readonly FrameHeader Header; public TimedFrame(double time) { Time = time; } public TimedFrame(double time, FrameHeader header) { Time = time; Header = header; } public int CompareTo(TimedFrame other) => Time.CompareTo(other.Time); } } }