From af478fb2eb6052ddfcc0b99137d9605d2ff06681 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Thu, 1 Apr 2021 22:02:32 +0900 Subject: [PATCH 1/7] Add abstract spectator screen class --- osu.Game/Screens/Spectate/GameplayState.cs | 37 +++ osu.Game/Screens/Spectate/SpectatorScreen.cs | 237 +++++++++++++++++++ 2 files changed, 274 insertions(+) create mode 100644 osu.Game/Screens/Spectate/GameplayState.cs create mode 100644 osu.Game/Screens/Spectate/SpectatorScreen.cs diff --git a/osu.Game/Screens/Spectate/GameplayState.cs b/osu.Game/Screens/Spectate/GameplayState.cs new file mode 100644 index 0000000000..4579b9c07c --- /dev/null +++ b/osu.Game/Screens/Spectate/GameplayState.cs @@ -0,0 +1,37 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Game.Beatmaps; +using osu.Game.Rulesets; +using osu.Game.Scoring; + +namespace osu.Game.Screens.Spectate +{ + /// + /// The gameplay state of a spectated user. This class is immutable. + /// + public class GameplayState + { + /// + /// The score which the user is playing. + /// + public readonly Score Score; + + /// + /// The ruleset which the user is playing. + /// + public readonly Ruleset Ruleset; + + /// + /// The beatmap which the user is playing. + /// + public readonly WorkingBeatmap Beatmap; + + public GameplayState(Score score, Ruleset ruleset, WorkingBeatmap beatmap) + { + Score = score; + Ruleset = ruleset; + Beatmap = beatmap; + } + } +} diff --git a/osu.Game/Screens/Spectate/SpectatorScreen.cs b/osu.Game/Screens/Spectate/SpectatorScreen.cs new file mode 100644 index 0000000000..a534581807 --- /dev/null +++ b/osu.Game/Screens/Spectate/SpectatorScreen.cs @@ -0,0 +1,237 @@ +// 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.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 + { + private readonly int[] userIds; + + [Resolved] + private BeatmapManager beatmaps { get; set; } + + [Resolved] + private RulesetStore rulesets { get; set; } + + [Resolved] + private SpectatorStreamingClient spectatorClient { get; set; } + + [Resolved] + private UserLookupCache userLookupCache { get; set; } + + private readonly object stateLock = new object(); + + private readonly Dictionary userMap = new Dictionary(); + private readonly Dictionary spectatorStates = 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 = userIds; + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + spectatorClient.OnUserBeganPlaying += userBeganPlaying; + spectatorClient.OnUserFinishedPlaying += userFinishedPlaying; + spectatorClient.OnNewFrames += userSentFrames; + + foreach (var id in userIds) + { + userLookupCache.GetUserAsync(id).ContinueWith(u => Schedule(() => + { + if (u.Result == null) + return; + + lock (stateLock) + userMap[id] = u.Result; + + spectatorClient.WatchUser(id); + }), TaskContinuationOptions.OnlyOnRanToCompletion); + } + + managerUpdated = beatmaps.ItemUpdated.GetBoundCopy(); + managerUpdated.BindValueChanged(beatmapUpdated); + } + + private void beatmapUpdated(ValueChangedEvent> beatmap) + { + if (!beatmap.NewValue.TryGetTarget(out var beatmapSet)) + return; + + lock (stateLock) + { + foreach (var (userId, state) in spectatorStates) + { + if (beatmapSet.Beatmaps.Any(b => b.OnlineBeatmapID == state.BeatmapID)) + updateGameplayState(userId); + } + } + } + + private void userBeganPlaying(int userId, SpectatorState state) + { + if (state.RulesetID == null || state.BeatmapID == null) + return; + + lock (stateLock) + { + if (!userMap.ContainsKey(userId)) + return; + + spectatorStates[userId] = state; + OnUserStateChanged(userId, state); + + updateGameplayState(userId); + } + } + + private void updateGameplayState(int userId) + { + lock (stateLock) + { + Debug.Assert(userMap.ContainsKey(userId)); + + var spectatorState = spectatorStates[userId]; + var user = userMap[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; + StartGameplay(userId, gameplayState); + } + } + + private void userSentFrames(int userId, FrameDataBundle bundle) + { + lock (stateLock) + { + 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); + } + } + } + + private void userFinishedPlaying(int userId, SpectatorState state) + { + lock (stateLock) + { + if (!userMap.ContainsKey(userId)) + return; + + if (!gameplayStates.TryGetValue(userId, out var gameplayState)) + return; + + gameplayState.Score.Replay.HasReceivedAllFrames = true; + + gameplayStates.Remove(userId); + EndGameplay(userId); + } + } + + /// + /// 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); + + protected override void Dispose(bool isDisposing) + { + base.Dispose(isDisposing); + + if (spectatorClient != null) + { + spectatorClient.OnUserBeganPlaying -= userBeganPlaying; + spectatorClient.OnUserFinishedPlaying -= userFinishedPlaying; + spectatorClient.OnNewFrames -= userSentFrames; + + lock (stateLock) + { + foreach (var (userId, _) in userMap) + spectatorClient.StopWatchingUser(userId); + } + } + + managerUpdated?.UnbindAll(); + } + } +} From 9e95441aa63ef92065204aa349cbd7abe2f44f62 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Thu, 1 Apr 2021 22:08:52 +0900 Subject: [PATCH 2/7] Rename Spectator -> SoloSpectator --- osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs | 8 ++++---- osu.Game/Overlays/Dashboard/CurrentlyPlayingDisplay.cs | 2 +- osu.Game/Screens/Play/{Spectator.cs => SoloSpectator.cs} | 4 ++-- 3 files changed, 7 insertions(+), 7 deletions(-) rename osu.Game/Screens/Play/{Spectator.cs => SoloSpectator.cs} (99%) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs index 4a0e1282c4..b59f6a4eb7 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs @@ -32,7 +32,7 @@ namespace osu.Game.Tests.Visual.Gameplay // used just to show beatmap card for the time being. protected override bool UseOnlineAPI => true; - private Spectator spectatorScreen; + private SoloSpectator spectatorScreen; [Resolved] private OsuGameBase game { get; set; } @@ -69,7 +69,7 @@ namespace osu.Game.Tests.Visual.Gameplay { loadSpectatingScreen(); - AddAssert("screen hasn't changed", () => Stack.CurrentScreen is Spectator); + AddAssert("screen hasn't changed", () => Stack.CurrentScreen is SoloSpectator); start(); sendFrames(); @@ -195,7 +195,7 @@ namespace osu.Game.Tests.Visual.Gameplay start(-1234); sendFrames(); - AddAssert("screen didn't change", () => Stack.CurrentScreen is Spectator); + AddAssert("screen didn't change", () => Stack.CurrentScreen is SoloSpectator); } private OsuFramedReplayInputHandler replayHandler => @@ -226,7 +226,7 @@ namespace osu.Game.Tests.Visual.Gameplay private void loadSpectatingScreen() { - AddStep("load screen", () => LoadScreen(spectatorScreen = new Spectator(testSpectatorStreamingClient.StreamingUser))); + AddStep("load screen", () => LoadScreen(spectatorScreen = new SoloSpectator(testSpectatorStreamingClient.StreamingUser))); AddUntilStep("wait for screen load", () => spectatorScreen.LoadState == LoadState.Loaded); } diff --git a/osu.Game/Overlays/Dashboard/CurrentlyPlayingDisplay.cs b/osu.Game/Overlays/Dashboard/CurrentlyPlayingDisplay.cs index c89699f2ee..336430fd9b 100644 --- a/osu.Game/Overlays/Dashboard/CurrentlyPlayingDisplay.cs +++ b/osu.Game/Overlays/Dashboard/CurrentlyPlayingDisplay.cs @@ -137,7 +137,7 @@ namespace osu.Game.Overlays.Dashboard Text = "Watch", Anchor = Anchor.TopCentre, Origin = Anchor.TopCentre, - Action = () => game?.PerformFromScreen(s => s.Push(new Spectator(User))), + Action = () => game?.PerformFromScreen(s => s.Push(new SoloSpectator(User))), Enabled = { Value = User.Id != api.LocalUser.Value.Id } } } diff --git a/osu.Game/Screens/Play/Spectator.cs b/osu.Game/Screens/Play/SoloSpectator.cs similarity index 99% rename from osu.Game/Screens/Play/Spectator.cs rename to osu.Game/Screens/Play/SoloSpectator.cs index 28311f5113..9be42b52b3 100644 --- a/osu.Game/Screens/Play/Spectator.cs +++ b/osu.Game/Screens/Play/SoloSpectator.cs @@ -37,7 +37,7 @@ using osuTK; namespace osu.Game.Screens.Play { [Cached(typeof(IPreviewTrackOwner))] - public class Spectator : OsuScreen, IPreviewTrackOwner + public class SoloSpectator : OsuScreen, IPreviewTrackOwner { private readonly User targetUser; @@ -88,7 +88,7 @@ namespace osu.Game.Screens.Play /// private bool newStatePending; - public Spectator([NotNull] User targetUser) + public SoloSpectator([NotNull] User targetUser) { this.targetUser = targetUser ?? throw new ArgumentNullException(nameof(targetUser)); } From 9bc2a486e0c40baf2db334f6ef4cf1fdc5977a0a Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Thu, 1 Apr 2021 22:09:51 +0900 Subject: [PATCH 3/7] Make SoloSpectator use the new SpectatorScreen class --- osu.Game/Screens/Play/SoloSpectator.cs | 233 ++++++------------------- 1 file changed, 53 insertions(+), 180 deletions(-) diff --git a/osu.Game/Screens/Play/SoloSpectator.cs b/osu.Game/Screens/Play/SoloSpectator.cs index 9be42b52b3..f8ed7b585f 100644 --- a/osu.Game/Screens/Play/SoloSpectator.cs +++ b/osu.Game/Screens/Play/SoloSpectator.cs @@ -1,13 +1,9 @@ // 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.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; @@ -24,73 +20,54 @@ using osu.Game.Online.API.Requests; using osu.Game.Online.Spectator; using osu.Game.Overlays.BeatmapListing.Panels; using osu.Game.Overlays.Settings; -using osu.Game.Replays; using osu.Game.Rulesets; -using osu.Game.Rulesets.Mods; -using osu.Game.Rulesets.Replays; -using osu.Game.Rulesets.Replays.Types; -using osu.Game.Scoring; using osu.Game.Screens.OnlinePlay.Match.Components; +using osu.Game.Screens.Spectate; using osu.Game.Users; using osuTK; namespace osu.Game.Screens.Play { [Cached(typeof(IPreviewTrackOwner))] - public class SoloSpectator : OsuScreen, IPreviewTrackOwner + public class SoloSpectator : SpectatorScreen, IPreviewTrackOwner { + [NotNull] private readonly User targetUser; - [Resolved] - private Bindable beatmap { get; set; } - - [Resolved] - private Bindable ruleset { get; set; } - - private Ruleset rulesetInstance; - - [Resolved] - private Bindable> mods { get; set; } - [Resolved] private IAPIProvider api { get; set; } [Resolved] - private SpectatorStreamingClient spectatorStreaming { get; set; } - - [Resolved] - private BeatmapManager beatmaps { get; set; } + private PreviewTrackManager previewTrackManager { get; set; } [Resolved] private RulesetStore rulesets { get; set; } [Resolved] - private PreviewTrackManager previewTrackManager { get; set; } - - private Score score; - - private readonly object scoreLock = new object(); + private BeatmapManager beatmaps { get; set; } private Container beatmapPanelContainer; - - private SpectatorState state; - - private IBindable> managerUpdated; - private TriangleButton watchButton; - private SettingsCheckbox automaticDownload; - private BeatmapSetInfo onlineBeatmap; /// - /// Becomes true if a new state is waiting to be loaded (while this screen was not active). + /// The player's immediate online gameplay state. + /// This doesn't reflect the gameplay state being watched by the user if is non-null. /// - private bool newStatePending; + private GameplayState immediateGameplayState; + + /// + /// The gameplay state that is pending to be watched, upon this screen becoming current. + /// + private GameplayState pendingGameplayState; + + private GetBeatmapSetRequest onlineBeatmapRequest; public SoloSpectator([NotNull] User targetUser) + : base(targetUser.Id) { - this.targetUser = targetUser ?? throw new ArgumentNullException(nameof(targetUser)); + this.targetUser = targetUser; } [BackgroundDependencyLoader] @@ -173,7 +150,7 @@ namespace osu.Game.Screens.Play Width = 250, Anchor = Anchor.Centre, Origin = Anchor.Centre, - Action = attemptStart, + Action = () => attemptStart(immediateGameplayState), Enabled = { Value = false } } } @@ -185,169 +162,81 @@ namespace osu.Game.Screens.Play protected override void LoadComplete() { base.LoadComplete(); - - spectatorStreaming.OnUserBeganPlaying += userBeganPlaying; - spectatorStreaming.OnUserFinishedPlaying += userFinishedPlaying; - spectatorStreaming.OnNewFrames += userSentFrames; - - spectatorStreaming.WatchUser(targetUser.Id); - - managerUpdated = beatmaps.ItemUpdated.GetBoundCopy(); - managerUpdated.BindValueChanged(beatmapUpdated); - automaticDownload.Current.BindValueChanged(_ => checkForAutomaticDownload()); } - private void beatmapUpdated(ValueChangedEvent> beatmap) + protected override void OnUserStateChanged(int userId, SpectatorState spectatorState) => Schedule(() => { - if (beatmap.NewValue.TryGetTarget(out var beatmapSet) && beatmapSet.Beatmaps.Any(b => b.OnlineBeatmapID == state.BeatmapID)) - Schedule(attemptStart); - } + clearDisplay(); + showBeatmapPanel(spectatorState); + }); - private void userSentFrames(int userId, FrameDataBundle data) + protected override void StartGameplay(int userId, GameplayState gameplayState) => Schedule(() => { - // this is not scheduled as it handles propagation of frames even when in a child screen (at which point we are not alive). - // probably not the safest way to handle this. - - if (userId != targetUser.Id) - return; - - lock (scoreLock) - { - // this should never happen as the server sends the user's state on watching, - // but is here as a safety measure. - if (score == null) - return; - - // rulesetInstance should be guaranteed to be in sync with the score via scoreLock. - Debug.Assert(rulesetInstance != null && rulesetInstance.RulesetInfo.Equals(score.ScoreInfo.Ruleset)); - - foreach (var frame in data.Frames) - { - IConvertibleReplayFrame convertibleFrame = rulesetInstance.CreateConvertibleReplayFrame(); - convertibleFrame.FromLegacy(frame, beatmap.Value.Beatmap); - - var convertedFrame = (ReplayFrame)convertibleFrame; - convertedFrame.Time = frame.Time; - - score.Replay.Frames.Add(convertedFrame); - } - } - } - - private void userBeganPlaying(int userId, SpectatorState state) - { - if (userId != targetUser.Id) - return; - - this.state = state; + pendingGameplayState = null; + immediateGameplayState = gameplayState; if (this.IsCurrentScreen()) - Schedule(attemptStart); + attemptStart(gameplayState); else - newStatePending = true; - } + pendingGameplayState = gameplayState; + + watchButton.Enabled.Value = true; + }); + + protected override void EndGameplay(int userId) => Schedule(() => + { + pendingGameplayState = null; + immediateGameplayState = null; + + Schedule(clearDisplay); + + watchButton.Enabled.Value = false; + }); public override void OnResuming(IScreen last) { base.OnResuming(last); - if (newStatePending) + if (pendingGameplayState != null) { - attemptStart(); - newStatePending = false; + attemptStart(pendingGameplayState); + pendingGameplayState = null; } } - private void userFinishedPlaying(int userId, SpectatorState state) - { - if (userId != targetUser.Id) - return; - - lock (scoreLock) - { - if (score != null) - { - score.Replay.HasReceivedAllFrames = true; - score = null; - } - } - - Schedule(clearDisplay); - } - private void clearDisplay() { watchButton.Enabled.Value = false; + onlineBeatmapRequest?.Cancel(); beatmapPanelContainer.Clear(); previewTrackManager.StopAnyPlaying(this); } - private void attemptStart() + private void attemptStart(GameplayState gameplayState) { - clearDisplay(); - showBeatmapPanel(state); - - var resolvedRuleset = rulesets.AvailableRulesets.FirstOrDefault(r => r.ID == state.RulesetID)?.CreateInstance(); - - // ruleset not available - if (resolvedRuleset == null) + if (gameplayState == null) return; - if (state.BeatmapID == null) - return; + Beatmap.Value = gameplayState.Beatmap; + Ruleset.Value = gameplayState.Ruleset.RulesetInfo; - var resolvedBeatmap = beatmaps.QueryBeatmap(b => b.OnlineBeatmapID == state.BeatmapID); - - if (resolvedBeatmap == null) - { - return; - } - - lock (scoreLock) - { - score = new Score - { - ScoreInfo = new ScoreInfo - { - Beatmap = resolvedBeatmap, - User = targetUser, - Mods = state.Mods.Select(m => m.ToMod(resolvedRuleset)).ToArray(), - Ruleset = resolvedRuleset.RulesetInfo, - }, - Replay = new Replay { HasReceivedAllFrames = false }, - }; - - ruleset.Value = resolvedRuleset.RulesetInfo; - rulesetInstance = resolvedRuleset; - - beatmap.Value = beatmaps.GetWorkingBeatmap(resolvedBeatmap); - watchButton.Enabled.Value = true; - - this.Push(new SpectatorPlayerLoader(score)); - } + this.Push(new SpectatorPlayerLoader(gameplayState.Score)); } private void showBeatmapPanel(SpectatorState state) { - if (state?.BeatmapID == null) - { - onlineBeatmap = null; - return; - } + Debug.Assert(state.BeatmapID != null); - var req = new GetBeatmapSetRequest(state.BeatmapID.Value, BeatmapSetLookupType.BeatmapId); - req.Success += res => Schedule(() => + onlineBeatmapRequest = new GetBeatmapSetRequest(state.BeatmapID.Value, BeatmapSetLookupType.BeatmapId); + onlineBeatmapRequest.Success += res => Schedule(() => { - if (state != this.state) - return; - onlineBeatmap = res.ToBeatmapSet(rulesets); beatmapPanelContainer.Child = new GridBeatmapPanel(onlineBeatmap); checkForAutomaticDownload(); }); - api.Queue(req); + api.Queue(onlineBeatmapRequest); } private void checkForAutomaticDownload() @@ -369,21 +258,5 @@ namespace osu.Game.Screens.Play previewTrackManager.StopAnyPlaying(this); return base.OnExiting(next); } - - protected override void Dispose(bool isDisposing) - { - base.Dispose(isDisposing); - - if (spectatorStreaming != null) - { - spectatorStreaming.OnUserBeganPlaying -= userBeganPlaying; - spectatorStreaming.OnUserFinishedPlaying -= userFinishedPlaying; - spectatorStreaming.OnNewFrames -= userSentFrames; - - spectatorStreaming.StopWatchingUser(targetUser.Id); - } - - managerUpdated?.UnbindAll(); - } } } From c3c7c18549e281503d3225db9bc0d3fbc85c1511 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Thu, 1 Apr 2021 23:48:26 +0900 Subject: [PATCH 4/7] Fix tests --- .../Visual/Gameplay/TestSceneSpectator.cs | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs index b59f6a4eb7..9d85a9995d 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs @@ -3,6 +3,8 @@ using System.Collections.Generic; using System.Linq; +using System.Threading; +using System.Threading.Tasks; using NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Bindables; @@ -11,6 +13,7 @@ using osu.Framework.Screens; using osu.Framework.Testing; using osu.Framework.Utils; using osu.Game.Beatmaps; +using osu.Game.Database; using osu.Game.Online; using osu.Game.Online.Spectator; using osu.Game.Replays.Legacy; @@ -29,6 +32,9 @@ namespace osu.Game.Tests.Visual.Gameplay [Cached(typeof(SpectatorStreamingClient))] private TestSpectatorStreamingClient testSpectatorStreamingClient = new TestSpectatorStreamingClient(); + [Cached(typeof(UserLookupCache))] + private UserLookupCache lookupCache = new TestUserLookupCache(); + // used just to show beatmap card for the time being. protected override bool UseOnlineAPI => true; @@ -301,5 +307,14 @@ namespace osu.Game.Tests.Visual.Gameplay }); } } + + internal class TestUserLookupCache : UserLookupCache + { + protected override Task ComputeValueAsync(int lookup, CancellationToken token = default) => Task.FromResult(new User + { + Id = lookup, + Username = $"User {lookup}" + }); + } } } From 45d16fb9164014de9990b6342f2a2d04b3805ba8 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 2 Apr 2021 16:56:47 +0900 Subject: [PATCH 5/7] Rename event parameter for clarity --- osu.Game/Screens/Spectate/SpectatorScreen.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/Spectate/SpectatorScreen.cs b/osu.Game/Screens/Spectate/SpectatorScreen.cs index a534581807..25dd482112 100644 --- a/osu.Game/Screens/Spectate/SpectatorScreen.cs +++ b/osu.Game/Screens/Spectate/SpectatorScreen.cs @@ -83,9 +83,9 @@ namespace osu.Game.Screens.Spectate managerUpdated.BindValueChanged(beatmapUpdated); } - private void beatmapUpdated(ValueChangedEvent> beatmap) + private void beatmapUpdated(ValueChangedEvent> e) { - if (!beatmap.NewValue.TryGetTarget(out var beatmapSet)) + if (!e.NewValue.TryGetTarget(out var beatmapSet)) return; lock (stateLock) From d2950105fb391fffe4463248ca3d172286b54d09 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Fri, 2 Apr 2021 20:31:34 +0900 Subject: [PATCH 6/7] Add comment explaining use of lock --- osu.Game/Screens/Spectate/SpectatorScreen.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/osu.Game/Screens/Spectate/SpectatorScreen.cs b/osu.Game/Screens/Spectate/SpectatorScreen.cs index 25dd482112..4d285d519e 100644 --- a/osu.Game/Screens/Spectate/SpectatorScreen.cs +++ b/osu.Game/Screens/Spectate/SpectatorScreen.cs @@ -40,6 +40,7 @@ namespace osu.Game.Screens.Spectate [Resolved] private UserLookupCache userLookupCache { get; set; } + // A lock is used to synchronise access to spectator/gameplay states, since this class is a screen which may become non-current and stop receiving updates at any point. private readonly object stateLock = new object(); private readonly Dictionary userMap = new Dictionary(); From cd53074941d864e0e1eaa33d8008f1730e8339a6 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Fri, 2 Apr 2021 21:27:20 +0900 Subject: [PATCH 7/7] Schedule spectator callbacks --- osu.Game/Screens/Play/SoloSpectator.cs | 69 +++++++++----------- osu.Game/Screens/Spectate/SpectatorScreen.cs | 6 +- 2 files changed, 33 insertions(+), 42 deletions(-) diff --git a/osu.Game/Screens/Play/SoloSpectator.cs b/osu.Game/Screens/Play/SoloSpectator.cs index f8ed7b585f..820d776e63 100644 --- a/osu.Game/Screens/Play/SoloSpectator.cs +++ b/osu.Game/Screens/Play/SoloSpectator.cs @@ -9,6 +9,7 @@ using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Sprites; using osu.Framework.Screens; +using osu.Framework.Threading; using osu.Game.Audio; using osu.Game.Beatmaps; using osu.Game.Configuration; @@ -53,15 +54,10 @@ namespace osu.Game.Screens.Play /// /// The player's immediate online gameplay state. - /// This doesn't reflect the gameplay state being watched by the user if is non-null. + /// This doesn't always reflect the gameplay state being watched. /// private GameplayState immediateGameplayState; - /// - /// The gameplay state that is pending to be watched, upon this screen becoming current. - /// - private GameplayState pendingGameplayState; - private GetBeatmapSetRequest onlineBeatmapRequest; public SoloSpectator([NotNull] User targetUser) @@ -150,7 +146,7 @@ namespace osu.Game.Screens.Play Width = 250, Anchor = Anchor.Centre, Origin = Anchor.Centre, - Action = () => attemptStart(immediateGameplayState), + Action = () => scheduleStart(immediateGameplayState), Enabled = { Value = false } } } @@ -165,44 +161,27 @@ namespace osu.Game.Screens.Play automaticDownload.Current.BindValueChanged(_ => checkForAutomaticDownload()); } - protected override void OnUserStateChanged(int userId, SpectatorState spectatorState) => Schedule(() => + protected override void OnUserStateChanged(int userId, SpectatorState spectatorState) { clearDisplay(); showBeatmapPanel(spectatorState); - }); + } - protected override void StartGameplay(int userId, GameplayState gameplayState) => Schedule(() => + protected override void StartGameplay(int userId, GameplayState gameplayState) { - pendingGameplayState = null; immediateGameplayState = gameplayState; - - if (this.IsCurrentScreen()) - attemptStart(gameplayState); - else - pendingGameplayState = gameplayState; - watchButton.Enabled.Value = true; - }); - protected override void EndGameplay(int userId) => Schedule(() => + scheduleStart(gameplayState); + } + + protected override void EndGameplay(int userId) { - pendingGameplayState = null; + scheduledStart?.Cancel(); immediateGameplayState = null; - - Schedule(clearDisplay); - watchButton.Enabled.Value = false; - }); - public override void OnResuming(IScreen last) - { - base.OnResuming(last); - - if (pendingGameplayState != null) - { - attemptStart(pendingGameplayState); - pendingGameplayState = null; - } + clearDisplay(); } private void clearDisplay() @@ -213,15 +192,27 @@ namespace osu.Game.Screens.Play previewTrackManager.StopAnyPlaying(this); } - private void attemptStart(GameplayState gameplayState) + private ScheduledDelegate scheduledStart; + + private void scheduleStart(GameplayState gameplayState) { - if (gameplayState == null) - return; + // This function may be called multiple times in quick succession once the screen becomes current again. + scheduledStart?.Cancel(); + scheduledStart = Schedule(() => + { + if (this.IsCurrentScreen()) + start(); + else + scheduleStart(gameplayState); + }); - Beatmap.Value = gameplayState.Beatmap; - Ruleset.Value = gameplayState.Ruleset.RulesetInfo; + void start() + { + Beatmap.Value = gameplayState.Beatmap; + Ruleset.Value = gameplayState.Ruleset.RulesetInfo; - this.Push(new SpectatorPlayerLoader(gameplayState.Score)); + this.Push(new SpectatorPlayerLoader(gameplayState.Score)); + } } private void showBeatmapPanel(SpectatorState state) diff --git a/osu.Game/Screens/Spectate/SpectatorScreen.cs b/osu.Game/Screens/Spectate/SpectatorScreen.cs index 4d285d519e..6dd3144fc8 100644 --- a/osu.Game/Screens/Spectate/SpectatorScreen.cs +++ b/osu.Game/Screens/Spectate/SpectatorScreen.cs @@ -110,7 +110,7 @@ namespace osu.Game.Screens.Spectate return; spectatorStates[userId] = state; - OnUserStateChanged(userId, state); + Schedule(() => OnUserStateChanged(userId, state)); updateGameplayState(userId); } @@ -148,7 +148,7 @@ namespace osu.Game.Screens.Spectate var gameplayState = new GameplayState(score, resolvedRuleset, beatmaps.GetWorkingBeatmap(resolvedBeatmap)); gameplayStates[userId] = gameplayState; - StartGameplay(userId, gameplayState); + Schedule(() => StartGameplay(userId, gameplayState)); } } @@ -191,7 +191,7 @@ namespace osu.Game.Screens.Spectate gameplayState.Score.Replay.HasReceivedAllFrames = true; gameplayStates.Remove(userId); - EndGameplay(userId); + Schedule(() => EndGameplay(userId)); } }