osu/osu.Game/Screens/Spectate/SpectatorScreen.cs

249 lines
8.5 KiB
C#
Raw Normal View History

2021-04-01 13:02:32 +00:00
// 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;
2021-04-01 13:02:32 +00:00
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;
2021-04-01 13:02:32 +00:00
using osu.Game.Beatmaps;
using osu.Game.Database;
using osu.Game.Online.API.Requests.Responses;
2021-04-01 13:02:32 +00:00
using osu.Game.Online.Spectator;
using osu.Game.Replays;
using osu.Game.Rulesets;
using osu.Game.Scoring;
using Realms;
2021-04-01 13:02:32 +00:00
namespace osu.Game.Screens.Spectate
{
/// <summary>
/// A <see cref="OsuScreen"/> which spectates one or more users.
/// </summary>
public abstract class SpectatorScreen : OsuScreen
{
protected IReadOnlyList<int> Users => users;
private readonly List<int> users = new List<int>();
2021-04-01 13:02:32 +00:00
[Resolved]
private BeatmapManager beatmaps { get; set; }
[Resolved]
private RulesetStore rulesets { get; set; }
[Resolved]
private SpectatorClient spectatorClient { get; set; }
2021-04-01 13:02:32 +00:00
[Resolved]
private UserLookupCache userLookupCache { get; set; }
private readonly IBindableDictionary<int, SpectatorState> userStates = new BindableDictionary<int, SpectatorState>();
private readonly Dictionary<int, APIUser> userMap = new Dictionary<int, APIUser>();
private readonly Dictionary<int, SpectatorGameplayState> gameplayStates = new Dictionary<int, SpectatorGameplayState>();
2021-04-01 13:02:32 +00:00
/// <summary>
/// Creates a new <see cref="SpectatorScreen"/>.
/// </summary>
/// <param name="users">The users to spectate.</param>
protected SpectatorScreen(params int[] users)
2021-04-01 13:02:32 +00:00
{
this.users.AddRange(users);
2021-04-01 13:02:32 +00:00
}
[Resolved]
private RealmAccess realm { get; set; }
private IDisposable realmSubscription;
2021-04-01 13:02:32 +00:00
protected override void LoadComplete()
{
base.LoadComplete();
userLookupCache.GetUsersAsync(users.ToArray()).ContinueWith(task => Schedule(() =>
{
2022-01-06 13:54:43 +00:00
var foundUsers = task.GetResultSafely();
foreach (var u in foundUsers)
{
if (u == null)
continue;
2021-05-20 09:52:20 +00:00
userMap[u.Id] = u;
}
2021-05-20 09:52:20 +00:00
2022-02-09 03:09:04 +00:00
userStates.BindTo(spectatorClient.WatchedUserStates);
userStates.BindCollectionChanged(onUserStatesChanged, true);
realmSubscription = realm.RegisterForNotifications(
realm => realm.All<BeatmapSetInfo>().Where(s => !s.DeletePending), beatmapsChanged);
foreach ((int id, var _) in userMap)
2021-05-20 09:52:20 +00:00
spectatorClient.WatchUser(id);
}));
}
2021-04-01 13:02:32 +00:00
private void beatmapsChanged(IRealmCollection<BeatmapSetInfo> items, ChangeSet changes, Exception ___)
{
if (changes?.InsertedIndices == null) return;
foreach (int c in changes.InsertedIndices) beatmapUpdated(items[c]);
}
private void beatmapUpdated(BeatmapSetInfo beatmapSet)
2021-04-01 13:02:32 +00:00
{
foreach ((int userId, _) in userMap)
2021-04-01 13:02:32 +00:00
{
if (!userStates.TryGetValue(userId, out var userState))
2021-05-20 09:52:20 +00:00
continue;
if (beatmapSet.Beatmaps.Any(b => b.OnlineID == userState.BeatmapID))
startGameplay(userId);
2021-04-01 13:02:32 +00:00
}
}
private void onUserStatesChanged(object sender, NotifyDictionaryChangedEventArgs<int, SpectatorState> e)
{
switch (e.Action)
{
case NotifyDictionaryChangedAction.Add:
2022-02-03 03:44:33 +00:00
case NotifyDictionaryChangedAction.Replace:
foreach ((int userId, var state) in e.NewItems.AsNonNull())
onUserStateChanged(userId, state);
break;
case NotifyDictionaryChangedAction.Remove:
foreach ((int userId, SpectatorState state) in e.OldItems.AsNonNull())
onUserStateRemoved(userId, state);
break;
}
}
private void onUserStateChanged(int userId, SpectatorState newState)
2021-04-01 13:02:32 +00:00
{
if (newState.RulesetID == null || newState.BeatmapID == null)
2021-04-01 13:02:32 +00:00
return;
2021-05-20 09:52:20 +00:00
if (!userMap.ContainsKey(userId))
return;
2021-04-01 13:02:32 +00:00
2022-02-03 03:44:33 +00:00
switch (newState.State)
{
2022-02-09 03:09:04 +00:00
case SpectatedUserState.Passed:
2022-02-03 03:44:33 +00:00
// Make sure that gameplay completes to the end.
if (gameplayStates.TryGetValue(userId, out var gameplayState))
gameplayState.Score.Replay.HasReceivedAllFrames = true;
break;
2022-02-09 03:09:04 +00:00
case SpectatedUserState.Playing:
Schedule(() => OnNewPlayingUserState(userId, newState));
startGameplay(userId);
2022-02-03 03:44:33 +00:00
break;
}
}
private void onUserStateRemoved(int userId, SpectatorState state)
{
if (!userMap.ContainsKey(userId))
2021-05-20 09:52:20 +00:00
return;
if (!gameplayStates.TryGetValue(userId, out var gameplayState))
return;
2021-04-01 13:02:32 +00:00
gameplayState.Score.Replay.HasReceivedAllFrames = true;
gameplayStates.Remove(userId);
Schedule(() => EndGameplay(userId, state));
2021-04-01 13:02:32 +00:00
}
private void startGameplay(int userId)
2021-04-01 13:02:32 +00:00
{
2021-05-20 09:52:20 +00:00
Debug.Assert(userMap.ContainsKey(userId));
2021-04-01 13:02:32 +00:00
2021-05-20 09:52:20 +00:00
var user = userMap[userId];
var spectatorState = userStates[userId];
2021-04-01 13:02:32 +00:00
var resolvedRuleset = rulesets.AvailableRulesets.FirstOrDefault(r => r.OnlineID == spectatorState.RulesetID)?.CreateInstance();
2021-05-20 09:52:20 +00:00
if (resolvedRuleset == null)
return;
2021-04-01 13:02:32 +00:00
var resolvedBeatmap = beatmaps.QueryBeatmap(b => b.OnlineID == spectatorState.BeatmapID);
2021-05-20 09:52:20 +00:00
if (resolvedBeatmap == null)
return;
2021-04-01 13:02:32 +00:00
2021-05-20 09:52:20 +00:00
var score = new Score
{
ScoreInfo = new ScoreInfo
2021-04-01 13:02:32 +00:00
{
BeatmapInfo = resolvedBeatmap,
2021-05-20 09:52:20 +00:00
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));
2021-05-20 09:52:20 +00:00
gameplayStates[userId] = gameplayState;
Schedule(() => StartGameplay(userId, gameplayState));
2021-04-01 13:02:32 +00:00
}
/// <summary>
/// Invoked when a spectated user's state has changed to a new state indicating the player is currently playing.
2021-04-01 13:02:32 +00:00
/// </summary>
/// <param name="userId">The user whose state has changed.</param>
/// <param name="spectatorState">The new state.</param>
protected abstract void OnNewPlayingUserState(int userId, [NotNull] SpectatorState spectatorState);
2021-04-01 13:02:32 +00:00
/// <summary>
/// Starts gameplay for a user.
/// </summary>
/// <param name="userId">The user to start gameplay for.</param>
/// <param name="spectatorGameplayState">The gameplay state.</param>
protected abstract void StartGameplay(int userId, [NotNull] SpectatorGameplayState spectatorGameplayState);
2021-04-01 13:02:32 +00:00
/// <summary>
/// Ends gameplay for a user.
/// </summary>
/// <param name="userId">The user to end gameplay for.</param>
/// <param name="state">The final user state.</param>
protected abstract void EndGameplay(int userId, SpectatorState state);
2021-04-01 13:02:32 +00:00
/// <summary>
/// Stops spectating a user.
/// </summary>
/// <param name="userId">The user to stop spectating.</param>
protected void RemoveUser(int userId)
{
if (!userStates.TryGetValue(userId, out var state))
return;
onUserStateRemoved(userId, state);
users.Remove(userId);
2021-05-20 09:52:20 +00:00
userMap.Remove(userId);
2021-05-20 09:52:20 +00:00
spectatorClient.StopWatchingUser(userId);
}
2021-04-01 13:02:32 +00:00
protected override void Dispose(bool isDisposing)
{
base.Dispose(isDisposing);
if (spectatorClient != null)
{
foreach ((int userId, var _) in userMap)
2021-05-20 09:52:20 +00:00
spectatorClient.StopWatchingUser(userId);
2021-04-01 13:02:32 +00:00
}
realmSubscription?.Dispose();
2021-04-01 13:02:32 +00:00
}
}
}