2021-04-01 21:02:32 +08: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.
|
|
|
|
|
|
2021-12-17 18:26:41 +08:00
|
|
|
|
using System;
|
2021-04-01 21:02:32 +08:00
|
|
|
|
using System.Collections.Generic;
|
|
|
|
|
using System.Diagnostics;
|
|
|
|
|
using System.Linq;
|
|
|
|
|
using osu.Framework.Allocation;
|
|
|
|
|
using osu.Framework.Bindables;
|
2022-01-03 16:31:12 +08:00
|
|
|
|
using osu.Framework.Extensions;
|
2021-05-20 18:27:34 +08:00
|
|
|
|
using osu.Framework.Extensions.ObjectExtensions;
|
2021-04-01 21:02:32 +08:00
|
|
|
|
using osu.Game.Beatmaps;
|
|
|
|
|
using osu.Game.Database;
|
2021-11-04 17:02:44 +08:00
|
|
|
|
using osu.Game.Online.API.Requests.Responses;
|
2021-04-01 21:02:32 +08:00
|
|
|
|
using osu.Game.Online.Spectator;
|
|
|
|
|
using osu.Game.Replays;
|
|
|
|
|
using osu.Game.Rulesets;
|
|
|
|
|
using osu.Game.Scoring;
|
2022-01-23 18:42:26 +08:00
|
|
|
|
using Realms;
|
2021-04-01 21:02:32 +08:00
|
|
|
|
|
|
|
|
|
namespace osu.Game.Screens.Spectate
|
|
|
|
|
{
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// A <see cref="OsuScreen"/> which spectates one or more users.
|
|
|
|
|
/// </summary>
|
2022-11-24 13:32:20 +08:00
|
|
|
|
public abstract partial class SpectatorScreen : OsuScreen
|
2021-04-01 21:02:32 +08:00
|
|
|
|
{
|
2021-08-10 17:39:20 +08:00
|
|
|
|
protected IReadOnlyList<int> Users => users;
|
2021-05-03 13:25:52 +08:00
|
|
|
|
|
2021-08-10 17:39:20 +08:00
|
|
|
|
private readonly List<int> users = new List<int>();
|
2021-04-01 21:02:32 +08:00
|
|
|
|
|
|
|
|
|
[Resolved]
|
2023-11-22 14:14:25 +08:00
|
|
|
|
private BeatmapManager beatmaps { get; set; } = null!;
|
|
|
|
|
|
|
|
|
|
[Resolved]
|
|
|
|
|
private RulesetStore rulesets { get; set; } = null!;
|
2021-04-01 21:02:32 +08:00
|
|
|
|
|
|
|
|
|
[Resolved]
|
2023-11-22 14:14:25 +08:00
|
|
|
|
private SpectatorClient spectatorClient { get; set; } = null!;
|
2021-04-01 21:02:32 +08:00
|
|
|
|
|
|
|
|
|
[Resolved]
|
2023-11-22 14:14:25 +08:00
|
|
|
|
private UserLookupCache userLookupCache { get; set; } = null!;
|
2021-04-01 21:02:32 +08:00
|
|
|
|
|
|
|
|
|
[Resolved]
|
2023-11-22 14:14:25 +08:00
|
|
|
|
private RealmAccess realm { get; set; } = null!;
|
2021-04-01 21:02:32 +08:00
|
|
|
|
|
2022-01-26 21:21:07 +08:00
|
|
|
|
private readonly IBindableDictionary<int, SpectatorState> userStates = new BindableDictionary<int, SpectatorState>();
|
2021-05-20 18:27:34 +08:00
|
|
|
|
|
2021-11-04 17:02:44 +08:00
|
|
|
|
private readonly Dictionary<int, APIUser> userMap = new Dictionary<int, APIUser>();
|
2021-10-02 01:08:56 +08:00
|
|
|
|
private readonly Dictionary<int, SpectatorGameplayState> gameplayStates = new Dictionary<int, SpectatorGameplayState>();
|
2021-04-01 21:02:32 +08:00
|
|
|
|
|
2023-11-22 14:14:25 +08:00
|
|
|
|
private IDisposable? realmSubscription;
|
|
|
|
|
|
2021-04-01 21:02:32 +08:00
|
|
|
|
/// <summary>
|
|
|
|
|
/// Creates a new <see cref="SpectatorScreen"/>.
|
|
|
|
|
/// </summary>
|
2021-08-10 17:39:20 +08:00
|
|
|
|
/// <param name="users">The users to spectate.</param>
|
|
|
|
|
protected SpectatorScreen(params int[] users)
|
2021-04-01 21:02:32 +08:00
|
|
|
|
{
|
2021-08-10 17:39:20 +08:00
|
|
|
|
this.users.AddRange(users);
|
2021-04-01 21:02:32 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
protected override void LoadComplete()
|
|
|
|
|
{
|
|
|
|
|
base.LoadComplete();
|
|
|
|
|
|
2022-01-03 16:31:12 +08:00
|
|
|
|
userLookupCache.GetUsersAsync(users.ToArray()).ContinueWith(task => Schedule(() =>
|
2021-04-16 17:22:22 +08:00
|
|
|
|
{
|
2022-01-06 21:54:43 +08:00
|
|
|
|
var foundUsers = task.GetResultSafely();
|
2022-01-03 16:31:12 +08:00
|
|
|
|
|
|
|
|
|
foreach (var u in foundUsers)
|
2021-07-05 20:30:24 +08:00
|
|
|
|
{
|
|
|
|
|
if (u == null)
|
|
|
|
|
continue;
|
|
|
|
|
|
2021-05-20 17:52:20 +08:00
|
|
|
|
userMap[u.Id] = u;
|
2021-07-05 20:30:24 +08:00
|
|
|
|
}
|
2021-05-20 17:52:20 +08:00
|
|
|
|
|
2022-02-09 11:09:04 +08:00
|
|
|
|
userStates.BindTo(spectatorClient.WatchedUserStates);
|
2022-01-26 21:21:07 +08:00
|
|
|
|
userStates.BindCollectionChanged(onUserStatesChanged, true);
|
2021-05-20 18:27:34 +08:00
|
|
|
|
|
2022-01-24 18:59:58 +08:00
|
|
|
|
realmSubscription = realm.RegisterForNotifications(
|
2022-01-23 18:42:26 +08:00
|
|
|
|
realm => realm.All<BeatmapSetInfo>().Where(s => !s.DeletePending), beatmapsChanged);
|
2021-04-16 17:22:22 +08:00
|
|
|
|
|
2021-10-27 12:04:41 +08:00
|
|
|
|
foreach ((int id, var _) in userMap)
|
2021-05-20 17:52:20 +08:00
|
|
|
|
spectatorClient.WatchUser(id);
|
2021-04-16 17:22:22 +08:00
|
|
|
|
}));
|
|
|
|
|
}
|
2021-04-01 21:02:32 +08:00
|
|
|
|
|
2023-11-22 14:14:25 +08:00
|
|
|
|
private void beatmapsChanged(IRealmCollection<BeatmapSetInfo> items, ChangeSet? changes)
|
2022-01-23 18:42:26 +08:00
|
|
|
|
{
|
|
|
|
|
if (changes?.InsertedIndices == null) return;
|
|
|
|
|
|
|
|
|
|
foreach (int c in changes.InsertedIndices) beatmapUpdated(items[c]);
|
|
|
|
|
}
|
|
|
|
|
|
2021-11-05 17:05:31 +08:00
|
|
|
|
private void beatmapUpdated(BeatmapSetInfo beatmapSet)
|
2021-04-01 21:02:32 +08:00
|
|
|
|
{
|
2021-11-05 17:05:31 +08:00
|
|
|
|
foreach ((int userId, _) in userMap)
|
2021-04-01 21:02:32 +08:00
|
|
|
|
{
|
2022-01-26 21:21:07 +08:00
|
|
|
|
if (!userStates.TryGetValue(userId, out var userState))
|
2021-05-20 17:52:20 +08:00
|
|
|
|
continue;
|
2021-04-19 15:48:55 +08:00
|
|
|
|
|
2021-11-12 16:45:05 +08:00
|
|
|
|
if (beatmapSet.Beatmaps.Any(b => b.OnlineID == userState.BeatmapID))
|
2022-02-09 11:20:07 +08:00
|
|
|
|
startGameplay(userId);
|
2021-04-01 21:02:32 +08:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2023-11-22 14:14:25 +08:00
|
|
|
|
private void onUserStatesChanged(object? sender, NotifyDictionaryChangedEventArgs<int, SpectatorState> e)
|
2021-05-20 18:27:34 +08:00
|
|
|
|
{
|
|
|
|
|
switch (e.Action)
|
|
|
|
|
{
|
|
|
|
|
case NotifyDictionaryChangedAction.Add:
|
2022-02-03 11:44:33 +08:00
|
|
|
|
case NotifyDictionaryChangedAction.Replace:
|
2022-08-06 10:51:57 +08:00
|
|
|
|
foreach ((int userId, SpectatorState state) in e.NewItems.AsNonNull())
|
2022-01-26 21:21:07 +08:00
|
|
|
|
onUserStateChanged(userId, state);
|
2021-05-20 18:27:34 +08:00
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2022-01-26 21:21:07 +08:00
|
|
|
|
private void onUserStateChanged(int userId, SpectatorState newState)
|
2021-04-01 21:02:32 +08:00
|
|
|
|
{
|
2022-01-26 21:21:07 +08:00
|
|
|
|
if (newState.RulesetID == null || newState.BeatmapID == null)
|
2021-04-01 21:02:32 +08:00
|
|
|
|
return;
|
|
|
|
|
|
2021-05-20 17:52:20 +08:00
|
|
|
|
if (!userMap.ContainsKey(userId))
|
|
|
|
|
return;
|
2021-04-01 21:02:32 +08:00
|
|
|
|
|
2022-02-03 11:44:33 +08:00
|
|
|
|
switch (newState.State)
|
2022-01-26 21:21:07 +08:00
|
|
|
|
{
|
2022-02-09 11:09:04 +08:00
|
|
|
|
case SpectatedUserState.Playing:
|
2023-11-24 13:21:52 +08:00
|
|
|
|
OnNewPlayingUserState(userId, newState);
|
2022-02-09 11:20:07 +08:00
|
|
|
|
startGameplay(userId);
|
2022-02-03 11:44:33 +08:00
|
|
|
|
break;
|
2021-05-20 18:27:34 +08:00
|
|
|
|
|
2022-08-06 10:51:57 +08:00
|
|
|
|
case SpectatedUserState.Passed:
|
2022-08-08 06:37:43 +08:00
|
|
|
|
markReceivedAllFrames(userId);
|
|
|
|
|
break;
|
|
|
|
|
|
2023-11-22 16:53:35 +08:00
|
|
|
|
case SpectatedUserState.Failed:
|
|
|
|
|
failGameplay(userId);
|
|
|
|
|
break;
|
|
|
|
|
|
2022-08-06 10:51:57 +08:00
|
|
|
|
case SpectatedUserState.Quit:
|
2022-08-08 06:37:43 +08:00
|
|
|
|
quitGameplay(userId);
|
2022-08-06 10:51:57 +08:00
|
|
|
|
break;
|
|
|
|
|
}
|
2021-04-01 21:02:32 +08:00
|
|
|
|
}
|
|
|
|
|
|
2022-02-09 11:20:07 +08:00
|
|
|
|
private void startGameplay(int userId)
|
2021-04-01 21:02:32 +08:00
|
|
|
|
{
|
2021-05-20 17:52:20 +08:00
|
|
|
|
Debug.Assert(userMap.ContainsKey(userId));
|
2021-04-01 21:02:32 +08:00
|
|
|
|
|
2021-05-20 17:52:20 +08:00
|
|
|
|
var user = userMap[userId];
|
2022-01-26 21:21:07 +08:00
|
|
|
|
var spectatorState = userStates[userId];
|
2021-04-01 21:02:32 +08:00
|
|
|
|
|
2021-11-24 14:25:49 +08:00
|
|
|
|
var resolvedRuleset = rulesets.AvailableRulesets.FirstOrDefault(r => r.OnlineID == spectatorState.RulesetID)?.CreateInstance();
|
2021-05-20 17:52:20 +08:00
|
|
|
|
if (resolvedRuleset == null)
|
|
|
|
|
return;
|
2021-04-01 21:02:32 +08:00
|
|
|
|
|
2021-11-12 16:45:05 +08:00
|
|
|
|
var resolvedBeatmap = beatmaps.QueryBeatmap(b => b.OnlineID == spectatorState.BeatmapID);
|
2021-05-20 17:52:20 +08:00
|
|
|
|
if (resolvedBeatmap == null)
|
|
|
|
|
return;
|
2021-04-01 21:02:32 +08:00
|
|
|
|
|
2021-05-20 17:52:20 +08:00
|
|
|
|
var score = new Score
|
|
|
|
|
{
|
|
|
|
|
ScoreInfo = new ScoreInfo
|
2021-04-01 21:02:32 +08:00
|
|
|
|
{
|
2021-10-04 16:35:53 +08:00
|
|
|
|
BeatmapInfo = resolvedBeatmap,
|
2021-05-20 17:52:20 +08:00
|
|
|
|
User = user,
|
|
|
|
|
Mods = spectatorState.Mods.Select(m => m.ToMod(resolvedRuleset)).ToArray(),
|
|
|
|
|
Ruleset = resolvedRuleset.RulesetInfo,
|
|
|
|
|
},
|
|
|
|
|
Replay = new Replay { HasReceivedAllFrames = false },
|
|
|
|
|
};
|
|
|
|
|
|
2021-10-02 01:08:56 +08:00
|
|
|
|
var gameplayState = new SpectatorGameplayState(score, resolvedRuleset, beatmaps.GetWorkingBeatmap(resolvedBeatmap));
|
2021-05-20 17:52:20 +08:00
|
|
|
|
|
|
|
|
|
gameplayStates[userId] = gameplayState;
|
2023-11-24 13:21:52 +08:00
|
|
|
|
StartGameplay(userId, gameplayState);
|
2021-04-01 21:02:32 +08:00
|
|
|
|
}
|
|
|
|
|
|
2022-08-08 06:37:43 +08:00
|
|
|
|
/// <summary>
|
|
|
|
|
/// Marks an existing gameplay session as received all frames.
|
|
|
|
|
/// </summary>
|
|
|
|
|
private void markReceivedAllFrames(int userId)
|
|
|
|
|
{
|
|
|
|
|
if (gameplayStates.TryGetValue(userId, out var gameplayState))
|
|
|
|
|
gameplayState.Score.Replay.HasReceivedAllFrames = true;
|
|
|
|
|
}
|
|
|
|
|
|
2023-11-22 16:53:35 +08:00
|
|
|
|
private void failGameplay(int userId)
|
|
|
|
|
{
|
|
|
|
|
if (!userMap.ContainsKey(userId))
|
|
|
|
|
return;
|
|
|
|
|
|
|
|
|
|
if (!gameplayStates.ContainsKey(userId))
|
|
|
|
|
return;
|
|
|
|
|
|
|
|
|
|
markReceivedAllFrames(userId);
|
|
|
|
|
|
|
|
|
|
gameplayStates.Remove(userId);
|
|
|
|
|
FailGameplay(userId);
|
|
|
|
|
}
|
|
|
|
|
|
2022-08-08 06:37:43 +08:00
|
|
|
|
private void quitGameplay(int userId)
|
2022-08-06 10:51:57 +08:00
|
|
|
|
{
|
|
|
|
|
if (!userMap.ContainsKey(userId))
|
|
|
|
|
return;
|
|
|
|
|
|
2022-08-08 06:37:43 +08:00
|
|
|
|
if (!gameplayStates.ContainsKey(userId))
|
2022-08-06 10:51:57 +08:00
|
|
|
|
return;
|
|
|
|
|
|
2022-08-08 06:37:43 +08:00
|
|
|
|
markReceivedAllFrames(userId);
|
2022-08-06 10:51:57 +08:00
|
|
|
|
|
|
|
|
|
gameplayStates.Remove(userId);
|
2023-11-24 13:21:52 +08:00
|
|
|
|
QuitGameplay(userId);
|
2022-08-06 10:51:57 +08:00
|
|
|
|
}
|
|
|
|
|
|
2021-04-01 21:02:32 +08:00
|
|
|
|
/// <summary>
|
2022-02-09 11:20:07 +08:00
|
|
|
|
/// Invoked when a spectated user's state has changed to a new state indicating the player is currently playing.
|
2023-11-24 13:21:52 +08:00
|
|
|
|
/// Thread safety is not guaranteed – should be scheduled as required.
|
2021-04-01 21:02:32 +08:00
|
|
|
|
/// </summary>
|
|
|
|
|
/// <param name="userId">The user whose state has changed.</param>
|
|
|
|
|
/// <param name="spectatorState">The new state.</param>
|
2023-11-22 14:14:25 +08:00
|
|
|
|
protected abstract void OnNewPlayingUserState(int userId, SpectatorState spectatorState);
|
2021-04-01 21:02:32 +08:00
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// Starts gameplay for a user.
|
2023-11-24 13:21:52 +08:00
|
|
|
|
/// Thread safety is not guaranteed – should be scheduled as required.
|
2021-04-01 21:02:32 +08:00
|
|
|
|
/// </summary>
|
|
|
|
|
/// <param name="userId">The user to start gameplay for.</param>
|
2021-10-02 01:08:56 +08:00
|
|
|
|
/// <param name="spectatorGameplayState">The gameplay state.</param>
|
2023-11-22 14:14:25 +08:00
|
|
|
|
protected abstract void StartGameplay(int userId, SpectatorGameplayState spectatorGameplayState);
|
2021-04-01 21:02:32 +08:00
|
|
|
|
|
|
|
|
|
/// <summary>
|
2022-08-08 06:37:43 +08:00
|
|
|
|
/// Quits gameplay for a user.
|
2023-11-24 13:21:52 +08:00
|
|
|
|
/// Thread safety is not guaranteed – should be scheduled as required.
|
2021-04-01 21:02:32 +08:00
|
|
|
|
/// </summary>
|
2022-08-08 06:37:43 +08:00
|
|
|
|
/// <param name="userId">The user to quit gameplay for.</param>
|
|
|
|
|
protected abstract void QuitGameplay(int userId);
|
2021-04-01 21:02:32 +08:00
|
|
|
|
|
2023-11-22 16:53:35 +08:00
|
|
|
|
/// <summary>
|
|
|
|
|
/// Fails gameplay for a user.
|
|
|
|
|
/// Thread safety is not guaranteed – should be scheduled as required.
|
|
|
|
|
/// </summary>
|
|
|
|
|
/// <param name="userId">The user to fail gameplay for.</param>
|
|
|
|
|
protected abstract void FailGameplay(int userId);
|
|
|
|
|
|
2021-04-26 20:25:34 +08:00
|
|
|
|
/// <summary>
|
|
|
|
|
/// Stops spectating a user.
|
|
|
|
|
/// </summary>
|
|
|
|
|
/// <param name="userId">The user to stop spectating.</param>
|
|
|
|
|
protected void RemoveUser(int userId)
|
|
|
|
|
{
|
2022-08-08 06:37:43 +08:00
|
|
|
|
if (!userStates.ContainsKey(userId))
|
2022-02-03 20:50:15 +08:00
|
|
|
|
return;
|
|
|
|
|
|
2022-08-08 06:37:43 +08:00
|
|
|
|
quitGameplay(userId);
|
2021-04-26 20:25:34 +08:00
|
|
|
|
|
2021-08-10 17:39:20 +08:00
|
|
|
|
users.Remove(userId);
|
2021-05-20 17:52:20 +08:00
|
|
|
|
userMap.Remove(userId);
|
2021-04-26 20:25:34 +08:00
|
|
|
|
|
2021-05-20 17:52:20 +08:00
|
|
|
|
spectatorClient.StopWatchingUser(userId);
|
2021-04-26 20:25:34 +08:00
|
|
|
|
}
|
|
|
|
|
|
2021-04-01 21:02:32 +08:00
|
|
|
|
protected override void Dispose(bool isDisposing)
|
|
|
|
|
{
|
|
|
|
|
base.Dispose(isDisposing);
|
|
|
|
|
|
2023-11-22 14:14:25 +08:00
|
|
|
|
if (spectatorClient.IsNotNull())
|
2021-04-01 21:02:32 +08:00
|
|
|
|
{
|
2021-10-27 12:04:41 +08:00
|
|
|
|
foreach ((int userId, var _) in userMap)
|
2021-05-20 17:52:20 +08:00
|
|
|
|
spectatorClient.StopWatchingUser(userId);
|
2021-04-01 21:02:32 +08:00
|
|
|
|
}
|
|
|
|
|
|
2021-12-17 18:26:41 +08:00
|
|
|
|
realmSubscription?.Dispose();
|
2021-04-01 21:02:32 +08:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|