// 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 JetBrains.Annotations; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Extensions; using osu.Framework.Extensions.ObjectExtensions; using osu.Game.Beatmaps; using osu.Game.Database; using osu.Game.Online.API.Requests.Responses; using osu.Game.Online.Spectator; using osu.Game.Replays; using osu.Game.Rulesets; using osu.Game.Scoring; namespace osu.Game.Screens.Spectate { /// /// A which spectates one or more users. /// public abstract class SpectatorScreen : OsuScreen { protected IReadOnlyList Users => users; private readonly List users = 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(); /// /// Creates a new . /// /// The users to spectate. protected SpectatorScreen(params int[] users) { this.users.AddRange(users); } [Resolved] private RealmContextFactory realmContextFactory { get; set; } private IDisposable realmSubscription; protected override void LoadComplete() { base.LoadComplete(); userLookupCache.GetUsersAsync(users.ToArray()).ContinueWith(task => Schedule(() => { var foundUsers = task.GetResultSafely(); foreach (var u in foundUsers) { if (u == null) continue; userMap[u.Id] = u; } playingUserStates.BindTo(spectatorClient.PlayingUserStates); playingUserStates.BindCollectionChanged(onPlayingUserStatesChanged, true); realmSubscription = realmContextFactory.Register(realm => realm.All() .Where(s => !s.DeletePending) .QueryAsyncWithNotifications((items, changes, ___) => { if (changes?.InsertedIndices == null) return; foreach (int c in changes.InsertedIndices) beatmapUpdated(items[c]); })); foreach ((int id, var _) in userMap) spectatorClient.WatchUser(id); })); } private void beatmapUpdated(BeatmapSetInfo beatmapSet) { foreach ((int userId, _) in userMap) { if (!playingUserStates.TryGetValue(userId, out var userState)) continue; if (beatmapSet.Beatmaps.Any(b => b.OnlineID == userState.BeatmapID)) updateGameplayState(userId); } } private void onPlayingUserStatesChanged(object sender, NotifyDictionaryChangedEventArgs e) { switch (e.Action) { case NotifyDictionaryChangedAction.Add: foreach ((int userId, var state) in e.NewItems.AsNonNull()) onUserStateAdded(userId, state); break; case NotifyDictionaryChangedAction.Remove: foreach ((int userId, var _) in e.OldItems.AsNonNull()) onUserStateRemoved(userId); break; case NotifyDictionaryChangedAction.Replace: foreach ((int userId, var _) in e.OldItems.AsNonNull()) onUserStateRemoved(userId); foreach ((int userId, var 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.OnlineID == spectatorState.RulesetID)?.CreateInstance(); if (resolvedRuleset == null) return; var resolvedBeatmap = beatmaps.QueryBeatmap(b => b.OnlineID == spectatorState.BeatmapID); if (resolvedBeatmap == null) return; var score = new Score { ScoreInfo = new ScoreInfo { BeatmapInfo = resolvedBeatmap, User = user, Mods = spectatorState.Mods.Select(m => m.ToMod(resolvedRuleset)).ToArray(), Ruleset = resolvedRuleset.RulesetInfo, }, Replay = new Replay { HasReceivedAllFrames = false }, }; var gameplayState = new SpectatorGameplayState(score, resolvedRuleset, beatmaps.GetWorkingBeatmap(resolvedBeatmap)); gameplayStates[userId] = gameplayState; Schedule(() => StartGameplay(userId, gameplayState)); } /// /// 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] SpectatorGameplayState spectatorGameplayState); /// /// 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); users.Remove(userId); userMap.Remove(userId); spectatorClient.StopWatchingUser(userId); } protected override void Dispose(bool isDisposing) { base.Dispose(isDisposing); if (spectatorClient != null) { foreach ((int userId, var _) in userMap) spectatorClient.StopWatchingUser(userId); } realmSubscription?.Dispose(); } } }