1
0
mirror of https://github.com/ppy/osu.git synced 2024-12-17 03:02:56 +08:00
osu-lazer/osu.Game/Screens/Spectate/SpectatorScreen.cs

273 lines
9.2 KiB
C#
Raw Normal View History

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.
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;
2021-04-01 21:02:32 +08:00
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
{
/// <summary>
/// A <see cref="OsuScreen"/> which spectates one or more users.
/// </summary>
public abstract class SpectatorScreen : OsuScreen
{
protected IReadOnlyList<int> UserIds => userIds;
private readonly List<int> userIds = new List<int>();
2021-04-01 21:02:32 +08:00
[Resolved]
private BeatmapManager beatmaps { get; set; }
[Resolved]
private RulesetStore rulesets { get; set; }
[Resolved]
private SpectatorClient spectatorClient { get; set; }
2021-04-01 21:02:32 +08:00
[Resolved]
private UserLookupCache userLookupCache { get; set; }
private readonly IBindableDictionary<int, SpectatorState> playingUserStates = new BindableDictionary<int, SpectatorState>();
2021-04-01 21:02:32 +08:00
private readonly Dictionary<int, User> userMap = new Dictionary<int, User>();
private readonly Dictionary<int, GameplayState> gameplayStates = new Dictionary<int, GameplayState>();
private IBindable<WeakReference<BeatmapSetInfo>> managerUpdated;
/// <summary>
/// Creates a new <see cref="SpectatorScreen"/>.
/// </summary>
/// <param name="userIds">The users to spectate.</param>
protected SpectatorScreen(params int[] userIds)
{
this.userIds.AddRange(userIds);
2021-04-01 21:02:32 +08:00
}
protected override void LoadComplete()
{
base.LoadComplete();
2021-05-20 17:52:20 +08:00
getAllUsers().ContinueWith(users => Schedule(() =>
{
2021-05-20 17:52:20 +08:00
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);
2021-05-20 17:52:20 +08:00
foreach (var (id, _) in userMap)
spectatorClient.WatchUser(id);
}));
}
2021-04-01 21:02:32 +08:00
2021-05-20 17:52:20 +08:00
private Task<User[]> getAllUsers()
{
2021-05-20 17:52:20 +08:00
var userLookupTasks = new List<Task<User>>();
foreach (var u in userIds)
2021-04-01 21:02:32 +08:00
{
userLookupTasks.Add(userLookupCache.GetUserAsync(u).ContinueWith(task =>
2021-04-01 21:02:32 +08:00
{
if (!task.IsCompletedSuccessfully)
2021-05-20 17:52:20 +08:00
return null;
2021-04-01 21:02:32 +08:00
2021-05-20 17:52:20 +08:00
return task.Result;
}));
2021-04-01 21:02:32 +08:00
}
return Task.WhenAll(userLookupTasks);
2021-04-01 21:02:32 +08:00
}
2021-04-02 15:56:47 +08:00
private void beatmapUpdated(ValueChangedEvent<WeakReference<BeatmapSetInfo>> e)
2021-04-01 21:02:32 +08:00
{
2021-04-02 15:56:47 +08:00
if (!e.NewValue.TryGetTarget(out var beatmapSet))
2021-04-01 21:02:32 +08:00
return;
2021-05-20 17:52:20 +08:00
foreach (var (userId, _) in userMap)
2021-04-01 21:02:32 +08:00
{
if (!playingUserStates.TryGetValue(userId, out var userState))
2021-05-20 17:52:20 +08:00
continue;
2021-05-20 17:52:20 +08:00
if (beatmapSet.Beatmaps.Any(b => b.OnlineBeatmapID == userState.BeatmapID))
updateGameplayState(userId);
2021-04-01 21:02:32 +08:00
}
}
private void onPlayingUserStatesChanged(object sender, NotifyDictionaryChangedEventArgs<int, SpectatorState> 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;
2021-05-20 18:37:43 +08:00
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)
2021-04-01 21:02:32 +08:00
{
if (state.RulesetID == null || state.BeatmapID == null)
return;
2021-05-20 17:52:20 +08:00
if (!userMap.ContainsKey(userId))
return;
2021-04-01 21:02:32 +08:00
Schedule(() => OnUserStateChanged(userId, state));
updateGameplayState(userId);
}
private void onUserStateRemoved(int userId)
{
if (!userMap.ContainsKey(userId))
2021-05-20 17:52:20 +08:00
return;
if (!gameplayStates.TryGetValue(userId, out var gameplayState))
return;
2021-04-01 21:02:32 +08:00
gameplayState.Score.Replay.HasReceivedAllFrames = true;
gameplayStates.Remove(userId);
Schedule(() => EndGameplay(userId));
2021-04-01 21:02:32 +08:00
}
private void updateGameplayState(int userId)
{
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];
var spectatorState = playingUserStates[userId];
2021-04-01 21:02:32 +08:00
2021-05-20 17:52:20 +08:00
var resolvedRuleset = rulesets.AvailableRulesets.FirstOrDefault(r => r.ID == spectatorState.RulesetID)?.CreateInstance();
if (resolvedRuleset == null)
return;
2021-04-01 21:02:32 +08:00
2021-05-20 17:52:20 +08:00
var resolvedBeatmap = beatmaps.QueryBeatmap(b => b.OnlineBeatmapID == spectatorState.BeatmapID);
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-05-20 17:52:20 +08:00
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));
2021-04-01 21:02:32 +08:00
}
private void userSentFrames(int userId, FrameDataBundle bundle)
{
2021-05-20 17:52:20 +08:00
if (!userMap.ContainsKey(userId))
return;
2021-04-01 21:02:32 +08:00
2021-05-20 17:52:20 +08:00
if (!gameplayStates.TryGetValue(userId, out var gameplayState))
return;
2021-04-01 21:02:32 +08:00
2021-05-20 17:52:20 +08:00
// 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));
2021-04-01 21:02:32 +08:00
2021-05-20 17:52:20 +08:00
foreach (var frame in bundle.Frames)
{
IConvertibleReplayFrame convertibleFrame = gameplayState.Ruleset.CreateConvertibleReplayFrame();
convertibleFrame.FromLegacy(frame, gameplayState.Beatmap.Beatmap);
2021-04-01 21:02:32 +08:00
2021-05-20 17:52:20 +08:00
var convertedFrame = (ReplayFrame)convertibleFrame;
convertedFrame.Time = frame.Time;
2021-04-01 21:02:32 +08:00
2021-05-20 17:52:20 +08:00
gameplayState.Score.Replay.Frames.Add(convertedFrame);
2021-04-01 21:02:32 +08:00
}
}
/// <summary>
/// Invoked when a spectated user's state has changed.
/// </summary>
/// <param name="userId">The user whose state has changed.</param>
/// <param name="spectatorState">The new state.</param>
protected abstract void OnUserStateChanged(int userId, [NotNull] SpectatorState spectatorState);
/// <summary>
/// Starts gameplay for a user.
/// </summary>
/// <param name="userId">The user to start gameplay for.</param>
/// <param name="gameplayState">The gameplay state.</param>
protected abstract void StartGameplay(int userId, [NotNull] GameplayState gameplayState);
/// <summary>
/// Ends gameplay for a user.
/// </summary>
/// <param name="userId">The user to end gameplay for.</param>
protected abstract void EndGameplay(int userId);
/// <summary>
/// Stops spectating a user.
/// </summary>
/// <param name="userId">The user to stop spectating.</param>
protected void RemoveUser(int userId)
{
onUserStateRemoved(userId);
2021-05-20 17:52:20 +08:00
userIds.Remove(userId);
userMap.Remove(userId);
2021-05-20 17:52:20 +08:00
spectatorClient.StopWatchingUser(userId);
}
2021-04-01 21:02:32 +08:00
protected override void Dispose(bool isDisposing)
{
base.Dispose(isDisposing);
if (spectatorClient != null)
{
spectatorClient.OnNewFrames -= userSentFrames;
2021-05-20 17:52:20 +08:00
foreach (var (userId, _) in userMap)
spectatorClient.StopWatchingUser(userId);
2021-04-01 21:02:32 +08:00
}
managerUpdated?.UnbindAll();
}
}
}