// 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.Diagnostics; using System.Linq; using System.Threading.Tasks; using JetBrains.Annotations; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Extensions.ObjectExtensions; using osu.Game.Beatmaps; using osu.Game.Database; using osu.Game.Online.Spectator; using osu.Game.Replays; using osu.Game.Rulesets; using osu.Game.Rulesets.Replays; using osu.Game.Rulesets.Replays.Types; using osu.Game.Scoring; using osu.Game.Users; namespace osu.Game.Screens.Spectate { /// /// A which spectates one or more users. /// public abstract class SpectatorScreen : OsuScreen { protected IReadOnlyList UserIds => userIds; private readonly List userIds = new List(); [Resolved] private BeatmapManager beatmaps { get; set; } [Resolved] private RulesetStore rulesets { get; set; } [Resolved] private SpectatorClient spectatorClient { get; set; } [Resolved] private UserLookupCache userLookupCache { get; set; } private readonly IBindableDictionary playingUserStates = new BindableDictionary(); private readonly Dictionary userMap = new Dictionary(); private readonly Dictionary gameplayStates = new Dictionary(); private IBindable> managerUpdated; /// /// Creates a new . /// /// The users to spectate. protected SpectatorScreen(params int[] userIds) { this.userIds.AddRange(userIds); } protected override void LoadComplete() { base.LoadComplete(); getAllUsers().ContinueWith(users => Schedule(() => { foreach (var u in users.Result) userMap[u.Id] = u; playingUserStates.BindTo(spectatorClient.PlayingUserStates); playingUserStates.BindCollectionChanged(onPlayingUserStatesChanged, true); spectatorClient.OnNewFrames += userSentFrames; managerUpdated = beatmaps.ItemUpdated.GetBoundCopy(); managerUpdated.BindValueChanged(beatmapUpdated); foreach (var (id, _) in userMap) spectatorClient.WatchUser(id); })); } private Task getAllUsers() { var userLookupTasks = new List>(); foreach (var u in userIds) { userLookupTasks.Add(userLookupCache.GetUserAsync(u).ContinueWith(task => { if (!task.IsCompletedSuccessfully) return null; return task.Result; })); } return Task.WhenAll(userLookupTasks); } private void beatmapUpdated(ValueChangedEvent> e) { if (!e.NewValue.TryGetTarget(out var beatmapSet)) return; foreach (var (userId, _) in userMap) { if (!playingUserStates.TryGetValue(userId, out var userState)) continue; if (beatmapSet.Beatmaps.Any(b => b.OnlineBeatmapID == userState.BeatmapID)) updateGameplayState(userId); } } private void onPlayingUserStatesChanged(object sender, NotifyDictionaryChangedEventArgs e) { switch (e.Action) { case NotifyDictionaryChangedAction.Add: foreach (var (userId, state) in e.NewItems.AsNonNull()) onUserStateAdded(userId, state); break; case NotifyDictionaryChangedAction.Remove: foreach (var (userId, _) in e.OldItems.AsNonNull()) onUserStateRemoved(userId); break; case NotifyDictionaryChangedAction.Replace: foreach (var (userId, _) in e.OldItems.AsNonNull()) onUserStateRemoved(userId); foreach (var (userId, state) in e.NewItems.AsNonNull()) onUserStateAdded(userId, state); break; } } private void onUserStateAdded(int userId, SpectatorState state) { if (state.RulesetID == null || state.BeatmapID == null) return; if (!userMap.ContainsKey(userId)) return; Schedule(() => OnUserStateChanged(userId, state)); updateGameplayState(userId); } private void onUserStateRemoved(int userId) { if (!userMap.ContainsKey(userId)) return; if (!gameplayStates.TryGetValue(userId, out var gameplayState)) return; gameplayState.Score.Replay.HasReceivedAllFrames = true; gameplayStates.Remove(userId); Schedule(() => EndGameplay(userId)); } private void updateGameplayState(int userId) { Debug.Assert(userMap.ContainsKey(userId)); var user = userMap[userId]; var spectatorState = playingUserStates[userId]; var resolvedRuleset = rulesets.AvailableRulesets.FirstOrDefault(r => r.ID == spectatorState.RulesetID)?.CreateInstance(); if (resolvedRuleset == null) return; var resolvedBeatmap = beatmaps.QueryBeatmap(b => b.OnlineBeatmapID == spectatorState.BeatmapID); if (resolvedBeatmap == null) return; var score = new Score { ScoreInfo = new ScoreInfo { Beatmap = resolvedBeatmap, User = user, Mods = spectatorState.Mods.Select(m => m.ToMod(resolvedRuleset)).ToArray(), Ruleset = resolvedRuleset.RulesetInfo, }, Replay = new Replay { HasReceivedAllFrames = false }, }; var gameplayState = new GameplayState(score, resolvedRuleset, beatmaps.GetWorkingBeatmap(resolvedBeatmap)); gameplayStates[userId] = gameplayState; Schedule(() => StartGameplay(userId, gameplayState)); } private void userSentFrames(int userId, FrameDataBundle bundle) { if (!userMap.ContainsKey(userId)) return; if (!gameplayStates.TryGetValue(userId, out var gameplayState)) return; // The ruleset instance should be guaranteed to be in sync with the score via ScoreLock. Debug.Assert(gameplayState.Ruleset != null && gameplayState.Ruleset.RulesetInfo.Equals(gameplayState.Score.ScoreInfo.Ruleset)); foreach (var frame in bundle.Frames) { IConvertibleReplayFrame convertibleFrame = gameplayState.Ruleset.CreateConvertibleReplayFrame(); convertibleFrame.FromLegacy(frame, gameplayState.Beatmap.Beatmap); var convertedFrame = (ReplayFrame)convertibleFrame; convertedFrame.Time = frame.Time; gameplayState.Score.Replay.Frames.Add(convertedFrame); } } /// /// Invoked when a spectated user's state has changed. /// /// The user whose state has changed. /// The new state. protected abstract void OnUserStateChanged(int userId, [NotNull] SpectatorState spectatorState); /// /// Starts gameplay for a user. /// /// The user to start gameplay for. /// The gameplay state. protected abstract void StartGameplay(int userId, [NotNull] GameplayState gameplayState); /// /// Ends gameplay for a user. /// /// The user to end gameplay for. protected abstract void EndGameplay(int userId); /// /// Stops spectating a user. /// /// The user to stop spectating. protected void RemoveUser(int userId) { onUserStateRemoved(userId); userIds.Remove(userId); userMap.Remove(userId); spectatorClient.StopWatchingUser(userId); } protected override void Dispose(bool isDisposing) { base.Dispose(isDisposing); if (spectatorClient != null) { spectatorClient.OnNewFrames -= userSentFrames; foreach (var (userId, _) in userMap) spectatorClient.StopWatchingUser(userId); } managerUpdated?.UnbindAll(); } } }